mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-143404 Support git worktree
Use GitRepositoryFiles to identify and store actual paths for .git/refs, .git/HEAD, etc.
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user