From 69a1dc1434012dcd6823e4f49ce8322861cece54 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Sat, 30 Jan 2016 16:13:18 +0300 Subject: [PATCH] IDEA-143404 Support git worktree Use GitRepositoryFiles to identify and store actual paths for .git/refs, .git/HEAD, etc. --- .../src/git4idea/repo/GitRepositoryFiles.java | 92 ++++++++++++---- .../src/git4idea/repo/GitRepositoryImpl.java | 2 +- .../git4idea/repo/GitRepositoryReader.java | 32 +++--- .../git4idea/repo/GitRepositoryUpdater.java | 8 +- .../tests/git4idea/repo/GitConfigTest.java | 4 +- .../repo/GitRepositoryReaderNewTest.java | 5 +- .../repo/GitRepositoryReaderTest.java | 3 +- .../tests/git4idea/repo/GitWorkTreeTest.kt | 101 ++++++++++++++++++ .../tests/git4idea/test/GitTestUtil.java | 2 +- 9 files changed, 204 insertions(+), 45 deletions(-) create mode 100644 plugins/git4idea/tests/git4idea/repo/GitWorkTreeTest.kt diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java index f6da0f99053b..9fa49dadeecf 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryFiles.java @@ -19,10 +19,13 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VirtualFile; import git4idea.util.GitFileUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.io.File; import java.util.Arrays; import java.util.Collection; +import static com.intellij.psi.impl.SyntheticFileSystemItem.LOG; import static git4idea.GitUtil.DOT_GIT; /** @@ -45,18 +48,16 @@ public class GitRepositoryFiles { public static final String REBASE_APPLY = "rebase-apply"; public static final String REBASE_MERGE = "rebase-merge"; public static final String PACKED_REFS = "packed-refs"; - public static final String REFS_HEADS = "refs/heads"; - public static final String REFS_REMOTES = "refs/remotes"; - public static final String REFS_TAGS = "refs/tags"; + public static final String REFS = "refs"; + public static final String HEADS = "heads"; + public static final String TAGS = "tags"; + public static final String REMOTES = "remotes"; public static final String SQUASH_MSG = "SQUASH_MSG"; public static final String GIT_HEAD = DOT_GIT + slash(HEAD); - public static final String GIT_REFS_REMOTES = DOT_GIT + slash(REFS_REMOTES); - public static final String GIT_PACKED_REFS = DOT_GIT + slash(PACKED_REFS); public static final String GIT_MERGE_HEAD = DOT_GIT + slash(MERGE_HEAD); public static final String GIT_MERGE_MSG = DOT_GIT + slash(MERGE_MSG); public static final String GIT_SQUASH_MSG = DOT_GIT + slash(SQUASH_MSG); - public static final String GIT_COMMIT_EDITMSG = DOT_GIT + slash(COMMIT_EDITMSG); private final String myGitDirPath; private final String myConfigFilePath; @@ -71,32 +72,64 @@ public class GitRepositoryFiles { private final String myRefsRemotesDirPath; private final String myRefsTagsPath; private final String myCommitMessagePath; + private final String myInfoDirPath; private final String myExcludePath; - public static GitRepositoryFiles getInstance(@NotNull VirtualFile gitDir) { - // maybe will be cached later to store a single GitRepositoryFiles for a root. - return new GitRepositoryFiles(gitDir); - } - - private GitRepositoryFiles(@NotNull VirtualFile gitDir) { - // add .git/ and .git/refs/heads to the VFS - // save paths of the files, that we will watch + private GitRepositoryFiles(@NotNull VirtualFile gitDir, @NotNull File headFile, @NotNull File refsDir, @NotNull File packedRefsFile) { myGitDirPath = GitFileUtils.stripFileProtocolPrefix(gitDir.getPath()); myConfigFilePath = myGitDirPath + slash(CONFIG); - myHeadFilePath = myGitDirPath + slash(HEAD); + myHeadFilePath = FileUtil.toSystemIndependentName(headFile.getPath()); myIndexFilePath = myGitDirPath + slash(INDEX); myMergeHeadPath = myGitDirPath + slash(MERGE_HEAD); myOrigHeadPath = myGitDirPath + slash(ORIG_HEAD); myCommitMessagePath = myGitDirPath + slash(COMMIT_EDITMSG); myRebaseApplyPath = myGitDirPath + slash(REBASE_APPLY); myRebaseMergePath = myGitDirPath + slash(REBASE_MERGE); - myPackedRefsPath = myGitDirPath + slash(PACKED_REFS); - myRefsHeadsDirPath = myGitDirPath + slash(REFS_HEADS); - myRefsTagsPath = myGitDirPath + slash(REFS_TAGS); - myRefsRemotesDirPath = myGitDirPath + slash(REFS_REMOTES); + myPackedRefsPath = FileUtil.toSystemIndependentName(packedRefsFile.getPath()); + String refsPath = FileUtil.toSystemIndependentName(refsDir.getPath()); + myRefsHeadsDirPath = refsPath + slash(HEADS); + myRefsTagsPath = refsPath + slash(TAGS); + myRefsRemotesDirPath = refsPath + slash(REMOTES); + myInfoDirPath = myGitDirPath + slash(INFO); myExcludePath = myGitDirPath + slash(INFO_EXCLUDE); } + @NotNull + public static GitRepositoryFiles getInstance(@NotNull VirtualFile gitDir) { + VirtualFile gitDirForWorktree = getMainGitDirForWorktree(gitDir); + File headFile = new File(gitDir.getPath(), HEAD); + File refsDir; + File packedRefsFile; + if (gitDirForWorktree == null) { + refsDir = new File(gitDir.getPath(), REFS); + packedRefsFile = new File(gitDir.getPath(), PACKED_REFS); + } + else { + refsDir = new File(gitDirForWorktree.getPath(), REFS); + packedRefsFile = new File(gitDirForWorktree.getPath(), PACKED_REFS); + } + return new GitRepositoryFiles(gitDir, headFile, refsDir, packedRefsFile); + } + + /** + * Checks if the given .git directory is actually a worktree's git directory, and returns the main .git directory if it is true. + * If it is not a worktree, returns null. + *

+ * Worktree's ".git" file references {@code /.git/worktrees/} + */ + @Nullable + private static VirtualFile getMainGitDirForWorktree(@NotNull VirtualFile gitDir) { + VirtualFile parent = gitDir.getParent(); + if (parent == null) return null; + VirtualFile grandParent = parent.getParent(); + if (grandParent == null) return null; + if (!gitDir.getName().equals(DOT_GIT) && parent.getName().equals("worktrees") && grandParent.getName().equals(DOT_GIT)) { + LOG.info("git dir " + gitDir.getPath() + " is a worktree"); + return grandParent; + } + return null; + } + @NotNull private static String slash(@NotNull String s) { return "/" + s; @@ -106,10 +139,15 @@ public class GitRepositoryFiles { * Returns subdirectories of .git which we are interested in - they should be watched by VFS. */ @NotNull - static Collection getSubDirRelativePaths() { - return Arrays.asList(slash(REFS_HEADS), slash(REFS_REMOTES), slash(REFS_TAGS), slash(INFO)); + Collection getDirsToWatch() { + return Arrays.asList(myRefsHeadsDirPath, myRefsRemotesDirPath, myRefsTagsPath, myInfoDirPath); } - + + @NotNull + String getGitDirPath() { + return myGitDirPath; + } + @NotNull String getRefsHeadsPath() { return myRefsHeadsDirPath; @@ -125,6 +163,16 @@ public class GitRepositoryFiles { return myRefsTagsPath; } + @NotNull + public String getPackedRefsPath() { + return myPackedRefsPath; + } + + @NotNull + public String getHeadPath() { + return myHeadFilePath; + } + /** * {@code .git/config} */ diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java index ea72f96a1824..1ea380ee8a83 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java @@ -57,7 +57,7 @@ public class GitRepositoryImpl extends RepositoryImpl implements GitRepository { myPlatformFacade = facade; myVcs = assertNotNull(GitVcs.getInstance(project)); myGitDir = gitDir; - myReader = new GitRepositoryReader(VfsUtilCore.virtualToIoFile(myGitDir)); + myReader = new GitRepositoryReader(myGitDir); myInfo = readRepoInfo(); if (!light) { myUntrackedFilesHolder = new GitUntrackedFilesHolder(this); diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java index 3fbcd6d9bf76..cb00cb66e907 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java @@ -23,6 +23,7 @@ import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.LineTokenizer; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Function; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; @@ -67,14 +68,15 @@ class GitRepositoryReader { @NotNull private final File myRefsRemotesDir; // .git/refs/remotes/ @NotNull private final File myPackedRefsFile; // .git/packed-refs - GitRepositoryReader(@NotNull File gitDir) { - myGitDir = gitDir; - DvcsUtil.assertFileExists(myGitDir, ".git directory not found in " + gitDir); - myHeadFile = new File(myGitDir, "HEAD"); - DvcsUtil.assertFileExists(myHeadFile, ".git/HEAD file not found in " + gitDir); - myRefsHeadsDir = new File(new File(myGitDir, "refs"), "heads"); - myRefsRemotesDir = new File(new File(myGitDir, "refs"), "remotes"); - myPackedRefsFile = new File(myGitDir, "packed-refs"); + GitRepositoryReader(@NotNull VirtualFile gitDir) { + GitRepositoryFiles gitFiles = GitRepositoryFiles.getInstance(gitDir); + myGitDir = new File(FileUtil.toSystemDependentName(gitFiles.getGitDirPath())); + DvcsUtil.assertFileExists(myGitDir, ".git directory not found in " + myGitDir); + myHeadFile = new File(FileUtil.toSystemDependentName(gitFiles.getHeadPath())); + DvcsUtil.assertFileExists(myHeadFile, ".git/HEAD file not found in " + myGitDir); + myRefsHeadsDir = new File(FileUtil.toSystemDependentName(gitFiles.getRefsHeadsPath())); + myRefsRemotesDir = new File(FileUtil.toSystemDependentName(gitFiles.getRefsRemotesPath())); + myPackedRefsFile = new File(FileUtil.toSystemDependentName(gitFiles.getPackedRefsPath())); } @NotNull @@ -229,8 +231,8 @@ class GitRepositoryReader { @NotNull private Map readBranchRefsFromFiles() { Map result = ContainerUtil.newHashMap(readPackedBranches()); // reading from packed-refs first to overwrite values by values from unpacked refs - result.putAll(readFromBranchFiles(myRefsHeadsDir)); - result.putAll(readFromBranchFiles(myRefsRemotesDir)); + result.putAll(readFromBranchFiles(myRefsHeadsDir, REFS_HEADS_PREFIX)); + result.putAll(readFromBranchFiles(myRefsRemotesDir, REFS_REMOTES_PREFIX)); result.remove(REFS_REMOTES_PREFIX + GitUtil.ORIGIN_HEAD); return result; } @@ -265,18 +267,18 @@ class GitRepositoryReader { } @NotNull - private Map readFromBranchFiles(@NotNull File rootDir) { - if (!rootDir.exists()) { + private static Map readFromBranchFiles(@NotNull final File refsRootDir, @NotNull final String prefix) { + if (!refsRootDir.exists()) { return Collections.emptyMap(); } final Map result = new HashMap(); - FileUtil.processFilesRecursively(rootDir, new Processor() { + FileUtil.processFilesRecursively(refsRootDir, new Processor() { @Override public boolean process(File file) { if (!file.isDirectory() && !isHidden(file)) { - String relativePath = FileUtil.getRelativePath(myGitDir, file); + String relativePath = FileUtil.getRelativePath(refsRootDir, file); if (relativePath != null) { - String branchName = FileUtil.toSystemIndependentName(relativePath); + String branchName = prefix + FileUtil.toSystemIndependentName(relativePath); String hash = loadHashFromBranchFile(file); if (hash != null) { result.put(branchName, hash); diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryUpdater.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryUpdater.java index b3e9c0ce6b22..54bb7088df23 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryUpdater.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryUpdater.java @@ -53,7 +53,7 @@ final class GitRepositoryUpdater implements Disposable, BulkFileListener { myWatchRequest = LocalFileSystem.getInstance().addRootToWatch(gitDir.getPath(), true); myRepositoryFiles = GitRepositoryFiles.getInstance(gitDir); - DvcsUtil.visitVcsDirVfs(gitDir, GitRepositoryFiles.getSubDirRelativePaths()); + visitSubDirsInVfs(gitDir); myHeadsDir = VcsUtil.getVirtualFile(myRepositoryFiles.getRefsHeadsPath()); myRemotesDir = VcsUtil.getVirtualFile(myRepositoryFiles.getRefsRemotesPath()); myTagsDir = VcsUtil.getVirtualFile(myRepositoryFiles.getRefsTagsPath()); @@ -128,4 +128,10 @@ final class GitRepositoryUpdater implements Disposable, BulkFileListener { } } + private void visitSubDirsInVfs(@NotNull VirtualFile gitDir) { + gitDir.getChildren(); + for (String path : myRepositoryFiles.getDirsToWatch()) { + DvcsUtil.ensureAllChildrenInVfs(LocalFileSystem.getInstance().refreshAndFindFileByPath(path)); + } + } } diff --git a/plugins/git4idea/tests/git4idea/repo/GitConfigTest.java b/plugins/git4idea/tests/git4idea/repo/GitConfigTest.java index 813ec29cf482..2d95c099c31b 100644 --- a/plugins/git4idea/tests/git4idea/repo/GitConfigTest.java +++ b/plugins/git4idea/tests/git4idea/repo/GitConfigTest.java @@ -22,8 +22,8 @@ import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsTestUtil; +import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.util.containers.ContainerUtil; -import git4idea.GitBranch; import git4idea.GitLocalBranch; import git4idea.GitRemoteBranch; import git4idea.GitStandardRemoteBranch; @@ -64,7 +64,7 @@ public class GitConfigTest extends GitPlatformTest { File gitDir = new File(myProjectPath, ".git"); GitConfig config = GitConfig.read(myPlatformFacade, new File(gitDir, "config")); - GitBranchState state = new GitRepositoryReader(gitDir).readState(config.parseRemotes()); + GitBranchState state = new GitRepositoryReader(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(gitDir)).readState(config.parseRemotes()); Collection trackInfos = config.parseTrackInfos(state.getLocalBranches().keySet(), state.getRemoteBranches().keySet()); assertTrue("Couldn't find correct a#branch tracking information among: [" + trackInfos + "]", ContainerUtil.exists(trackInfos, new Condition() { diff --git a/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderNewTest.java b/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderNewTest.java index 114c58edcab9..bf6da7298c27 100644 --- a/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderNewTest.java +++ b/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderNewTest.java @@ -17,6 +17,7 @@ package git4idea.repo; import com.intellij.dvcs.repo.Repository; import com.intellij.openapi.util.Condition; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import git4idea.GitLocalBranch; @@ -115,8 +116,8 @@ public class GitRepositoryReaderNewTest extends GitSingleRepoTest { @NotNull private GitBranchState readState() { - File gitDir = new File(myRepo.getRoot().getPath(), ".git"); - GitConfig config = GitConfig.read(myPlatformFacade, new File(gitDir, "config")); + VirtualFile gitDir = myRepo.getGitDir(); + GitConfig config = GitConfig.read(myPlatformFacade, new File(gitDir.getPath(), "config")); GitRepositoryReader reader = new GitRepositoryReader(gitDir); Collection remotes = config.parseRemotes(); return reader.readState(remotes); diff --git a/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderTest.java b/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderTest.java index dc56a6eb1f84..26f848b97a85 100644 --- a/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderTest.java +++ b/plugins/git4idea/tests/git4idea/repo/GitRepositoryReaderTest.java @@ -20,6 +20,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsTestUtil; +import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.testFramework.EdtTestUtil; import com.intellij.util.Function; import com.intellij.util.ThrowableRunnable; @@ -114,7 +115,7 @@ public class GitRepositoryReaderTest extends GitPlatformTest { } FileUtil.rename(dotGit, myGitDir); TestCase.assertTrue(myGitDir.exists()); - myRepositoryReader = new GitRepositoryReader(myGitDir); + myRepositoryReader = new GitRepositoryReader(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(myGitDir)); } diff --git a/plugins/git4idea/tests/git4idea/repo/GitWorkTreeTest.kt b/plugins/git4idea/tests/git4idea/repo/GitWorkTreeTest.kt new file mode 100644 index 000000000000..057cbaf2cdf2 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/repo/GitWorkTreeTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.repo + +import com.intellij.openapi.vfs.LocalFileSystem +import git4idea.GitUtil +import git4idea.branch.GitBranchesCollection +import git4idea.config.GitVersion +import git4idea.test.GitExecutor.* +import git4idea.test.GitPlatformTest +import git4idea.test.GitTestUtil +import git4idea.test.GitTestUtil.initRepo +import org.junit.Assume.assumeTrue +import java.io.File + +class GitWorkTreeTest : GitPlatformTest() { + + private lateinit var myMainRoot: String + private lateinit var myRepo : GitRepository + + override fun setUp() { + super.setUp() + cd(myTestRoot) + assumeTrue(GitVersion.parse(git("version")).isLaterOrEqual(GitVersion(2, 5, 0, 0))) + + val mainDir = File(myTestRoot, "main") + assertTrue(mainDir.mkdir()) + myMainRoot = mainDir.path + initRepo(myMainRoot, true) + + git("worktree add $myProjectPath") + val gitDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(myProjectPath, GitUtil.DOT_GIT)) + assertNotNull(gitDir) + myRepo = GitTestUtil.registerRepo(project, myProjectPath) + assertEquals(1, myGitRepositoryManager.repositories.size) + assertNotNull(myGitRepositoryManager.getRepositoryForRoot(myProjectRoot)) + } + + fun `test local branches`() { + cd(myMainRoot) + val masterHead = last() + git("checkout -b feature") + val featureHead = tac("f.txt") + + myRepo.update() + + val branches = myRepo.branches + val expectedBranches = listOf("master", "feature", "project") // 'project' is created automatically by `git worktree add` + assertSameElements(branches.localBranches.map {it.name}, expectedBranches) + assertBranchHash(masterHead, branches, "master") + assertBranchHash(featureHead, branches, "feature") + } + + fun `test remote branches`() { + cd(myTestRoot) + git("clone --bare $myMainRoot parent.git") + cd(myMainRoot) + val parentPath = File(myTestRoot, "parent.git").path + git("remote add origin $parentPath") + git("push origin master") + + val masterHead = last() + git("checkout -b feature") + val featureHead = tac("f.txt") + git("push origin feature") + + myRepo.update() + + val branches = myRepo.branches + assertSameElements(branches.remoteBranches.map {it.nameForLocalOperations}, listOf("origin/master", "origin/feature")) + assertBranchHash(masterHead, branches, "origin/master") + assertBranchHash(featureHead, branches, "origin/feature") + } + + fun `test HEAD`() { + cd(myRepo) + git("checkout -b feature") + val featureHead = tac("f.txt") + myRepo.update() + + assertEquals("Incorrect current branch", "feature", myRepo.currentBranchName) + assertEquals("Incorrect current revision", featureHead, myRepo.currentRevision) + } + + private fun assertBranchHash(expectedHash: String, branches: GitBranchesCollection, branchName: String) { + assertEquals(expectedHash, branches.getHash(branches.findBranchByName(branchName)!!)!!.asString()) + } +} diff --git a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java index 4e17719bd23c..39cb8865a693 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java @@ -78,7 +78,7 @@ public class GitTestUtil { } } - private static void initRepo(@NotNull String repoRoot, boolean makeInitialCommit) { + public static void initRepo(@NotNull String repoRoot, boolean makeInitialCommit) { cd(repoRoot); git("init"); setupUsername();