blob: 17ef984c65577f4661f6778d7727796cb9f7acfa [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.status;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
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.vfs.VirtualFile;
import git4idea.GitContentRevision;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.GitCommand;
import git4idea.commands.GitSimpleHandler;
import git4idea.util.StringScanner;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* <p>
* Collects changes from the Git repository in the specified {@link com.intellij.openapi.vcs.changes.VcsDirtyScope}
* using the older technique that is replaced by {@link GitNewChangesCollector} for Git later than 1.7.0 inclusive.
* This class is used for Git older than 1.7.0 not inclusive, that don't have <code>'git status --porcelain'</code>.
* </p>
* <p>
* The method used by this class is less efficient and more error-prone than {@link GitNewChangesCollector} method.
* Thus this class is considered as a legacy code for Git 1.6.*. Read further for the implementation details and the ground for
* transferring to use {@code 'git status --porcelain'}.
* </p>
* <p>
* The following Git commands are called to get the changes, i.e. the state of the working tree combined with the state of index.
* <ul>
* <li>
* <b><code>'git update-index --refresh'</code></b> (called on the whole repository) - probably unnecessary (especially before 'git diff'),
* but is left not to break some older Gits occasionally. See the following links for some details:
* <a href="http://us.generation-nt.com/answer/bug-596126-git-status-does-not-refresh-index-fixed-since-1-7-1-1-please-consider-upgrading-1-7-1-2-squeeze-help-200234171.html">
* gitk doesn't refresh the index statinfo</a>;
* <a href="http://thread.gmane.org/gmane.comp.version-control.git/144176/focus">
* "Most git porcelain silently refreshes stat-dirty index entries"</a>;
* <a href="https://git.wiki.kernel.org/index.php/GitFaq#Can_I_import_from_tar_files_.28archives.29.3">update-index to import from tar files</a>.
* </li>
* <li>
* <b><code>'git ls-files --unmerged'</code></b> (called on the whole repository) - to get the list of unmerged files.
* It is not clear why it should be called on the whole repository. The decision to call it on the whole repository was made in
* <code>45687fe "<a href="http://youtrack.jetbrains.net/issue/IDEA-50573">IDEADEV-40577</a>: The ignored unmerged files are now reported"</code>,
* but neither the rollback & test, nor the analysis didn't recover the need for that. It is left however, since it is a legacy code.
* </li>
* <li>
* <b><code>'git ls-files --others --exclude-standard'</code></b> (called on the dirty scope) - to get the list of unversioned files.
* Note that this command is the only way to get the list of unversioned files, besides <code>'git status'</code>.
* </li>
* <li>
* <b><code>'git diff --name-status -M HEAD -- </code></b> (called on the dirty scope) - to get all other changes (except unversioned and
* unmerged).
* Note that there is also no way to get all tracked changes by a single command (except <code>'git status'</code>), since
* <code>'git diff'</code> returns either only not-staged changes, either (<code>'git diff HEAD'</code>) treats unmerged as modified.
* </li>
* </ul>
* </p>
* <p>
* <b>Performance measurement</b>
* was performed on a large repository (like IntelliJ IDEA), on a single machine, after several "warm-ups" when <code>'git status'</code> duration
* stabilizes.
* For the whole repository:
* <code>'git status'</code> takes ~ 1300 ms while these 4 commands take ~ 1870 ms
* ('update-index' ~ 270 ms, 'ls-files --unmerged' ~ 46 ms, 'ls files --others' ~ 820 ms, 'diff' ~ 650 ms)
* ; for a single file:
* <code>'git status'</code> takes ~ 375 ms, these 4 commands take ~ 750 ms.
* </p>
* <p>
* The class is immutable: collect changes and get the instance from where they can be retrieved by {@link #collect}.
* </p>
*
* @author Constantine Plotnikov
* @author Kirill Likhodedov
*/
class GitOldChangesCollector extends GitChangesCollector {
private final List<VirtualFile> myUnversioned = new ArrayList<VirtualFile>(); // Unversioned files
private final Set<String> myUnmergedNames = new HashSet<String>(); // Names of unmerged files
private final List<Change> myChanges = new ArrayList<Change>(); // all changes
/**
* 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 GitOldChangesCollector collect(@NotNull Project project, @NotNull ChangeListManager changeListManager,
@NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs,
@NotNull VcsDirtyScope dirtyScope, @NotNull VirtualFile vcsRoot) throws VcsException {
return new GitOldChangesCollector(project, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
}
@NotNull
@Override
Collection<VirtualFile> getUnversionedFiles() {
return myUnversioned;
}
@NotNull
@Override
Collection<Change> getChanges(){
return myChanges;
}
private GitOldChangesCollector(@NotNull Project project, @NotNull ChangeListManager changeListManager,
@NotNull ProjectLevelVcsManager vcsManager, @NotNull AbstractVcs vcs, @NotNull VcsDirtyScope dirtyScope,
@NotNull VirtualFile vcsRoot) throws VcsException {
super(project, changeListManager, vcsManager, vcs, dirtyScope, vcsRoot);
updateIndex();
collectUnmergedAndUnversioned();
collectDiffChanges();
}
private void updateIndex() throws VcsException {
GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.UPDATE_INDEX);
handler.addParameters("--refresh", "--ignore-missing");
handler.setSilent(true);
handler.setStdoutSuppressed(true);
handler.ignoreErrorCode(1);
handler.run();
}
/**
* Collect diff with head
*
* @throws VcsException if there is a problem with running git
*/
private void collectDiffChanges() throws VcsException {
Collection<FilePath> dirtyPaths = dirtyPaths(true);
if (dirtyPaths.isEmpty()) {
return;
}
try {
String output = GitChangeUtils.getDiffOutput(myProject, myVcsRoot, "HEAD", dirtyPaths);
GitChangeUtils.parseChanges(myProject, myVcsRoot, null, GitChangeUtils.resolveReference(myProject, myVcsRoot, "HEAD"), output, myChanges,
myUnmergedNames);
}
catch (VcsException ex) {
if (!GitChangeUtils.isHeadMissing(ex)) {
throw ex;
}
GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
handler.addParameters("--cached");
handler.setSilent(true);
handler.setStdoutSuppressed(true);
// During init diff does not works because HEAD
// will appear only after the first commit.
// In that case added files are cached in index.
String output = handler.run();
if (output.length() > 0) {
StringTokenizer tokenizer = new StringTokenizer(output, "\n\r");
while (tokenizer.hasMoreTokens()) {
final String s = tokenizer.nextToken();
Change ch = new Change(null, GitContentRevision.createRevision(myVcsRoot, s, null, myProject, false, false, true), FileStatus.ADDED);
myChanges.add(ch);
}
}
}
}
/**
* Collect unversioned and unmerged files
*
* @throws VcsException if there is a problem with running git
*/
private void collectUnmergedAndUnversioned() throws VcsException {
Collection<FilePath> dirtyPaths = dirtyPaths(false);
if (dirtyPaths.isEmpty()) {
return;
}
// prepare handler
GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
handler.addParameters("-v", "--unmerged");
handler.setSilent(true);
handler.setStdoutSuppressed(true);
// run handler and collect changes
parseFiles(handler.run());
// prepare handler
handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
handler.addParameters("-v", "--others", "--exclude-standard");
handler.setSilent(true);
handler.setStdoutSuppressed(true);
handler.endOptions();
handler.addRelativePaths(dirtyPaths);
if(handler.isLargeCommandLine()) {
handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
handler.addParameters("-v", "--others", "--exclude-standard");
handler.setSilent(true);
handler.setStdoutSuppressed(true);
handler.endOptions();
}
// run handler and collect changes
parseFiles(handler.run());
}
private void parseFiles(String list) throws VcsException {
for (StringScanner sc = new StringScanner(list); sc.hasMoreData();) {
if (sc.isEol()) {
sc.nextLine();
continue;
}
char status = sc.peek();
sc.skipChars(2);
if ('?' == status) {
VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
if (Comparing.equal(GitUtil.gitRootOrNull(file), myVcsRoot)) {
myUnversioned.add(file);
}
}
else { //noinspection HardCodedStringLiteral
if ('M' == status) {
sc.boundedToken('\t');
String file = GitUtil.unescapePath(sc.line());
VirtualFile vFile = myVcsRoot.findFileByRelativePath(file);
if (!Comparing.equal(GitUtil.gitRootOrNull(vFile), myVcsRoot)) {
continue;
}
if (!myUnmergedNames.add(file)) {
continue;
}
// assume modify-modify conflict
ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, false, true,
true);
ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false, false, true);
myChanges.add(new Change(before, after, FileStatus.MERGED_WITH_CONFLICTS));
}
else {
throw new VcsException("Unsupported type of the merge conflict detected: " + status);
}
}
}
}
}