/* * 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 org.community.intellij.plugins.communitycase.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.history.VcsRevisionNumber; import com.intellij.openapi.vcs.update.FileGroup; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.Vcs; import org.community.intellij.plugins.communitycase.commands.Command; import org.community.intellij.plugins.communitycase.commands.SimpleHandler; import org.community.intellij.plugins.communitycase.commands.StringScanner; import org.community.intellij.plugins.communitycase.history.HistoryUtils; import java.io.File; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.TreeSet; /** * Collect change for merge or pull operations */ public class MergeChangeCollector { /** * Unmerged paths */ private final HashSet<String> myUnmergedPaths = new HashSet<String>(); /** * The context project */ private final Project myProject; /** * The git root */ private final VirtualFile myRoot; /** * Updates container */ private final UpdatedFiles myUpdates; /** * Revision number before update (used for diff) */ private final VcsRevisionNumber myStart; /** * A constructor * * @param project the context project * @param root the git root * @param start the start revision * @param updates a container for updates */ public MergeChangeCollector(final Project project, final VirtualFile root, final VcsRevisionNumber start, final UpdatedFiles updates) { myStart = start; myProject = project; myRoot = root; myUpdates = updates; } /** * Collect changes * * @param exceptions a list of exceptions */ public void collect(List<VcsException> exceptions) { final VcsKey vcsKey = Vcs.getKey(); try { // collect unmerged String root = myRoot.getPath(); SimpleHandler h = new SimpleHandler(myProject, myRoot, Command.LS_FILES); h.setRemote(true); h.setSilent(true); h.addParameters("--unmerged"); for (StringScanner s = new StringScanner(h.run()); s.hasMoreData();) { if (s.isEol()) { s.nextLine(); continue; } s.boundedToken('\t'); final String relative = s.line(); if (!myUnmergedPaths.add(relative)) { continue; } String path = root + "/" + Util.unescapePath(relative); myUpdates.getGroupById(FileGroup.MERGED_WITH_CONFLICT_ID).add(path, vcsKey, null); } VcsRevisionNumber currentHead=HistoryUtils.getCurrentRevision(myProject,myRoot); // collect other changes (ignoring unmerged) TreeSet<String> updated = new TreeSet<String>(); TreeSet<String> created = new TreeSet<String>(); TreeSet<String> removed = new TreeSet<String>(); 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, ".git/MERGE_HEAD"); try { if (mergeHeadsFile.exists()) { String mergeHeads = new String(FileUtil.loadFileText(mergeHeadsFile, Util.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 processDiff(root, updated, created, removed, myStart.asString() + "..." + head); } } } catch (IOException e) { //noinspection ThrowableInstanceNeverThrown exceptions.add(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. processDiff(root, updated, created, removed, myStart.asString() + "..HEAD"); } addAll(FileGroup.UPDATED_ID, updated); addAll(FileGroup.CREATED_ID, created); addAll(FileGroup.REMOVED_FROM_REPOSITORY_ID, removed); } catch (VcsException e) { exceptions.add(e); } } /** * Process diff * * @param root the vcs root * @param updated the set of updated files * @param created the set of created files * @param removed the set of removed files * @param revisions the diff expressions * @throws VcsException if there is a problem with running git */ private void processDiff(String root, TreeSet<String> updated, TreeSet<String> created, TreeSet<String> removed, String revisions) throws VcsException { SimpleHandler h = new SimpleHandler(myProject, myRoot, Command.DIFF); h.setSilent(true); h.setRemote(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 + "/" + Util.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 * * @param id the group identifier * @param paths the set of paths */ private void addAll(String id, TreeSet<String> paths) { FileGroup fileGroup = myUpdates.getGroupById(id); final VcsKey vcsKey = Vcs.getKey(); for (String path : paths) { fileGroup.add(path, vcsKey, null); } } }