blob: c72e738851fac944ef420dde399e09b689193d6e [file] [log] [blame]
/*
* 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);
}
}
}