Support gitdir references in working tree .git file

A '.git' file in a repository's working tree root is now parsed
as a ref to a folder located elsewhere.  This supports submodules
having their repository location outside of the parent repository's
working directory such as in the parent repository's '.git/modules'
directory.

This adds support to BaseRepositoryBuilder for repositories created
with the '--separate-git-dir' option specified to 'git init'.

Change-Id: I73c538f6d845bdbc0c4e2bce5a77f900cf36e1a9
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
index c4bf33b..89843fc 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
@@ -44,10 +44,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.io.FileWriter;
 import java.io.IOException;
 
 import org.eclipse.jgit.dircache.DirCache;
@@ -58,7 +60,9 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.junit.Test;
 
@@ -97,8 +101,6 @@ public void apply(DirCacheEntry ent) {
 		assertEquals(path, gen.getPath());
 		assertEquals(id, gen.getObjectId());
 		assertEquals(new File(db.getWorkTree(), path), gen.getDirectory());
-		assertEquals(new File(db.getWorkTree(), path + File.separatorChar
-				+ Constants.DOT_GIT), gen.getGitDirectory());
 		assertNull(gen.getConfigUpdate());
 		assertNull(gen.getConfigUrl());
 		assertNull(gen.getModulesPath());
@@ -109,6 +111,101 @@ public void apply(DirCacheEntry ent) {
 	}
 
 	@Test
+	public void repositoryWithRootLevelSubmoduleAbsoluteRef()
+			throws IOException, ConfigInvalidException {
+		final ObjectId id = ObjectId
+				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+		final String path = "sub";
+		File dotGit = new File(db.getWorkTree(), path + File.separatorChar
+				+ Constants.DOT_GIT);
+		if (!dotGit.getParentFile().exists())
+			dotGit.getParentFile().mkdirs();
+
+		File modulesGitDir = new File(db.getDirectory(), "modules"
+				+ File.separatorChar + path);
+		new FileWriter(dotGit).append(
+				"gitdir: " + modulesGitDir.getAbsolutePath()).close();
+		FileRepositoryBuilder builder = new FileRepositoryBuilder();
+		builder.setWorkTree(new File(db.getWorkTree(), path));
+		builder.build().create();
+
+		DirCache cache = db.lockDirCache();
+		DirCacheEditor editor = cache.editor();
+		editor.add(new PathEdit(path) {
+
+			public void apply(DirCacheEntry ent) {
+				ent.setFileMode(FileMode.GITLINK);
+				ent.setObjectId(id);
+			}
+		});
+		editor.commit();
+
+		SubmoduleWalk gen = SubmoduleWalk.forIndex(db);
+		assertTrue(gen.next());
+		assertEquals(path, gen.getPath());
+		assertEquals(id, gen.getObjectId());
+		assertEquals(new File(db.getWorkTree(), path), gen.getDirectory());
+		assertNull(gen.getConfigUpdate());
+		assertNull(gen.getConfigUrl());
+		assertNull(gen.getModulesPath());
+		assertNull(gen.getModulesUpdate());
+		assertNull(gen.getModulesUrl());
+		Repository subRepo = gen.getRepository();
+		assertNotNull(subRepo);
+		assertEquals(modulesGitDir, subRepo.getDirectory());
+		assertEquals(new File(db.getWorkTree(), path), subRepo.getWorkTree());
+		assertFalse(gen.next());
+	}
+
+	@Test
+	public void repositoryWithRootLevelSubmoduleRelativeRef()
+			throws IOException, ConfigInvalidException {
+		final ObjectId id = ObjectId
+				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
+		final String path = "sub";
+		File dotGit = new File(db.getWorkTree(), path + File.separatorChar
+				+ Constants.DOT_GIT);
+		if (!dotGit.getParentFile().exists())
+			dotGit.getParentFile().mkdirs();
+
+		File modulesGitDir = new File(db.getDirectory(), "modules"
+				+ File.separatorChar + path);
+		new FileWriter(dotGit).append(
+				"gitdir: " + "../" + Constants.DOT_GIT + "/modules/" + path)
+				.close();
+		FileRepositoryBuilder builder = new FileRepositoryBuilder();
+		builder.setWorkTree(new File(db.getWorkTree(), path));
+		builder.build().create();
+
+		DirCache cache = db.lockDirCache();
+		DirCacheEditor editor = cache.editor();
+		editor.add(new PathEdit(path) {
+
+			public void apply(DirCacheEntry ent) {
+				ent.setFileMode(FileMode.GITLINK);
+				ent.setObjectId(id);
+			}
+		});
+		editor.commit();
+
+		SubmoduleWalk gen = SubmoduleWalk.forIndex(db);
+		assertTrue(gen.next());
+		assertEquals(path, gen.getPath());
+		assertEquals(id, gen.getObjectId());
+		assertEquals(new File(db.getWorkTree(), path), gen.getDirectory());
+		assertNull(gen.getConfigUpdate());
+		assertNull(gen.getConfigUrl());
+		assertNull(gen.getModulesPath());
+		assertNull(gen.getModulesUpdate());
+		assertNull(gen.getModulesUrl());
+		Repository subRepo = gen.getRepository();
+		assertNotNull(subRepo);
+		assertEquals(modulesGitDir, subRepo.getDirectory());
+		assertEquals(new File(db.getWorkTree(), path), subRepo.getWorkTree());
+		assertFalse(gen.next());
+	}
+
+	@Test
 	public void repositoryWithNestedSubmodule() throws IOException,
 			ConfigInvalidException {
 		final ObjectId id = ObjectId
@@ -130,8 +227,6 @@ public void apply(DirCacheEntry ent) {
 		assertEquals(path, gen.getPath());
 		assertEquals(id, gen.getObjectId());
 		assertEquals(new File(db.getWorkTree(), path), gen.getDirectory());
-		assertEquals(new File(db.getWorkTree(), path + File.separatorChar
-				+ Constants.DOT_GIT), gen.getGitDirectory());
 		assertNull(gen.getConfigUpdate());
 		assertNull(gen.getConfigUrl());
 		assertNull(gen.getModulesPath());
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
index 5b22801..8a5e023 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
@@ -234,6 +234,7 @@
 invalidCharacterInBase64Data=Invalid character in Base64 data.
 invalidCommitParentNumber=Invalid commit parent number
 invalidEncryption=Invalid encryption
+invalidGitdirRef = Invalid .git reference in file ''{0}''
 invalidGitType=invalid git type: {0}
 invalidId=Invalid id {0}
 invalidIdLength=Invalid id length {0}; should be {1}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
index dbc3bca..bcd14c6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
@@ -294,6 +294,7 @@ public static JGitText get() {
 	/***/ public String invalidCharacterInBase64Data;
 	/***/ public String invalidCommitParentNumber;
 	/***/ public String invalidEncryption;
+	/***/ public String invalidGitdirRef;
 	/***/ public String invalidGitType;
 	/***/ public String invalidId;
 	/***/ public String invalidIdLength;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index 22a2ae5..7db0b9f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -69,6 +69,8 @@
 import org.eclipse.jgit.storage.file.FileRepository;
 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
 import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.SystemReader;
 
 /**
@@ -85,6 +87,19 @@
  * @see FileRepositoryBuilder
  */
 public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Repository> {
+	private static boolean isSymRef(byte[] ref) {
+		if (ref.length < 9)
+			return false;
+		return /**/ref[0] == 'g' //
+				&& ref[1] == 'i' //
+				&& ref[2] == 't' //
+				&& ref[3] == 'd' //
+				&& ref[4] == 'i' //
+				&& ref[5] == 'r' //
+				&& ref[6] == ':' //
+				&& ref[7] == ' ';
+	}
+
 	private FS fs;
 
 	private File gitDir;
@@ -546,10 +561,37 @@ protected void requireGitDirOrWorkTree() {
 	 *             the repository could not be accessed
 	 */
 	protected void setupGitDir() throws IOException {
-		// No gitDir? Try to assume its under the workTree.
-		//
-		if (getGitDir() == null && getWorkTree() != null)
-			setGitDir(new File(getWorkTree(), DOT_GIT));
+		// No gitDir? Try to assume its under the workTree or a ref to another
+		// location
+		if (getGitDir() == null && getWorkTree() != null) {
+			File dotGit = new File(getWorkTree(), DOT_GIT);
+			if (!dotGit.isFile())
+				setGitDir(dotGit);
+			else {
+				byte[] content = IO.readFully(dotGit);
+				if (!isSymRef(content))
+					throw new IOException(MessageFormat.format(
+							JGitText.get().invalidGitdirRef,
+							dotGit.getAbsolutePath()));
+				int pathStart = 8;
+				int lineEnd = RawParseUtils.nextLF(content, pathStart);
+				if (content[lineEnd - 1] == '\n')
+					lineEnd--;
+				if (lineEnd == pathStart)
+					throw new IOException(MessageFormat.format(
+							JGitText.get().invalidGitdirRef,
+							dotGit.getAbsolutePath()));
+
+				String gitdirPath = RawParseUtils.decode(content, pathStart,
+						lineEnd);
+				File gitdirFile = new File(gitdirPath);
+				if (gitdirFile.isAbsolute())
+					setGitDir(gitdirFile);
+				else
+					setGitDir(new File(getWorkTree(), gitdirPath)
+							.getCanonicalFile());
+			}
+		}
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
index 78896f8..f07f4d6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
@@ -155,27 +155,32 @@ public static File getSubmoduleDirectory(final Repository parent,
 	 */
 	public static Repository getSubmoduleRepository(final Repository parent,
 			final String path) throws IOException {
-		File directory = getSubmoduleGitDirectory(parent, path);
-		if (!directory.isDirectory())
-			return null;
-		try {
-			return new RepositoryBuilder().setMustExist(true)
-					.setFS(FS.DETECTED).setGitDir(directory).build();
-		} catch (RepositoryNotFoundException e) {
-			return null;
-		}
+		return getSubmoduleRepository(parent.getWorkTree(), path);
 	}
 
 	/**
-	 * Get the .git directory for a repository submodule path
+	 * Get submodule repository at path
 	 *
 	 * @param parent
 	 * @param path
-	 * @return .git for submodule repository
+	 * @return repository or null if repository doesn't exist
+	 * @throws IOException
 	 */
-	public static File getSubmoduleGitDirectory(final Repository parent,
-			final String path) {
-		return new File(getSubmoduleDirectory(parent, path), Constants.DOT_GIT);
+	public static Repository getSubmoduleRepository(final File parent,
+			final String path) throws IOException {
+		File subWorkTree = new File(parent, path);
+		if (!subWorkTree.isDirectory())
+			return null;
+		File workTree = new File(parent, path);
+		try {
+			return new RepositoryBuilder() //
+					.setMustExist(true) //
+					.setFS(FS.DETECTED) //
+					.setWorkTree(workTree) //
+					.build();
+		} catch (RepositoryNotFoundException e) {
+			return null;
+		}
 	}
 
 	/**
@@ -349,15 +354,6 @@ public File getDirectory() {
 	}
 
 	/**
-	 * Get the .git directory for the current submodule entry
-	 *
-	 * @return .git for submodule repository
-	 */
-	public File getGitDirectory() {
-		return getSubmoduleGitDirectory(repository, path);
-	}
-
-	/**
 	 * Advance to next submodule in the index tree.
 	 *
 	 * The object id and path of the next entry can be obtained by calling
@@ -467,19 +463,8 @@ public String getModulesUpdate() throws IOException, ConfigInvalidException {
 	}
 
 	/**
-	 * Does the current submodule entry have a .git directory in the parent
-	 * repository's working tree?
-	 *
-	 * @return true if .git directory exists, false otherwise
-	 */
-	public boolean hasGitDirectory() {
-		return getGitDirectory().isDirectory();
-	}
-
-	/**
 	 * Get repository for current submodule entry
 	 *
-	 * @see #hasGitDirectory()
 	 * @return repository or null if non-existent
 	 * @throws IOException
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
index c7ae775..315d909 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
@@ -159,7 +159,7 @@ static public class FileEntry extends Entry {
 			file = f;
 
 			if (f.isDirectory()) {
-				if (new File(f, Constants.DOT_GIT).isDirectory())
+				if (new File(f, Constants.DOT_GIT).exists())
 					mode = FileMode.GITLINK;
 				else
 					mode = FileMode.TREE;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index fa57b71..8940399 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -76,7 +76,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryBuilder;
+import org.eclipse.jgit.submodule.SubmoduleWalk;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.io.EolCanonicalizingInputStream;
@@ -280,18 +280,16 @@ public boolean hasId() {
 	 * @return non-null submodule id
 	 */
 	protected byte[] idSubmodule(File directory, Entry e) {
-		final String gitDirPath = e.getName() + "/" + Constants.DOT_GIT;
-		final File submoduleGitDir = new File(directory, gitDirPath);
-		if (!submoduleGitDir.isDirectory())
-			return zeroid;
 		final Repository submoduleRepo;
 		try {
-			FS fs = repository != null ? repository.getFS() : FS.DETECTED;
-			submoduleRepo = new RepositoryBuilder().setGitDir(submoduleGitDir)
-					.setMustExist(true).setFS(fs).build();
+			submoduleRepo = SubmoduleWalk.getSubmoduleRepository(directory,
+					e.getName());
 		} catch (IOException exception) {
 			return zeroid;
 		}
+		if (submoduleRepo == null)
+			return zeroid;
+
 		final ObjectId head;
 		try {
 			head = submoduleRepo.resolve(Constants.HEAD);