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");