diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchesCollection.java b/plugins/git4idea/src/git4idea/branch/GitBranchesCollection.java new file mode 100644 index 000000000000..6992ebe851eb --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitBranchesCollection.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2011 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.branch; + +import git4idea.GitBranch; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; + +/** + *

+ * Storage for local, remote and current branches. + * The reason of creating this special collection is that + * in the terms of performance, they are detected by {@link git4idea.repo.GitRepositoryReader} at once; + * and also usually both sets of branches are needed by components, but are treated differently, + * so it is more convenient to have them separated, but in a single container. + *

+ * + * @author Kirill Likhodedov + */ +public final class GitBranchesCollection { + + public static final GitBranchesCollection EMPTY = new GitBranchesCollection(null, new HashSet(), new HashSet()); + + private final Set myLocalBranches; + private final Set myRemoteBranches; + private final GitBranch myCurrentBranch; + + public GitBranchesCollection(@Nullable GitBranch currentBranch, @NotNull Set localBranches, @NotNull Set remoteBranches) { + myCurrentBranch = currentBranch; + myRemoteBranches = remoteBranches; + myLocalBranches = localBranches; + } + + @NotNull + public Set getLocalBranches() { + return myLocalBranches; + } + + @NotNull + public Set getRemoteBranches() { + return myRemoteBranches; + } + + /** + * @return currently active branch. + * Current branch may be null in the case of detached head, + */ + @Nullable + public GitBranch getCurrentBranch() { + return myCurrentBranch; + } +} diff --git a/plugins/git4idea/src/git4idea/repo/GitRepository.java b/plugins/git4idea/src/git4idea/repo/GitRepository.java index 0e7fcf04e89b..4dbbd98a0c48 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepository.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepository.java @@ -23,6 +23,7 @@ import com.intellij.util.messages.MessageBus; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import git4idea.GitBranch; +import git4idea.branch.GitBranchesCollection; import git4idea.status.GitUntrackedFilesHolder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -64,11 +65,13 @@ public final class GitRepository implements Disposable { private volatile State myState; private volatile String myCurrentRevision; - private volatile GitBranch myCurrentBranch; + private volatile GitBranchesCollection myBranches = GitBranchesCollection.EMPTY; + private final ReadWriteLock STATE_LOCK = new ReentrantReadWriteLock(); private final ReadWriteLock CUR_REV_LOCK = new ReentrantReadWriteLock(); private final ReadWriteLock CUR_BRANCH_LOCK = new ReentrantReadWriteLock(); + private final ReadWriteLock BRANCHES_LOCK = new ReentrantReadWriteLock(); /** * Current state of the repository. @@ -112,6 +115,11 @@ public final class GitRepository implements Disposable { repository.updateCurrentBranch(); } }, + BRANCHES { + @Override void update(GitRepository repository) { + repository.updateBranchList(); + } + }, ALL_CURRENT { @Override void update(GitRepository repository) { STATE.update(repository); @@ -122,6 +130,7 @@ public final class GitRepository implements Disposable { ALL { @Override void update(GitRepository repository) { ALL_CURRENT.update(repository); + BRANCHES.update(repository); } }; @@ -220,6 +229,17 @@ public final class GitRepository implements Disposable { public boolean isOnBranch() { return getState() != State.DETACHED && getState() != State.REBASING; } + + @NotNull + public GitBranchesCollection getBranches() { + try { + BRANCHES_LOCK.readLock().lock(); + return myBranches; + } + finally { + BRANCHES_LOCK.readLock().unlock(); + } + } public void addListener(GitRepositoryChangeListener listener) { MessageBusConnection connection = myMessageBus.connect(); @@ -280,6 +300,18 @@ public final class GitRepository implements Disposable { notifyListeners(); } + + private void updateBranchList() { + GitBranchesCollection branches = myReader.readBranches(); + try { + BRANCHES_LOCK.writeLock().lock(); + myBranches = branches; + } + finally { + BRANCHES_LOCK.writeLock().unlock(); + } + notifyListeners(); + } private void notifyListeners() { myMessageBus.syncPublisher(GIT_REPO_CHANGE).repositoryChanged(); diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java index 927b73443309..6e3f30656766 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryReader.java @@ -20,6 +20,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.util.Processor; import git4idea.GitBranch; +import git4idea.branch.GitBranchesCollection; import git4idea.merge.GitMergeUtil; import git4idea.rebase.GitRebaseUtils; import org.jetbrains.annotations.NotNull; @@ -30,8 +31,11 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,12 +57,15 @@ class GitRepositoryReader { private static Pattern PACKED_REFS_TAGREF_LINE = Pattern.compile("\\^[0-9a-fA-F]+"); // tag reference in .git/packed-refs private static final String REFS_HEADS_PREFIX = "refs/heads/"; + private static final String REFS_REMOTES_PREFIX = "refs/remotes/"; private static final int IO_RETRIES = 3; // number of retries before fail if an IOException happens during file read. private final GitRepository myRepository; - private final File myGitDir; // .git/ - private final File myHeadFile; // .git/HEAD - private final File myRefsHeadsDir; // .git/refs/heads/ + private final File myGitDir; // .git/ + private final File myHeadFile; // .git/HEAD + private final File myRefsHeadsDir; // .git/refs/heads/ + private final File myRefsRemotesDir; // .git/refs/remotes/ + private final File myPackedRefsFile; // .git/packed-refs GitRepositoryReader(@NotNull GitRepository repository) { myRepository = repository; @@ -67,6 +74,8 @@ class GitRepositoryReader { myHeadFile = new File(myGitDir, "HEAD"); assertFileExists(myHeadFile, ".git/HEAD file not found in " + myRepository.getRoot()); myRefsHeadsDir = new File(new File(myGitDir, "refs"), "heads"); + myRefsRemotesDir = new File(new File(myGitDir, "refs"), "remotes"); + myPackedRefsFile = new File(myGitDir, "packed-refs"); } @NotNull @@ -97,7 +106,7 @@ class GitRepositoryReader { // look in /refs/heads/ File branchFile = null; - for (Map.Entry entry : getLocalBranches().entrySet()) { + for (Map.Entry entry : readLocalBranches().entrySet()) { if (entry.getKey().equals(head.ref)) { branchFile = entry.getValue(); } @@ -167,8 +176,7 @@ class GitRepositoryReader { */ @Nullable private String findBranchRevisionInPackedRefs(final String ref) { - final File packedRefs = new File(myGitDir, "packed-refs"); - if (!packedRefs.exists()) { + if (!myPackedRefsFile.exists()) { return null; } @@ -177,12 +185,23 @@ class GitRepositoryReader { public String call() throws Exception { BufferedReader reader = null; try { - reader = new BufferedReader(new FileReader(packedRefs)); + reader = new BufferedReader(new FileReader(myPackedRefsFile)); String line; while ((line = reader.readLine()) != null) { - String hash = findRefHashInPackedRefsLine(line, ref); - if (hash != null) { - return hash; + final AtomicReference hashRef = new AtomicReference(); + parsePackedRefsLine(line, new PackedRefsLineResultHandler() { + @Override public void handleResult(String hash, String branchName) { + if (hash == null || branchName == null) { + return; + } + if (branchName.endsWith(ref)) { + hashRef.set(hash); + } + } + }); + + if (hashRef.get() != null) { + return hashRef.get(); } } return null; @@ -193,14 +212,14 @@ class GitRepositoryReader { } } } - }, packedRefs); + }, myPackedRefsFile); } /** * @return the list of local branches in this Git repository. * key is the branch name, value is the file. */ - private Map getLocalBranches() { + private Map readLocalBranches() { final Map branches = new HashMap(); FileUtil.processFilesRecursively(myRefsHeadsDir, new Processor() { @Override @@ -214,28 +233,96 @@ class GitRepositoryReader { return branches; } - @Nullable - private static String findRefHashInPackedRefsLine(String line, String ref) { - if (line.startsWith("#")) { // ignoring comments - return null; + /** + * @return all branches in this repository. local/remote/active information is stored in branch objects themselves. + */ + GitBranchesCollection readBranches() { + Set localBranches = readUnpackedLocalBranches(); + Set remoteBranches = readUnpackedRemoteBranches(); + GitBranchesCollection packedBranches = readPackedBranches(); + localBranches.addAll(packedBranches.getLocalBranches()); + remoteBranches.addAll(packedBranches.getRemoteBranches()); + + // note that even the active branch may be packed. So at first we collect branches, then we find the active. + GitBranch currentBranch = readCurrentBranch(); + markActiveBranch(localBranches, currentBranch); + + return new GitBranchesCollection(currentBranch, localBranches, remoteBranches); + } + + /** + * Sets the 'active' flag to the current branch if it is contained in the specified collection. + * @param branches branches to be walked through. + * @param currentBranch current branch. + */ + private static void markActiveBranch(@NotNull Set branches, @Nullable GitBranch currentBranch) { + if (currentBranch == null) { + return; } - if (PACKED_REFS_TAGREF_LINE.matcher(line).matches()) { // ignoring the hash which an annotated tag above points to - return null; - } - Matcher matcher = PACKED_REFS_BRANCH_LINE.matcher(line); - if (matcher.matches()) { - String hash = matcher.group(1); - String branch = matcher.group(2); - if (branch.endsWith(ref)) { - return hash; + for (GitBranch branch : branches) { + if (branch.getName().equals(currentBranch.getName())) { + branch.setActive(true); } - } else { - LOG.info("Ignoring invalid packed-refs line: [" + line + "]"); - return null; } - return null; } + /** + * @return list of branches from refs/heads. active branch is not marked as active - the caller should do this. + */ + @NotNull + private Set readUnpackedLocalBranches() { + Set branches = new HashSet(); + for (String branchName : readLocalBranches().keySet()) { + branches.add(new GitBranch(branchName, false, false)); + } + return branches; + } + + /** + * @return list of branches from refs/remotes. + */ + private Set readUnpackedRemoteBranches() { + final Set branches = new HashSet(); + FileUtil.processFilesRecursively(myRefsRemotesDir, new Processor() { + @Override + public boolean process(File file) { + if (!file.isDirectory()) { + final String relativePath = FileUtil.getRelativePath(myRefsRemotesDir, file); + branches.add(new GitBranch(relativePath, false, true)); + } + return true; + } + }); + return branches; + } + + /** + * @return list of local and remote branches from packed-refs. Active branch is not marked as active. + */ + @NotNull + private GitBranchesCollection readPackedBranches() { + final Set localBranches = new HashSet(); + final Set remoteBranches = new HashSet(); + final String content = tryLoadFile(myPackedRefsFile); + + for (String line : content.split("\n")) { + parsePackedRefsLine(line, new PackedRefsLineResultHandler() { + @Override public void handleResult(@Nullable String hash, @Nullable String branchName) { + if (hash == null || branchName == null) { + return; + } + if (branchName.startsWith(REFS_HEADS_PREFIX)) { + localBranches.add(new GitBranch(branchName.substring(REFS_HEADS_PREFIX.length()), false, false)); + } else if (branchName.startsWith(REFS_REMOTES_PREFIX)) { + remoteBranches.add(new GitBranch(branchName.substring(REFS_REMOTES_PREFIX.length()), false, true)); + } + } + }); + } + return new GitBranchesCollection(null, localBranches, remoteBranches); + } + + private static String readBranchFile(File branchFile) { String rev = tryLoadFile(branchFile); return rev.trim(); @@ -297,6 +384,42 @@ class GitRepositoryReader { throw new GitRepoStateException("Couldn't load file " + fileToLoad, cause); } + /** + * Parses a line from the .git/packed-refs file. + * Passes the parsed hash-branch pair to the resultHandler. + * Comments, tags and incorrectly formatted lines are ignored, and (null, null) is passed to the handler then. + * Using a special handler may seem to be an overhead, but it is to avoid code duplication in two methods that parse packed-refs. + */ + private static void parsePackedRefsLine(String line, PackedRefsLineResultHandler resultHandler) { + line = line.trim(); + if (line.startsWith("#")) { // ignoring comments + resultHandler.handleResult(null, null); + return; + } + if (PACKED_REFS_TAGREF_LINE.matcher(line).matches()) { // ignoring the hash which an annotated tag above points to + resultHandler.handleResult(null, null); + return; + } + Matcher matcher = PACKED_REFS_BRANCH_LINE.matcher(line); + if (matcher.matches()) { + String hash = matcher.group(1); + String branch = matcher.group(2); + resultHandler.handleResult(hash, branch); + } else { + LOG.info("Ignoring invalid packed-refs line: [" + line + "]"); + resultHandler.handleResult(null, null); + return; + } + resultHandler.handleResult(null, null); + } + + private interface PackedRefsLineResultHandler { + void handleResult(@Nullable String hash, @Nullable String branchName); + } + + /** + * Container to hold two information items: current .git/HEAD value and is Git on branch. + */ private static class Head { private final String ref; private final boolean isBranch; @@ -305,7 +428,7 @@ class GitRepositoryReader { isBranch = branch; this.ref = ref; } - } + }