Bug 510062 - Git log graph is broken on Node

For a given file's history, the parent commit should be pointing at
the previous commit that modified the file and not the parent commit
in the repository's history.

To achieve this, for a given commit that is being filtered, we
examine the list of returned commits and see if any of its parents
are pointing at that filtered commit. If it is, then that commit will
point to the filtered commit's parent instead. This allows the chain
to be preserved as the returned commit will keep pointing at a
filtered commit's parent until it is a commit that hasn't been
filtered.

Signed-off-by: Remy Suen <remy.suen@gmail.com>
diff --git a/modules/orionode/lib/git/commit.js b/modules/orionode/lib/git/commit.js
index 646a665..ad1f943 100644
--- a/modules/orionode/lib/git/commit.js
+++ b/modules/orionode/lib/git/commit.js
@@ -286,6 +286,7 @@
 		});
 
 		var count = 0;
+		var commitJSONs = {};
 		filterPath = clone.getfileRelativePath(repo,req); 
 		function walk() {
 			return revWalk.next()
@@ -298,11 +299,22 @@
 				.then(function(commit) {
 					function applyFilter(filter) {
 						if (filter || filterCommit(commit) || page && count++ < skipCount) {//skip pages
+							var keys = Object.keys(commitJSONs);
+							for (var i = 0 ; i < keys.length; i++) {
+								for (var j = 0; j < commitJSONs[keys[i]].Parents.length; j++) {
+									if (commitJSONs[keys[i]].Parents[j].Name === commit.sha() && commit.parentcount() !== 0) {
+										commitJSONs[keys[i]].Parents[j] = createParentJSON(commit.parentId(0).toString(), fileDir);
+										return walk();
+									}
+								}
+							}
 							return walk();
 						}
 						return Promise.all([getDiff(repo, commit, fileDir), getCommitParents(repo, commit, fileDir)])
 						.then(function(stuff) {
-							commits.push(commitJSON(commit, fileDir, stuff[0], stuff[1]));
+							var json = commitJSON(commit, fileDir, stuff[0], stuff[1]);
+							commitJSONs[commit.sha()] = json;
+							commits.push(json);
 							if (pageSize && commits.length === pageSize) {//page done
 								sendResponse();
 								return;
@@ -406,14 +418,18 @@
 	return commit.getParents()
 	.then(function(parents) {
 		return parents.map(function(parent) {
-			return {
-				"Location": gitRoot + "/commit/" + parent.sha() + fileDir,
-				"Name": parent.sha()
-			};
+			return createParentJSON(parent.sha(), fileDir);
 		});
 	});
 }
 
+function createParentJSON(sha, fileDir) {
+	return {
+		"Location": gitRoot + "/commit/" + sha + fileDir,
+		"Name": sha
+	};
+}
+
 function getCommitRefs(repo, fileDir, commits) {
 	return new Promise(function (fulfill){
 		if (!commits.length) return fulfill();
diff --git a/modules/orionode/test/test-git-api.js b/modules/orionode/test/test-git-api.js
index d562c53..69a7b63 100644
--- a/modules/orionode/test/test-git-api.js
+++ b/modules/orionode/test/test-git-api.js
@@ -182,7 +182,7 @@
 			request()
 			.post(CONTEXT_PATH + "/gitapi/commit/HEAD/file/" + client.getName())
 			.send({
-				Message: "Test commit!",
+				Message: "Test commit at " + Date.now(),
 				AuthorName: "test",
 				AuthorEmail: "test@test.com",
 				CommitterName: "test",
@@ -2316,6 +2316,217 @@
 					finished(err);
 				});
 			});
+
+			it("file gap", function(finished) {
+				var first, second, third;
+				var name = "test.txt";
+
+				var client = new GitClient("history-graph-file-gap");
+				client.init();
+				client.setFileContents(name, "1");
+				client.stage(name);
+				client.commit();
+				client.start().then(function(commit) {
+					first = commit.Id;
+
+					client.commit();
+					client.setFileContents(name, "2");
+					client.stage(name);
+					client.commit();
+					return client.start();
+				})
+				.then(function(commit) {
+					second = commit.Id;
+
+					client.commit();
+					client.setFileContents(name, "3");
+					client.stage(name);
+					client.commit();
+					return client.start();
+				})
+				.then(function(commit) {
+					third = commit.Id;
+
+					client.commit();
+					client.log("master", "master", name);
+					return client.start();
+				})
+				.then(function(log) {
+					assert.equal(log.Children.length, 3);
+					assert.equal(log.Children[0].Name, third);
+					assert.equal(log.Children[0].Parents.length, 1);
+					assert.equal(log.Children[0].Parents[0].Name, second);
+					assert.equal(log.Children[1].Name, second);
+					assert.equal(log.Children[1].Parents.length, 1);
+					assert.equal(log.Children[1].Parents[0].Name, first);
+					assert.equal(log.Children[2].Name, first);
+					assert.equal(log.Children[2].Parents.length, 1);
+					finished();
+				})
+				.catch(function(err) {
+					finished(err);
+				});
+			});
+
+			/**
+			 * Confirm the parent history information of the following graph.
+			 * Commit U is a commit that doesn't modify the file. The merge
+			 * commit M also doesn't modify anything. The returned history
+			 * should only consist of commits A and B.
+			 * 
+			 * Actual repository history:
+			 * 
+			 * M
+			 * |\
+			 * | \
+			 * |  \
+			 * U   B
+			 * |  /
+			 * | /
+			 * |/
+			 * A
+			 * 
+			 * Returned history for the file:
+			 * 
+			 * B
+			 * |
+			 * A
+			 */
+			it("file merge unchanged branch", function(finished) {
+				var commitA, commitB, local;
+				var name = "test.txt";
+
+				var client = new GitClient("history-graph-file-merge-unchnaged-branch");
+				client.init();
+				// create the file at commit A
+				client.setFileContents(name, "A");
+				client.stage(name);
+				client.commit();
+				client.start().then(function(commit) {
+					commitA = commit.Id;
+
+					// make the extra commit U
+					client.commit();
+					client.createBranch("other");
+					// reset
+					client.reset("HARD", commitA);
+					// create commit B
+					client.setFileContents(name, "B");
+					client.stage(name);
+					client.commit();
+					return client.start();
+				})
+				.then(function(commit) {
+					commitB = commit.Id;
+
+					// merge the unrelated branch
+					client.merge("other");
+					client.log("master", "master", name);
+					return client.start();
+				})
+				.then(function(log) {
+					assert.equal(log.Children.length, 2);
+					assert.equal(log.Children[0].Id, commitB);
+					assert.equal(log.Children[0].Parents.length, 1);
+					assert.equal(log.Children[0].Parents[0].Name, commitA);
+					assert.equal(log.Children[1].Id, commitA);
+					finished();
+				})
+				.catch(function(err) {
+					finished(err);
+				});
+			});
+
+			/**
+			 * Confirm the parent history information of the following graph.
+			 * We want to make sure that the extraneous commits A, B, C, and D
+			 * don't affect the returned history information of the modified file.
+			 * 
+			 * Actual repository history:
+			 * 
+			 * O
+			 * |\
+			 * | \
+			 * |  \
+			 * B   D
+			 * |   |
+			 * O   O
+			 * |  /
+			 * A C
+			 * |/
+			 * O
+			 * 
+			 * Returned history for the file:
+			 * 
+			 * O
+			 * |\
+			 * | \
+			 * |  \
+			 * O   O
+			 * |  /
+			 * | /
+			 * |/
+			 * O
+			 */
+			it("file merge gaps", function(finished) {
+				var initial, other, local;
+				var name = "test.txt";
+
+				var client = new GitClient("history-graph-file-merge-gaps");
+				client.init();
+				client.setFileContents(name, "1\n2\n3");
+				client.stage(name);
+				client.commit();
+				client.start().then(function(commit) {
+					initial = commit.Id;
+
+					// make the extra commit A
+					client.commit();
+					client.setFileContents(name, "1a\n2\n3");
+					client.stage(name);
+					client.commit();
+					return client.start();
+				})
+				.then(function(commit) {
+					other = commit.Id;
+					// make the extra commit B
+					client.commit();
+
+					client.createBranch("other");
+					client.reset("HARD", initial);
+					// make the extra commit C
+					client.commit();
+					client.setFileContents(name, "1\n2\n3a");
+					client.stage(name);
+					client.commit();
+					return client.start();
+				})
+				.then(function(commit) {
+					local = commit.Id;
+
+					// make the extra commit D
+					client.commit();
+					client.merge("other");
+					client.log("master", "master", name);
+					return client.start();
+				})
+				.then(function(log) {
+					assert.equal(log.Children.length, 4);
+					// merge commit with two parents
+					assert.equal(log.Children[0].Parents.length, 2);
+					assert.equal(log.Children[0].Parents[0].Name, local);
+					assert.equal(log.Children[0].Parents[1].Name, other);
+					assert.equal(log.Children[1].Id, other);
+					assert.equal(log.Children[1].Parents[0].Name, initial);
+					assert.equal(log.Children[2].Id, local);
+					assert.equal(log.Children[2].Parents[0].Name, initial);
+					assert.equal(log.Children[3].Id, initial);
+					finished();
+				})
+				.catch(function(err) {
+					finished(err);
+				});
+			});
 		}) // describe("Graph");
 	}); // describe("Log")