From 8e0fbc72f2f2d8d95b6546d56df8b15867d90ebd Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Mon, 25 Jul 2011 19:34:37 +0400 Subject: [PATCH] GitChangeProvider optimization (git status --porcelain) 1. Introduce GitNewChangesCollector that calls "git status --porcelain -z" to get all changes in a single command. New format is parsed, an unchecked GitFormatException is thrown in the case of parse error. 2. ChangeCollector -> GitOldChangesCollector. Made immutable, removed all the stuff controlling single and atomic state change. Added massive javadoc with the analysis of used git commands and performance measurement. 3. GitChangeProvider chooses the GitChangesCollector based on the GitVersionSpecialty.KNOWS_STATUS_PORCELAIN (since 1.7.0) 4. GitChangeProviderTest: +testUnversionedFiles. --- .../src/git4idea/GitFormatException.java | 29 ++ .../git4idea/changes/GitChangeProvider.java | 36 ++- .../git4idea/changes/GitChangesCollector.java | 139 +++++++++ .../changes/GitNewChangesCollector.java | 275 ++++++++++++++++++ ...ector.java => GitOldChangesCollector.java} | 195 +++++-------- .../src/git4idea/commands/GitCommand.java | 1 + .../git4idea/config/GitVersionSpecialty.java | 12 + .../git4idea/tests/GitChangeProviderTest.java | 14 + 8 files changed, 567 insertions(+), 134 deletions(-) create mode 100644 plugins/git4idea/src/git4idea/GitFormatException.java create mode 100644 plugins/git4idea/src/git4idea/changes/GitChangesCollector.java create mode 100644 plugins/git4idea/src/git4idea/changes/GitNewChangesCollector.java rename plugins/git4idea/src/git4idea/changes/{ChangeCollector.java => GitOldChangesCollector.java} (60%) diff --git a/plugins/git4idea/src/git4idea/GitFormatException.java b/plugins/git4idea/src/git4idea/GitFormatException.java new file mode 100644 index 000000000000..c2c7ef8d0cf5 --- /dev/null +++ b/plugins/git4idea/src/git4idea/GitFormatException.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * This exception is thrown during parsing of a Git command output, in the case of unexpected output format. + * The exception is unchecked: if it happens, it is either a format that we don't handle yet (and it should be fixed then), or an error + * in a specific situation (which also should be handled). + * + * @author Kirill Likhodedov + */ +public class GitFormatException extends RuntimeException { + public GitFormatException(String message) { + super(message); + } +} diff --git a/plugins/git4idea/src/git4idea/changes/GitChangeProvider.java b/plugins/git4idea/src/git4idea/changes/GitChangeProvider.java index 2427e4477c9b..71411c70b982 100644 --- a/plugins/git4idea/src/git4idea/changes/GitChangeProvider.java +++ b/plugins/git4idea/src/git4idea/changes/GitChangeProvider.java @@ -38,6 +38,8 @@ import git4idea.GitContentRevision; import git4idea.GitRevisionNumber; import git4idea.GitUtil; import git4idea.GitVcs; +import git4idea.config.GitVersion; +import git4idea.config.GitVersionSpecialty; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -62,15 +64,12 @@ public class GitChangeProvider implements ChangeProvider { myVcsManager = vcsManager; } - /** - * {@inheritDoc} - */ @Override public void getChanges(final VcsDirtyScope dirtyScope, final ChangelistBuilder builder, final ProgressIndicator progress, final ChangeListManagerGate addGate) throws VcsException { - + final Collection affected = dirtyScope.getAffectedContentRootsWithCheck(); if (dirtyScope.getAffectedContentRoots().size() != affected.size()) { final Set set = new HashSet(affected); @@ -85,13 +84,14 @@ public class GitChangeProvider implements ChangeProvider { final MyNonChangedHolder holder = new MyNonChangedHolder(myProject, dirtyScope.getDirtyFilesNoExpand(), addGate, myFileDocumentManager, myVcsManager); for (VirtualFile root : roots) { - ChangeCollector c = new ChangeCollector(myProject, myChangeListManager, dirtyScope, root); - final Collection changes = c.changes(); - holder.changed(changes); - for (Change file : changes) { + GitChangesCollector collector = isNewGitChangeProviderAvailable() + ? GitNewChangesCollector.collect(myProject, myChangeListManager, dirtyScope, root) + : GitOldChangesCollector.collect(myProject, myChangeListManager, dirtyScope, root); + holder.changed(collector.getChanges()); + for (Change file : collector.getChanges()) { builder.processChange(file, GitVcs.getKey()); } - for (VirtualFile f : c.unversioned()) { + for (VirtualFile f : collector.getUnversionedFiles()) { builder.processUnversionedFile(f); holder.unversioned(f); } @@ -105,6 +105,18 @@ public class GitChangeProvider implements ChangeProvider { } } + private boolean isNewGitChangeProviderAvailable() { + GitVcs vcs = GitVcs.getInstance(myProject); + if (vcs == null) { + return false; + } + final GitVersion version = vcs.getVersion(); + if (version == null) { + return false; + } + return GitVersionSpecialty.KNOWS_STATUS_PORCELAIN.existsIn(version); + } + private static class MyNonChangedHolder { private final Project myProject; private final Set myDirty; @@ -163,16 +175,10 @@ public class GitChangeProvider implements ChangeProvider { } } - /** - * {@inheritDoc} - */ public boolean isModifiedDocumentTrackingRequired() { return true; } - /** - * {@inheritDoc} - */ public void doCleanup(final List files) { } } diff --git a/plugins/git4idea/src/git4idea/changes/GitChangesCollector.java b/plugins/git4idea/src/git4idea/changes/GitChangesCollector.java new file mode 100644 index 000000000000..a3e698f07de5 --- /dev/null +++ b/plugins/git4idea/src/git4idea/changes/GitChangesCollector.java @@ -0,0 +1,139 @@ +/* + * 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.changes; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.FilePathImpl; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vcs.changes.VcsDirtyScope; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.*; + +/** + * Common + * Serves as a container of common utility functions to collect dirty paths for both {@link GitNewChangesCollector} and + * {@link GitOldChangesCollector}. + * + * @author Kirill Likhodedov + */ +abstract class GitChangesCollector { + protected final Project myProject; + protected final VirtualFile myVcsRoot; + private final ChangeListManager myChangeListManager; + private final VcsDirtyScope myDirtyScope; + + + GitChangesCollector(final Project project, + ChangeListManager changeListManager, + VcsDirtyScope dirtyScope, + final VirtualFile vcsRoot) { + myProject = project; + myChangeListManager = changeListManager; + myDirtyScope = dirtyScope; + myVcsRoot = vcsRoot; + } + + /** + * @return the set of unversioned files (from the specified dirty scope). + */ + abstract @NotNull Collection getUnversionedFiles(); + + /** + * @return the set of changes (changed files) from the specified dirty scope. + */ + abstract @NotNull Collection getChanges(); + + /** + * Collect dirty file paths + * + * @param includeChanges if true, previous changes are included in collection + * @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit + */ + protected Collection dirtyPaths(boolean includeChanges) { + final List allPaths = new ArrayList(); + + for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) { + addToPaths(p, allPaths); + } + for (FilePath p : myDirtyScope.getDirtyFilesNoExpand()) { + addToPaths(p, allPaths); + } + + if (includeChanges) { + try { + for (Change c : myChangeListManager.getChangesIn(myVcsRoot)) { + switch (c.getType()) { + case NEW: + case DELETED: + case MOVED: + if (c.getAfterRevision() != null) { + addToPaths(c.getAfterRevision().getFile(), allPaths); + } + if (c.getBeforeRevision() != null) { + addToPaths(c.getBeforeRevision().getFile(), allPaths); + } + case MODIFICATION: + default: + // do nothing + } + } + } + catch (Exception t) { + // ignore exceptions + } + } + + removeCommonParents(allPaths); + + final List paths = new ArrayList(allPaths.size()); + for (String p : allPaths) { + final File file = new File(p); + paths.add(new FilePathImpl(file, file.isDirectory())); + } + return paths; + } + + protected void addToPaths(FilePath pathToAdd, List paths) { + File file = pathToAdd.getIOFile(); + if (myVcsRoot.equals(GitUtil.getGitRootOrNull(file))) { + paths.add(file.getPath()); + } + } + + protected static void removeCommonParents(List allPaths) { + Collections.sort(allPaths); + + String prevPath = null; + Iterator it = allPaths.iterator(); + while (it.hasNext()) { + String path = it.next(); + if (prevPath != null && FileUtil.startsWith(path, prevPath)) { // the file is under previous file, so enough to check the parent + it.remove(); + } + else { + prevPath = path; + } + } + } + +} diff --git a/plugins/git4idea/src/git4idea/changes/GitNewChangesCollector.java b/plugins/git4idea/src/git4idea/changes/GitNewChangesCollector.java new file mode 100644 index 000000000000..826a38159ec2 --- /dev/null +++ b/plugins/git4idea/src/git4idea/changes/GitNewChangesCollector.java @@ -0,0 +1,275 @@ +/* + * 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.changes; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.FileStatus; +import com.intellij.openapi.vcs.VcsException; +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.GitFormatException; +import git4idea.GitRevisionNumber; +import git4idea.GitUtil; +import git4idea.commands.GitCommand; +import git4idea.commands.GitHandler; +import git4idea.commands.GitSimpleHandler; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashSet; + +/** + *

