| /* |
| * Copyright 2000-2009 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.merge; |
| |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.vcs.VcsException; |
| import com.intellij.openapi.vcs.VcsKey; |
| import com.intellij.openapi.vcs.update.FileGroup; |
| import com.intellij.openapi.vcs.update.UpdatedFiles; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import git4idea.GitRevisionNumber; |
| import git4idea.GitUtil; |
| import git4idea.GitVcs; |
| import git4idea.commands.GitCommand; |
| import git4idea.commands.GitSimpleHandler; |
| import git4idea.repo.GitRepositoryFiles; |
| import git4idea.util.StringScanner; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.*; |
| |
| /** |
| * Collect change for merge or pull operations |
| */ |
| public class MergeChangeCollector { |
| private final HashSet<String> myUnmergedPaths = new HashSet<String>(); |
| private final Project myProject; |
| private final VirtualFile myRoot; |
| private final GitRevisionNumber myStart; // Revision number before update (used for diff) |
| |
| public MergeChangeCollector(final Project project, final VirtualFile root, final GitRevisionNumber start) { |
| myStart = start; |
| myProject = project; |
| myRoot = root; |
| } |
| |
| /** |
| * Collects changed files during or after merge operation to the supplied <code>updates</code> container. |
| */ |
| public void collect(final UpdatedFiles updates, List<VcsException> exceptions) { |
| try { |
| // collect unmerged |
| Set<String> paths = getUnmergedPaths(); |
| addAll(updates, FileGroup.MERGED_WITH_CONFLICT_ID, paths); |
| |
| // collect other changes (ignoring unmerged) |
| TreeSet<String> updated = new TreeSet<String>(); |
| TreeSet<String> created = new TreeSet<String>(); |
| TreeSet<String> removed = new TreeSet<String>(); |
| |
| String revisionsForDiff = getRevisionsForDiff(); |
| if (revisionsForDiff == null) { |
| return; |
| } |
| getChangedFilesExceptUnmerged(updated, created, removed, revisionsForDiff); |
| addAll(updates, FileGroup.UPDATED_ID, updated); |
| addAll(updates, FileGroup.CREATED_ID, created); |
| addAll(updates, FileGroup.REMOVED_FROM_REPOSITORY_ID, removed); |
| } catch (VcsException e) { |
| exceptions.add(e); |
| } |
| } |
| |
| /** |
| * Returns absolute paths to files which are currently unmerged, and also populates myUnmergedPaths with relative paths. |
| */ |
| public @NotNull Set<String> getUnmergedPaths() throws VcsException { |
| String root = myRoot.getPath(); |
| final GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.LS_FILES); |
| h.setSilent(true); |
| h.addParameters("--unmerged"); |
| final String result = h.run(); |
| |
| final Set<String> paths = new HashSet<String>(); |
| for (StringScanner s = new StringScanner(result); s.hasMoreData();) { |
| if (s.isEol()) { |
| s.nextLine(); |
| continue; |
| } |
| s.boundedToken('\t'); |
| final String relative = s.line(); |
| if (!myUnmergedPaths.add(relative)) { |
| continue; |
| } |
| String path = root + "/" + GitUtil.unescapePath(relative); |
| paths.add(path); |
| } |
| return paths; |
| } |
| |
| /** |
| * @return The revision range which will be used to find merge diff (merge may be just finished, or in progress) |
| * or null in case of error or inconsistency. |
| */ |
| @Nullable |
| public String getRevisionsForDiff() throws VcsException { |
| String root = myRoot.getPath(); |
| GitRevisionNumber currentHead = GitRevisionNumber.resolve(myProject, myRoot, "HEAD"); |
| if (currentHead.equals(myStart)) { |
| // The head has not advanced. This means that this is a merge that did not commit. |
| // This could be caused by --no-commit option or by failed two-head merge. The MERGE_HEAD |
| // should be available. In case of --no-commit option, the MERGE_HEAD might contain |
| // multiple heads separated by newline. The changes are collected separately for each head |
| // and they are merged using TreeSet class (that also sorts the changes). |
| File mergeHeadsFile = new File(root, GitRepositoryFiles.GIT_MERGE_HEAD); |
| try { |
| if (mergeHeadsFile.exists()) { |
| String mergeHeads = new String(FileUtil.loadFileText(mergeHeadsFile, GitUtil.UTF8_ENCODING)); |
| for (StringScanner s = new StringScanner(mergeHeads); s.hasMoreData();) { |
| String head = s.line(); |
| if (head.length() == 0) { |
| continue; |
| } |
| // note that "..." cause the diff to start from common parent between head and merge head |
| return myStart.getRev() + "..." + head; |
| } |
| } |
| } catch (IOException e) { |
| //noinspection ThrowableInstanceNeverThrown |
| throw new VcsException("Unable to read the file " + mergeHeadsFile + ": " + e.getMessage(), e); |
| } |
| } else { |
| // Otherwise this is a merge that did created a commit. And because of this the incoming changes |
| // are diffs between old head and new head. The commit could have been multihead commit, |
| // and the expression below considers it as well. |
| return myStart.getRev() + "..HEAD"; |
| } |
| return null; |
| } |
| |
| /** |
| * Populates the supplied collections of modified, created and removed files returned by 'git diff #revisions' command, |
| * where revisions is the range of revisions to check. |
| */ |
| public void getChangedFilesExceptUnmerged(Collection<String> updated, Collection<String> created, Collection<String> removed, String revisions) |
| throws VcsException { |
| if (revisions == null) { |
| return; |
| } |
| String root = myRoot.getPath(); |
| GitSimpleHandler h = new GitSimpleHandler(myProject, myRoot, GitCommand.DIFF); |
| h.setSilent(true); |
| // note that moves are not detected here |
| h.addParameters("--name-status", "--diff-filter=ADMRUX", revisions); |
| for (StringScanner s = new StringScanner(h.run()); s.hasMoreData();) { |
| if (s.isEol()) { |
| s.nextLine(); |
| continue; |
| } |
| char status = s.peek(); |
| s.boundedToken('\t'); |
| final String relative = s.line(); |
| // eliminate conflicts |
| if (myUnmergedPaths.contains(relative)) { |
| continue; |
| } |
| String path = root + "/" + GitUtil.unescapePath(relative); |
| switch (status) { |
| case 'M': |
| updated.add(path); |
| break; |
| case 'A': |
| created.add(path); |
| break; |
| case 'D': |
| removed.add(path); |
| break; |
| default: |
| throw new IllegalStateException("Unexpected status: " + status); |
| } |
| } |
| } |
| |
| /** |
| * Add all paths to the group |
| */ |
| private static void addAll(final UpdatedFiles updates, String group_id, Set<String> paths) { |
| FileGroup fileGroup = updates.getGroupById(group_id); |
| final VcsKey vcsKey = GitVcs.getKey(); |
| for (String path : paths) { |
| fileGroup.add(path, vcsKey, null); |
| } |
| } |
| } |