IDEA-143404 Support git worktree

Use GitRepositoryFiles to identify and store
actual paths for .git/refs, .git/HEAD, etc.
This commit is contained in:
Kirill Likhodedov
2016-01-31 18:11:16 +03:00
parent 03e4ec5f1c
commit 69a1dc1434
9 changed files with 204 additions and 45 deletions
@@ -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.
* <p/>
* Worktree's ".git" file references {@code <main-project>/.git/worktrees/<worktree-name>}
*/
@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<String> getSubDirRelativePaths() {
return Arrays.asList(slash(REFS_HEADS), slash(REFS_REMOTES), slash(REFS_TAGS), slash(INFO));
Collection<String> 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}
*/
@@ -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);
@@ -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<String, String> readBranchRefsFromFiles() {
Map<String, String> 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<String, String> readFromBranchFiles(@NotNull File rootDir) {
if (!rootDir.exists()) {
private static Map<String, String> readFromBranchFiles(@NotNull final File refsRootDir, @NotNull final String prefix) {
if (!refsRootDir.exists()) {
return Collections.emptyMap();
}
final Map<String, String> result = new HashMap<String, String>();
FileUtil.processFilesRecursively(rootDir, new Processor<File>() {
FileUtil.processFilesRecursively(refsRootDir, new Processor<File>() {
@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);
@@ -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));
}
}
}
@@ -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<GitBranchTrackInfo> 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<GitBranchTrackInfo>() {
@@ -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<GitRemote> remotes = config.parseRemotes();
return reader.readState(remotes);
@@ -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));
}
@@ -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())
}
}
@@ -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();