Merge "Describe how to generate iplog."
diff --git a/.eclipse_iplog b/.eclipse_iplog
index bc3b22e..dc70169 100644
--- a/.eclipse_iplog
+++ b/.eclipse_iplog
@@ -4,6 +4,9 @@
 
 	skipCommit = 1a6964c8274c50f0253db75f010d78ef0e739343
 
+[review]
+	url = http://egit.eclipse.org/r/r/
+
 [CQ "3454"]
 	description = args4j Version: 2.0.12
 	license = BSD License
@@ -23,9 +26,9 @@
 	state = approved
 
 [CQ "3904"]
-	description = jsch Version: 0.1.37 (using Orbit CQ2014)
+	description = jsch Version: 0.1.37 (using Orbit CQ2002)
 	license = New BSD license
-	use = unmodified binary
+	use = unmodified source & binary
 	state = approved
 
 [CQ "3655"]
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
index 6b746e3..731b4ca 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/FileSender.java
@@ -64,7 +64,6 @@
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.util.IO;
 
 /**
  * Dumps a file over HTTP GET (or its information via HEAD).
@@ -122,7 +121,8 @@
 	String getTailChecksum() throws IOException {
 		final int n = 20;
 		final byte[] buf = new byte[n];
-		IO.readFully(source.getChannel(), fileLen - n, buf, 0, n);
+		source.seek(fileLen - n);
+		source.readFully(buf, 0, n);
 		return ObjectId.fromRaw(buf).getName();
 	}
 
@@ -140,6 +140,7 @@
 			final OutputStream out = rsp.getOutputStream();
 			try {
 				final byte[] buf = new byte[4096];
+				source.seek(pos);
 				while (pos < end) {
 					final int r = (int) Math.min(buf.length, end - pos);
 					final int n = source.read(buf, 0, r);
diff --git a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/CSV.java b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/CSV.java
index 3f80c7d..12dbf47 100644
--- a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/CSV.java
+++ b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/CSV.java
@@ -116,6 +116,8 @@
 			} else if (line.charAt(p) == ',') {
 				row.add("");
 				p++;
+				if (p == line.length())
+					row.add("");
 
 			} else {
 				int comma = line.indexOf(',', p);
diff --git a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogGenerator.java b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogGenerator.java
index a9fdb81..28df8b7 100644
--- a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogGenerator.java
+++ b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogGenerator.java
@@ -58,7 +58,6 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -88,7 +87,6 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.WindowCursor;
-import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -111,11 +109,12 @@
 
 	private static final String INDENT = "{http://xml.apache.org/xslt}indent-amount";
 
-	private static final FooterKey BUG = new FooterKey("Bug");
-
 	/** Projects indexed by their ID string, e.g. {@code technology.jgit}. */
 	private final Map<String, Project> projects = new TreeMap<String, Project>();
 
+	/** Projects indexed by their ID string, e.g. {@code technology.jgit}. */
+	private final Map<String, Project> consumedProjects = new TreeMap<String, Project>();
+
 	/** Known committers, indexed by their foundation ID. */
 	private final Map<String, Committer> committersById = new HashMap<String, Committer>();
 
@@ -134,6 +133,9 @@
 	/** The meta file we loaded to bootstrap our definitions. */
 	private IpLogMeta meta;
 
+	/** URL to obtain review information about a specific contribution. */
+	private String reviewUrl;
+
 	private String characterEncoding = "UTF-8";
 
 	private Repository db;
@@ -223,7 +225,11 @@
 			p.setVersion(version);
 			projects.put(p.getName(), p);
 		}
+		for (Project p : meta.getConsumedProjects()) {
+			consumedProjects.put(p.getName(), p);
+		}
 		cqs.addAll(meta.getCQs());
