| /* |
| * Copyright 2000-2013 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.dvcs.repo.RepoStateException; |
| import com.intellij.dvcs.repo.Repository; |
| import com.intellij.dvcs.repo.RepositoryUtil; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.util.Processor; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.vcs.log.Hash; |
| import com.intellij.vcs.log.impl.HashImpl; |
| import git4idea.GitBranch; |
| import git4idea.GitLocalBranch; |
| import git4idea.GitRemoteBranch; |
| import git4idea.branch.GitBranchUtil; |
| import git4idea.branch.GitBranchesCollection; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileReader; |
| import java.util.*; |
| import java.util.concurrent.Callable; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Reads information about the Git repository from Git service files located in the {@code .git} folder. |
| * NB: works with {@link java.io.File}, i.e. reads from disk. Consider using caching. |
| * Throws a {@link RepoStateException} in the case of incorrect Git file format. |
| * |
| * @author Kirill Likhodedov |
| */ |
| class GitRepositoryReader { |
| |
| private static final Logger LOG = Logger.getInstance(GitRepositoryReader.class); |
| |
| private static Pattern BRANCH_PATTERN = Pattern.compile("ref: refs/heads/(\\S+)"); // branch reference in .git/HEAD |
| // this format shouldn't appear, but we don't want to fail because of a space |
| private static Pattern BRANCH_WEAK_PATTERN = Pattern.compile(" *(ref:)? */?refs/heads/(\\S+)"); |
| private static Pattern COMMIT_PATTERN = Pattern.compile("[0-9a-fA-F]+"); // commit hash |
| |
| @NonNls private static final String REFS_HEADS_PREFIX = "refs/heads/"; |
| @NonNls private static final String REFS_REMOTES_PREFIX = "refs/remotes/"; |
| |
| @NotNull private final File myGitDir; // .git/ |
| @NotNull private final File myHeadFile; // .git/HEAD |
| @NotNull private final File myRefsHeadsDir; // .git/refs/heads/ |
| @NotNull private final File myRefsRemotesDir; // .git/refs/remotes/ |
| @NotNull private final File myPackedRefsFile; // .git/packed-refs |
| |
| GitRepositoryReader(@NotNull File gitDir) { |
| myGitDir = gitDir; |
| RepositoryUtil.assertFileExists(myGitDir, ".git directory not found in " + gitDir); |
| myHeadFile = new File(myGitDir, "HEAD"); |
| RepositoryUtil.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"); |
| } |
| |
| @Nullable |
| private static Hash createHash(@Nullable String hash) { |
| try { |
| return hash == null ? GitBranch.DUMMY_HASH : HashImpl.build(hash); |
| } |
| catch (Throwable t) { |
| LOG.info(t); |
| return null; |
| } |
| } |
| |
| @NotNull |
| public Repository.State readState() { |
| if (isMergeInProgress()) { |
| return Repository.State.MERGING; |
| } |
| if (isRebaseInProgress()) { |
| return Repository.State.REBASING; |
| } |
| Head head = readHead(); |
| if (!head.isBranch) { |
| return Repository.State.DETACHED; |
| } |
| return Repository.State.NORMAL; |
| } |
| |
| /** |
| * Finds current revision value. |
| * @return The current revision hash, or <b>{@code null}</b> if current revision is unknown - it is the initial repository state. |
| */ |
| @Nullable |
| String readCurrentRevision() { |
| final Head head = readHead(); |
| if (!head.isBranch) { // .git/HEAD is a commit |
| return head.ref; |
| } |
| |
| // look in /refs/heads/<branch name> |
| File branchFile = null; |
| for (Map.Entry<String, File> entry : readLocalBranches().entrySet()) { |
| if (entry.getKey().equals(head.ref)) { |
| branchFile = entry.getValue(); |
| } |
| } |
| if (branchFile != null) { |
| return readBranchFile(branchFile); |
| } |
| |
| // finally look in packed-refs |
| return findBranchRevisionInPackedRefs(head.ref); |
| } |
| |
| /** |
| * If the repository is on branch, returns the current branch |
| * If the repository is being rebased, returns the branch being rebased. |
| * In other cases of the detached HEAD returns {@code null}. |
| */ |
| @Nullable |
| GitLocalBranch readCurrentBranch() { |
| Head head = readHead(); |
| if (head.isBranch) { |
| String branchName = head.ref; |
| String hash = readCurrentRevision(); // TODO we know the branch name, so no need to read head twice |
| Hash h = createHash(hash); |
| if (h == null) { |
| return null; |
| } |
| return new GitLocalBranch(branchName, h); |
| } |
| if (isRebaseInProgress()) { |
| GitLocalBranch branch = readRebaseBranch("rebase-apply"); |
| if (branch == null) { |
| branch = readRebaseBranch("rebase-merge"); |
| } |
| return branch; |
| } |
| return null; |
| } |
| |
| /** |
| * Reads {@code .git/rebase-apply/head-name} or {@code .git/rebase-merge/head-name} to find out the branch which is currently being rebased, |
| * and returns the {@link GitBranch} for the branch name written there, or null if these files don't exist. |
| */ |
| @Nullable |
| private GitLocalBranch readRebaseBranch(@NonNls String rebaseDirName) { |
| File rebaseDir = new File(myGitDir, rebaseDirName); |
| if (!rebaseDir.exists()) { |
| return null; |
| } |
| File headName = new File(rebaseDir, "head-name"); |
| if (!headName.exists()) { |
| return null; |
| } |
| String branchName = RepositoryUtil.tryLoadFile(headName); |
| File branchFile = findBranchFile(branchName); |
| if (!branchFile.exists()) { // can happen when rebasing from detached HEAD: IDEA-93806 |
| return null; |
| } |
| Hash hash = createHash(readBranchFile(branchFile)); |
| if (hash == null) { |
| return null; |
| } |
| if (branchName.startsWith(REFS_HEADS_PREFIX)) { |
| branchName = branchName.substring(REFS_HEADS_PREFIX.length()); |
| } |
| return new GitLocalBranch(branchName, hash); |
| } |
| |
| @NotNull |
| private File findBranchFile(@NotNull String branchName) { |
| return new File(myGitDir.getPath() + File.separator + branchName); |
| } |
| |
| private boolean isMergeInProgress() { |
| File mergeHead = new File(myGitDir, "MERGE_HEAD"); |
| return mergeHead.exists(); |
| } |
| |
| private boolean isRebaseInProgress() { |
| File f = new File(myGitDir, "rebase-apply"); |
| if (f.exists()) { |
| return true; |
| } |
| f = new File(myGitDir, "rebase-merge"); |
| return f.exists(); |
| } |
| |
| /** |
| * Reads the {@code .git/packed-refs} file and tries to find the revision hash for the given reference (branch actually). |
| * @param ref short name of the reference to find. For example, {@code master}. |
| * @return commit hash, or {@code null} if the given ref wasn't found in {@code packed-refs} |
| */ |
| @Nullable |
| private String findBranchRevisionInPackedRefs(final String ref) { |
| if (!myPackedRefsFile.exists()) { |
| return null; |
| } |
| |
| List<HashAndName> hashAndNames = readPackedRefsFile(new Condition<HashAndName>() { |
| @Override |
| public boolean value(HashAndName hashAndName) { |
| return hashAndName.name.endsWith(ref); |
| } |
| }); |
| HashAndName item = ContainerUtil.getFirstItem(hashAndNames); |
| return item == null ? null : item.hash; |
| } |
| |
| /** |
| * @param firstMatchCondition If specified, we read the packed-refs file until the first entry which matches the given condition, |
| * and return a singleton list of this entry. |
| * If null, the whole file is read, and all valid entries are returned. |
| */ |
| private List<HashAndName> readPackedRefsFile(@Nullable final Condition<HashAndName> firstMatchCondition) { |
| return RepositoryUtil.tryOrThrow(new Callable<List<HashAndName>>() { |
| @Override |
| public List<HashAndName> call() throws Exception { |
| List<HashAndName> hashAndNames = ContainerUtil.newArrayList(); |
| BufferedReader reader = null; |
| try { |
| reader = new BufferedReader(new FileReader(myPackedRefsFile)); |
| for (String line = reader.readLine(); line != null ; line = reader.readLine()) { |
| HashAndName hashAndName = parsePackedRefsLine(line); |
| if (hashAndName == null) { |
| continue; |
| } |
| if (firstMatchCondition != null) { |
| if (firstMatchCondition.value(hashAndName)) { |
| return Collections.singletonList(hashAndName); |
| } |
| } |
| else { |
| hashAndNames.add(hashAndName); |
| } |
| } |
| } |
| finally { |
| if (reader != null) { |
| reader.close(); |
| } |
| } |
| return hashAndNames; |
| } |
| }, myPackedRefsFile); |
| } |
| |
| /** |
| * @return the list of local branches in this Git repository. |
| * key is the branch name, value is the file. |
| */ |
| private Map<String, File> readLocalBranches() { |
| final Map<String, File> branches = new HashMap<String, File>(); |
| if (!myRefsHeadsDir.exists()) { |
| return branches; |
| } |
| FileUtil.processFilesRecursively(myRefsHeadsDir, new Processor<File>() { |
| @Override |
| public boolean process(File file) { |
| if (!file.isDirectory()) { |
| String relativePath = FileUtil.getRelativePath(myRefsHeadsDir, file); |
| if (relativePath != null) { |
| branches.put(FileUtil.toSystemIndependentName(relativePath), file); |
| } |
| } |
| return true; |
| } |
| }); |
| return branches; |
| } |
| |
| /** |
| * @return all branches in this repository. local/remote/active information is stored in branch objects themselves. |
| * @param remotes |
| */ |
| GitBranchesCollection readBranches(@NotNull Collection<GitRemote> remotes) { |
| Set<GitLocalBranch> localBranches = readUnpackedLocalBranches(); |
| Set<GitRemoteBranch> remoteBranches = readUnpackedRemoteBranches(remotes); |
| GitBranchesCollection packedBranches = readPackedBranches(remotes); |
| localBranches.addAll(packedBranches.getLocalBranches()); |
| remoteBranches.addAll(packedBranches.getRemoteBranches()); |
| return new GitBranchesCollection(localBranches, remoteBranches); |
| } |
| |
| /** |
| * @return list of branches from refs/heads. active branch is not marked as active - the caller should do this. |
| */ |
| @NotNull |
| private Set<GitLocalBranch> readUnpackedLocalBranches() { |
| Set<GitLocalBranch> branches = new HashSet<GitLocalBranch>(); |
| for (Map.Entry<String, File> entry : readLocalBranches().entrySet()) { |
| String branchName = entry.getKey(); |
| File branchFile = entry.getValue(); |
| String hash = loadHashFromBranchFile(branchFile); |
| Hash h = createHash(hash); |
| if (h != null) { |
| branches.add(new GitLocalBranch(branchName, h)); |
| } |
| } |
| return branches; |
| } |
| |
| @Nullable |
| private static String loadHashFromBranchFile(@NotNull File branchFile) { |
| try { |
| return RepositoryUtil.tryLoadFile(branchFile); |
| } |
| catch (RepoStateException e) { // notify about error but don't break the process |
| LOG.error("Couldn't read " + branchFile, e); |
| } |
| return null; |
| } |
| |
| /** |
| * @return list of branches from refs/remotes. |
| * @param remotes |
| */ |
| @NotNull |
| private Set<GitRemoteBranch> readUnpackedRemoteBranches(@NotNull final Collection<GitRemote> remotes) { |
| final Set<GitRemoteBranch> branches = new HashSet<GitRemoteBranch>(); |
| if (!myRefsRemotesDir.exists()) { |
| return branches; |
| } |
| FileUtil.processFilesRecursively(myRefsRemotesDir, new Processor<File>() { |
| @Override |
| public boolean process(File file) { |
| if (!file.isDirectory() && !file.getName().equalsIgnoreCase(GitRepositoryFiles.HEAD)) { |
| final String relativePath = FileUtil.getRelativePath(myGitDir, file); |
| if (relativePath != null) { |
| String branchName = FileUtil.toSystemIndependentName(relativePath); |
| String hash = loadHashFromBranchFile(file); |
| Hash h = createHash(hash); |
| if (h != null) { |
| GitRemoteBranch remoteBranch = GitBranchUtil.parseRemoteBranch(branchName, h, remotes); |
| if (remoteBranch != null) { |
| branches.add(remoteBranch); |
| } |
| } |
| } |
| } |
| return true; |
| } |
| }); |
| return branches; |
| } |
| |
| /** |
| * @return list of local and remote branches from packed-refs. Active branch is not marked as active. |
| * @param remotes |
| */ |
| @NotNull |
| private GitBranchesCollection readPackedBranches(@NotNull final Collection<GitRemote> remotes) { |
| final Set<GitLocalBranch> localBranches = new HashSet<GitLocalBranch>(); |
| final Set<GitRemoteBranch> remoteBranches = new HashSet<GitRemoteBranch>(); |
| if (!myPackedRefsFile.exists()) { |
| return GitBranchesCollection.EMPTY; |
| } |
| |
| List<HashAndName> hashAndNames = readPackedRefsFile(null); |
| for (HashAndName hashAndName : hashAndNames) { |
| Hash hash = createHash(hashAndName.hash); |
| if (hash == null) { |
| continue; |
| } |
| String branchName = hashAndName.name; |
| |
| if (branchName.startsWith(REFS_HEADS_PREFIX)) { |
| localBranches.add(new GitLocalBranch(branchName, hash)); |
| } |
| else if (branchName.startsWith(REFS_REMOTES_PREFIX)) { |
| GitRemoteBranch remoteBranch = GitBranchUtil.parseRemoteBranch(branchName, hash, remotes); |
| if (remoteBranch != null) { |
| remoteBranches.add(remoteBranch); |
| } |
| } |
| } |
| return new GitBranchesCollection(localBranches, remoteBranches); |
| } |
| |
| @NotNull |
| private static String readBranchFile(@NotNull File branchFile) { |
| return RepositoryUtil.tryLoadFile(branchFile); |
| } |
| |
| @NotNull |
| private Head readHead() { |
| String headContent = RepositoryUtil.tryLoadFile(myHeadFile); |
| Matcher matcher = BRANCH_PATTERN.matcher(headContent); |
| if (matcher.matches()) { |
| return new Head(true, matcher.group(1)); |
| } |
| |
| if (COMMIT_PATTERN.matcher(headContent).matches()) { |
| return new Head(false, headContent); |
| } |
| matcher = BRANCH_WEAK_PATTERN.matcher(headContent); |
| if (matcher.matches()) { |
| LOG.info(".git/HEAD has not standard format: [" + headContent + "]. We've parsed branch [" + matcher.group(1) + "]"); |
| return new Head(true, matcher.group(1)); |
| } |
| throw new RepoStateException("Invalid format of the .git/HEAD file: [" + headContent + "]"); |
| } |
| |
| /** |
| * Parses a line from the .git/packed-refs file returning a pair of hash and ref name. |
| * Comments and tags are ignored, and null is returned. |
| * Incorrectly formatted lines are ignored, a warning is printed to the log, null is returned. |
| * A line indicating a hash which an annotated tag (specified in the previous line) points to, is ignored: null is returned. |
| */ |
| @Nullable |
| private static HashAndName parsePackedRefsLine(@NotNull String line) { |
| line = line.trim(); |
| if (line.isEmpty()) { |
| return null; |
| } |
| char firstChar = line.charAt(0); |
| if (firstChar == '#') { // ignoring comments |
| return null; |
| } |
| if (firstChar == '^') { |
| // ignoring the hash which an annotated tag above points to |
| return null; |
| } |
| String hash = null; |
| int i; |
| for (i = 0; i < line.length(); i++) { |
| char c = line.charAt(i); |
| if (!Character.isLetterOrDigit(c)) { |
| hash = line.substring(0, i); |
| break; |
| } |
| } |
| if (hash == null) { |
| LOG.warn("Ignoring invalid packed-refs line: [" + line + "]"); |
| return null; |
| } |
| |
| String branch = null; |
| int start = i; |
| if (start < line.length() && line.charAt(start++) == ' ') { |
| for (i = start; i < line.length(); i++) { |
| char c = line.charAt(i); |
| if (Character.isWhitespace(c)) { |
| break; |
| } |
| } |
| branch = line.substring(start, i); |
| } |
| |
| if (branch == null) { |
| LOG.warn("Ignoring invalid packed-refs line: [" + line + "]"); |
| return null; |
| } |
| return new HashAndName(shortBuffer(hash.trim()), shortBuffer(branch)); |
| } |
| |
| @NotNull |
| private static String shortBuffer(String raw) { |
| return new String(raw); |
| } |
| |
| private static class HashAndName { |
| private final String hash; |
| private final String name; |
| |
| public HashAndName(String hash, String name) { |
| this.hash = hash; |
| this.name = name; |
| } |
| } |
| |
| /** |
| * Container to hold two information items: current .git/HEAD value and is Git on branch. |
| */ |
| private static class Head { |
| @NotNull private final String ref; |
| private final boolean isBranch; |
| |
| Head(boolean branch, @NotNull String ref) { |
| isBranch = branch; |
| this.ref = ref; |
| } |
| } |
| |
| |
| } |