+ * 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. + *

+ *

+ * The class is immutable: collect changes and get the instance from where they can be retrieved by {@link #collect}. + *

+ * + * @author Kirill Likhodedov + */ +class GitNewChangesCollector extends GitChangesCollector { + + private Collection myChanges = new HashSet(); + private Collection myUnversionedFiles = new HashSet(); + + /** + * 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(final Project project, ChangeListManager changeListManager, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) throws VcsException { + return new GitNewChangesCollector(project, changeListManager, dirtyScope, vcsRoot); + } + + @Override + @NotNull + Collection getUnversionedFiles() { + return myUnversionedFiles; + } + + @NotNull + @Override + Collection getChanges() { + return myChanges; + } + + private GitNewChangesCollector(Project project, ChangeListManager changeListManager, VcsDirtyScope dirtyScope, VirtualFile vcsRoot) + throws VcsException + { + super(project, changeListManager, dirtyScope, vcsRoot); + Collection dirtyPaths = dirtyPaths(true); + if (dirtyPaths.isEmpty()) { + return; + } + + GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.STATUS); + final String[] params = {"--porcelain", "-z", "--untracked-files=all"}; // get all untracked files as a list without collapsing them into dirs. + handler.addParameters(params); + handler.setNoSSH(true); + 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.setNoSSH(true); + handler.setSilent(true); + handler.setStdoutSuppressed(true); + handler.endOptions(); + } + String output = handler.run(); + parseOutput(output, handler); + } + + /** + * Parses the output of the 'git status --porcelain -z' command filling myChanges and myUnversionedFiles. + * See Git man for details. + */ + // handler is here for debugging purposes in the case of parse error + private void parseOutput(String output, GitHandler handler) throws VcsException { + GitRevisionNumber head = null; + try { + head = GitChangeUtils.loadRevision(myProject, myVcsRoot, "HEAD"); // TODO substitute with a call to GitRepository#getCurrentRevision() + } + catch (VcsException e) { + if (!GitChangeUtils.isHeadMissing(e)) { // fresh repository + throw e; + } + } + + 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 { + throwYStatus(output, handler, line, xStatus, yStatus); + } + break; + + case 'M': + if (yStatus == ' ' || yStatus == 'M') { + reportModified(filepath, head); + } else if (yStatus == 'D') { + reportDeleted(filepath, head); + } else { + throwYStatus(output, handler, line, xStatus, yStatus); + } + break; + + case 'A': + case 'C': + if (yStatus == 'M' || yStatus == ' ') { + 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(head, filepath); + } else { + throwYStatus(output, handler, line, xStatus, yStatus); + } + break; + + case 'D': + if (yStatus == 'M' || yStatus == ' ') { + reportDeleted(filepath, head); + } else if (yStatus == 'U') { // DU - unmerged, deleted by us + reportConflict(head, filepath); + } 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') { + // UU - unmerged, both modified; UD - unmerged, deleted by them; UA - umerged, added by them + reportConflict(head, filepath); + } else { + throwYStatus(output, handler, line, xStatus, yStatus); + } + break; + + case 'R': + 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') { + reportRename(head, filepath, oldFilename); + } else { + throwYStatus(output, handler, line, xStatus, yStatus); + } + break; + + case '?': + reportUnversioned(filepath); + break; + + case '!': + throwGFE("Unexpected ignored file flag.", handler, output, line, xStatus, yStatus); + + default: + throwGFE("Unexpected symbol as xStatus.", handler, output, line, xStatus, yStatus); + + } + } + } + + 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, handler, output)); + } + + private void reportUnversioned(String filepath) throws VcsException { + VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(filepath)); + if (GitUtil.gitRootOrNull(file) == myVcsRoot) { // false if we've entered the sub-repository + myUnversionedFiles.add(file); + } + } + + private void reportModified(String filepath, GitRevisionNumber head) throws VcsException { + ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, false, true); + ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, 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); + reportChange(FileStatus.ADDED, before, after); + } + + private void reportDeleted(String filepath, GitRevisionNumber head) throws VcsException { + ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, true, true); + ContentRevision after = null; + reportChange(FileStatus.DELETED, before, after); + } + + private void reportRename(GitRevisionNumber head, String filepath, String oldFilename) throws VcsException { + ContentRevision before = GitContentRevision.createRevision(myVcsRoot, oldFilename, head, myProject, true, true); + ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, false, false); + reportChange(FileStatus.MODIFIED, before, after); + } + + private void reportConflict(GitRevisionNumber head, String filepath) throws VcsException { + ContentRevision before = GitContentRevision.createRevision(myVcsRoot, filepath, head, myProject, false, true); + ContentRevision after = GitContentRevision.createRevision(myVcsRoot, filepath, null, myProject, 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)); + } + +} diff --git a/plugins/git4idea/src/git4idea/changes/ChangeCollector.java b/plugins/git4idea/src/git4idea/changes/GitOldChangesCollector.java similarity index 60% rename from plugins/git4idea/src/git4idea/changes/ChangeCollector.java rename to plugins/git4idea/src/git4idea/changes/GitOldChangesCollector.java index 634d66f7618b..0267ee0c8388 100644 --- a/plugins/git4idea/src/git4idea/changes/ChangeCollector.java +++ b/plugins/git4idea/src/git4idea/changes/GitOldChangesCollector.java @@ -16,9 +16,7 @@ package git4idea.changes; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.FilePath; -import com.intellij.openapi.vcs.FilePathImpl; import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; @@ -32,67 +30,103 @@ import git4idea.GitUtil; import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import git4idea.commands.StringScanner; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.*; /** - * A collector for changes in the Git. It is introduced because changes are not - * cannot be got as a sum of stateless operations. + *

+ * 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 'git status --porcelain'. + *

+ *

+ * 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'}. + *

+ *

+ * The following Git commands are called to get the changes, i.e. the state of the working tree combined with the state of index. + *

    + *
  • + * 'git update-index --refresh' (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: + * + * gitk doesn't refresh the index statinfo; + * + * "Most git porcelain silently refreshes stat-dirty index entries"; + * update-index to import from tar files. + *
  • + *
  • + * 'git ls-files --unmerged' (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 + * 45687fe "IDEADEV-40577: The ignored unmerged files are now reported", + * 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. + *
  • + *
  • + * 'git ls-files --others --exclude-standard' (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 'git status'. + *
  • + *
  • + * 'git diff --name-status -M HEAD -- (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 'git status'), since + * 'git diff' returns either only not-staged changes, either ('git diff HEAD') treats unmerged as modified. + *
  • + *
+ *

+ *

+ * Performance measurement + * was performed on a large repository (like IntelliJ IDEA), on a single machine, after several "warm-ups" when 'git status' duration + * stabilizes. + * For the whole repository: + * 'git status' 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: + * 'git status' takes ~ 375 ms, these 4 commands take ~ 750 ms. + *

+ *

+ * The class is immutable: collect changes and get the instance from where they can be retrieved by {@link #collect}. + *

+ * + * @author Constantine Plotnikov + * @author Kirill Likhodedov */ -class ChangeCollector { - private final Project myProject; - private final ChangeListManager myChangeListManager; - private final VcsDirtyScope myDirtyScope; - private final VirtualFile myVcsRoot; +class GitOldChangesCollector extends GitChangesCollector { private final List myUnversioned = new ArrayList(); // Unversioned files private final Set myUnmergedNames = new HashSet(); // Names of unmerged files private final List myChanges = new ArrayList(); // all changes - private boolean myIsCollected = false; // indicates that collecting changes has been started - private boolean myIsFailed = true; // indicates that collecting changes has been failed. - - public ChangeCollector(final Project project, ChangeListManager changeListManager, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) { - myChangeListManager = changeListManager; - myDirtyScope = dirtyScope; - myVcsRoot = vcsRoot; - myProject = project; - } /** - * Get unversioned files + * Collects the changes from git command line and returns the instance of GitNewChangesCollector from which these changes can be retrieved. + * This may be lengthy. */ - public Collection unversioned() throws VcsException { - ensureCollected(); + @NotNull + static GitOldChangesCollector collect(final Project project, ChangeListManager changeListManager, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) throws VcsException { + return new GitOldChangesCollector(project, changeListManager, dirtyScope, vcsRoot); + } + + @NotNull + @Override + Collection getUnversionedFiles() { return myUnversioned; } - /** - * Get changes - */ - public Collection changes() throws VcsException { - ensureCollected(); + @NotNull + @Override + Collection getChanges(){ return myChanges; } - - /** - * Ensure that changes has been collected. - */ - private void ensureCollected() throws VcsException { - if (myIsCollected) { - if (myIsFailed) { - throw new IllegalStateException("The method should not be called after after exception has been thrown."); - } - else { - return; - } - } - myIsCollected = true; + private GitOldChangesCollector(final Project project, + ChangeListManager changeListManager, + VcsDirtyScope dirtyScope, + final VirtualFile vcsRoot) throws VcsException { + super(project, changeListManager, dirtyScope, vcsRoot); updateIndex(); collectUnmergedAndUnversioned(); collectDiffChanges(); - myIsFailed = false; } private void updateIndex() throws VcsException { @@ -105,79 +139,6 @@ class ChangeCollector { handler.run(); } - /** - * Collect dirty file paths - * - * @param includeChanges if true, previous changes are included in collection - * @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit - */ - private Collection dirtyPaths(boolean includeChanges) { - final List allPaths = new ArrayList(); - - for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) { - addToPaths(p, allPaths); - } - for (FilePath p : myDirtyScope.getDirtyFilesNoExpand()) { - addToPaths(p, allPaths); - } - - if (includeChanges) { - try { - for (Change c : myChangeListManager.getChangesIn(myVcsRoot)) { - switch (c.getType()) { - case NEW: - case DELETED: - case MOVED: - if (c.getAfterRevision() != null) { - addToPaths(c.getAfterRevision().getFile(), allPaths); - } - if (c.getBeforeRevision() != null) { - addToPaths(c.getBeforeRevision().getFile(), allPaths); - } - case MODIFICATION: - default: - // do nothing - } - } - } - catch (Exception t) { - // ignore exceptions - } - } - - removeCommonParents(allPaths); - - final List paths = new ArrayList(allPaths.size()); - for (String p : allPaths) { - final File file = new File(p); - paths.add(new FilePathImpl(file, file.isDirectory())); - } - return paths; - } - - private void addToPaths(FilePath pathToAdd, List paths) { - File file = pathToAdd.getIOFile(); - if (myVcsRoot.equals(GitUtil.getGitRootOrNull(file))) { - paths.add(file.getPath()); - } - } - - private static void removeCommonParents(List allPaths) { - Collections.sort(allPaths); - - String prevPath = null; - Iterator it = allPaths.iterator(); - while (it.hasNext()) { - String path = it.next(); - if (prevPath != null && FileUtil.startsWith(path, prevPath)) { // the file is under previous file, so enough to check the parent - it.remove(); - } - else { - prevPath = path; - } - } - } - /** * Collect diff with head * @@ -296,10 +257,6 @@ class ChangeCollector { if (!myUnmergedNames.add(file)) { continue; } - // TODO handle conflict rename-modify - // TODO handle conflict copy-modify - // TODO handle conflict delete-modify - // TODO handle conflict rename-delete // assume modify-modify conflict ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, false, true); ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false, false); diff --git a/plugins/git4idea/src/git4idea/commands/GitCommand.java b/plugins/git4idea/src/git4idea/commands/GitCommand.java index 30d18651006b..1260d3b50868 100644 --- a/plugins/git4idea/src/git4idea/commands/GitCommand.java +++ b/plugins/git4idea/src/git4idea/commands/GitCommand.java @@ -51,6 +51,7 @@ public class GitCommand { public static final GitCommand RM = write("rm"); public static final GitCommand SHOW = write("show"); public static final GitCommand STASH = write("stash"); + public static final GitCommand STATUS = read("status"); public static final GitCommand TAG = meta("tag"); public static final GitCommand UPDATE_INDEX = write("update-index"); public static final GitCommand VERSION = meta("version"); diff --git a/plugins/git4idea/src/git4idea/config/GitVersionSpecialty.java b/plugins/git4idea/src/git4idea/config/GitVersionSpecialty.java index 143cf853aa2d..7eeacfbf158c 100644 --- a/plugins/git4idea/src/git4idea/config/GitVersionSpecialty.java +++ b/plugins/git4idea/src/git4idea/config/GitVersionSpecialty.java @@ -64,6 +64,18 @@ public enum GitVersionSpecialty { public boolean existsIn(@NotNull GitVersion version) { return version.isLaterOrEqual(new GitVersion(1, 7, 2, 0)); } + }, + + /** + * Git understands 'git status --porcelain'. + * Since 1.7.0. + */ + KNOWS_STATUS_PORCELAIN { + @Override + public boolean existsIn(@NotNull GitVersion + version) { + return version.isLaterOrEqual(new GitVersion(1, 7, 0, 0)); + } }; public abstract boolean existsIn(@NotNull GitVersion version); diff --git a/plugins/git4idea/tests/git4idea/tests/GitChangeProviderTest.java b/plugins/git4idea/tests/git4idea/tests/GitChangeProviderTest.java index 7f8b89e62d22..ab9ff8657351 100644 --- a/plugins/git4idea/tests/git4idea/tests/GitChangeProviderTest.java +++ b/plugins/git4idea/tests/git4idea/tests/GitChangeProviderTest.java @@ -33,6 +33,7 @@ import git4idea.changes.GitChangeProvider; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -72,6 +73,19 @@ public class GitChangeProviderTest extends GitTest { myDirtyScope = new MockDirtyScope(myProject, GitVcs.getInstance(myProject)); } + @Test + public void testUnversionedFile() throws IOException, VcsException { + setStandardConfirmation(GitVcs.NAME, VcsConfiguration.StandardConfirmation.ADD, VcsShowConfirmationOption.Value.DO_NOTHING_SILENTLY); + VirtualFile file = create(myRootDir, "new.txt"); + + MockChangelistBuilder builder = new MockChangelistBuilder(); + myChangeProvider.getChanges(myDirtyScope, builder, new EmptyProgressIndicator(), + new MockChangeListManagerGate(ChangeListManager.getInstance(myProject))); + List unversionedFiles = builder.getUnversionedFiles(); + assertEquals(unversionedFiles.size(), 1, "Incorrect number of unversioned files."); + assertEquals(unversionedFiles.get(0), file, "Unversioned file doesn't match."); + } + @Test public void testCreateFile() throws Exception { VirtualFile file = create(myRootDir, "new.txt");