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