blob: a2188b74bcda9d0c37c0d93ce6aed9b76e8c7875 [file] [log] [blame]
/*
* 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.status;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.VcsDirtyScope;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitContentRevision;
import git4idea.GitFormatException;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommand;
import git4idea.commands.GitHandler;
import git4idea.commands.GitSimpleHandler;
import git4idea.repo.GitRepository;
import git4idea.repo.GitUntrackedFilesHolder;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* <p>
* Collects changes from the Git repository in the given {@link com.intellij.openapi.vcs.changes.VcsDirtyScope}
* by calling {@code 'git status --porcelain -z'} on it.
* Works only on Git 1.7.0 and later.
* </p>
* <p>
* The class is immutable: collect changes and get the instance from where they can be retrieved by {@link #collect}.
* </p>
*
* @author Kirill Likhodedov
*/
class GitNewChangesCollector extends GitChangesCollector {
private static final Logger LOG = Logger.getInstance(GitNewChangesCollector.class);
private final GitRepository myRepository;
private final Collection<Change> myChanges = new HashSet<Change>();
private final Set<VirtualFile> myUnversionedFiles = new HashSet<VirtualFile>();
@NotNull private final Git myGit;
/**
* Collects the changes from git command line and returns the instance of GitNewChangesCollector from which these changes can be retrieved.
* This may be lengthy.
*/
@NotNull
static GitNewChangesCollector collect(@NotNull Project project, @NotNull Git git, @NotNull ChangeListManager changeListManager,
@NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs,
@NotNull VcsDirtyScope dirtyScope, @NotNull VirtualFile vcsRoot) throws VcsException {
return new GitNewChangesCollector(project, git, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
}
@Override
@NotNull
Collection<VirtualFile> getUnversionedFiles() {
return myUnversionedFiles;
}
@NotNull
@Override
Collection<Change> getChanges() {
return myChanges;
}
private GitNewChangesCollector(@NotNull Project project, @NotNull Git git, @NotNull ChangeListManager changeListManager,
@NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs,
@NotNull VcsDirtyScope dirtyScope, @NotNull VirtualFile vcsRoot) throws VcsException
{
super(project, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
myGit = git;
myRepository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(vcsRoot);
Collection<FilePath> dirtyPaths = dirtyPaths(true);
if (!dirtyPaths.isEmpty()) {
collectChanges(dirtyPaths);
collectUnversionedFiles();
}
}
// calls 'git status' and parses the output, feeding myChanges.
private void collectChanges(Collection<FilePath> dirtyPaths) throws VcsException {
GitSimpleHandler handler = statusHandler(dirtyPaths);
String output = handler.run();
parseOutput(output, handler);
}
private void collectUnversionedFiles() throws VcsException {
if (myRepository == null) {
// if GitRepository was not initialized at the time of creation of the GitNewChangesCollector => collecting unversioned files by hands.
myUnversionedFiles.addAll(myGit.untrackedFiles(myProject, myVcsRoot, null));
} else {
GitUntrackedFilesHolder untrackedFilesHolder = myRepository.getUntrackedFilesHolder();
myUnversionedFiles.addAll(untrackedFilesHolder.retrieveUntrackedFiles());
}
}
private GitSimpleHandler statusHandler(Collection<FilePath> dirtyPaths) {
GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.STATUS);
final String[] params = {"--porcelain", "-z", "--untracked-files=no"}; // untracked files are stored separately
handler.addParameters(params);
handler.setSilent(true);
handler.setStdoutSuppressed(true);
handler.endOptions();
handler.addRelativePaths(dirtyPaths);
if (handler.isLargeCommandLine()) {
// if there are too much files, just get all changes for the project
handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.STATUS);
handler.addParameters(params);
handler.setSilent(true);
handler.setStdoutSuppressed(true);
handler.endOptions();
}
return handler;
}
/**
* Parses the output of the 'git status --porcelain -z' command filling myChanges and myUnversionedFiles.
* See <a href=http://www.kernel.org/pub/software/scm/git/docs/git-status.html#_output">Git man</a> for details.
*/
// handler is here for debugging purposes in the case of parse error
private void parseOutput(@NotNull String output, @NotNull GitHandler handler) throws VcsException {
VcsRevisionNumber head = getHead();
final String[] split = output.split("\u0000");
for (int pos = 0; pos < split.length; pos++) {
String line = split[pos];
if (StringUtil.isEmptyOrSpaces(line)) { // skip empty lines if any (e.g. the whole output may be empty on a clean working tree).
continue;
}
// format: XY_filename where _ stands for space.
if (line.length() < 4) { // X, Y, space and at least one symbol for the file
throwGFE("Line is too short.", handler, output, line, '0', '0');
}
final String xyStatus = line.substring(0, 2);
final String filepath = line.substring(3); // skipping the space
final char xStatus = xyStatus.charAt(0);
final char yStatus = xyStatus.charAt(1);
switch (xStatus) {
case ' ':
if (yStatus == 'M') {
reportModified(filepath, head);
} else if (yStatus == 'D') {
reportDeleted(filepath, head);
} else if (yStatus == 'T') {
reportTypeChanged(filepath, head);
} else if (yStatus == 'U') {
reportConflict(filepath, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'M':
if (yStatus == ' ' || yStatus == 'M' || yStatus == 'T') {
reportModified(filepath, head);
} else if (yStatus == 'D') {
reportDeleted(filepath, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'C':
//noinspection AssignmentToForLoopParameter
pos += 1; // read the "from" filepath which is separated also by NUL character.
// NB: no "break" here!
// we treat "Copy" as "Added", but we still have to read the old path not to break the format parsing.
case 'A':
if (yStatus == 'M' || yStatus == ' ' || yStatus == 'T') {
reportAdded(filepath);
} else if (yStatus == 'D') {
// added + deleted => no change (from IDEA point of view).
} else if (yStatus == 'U' || yStatus == 'A') { // AU - unmerged, added by us; AA - unmerged, both added
reportConflict(filepath, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'D':
if (yStatus == 'M' || yStatus == ' ' || yStatus == 'T') {
reportDeleted(filepath, head);
} else if (yStatus == 'U') { // DU - unmerged, deleted by us
reportConflict(filepath, head);
} else if (yStatus == 'D') { // DD - unmerged, both deleted
// TODO
// currently not displaying, because "both deleted" conflicts can't be handled by our conflict resolver.
// see IDEA-63156
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'U':
if (yStatus == 'U' || yStatus == 'A' || yStatus == 'D' || yStatus == 'T') {
// UU - unmerged, both modified; UD - unmerged, deleted by them; UA - umerged, added by them
reportConflict(filepath, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'R':
//noinspection AssignmentToForLoopParameter
pos += 1; // read the "from" filepath which is separated also by NUL character.
String oldFilename = split[pos];
if (yStatus == 'D') {
reportDeleted(filepath, head);
} else if (yStatus == ' ' || yStatus == 'M' || yStatus == 'T') {
reportRename(filepath, oldFilename, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case 'T'://TODO
if (yStatus == ' ' || yStatus == 'M') {
reportTypeChanged(filepath, head);
} else if (yStatus == 'D') {
reportDeleted(filepath, head);
} else {
throwYStatus(output, handler, line, xStatus, yStatus);
}
break;
case '?':
throwGFE("Unexpected unversioned file flag.", handler, output, line, xStatus, yStatus);
break;
case '!':
throwGFE("Unexpected ignored file flag.", handler, output, line, xStatus, yStatus);
default:
throwGFE("Unexpected symbol as xStatus.", handler, output, line, xStatus, yStatus);
}
}
}
@NotNull
private VcsRevisionNumber getHead() throws VcsException {
if (myRepository != null) {
// we force update the GitRepository, because update is asynchronous, and thus the GitChangeProvider may be asked for changes
// before the GitRepositoryUpdater has captures the current revision change and has updated the GitRepository.
myRepository.update();
final String rev = myRepository.getCurrentRevision();
return rev != null ? new GitRevisionNumber(rev) : VcsRevisionNumber.NULL;
} else {
// this may happen on the project startup, when GitChangeProvider may be queried before GitRepository has been initialized.
LOG.info("GitRepository is null for root " + myVcsRoot);
return getHeadFromGit();
}
}
@NotNull
private VcsRevisionNumber getHeadFromGit() throws VcsException {
VcsRevisionNumber nativeHead = VcsRevisionNumber.NULL;
try {
nativeHead = GitChangeUtils.resolveReference(myProject, myVcsRoot, "HEAD");
}
catch (VcsException e) {
if (!GitChangeUtils.isHeadMissing(e)) { // fresh repository
throw e;
}
}
return nativeHead;
}
private static void throwYStatus(String output, GitHandler handler, String line, char xStatus, char yStatus) {
throwGFE("Unexpected symbol as yStatus.", handler, output, line, xStatus, yStatus);
}
private static void throwGFE(String message, GitHandler handler, String output, String line, char xStatus, char yStatus) {
throw new GitFormatException(String.format("%s\n xStatus=[%s], yStatus=[%s], line=[%s], \n" +
"handler:\n%s\n output: \n%s",
message, xStatus, yStatus, line.replace('\u0000', '!'), handler, output));
}
private void reportModified(String filepath, VcsRevisionNumber head) throws VcsException {
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, false, true, false);
ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, false, false);
reportChange(FileStatus.MODIFIED, before, after);
}
private void reportTypeChanged(String filepath, VcsRevisionNumber head) throws VcsException {
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, false, true, false);
ContentRevision after = GitContentRevision.createRevisionForTypeChange(myProject, myVcsRoot, filepath, null, false);
reportChange(FileStatus.MODIFIED, before, after);
}
private void reportAdded(String filepath) throws VcsException {
ContentRevision before = null;
ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, false, false);
reportChange(FileStatus.ADDED, before, after);
}
private void reportDeleted(String filepath, VcsRevisionNumber head) throws VcsException {
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, true, true, false);
ContentRevision after = null;
reportChange(FileStatus.DELETED, before, after);
}
private void reportRename(String filepath, String oldFilename, VcsRevisionNumber head) throws VcsException {
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, oldFilename, head, myProject, true, true, false);
ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, false, false);
reportChange(FileStatus.MODIFIED, before, after);
}
private void reportConflict(String filepath, VcsRevisionNumber head) throws VcsException {
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, false, true, false);
ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, false, false);
reportChange(FileStatus.MERGED_WITH_CONFLICTS, before, after);
}
private void reportChange(FileStatus status, ContentRevision before, ContentRevision after) {
myChanges.add(new Change(before, after, status));
}
}