+		reviewUrl = meta.getReviewUrl();
 	}
 
 	private void loadCommitters(Repository repo) throws IOException {
@@ -352,20 +358,6 @@
 			String subj = commit.getShortMessage();
 			SingleContribution item = new SingleContribution(id, when, subj);
 
-			List<String> bugs = commit.getFooterLines(BUG);
-			if (1 == bugs.size()) {
-				item.setBugID(bugs.get(0));
-
-			} else if (2 <= bugs.size()) {
-				StringBuilder tmp = new StringBuilder();
-				for (String bug : bugs) {
-					if (tmp.length() > 0)
-						tmp.append(",");
-					tmp.append(bug);
-				}
-				item.setBugID(tmp.toString());
-			}
-
 			if (2 <= cnt) {
 				item.setSize("(merge)");
 				contributor.add(item);
@@ -480,8 +472,19 @@
 			root.appendChild(createProject(project));
 			licenses.addAll(project.getLicenses());
 		}
+
+		if (!consumedProjects.isEmpty())
+			appendBlankLine(root);
+		for (Project project : sort(consumedProjects, Project.COMPARATOR)) {
+			root.appendChild(createConsumes(project));
+			licenses.addAll(project.getLicenses());
+		}
+
 		for (RevCommit c : sort(commits))
 			root.appendChild(createCommitMeta(c));
+
+		if (licenses.size() > 1)
+			appendBlankLine(root);
 		for (String name : sort(licenses))
 			root.appendChild(createLicense(name));
 
@@ -509,11 +512,21 @@
 
 	private Element createProject(Project p) {
 		Element project = createElement("project");
+		populateProjectType(p, project);
+		return project;
+	}
+
+	private Element createConsumes(Project p) {
+		Element project = createElement("consumes");
+		populateProjectType(p, project);
+		return project;
+	}
+
+	private void populateProjectType(Project p, Element project) {
 		required(project, "id", p.getID());
 		required(project, "name", p.getName());
 		optional(project, "comments", p.getComments());
 		optional(project, "version", p.getVersion());
-		return project;
 	}
 
 	private Element createCommitMeta(RevCommit c) {
@@ -567,20 +580,15 @@
 	}
 
 	private Element createContribution(SingleContribution s) {
-		Element r = createElement("bug");
+		Element r = createElement("contribution");
 		required(r, "id", s.getID());
-		optional(r, "bug-id", s.getBugID());
+		required(r, "description", s.getSummary());
 		required(r, "size", s.getSize());
-		required(r, "type", "A"); // assume attachment type
-		required(r, "created", format(s.getCreated()));
-		required(r, "summary", s.getSummary());
+		if (reviewUrl != null)
+			optional(r, "url", reviewUrl + s.getID());
 		return r;
 	}
 
-	private String format(Date created) {
-		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(created);
-	}
-
 	private Element createElement(String name) {
 		return doc.createElementNS(IPLOG_NS, IPLOG_PFX + name);
 	}
diff --git a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogMeta.java b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogMeta.java
index 8d73b02..89695bd 100644
--- a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogMeta.java
+++ b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/IpLogMeta.java
@@ -73,8 +73,16 @@
 
 	private static final String S_CQ = "CQ";
 
+	private static final String S_CONSUMES = "consumes";
+
+	private static final String S_REVIEW = "review";
+
+	private static final String K_URL = "url";
+
 	private static final String K_NAME = "name";
 
+	private static final String K_VERSION = "version";
+
 	private static final String K_COMMENTS = "comments";
 
 	private static final String K_SKIP_COMMIT = "skipCommit";
@@ -89,31 +97,35 @@
 
 	private List<Project> projects = new ArrayList<Project>();
 
+	private List<Project> consumedProjects = new ArrayList<Project>();
+
 	private Set<CQ> cqs = new HashSet<CQ>();
 
+	private String reviewUrl;
+
 	List<Project> getProjects() {
 		return projects;
 	}
 
+	List<Project> getConsumedProjects() {
+		return consumedProjects;
+	}
+
 	Set<CQ> getCQs() {
 		return cqs;
 	}
 
+	String getReviewUrl() {
+		return reviewUrl;
+	}
+
 	void loadFrom(Config cfg) {
 		projects.clear();
+		consumedProjects.clear();
 		cqs.clear();
 
-		for (String id : cfg.getSubsections(S_PROJECT)) {
-			String name = cfg.getString(S_PROJECT, id, K_NAME);
-			Project project = new Project(id, name);
-			project.setComments(cfg.getString(S_PROJECT, id, K_COMMENTS));
-
-			for (String c : cfg.getStringList(S_PROJECT, id, K_SKIP_COMMIT))
-				project.addSkipCommit(ObjectId.fromString(c));
-			for (String license : cfg.getStringList(S_PROJECT, id, K_LICENSE))
-				project.addLicense(license);
-			projects.add(project);
-		}
+		projects.addAll(parseProjects(cfg, S_PROJECT));
+		consumedProjects.addAll(parseProjects(cfg, S_CONSUMES));
 
 		for (String id : cfg.getSubsections(S_CQ)) {
 			CQ cq = new CQ(Long.parseLong(id));
@@ -124,6 +136,26 @@
 			cq.setComments(cfg.getString(S_CQ, id, K_COMMENTS));
 			cqs.add(cq);
 		}
+
+		reviewUrl = cfg.getString(S_REVIEW, null, K_URL);
+	}
+
+	private List<Project> parseProjects(final Config cfg,
+			final String sectionName) {
+		final List<Project> dst = new ArrayList<Project>();
+		for (String id : cfg.getSubsections(sectionName)) {
+			String name = cfg.getString(sectionName, id, K_NAME);
+			Project project = new Project(id, name);
+			project.setVersion(cfg.getString(sectionName, id, K_VERSION));
+			project.setComments(cfg.getString(sectionName, id, K_COMMENTS));
+
+			for (String c : cfg.getStringList(sectionName, id, K_SKIP_COMMIT))
+				project.addSkipCommit(ObjectId.fromString(c));
+			for (String license : cfg.getStringList(sectionName, id, K_LICENSE))
+				project.addLicense(license);
+			dst.add(project);
+		}
+		return dst;
 	}
 
 	/**
diff --git a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/SingleContribution.java b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/SingleContribution.java
index 2cd5562..96f3def 100644
--- a/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/SingleContribution.java
+++ b/org.eclipse.jgit.iplog/src/org/eclipse/jgit/iplog/SingleContribution.java
@@ -61,8 +61,6 @@
 
 	private Date created;
 
-	private String bugId;
-
 	private String size;
 
 	/**
@@ -91,17 +89,6 @@
 		return summary;
 	}
 
-	/** @return Bugzilla bug id */
-	String getBugID() {
-		return bugId;
-	}
-
-	void setBugID(String id) {
-		if (id.startsWith("https://bugs.eclipse.org/"))
-			id = id.substring("https://bugs.eclipse.org/".length());
-		bugId = id;
-	}
-
 	String getSize() {
 		return size;
 	}
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
index edf31e9..d13c47d 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
@@ -8,6 +8,7 @@
 IPZillaPasswordPrompt=IPZilla Password
 authorInfo=Author: {0} <{1}>
 averageMSPerRead=average {0} ms/read
+branchDetachedHEAD=detached HEAD
 branchAlreadyExists=branch {0} already exists
 branchCreatedFrom =branch: Created from {0}
 branchIsNotAnAncestorOfYourCurrentHEAD=The branch '{0}' is not an ancestor of your current HEAD.\nIf you are sure you want to delete it, run 'jgit branch -D {0}'.
@@ -54,6 +55,7 @@
 metaVar_DAG=DAG
 metaVar_KEY=KEY
 metaVar_arg=ARG
+metaVar_author=AUTHOR
 metaVar_base=base
 metaVar_bucket=BUCKET
 metaVar_command=command
@@ -110,6 +112,8 @@
 timeInMilliSeconds={0} ms
 tooManyRefsGiven=Too many refs given
 unsupportedOperation=Unsupported operation: {0}
+usage_CommitAuthor=Override the author name used in the commit. You can use the standard A U Thor <author@example.com> format.
+usage_CommitMessage=Use the given <msg> as the commit message
 usage_CommandLineClientForamazonsS3Service=Command line client for Amazon's S3 service
 usage_CreateABareRepository=Create a bare repository
 usage_CreateATag=Create a tag
@@ -163,6 +167,7 @@
 usage_produceAnEclipseIPLog=Produce an Eclipse IP log
 usage_pruneStaleTrackingRefs=prune stale tracking refs
 usage_recurseIntoSubtrees=recurse into subtrees
+usage_recordChangesToRepository=Record changes to the repository
 usage_setTheGitRepositoryToOperateOn=set the git repository to operate on
 usage_showRefNamesMatchingCommits=Show ref names matching commits
 usage_symbolicVersionForTheProject=Symbolic version for the project
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java
index 8c811d4..bae895c 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java
@@ -63,6 +63,7 @@
 	/***/ public String averageMSPerRead;
 	/***/ public String branchAlreadyExists;
 	/***/ public String branchCreatedFrom;
+	/***/ public String branchDetachedHEAD;
 	/***/ public String branchIsNotAnAncestorOfYourCurrentHEAD;
 	/***/ public String branchNotFound;
 	/***/ public String cacheTreePathInfo;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
index 36e1a73..b26dde3 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
@@ -49,15 +49,15 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.kohsuke.args4j.Option;
 
-@Command(common = true, usage = "Record changes to the repository")
+@Command(common = true, usage = "usage_recordChangesToRepository")
 class Commit extends TextBuiltin {
 	// I don't support setting the committer, because also the native git
 	// command doesn't allow this.
 
-	@Option(name = "--author", metaVar="author", usage = "Override the author name used in the commit. You can use the standard A U Thor <author@example.com> format.")
+	@Option(name = "--author", metaVar="metaVar_author", usage = "usage_CommitAuthor")
 	private String author;
 
-	@Option(name = "--message", aliases = { "-m" }, metaVar="msg", usage="Use the given <msg> as the commit message", required=true)
+	@Option(name = "--message", aliases = { "-m" }, metaVar="metaVar_message", usage="usage_CommitMessage", required=true)
 	private String message;
 
 	@Override
@@ -73,7 +73,7 @@
 
 		String branchName;
 		if (!head.isSymbolic())
-			branchName="detached HEAD";
+			branchName = CLIText.get().branchDetachedHEAD;
 		else {
 			branchName = head.getTarget().getName();
 			if (branchName.startsWith(Constants.R_HEADS))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java
index 8f4e691..829832e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java
@@ -65,7 +65,6 @@
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.PackInvalidException;
 import org.eclipse.jgit.errors.PackMismatchException;
-import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.NB;
 import org.eclipse.jgit.util.RawParseUtils;
 
@@ -90,6 +89,9 @@
 
 	private RandomAccessFile fd;
 
+	/** Serializes reads performed against {@link #fd}. */
+	private final Object readLock = new Object();
+
 	long length;
 
 	private int activeWindows;
@@ -364,9 +366,11 @@
 		try {
 			if (invalid)
 				throw new PackInvalidException(packFile);
-			fd = new RandomAccessFile(packFile, "r");
-			length = fd.length();
-			onOpenPack();
+			synchronized (readLock) {
+				fd = new RandomAccessFile(packFile, "r");
+				length = fd.length();
+				onOpenPack();
+			}
 		} catch (IOException ioe) {
 			openFail();
 			throw ioe;
@@ -387,53 +391,61 @@
 	}
 
 	private void doClose() {
-		if (fd != null) {
-			try {
-				fd.close();
-			} catch (IOException err) {
-				// Ignore a close event. We had it open only for reading.
-				// There should not be errors related to network buffers
-				// not flushed, etc.
+		synchronized (readLock) {
+			if (fd != null) {
+				try {
+					fd.close();
+				} catch (IOException err) {
+					// Ignore a close event. We had it open only for reading.
+					// There should not be errors related to network buffers
+					// not flushed, etc.
+				}
+				fd = null;
 			}
-			fd = null;
 		}
 	}
 
 	ByteArrayWindow read(final long pos, int size) throws IOException {
-		if (length < pos + size)
-			size = (int) (length - pos);
-		final byte[] buf = new byte[size];
-		IO.readFully(fd.getChannel(), pos, buf, 0, size);
-		return new ByteArrayWindow(this, pos, buf);
+		synchronized (readLock) {
+			if (length < pos + size)
+				size = (int) (length - pos);
+			final byte[] buf = new byte[size];
+			fd.seek(pos);
+			fd.readFully(buf, 0, size);
+			return new ByteArrayWindow(this, pos, buf);
+		}
 	}
 
 	ByteWindow mmap(final long pos, int size) throws IOException {
-		if (length < pos + size)
-			size = (int) (length - pos);
+		synchronized (readLock) {
+			if (length < pos + size)
+				size = (int) (length - pos);
 
-		MappedByteBuffer map;
-		try {
-			map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
-		} catch (IOException ioe1) {
-			// The most likely reason this failed is the JVM has run out
-			// of virtual memory. We need to discard quickly, and try to
-			// force the GC to finalize and release any existing mappings.
-			//
-			System.gc();
-			System.runFinalization();
-			map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
+			MappedByteBuffer map;
+			try {
+				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
+			} catch (IOException ioe1) {
+				// The most likely reason this failed is the JVM has run out
+				// of virtual memory. We need to discard quickly, and try to
+				// force the GC to finalize and release any existing mappings.
+				//
+				System.gc();
+				System.runFinalization();
+				map = fd.getChannel().map(MapMode.READ_ONLY, pos, size);
+			}
+
+			if (map.hasArray())
+				return new ByteArrayWindow(this, pos, map.array());
+			return new ByteBufferWindow(this, pos, map);
 		}
-
-		if (map.hasArray())
-			return new ByteArrayWindow(this, pos, map.array());
-		return new ByteBufferWindow(this, pos, map);
 	}
 
 	private void onOpenPack() throws IOException {
 		final PackIndex idx = idx();
 		final byte[] buf = new byte[20];
 
-		IO.readFully(fd.getChannel(), 0, buf, 0, 12);
+		fd.seek(0);
+		fd.readFully(buf, 0, 12);
 		if (RawParseUtils.match(buf, 0, Constants.PACK_SIGNATURE) != 4)
 			throw new IOException(JGitText.get().notAPACKFile);
 		final long vers = NB.decodeUInt32(buf, 4);
@@ -445,7 +457,8 @@
 			throw new PackMismatchException(MessageFormat.format(
 					JGitText.get().packObjectCountMismatch, packCnt, idx.getObjectCount(), getPackFile()));
 
-		IO.readFully(fd.getChannel(), length - 20, buf, 0, 20);
+		fd.seek(length - 20);
+		fd.read(buf, 0, 20);
 		if (!Arrays.equals(buf, packChecksum))
 			throw new PackMismatchException(MessageFormat.format(
 					JGitText.get().packObjectCountMismatch
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
index 1778654..1f2042d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
@@ -51,8 +51,6 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
 import java.text.MessageFormat;
 
 import org.eclipse.jgit.JGitText;
@@ -139,36 +137,6 @@
 	}
 
 	/**
-	 * Read the entire byte array into memory, or throw an exception.
-	 *
-	 * @param fd
-	 *            file to read the data from.
-	 * @param pos
-	 *            position to read from the file at.
-	 * @param dst
-	 *            buffer that must be fully populated, [off, off+len).
-	 * @param off
-	 *            position within the buffer to start writing to.
-	 * @param len
-	 *            number of bytes that must be read.
-	 * @throws EOFException
-	 *             the stream ended before dst was fully populated.
-	 * @throws IOException
-	 *             there was an error reading from the stream.
-	 */
-	public static void readFully(final FileChannel fd, long pos,
-			final byte[] dst, int off, int len) throws IOException {
-		while (len > 0) {
-			final int r = fd.read(ByteBuffer.wrap(dst, off, len), pos);
-			if (r <= 0)
-				throw new EOFException(JGitText.get().shortReadOfBlock);
-			pos += r;
-			off += r;
-			len -= r;
-		}
-	}
-
-	/**
 	 * Skip an entire region of an input stream.
 	 * <p>
 	 * The input stream's position is moved forward by the number of requested