2 * Copyright 2000-2007 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.changes;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.vcs.FilePath;
20 import com.intellij.openapi.vcs.FileStatus;
21 import com.intellij.openapi.vcs.VcsException;
22 import com.intellij.openapi.vcs.changes.Change;
23 import com.intellij.openapi.vcs.changes.ChangeListManager;
24 import com.intellij.openapi.vcs.changes.ContentRevision;
25 import com.intellij.openapi.vcs.changes.VcsDirtyScope;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.vcsUtil.VcsUtil;
28 import git4idea.GitContentRevision;
29 import git4idea.GitRevisionNumber;
30 import git4idea.GitUtil;
31 import git4idea.commands.GitHandler;
32 import git4idea.commands.GitSimpleHandler;
33 import git4idea.commands.StringScanner;
38 * A collector for changes in the Git. It is introduced because changes are not
39 * cannot be got as a sum of stateless operations.
41 class ChangeCollector {
43 * The dirty scope used in the collector
45 private VcsDirtyScope myDirtyScope;
47 * a vcs root for changes
49 private final VirtualFile myVcsRoot;
51 * a project for change collection
53 private final Project myProject;
57 private final List<VirtualFile> myUnversioned = new ArrayList<VirtualFile>();
59 * Names that are listed as unmerged
61 private final Set<String> myUnmergedNames = new HashSet<String>();
63 * Names that are listed as unmerged
65 private final List<Change> myChanges = new ArrayList<Change>();
67 * This flag indicates that collecting changes has been failed.
69 private boolean myIsFailed = true;
71 * This flag indicates that collecting changes has been started
73 private boolean myIsCollected = false;
78 * @param project a project
79 * @param dirtyScope the dirty scope to check
80 * @param vcsRoot a vcs root
82 public ChangeCollector(final Project project, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) {
83 myDirtyScope = dirtyScope;
89 * Get unversioned files
91 * @return an unversioned files
92 * @throws VcsException if there is a problem with executing Git
94 public Collection<VirtualFile> unversioned() throws VcsException {
102 * @return an unversioned files
103 * @throws VcsException if there is a problem with executing Git
105 public Collection<Change> changes() throws VcsException {
112 * Ensure that changes has been collected.
114 * @throws VcsException an exception
116 private void ensureCollected() throws VcsException {
119 throw new IllegalStateException("The method should not be called after after exception has been thrown.");
125 myIsCollected = true;
126 collectUnmergedAndUnversioned();
127 collectDiffChanges();
132 * Collect dirty file paths
134 * @param includeChanges if true, previous changes are included in collection
135 * @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit
137 private Collection<FilePath> dirtyPaths(boolean includeChanges) {
138 // TODO collapse paths with common prefix
139 ArrayList<FilePath> paths = new ArrayList<FilePath>();
140 FilePath rootPath = VcsUtil.getFilePath(myVcsRoot.getPath(), true);
141 for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) {
142 addToPaths(rootPath, paths, p);
144 ArrayList<FilePath> candidatePaths = new ArrayList<FilePath>();
145 candidatePaths.addAll(myDirtyScope.getDirtyFilesNoExpand());
146 if (includeChanges) {
148 ChangeListManager cm = ChangeListManager.getInstance(myProject);
149 for (Change c : cm.getChangesIn(myVcsRoot)) {
150 switch (c.getType()) {
154 if (c.getAfterRevision() != null) {
155 addToPaths(rootPath, paths, c.getAfterRevision().getFile());
157 if (c.getBeforeRevision() != null) {
158 addToPaths(rootPath, paths, c.getBeforeRevision().getFile());
165 catch (Exception t) {
169 for (FilePath p : candidatePaths) {
170 addToPaths(rootPath, paths, p);
176 * Add path to the collection of the paths to check for this vcs root
178 * @param root the root path
179 * @param paths the existing paths
180 * @param toAdd the path to add
182 void addToPaths(FilePath root, Collection<FilePath> paths, FilePath toAdd) {
183 if (GitUtil.getGitRootOrNull(toAdd) != myVcsRoot) {
186 if (root.isUnder(toAdd, true)) {
189 for (Iterator<FilePath> i = paths.iterator(); i.hasNext();) {
190 FilePath p = i.next();
191 if (p.isUnder(toAdd, true)) {
194 if (toAdd.isUnder(p, false)) {
202 * Collect diff with head
204 * @throws VcsException if there is a problem with running git
206 private void collectDiffChanges() throws VcsException {
207 GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.DIFF);
208 handler.addParameters("--name-status", "--diff-filter=ADCMRUX", "-M", "HEAD");
209 handler.setNoSSH(true);
210 handler.setSilent(true);
211 handler.setStdoutSuppressed(true);
212 handler.endOptions();
213 handler.addRelativePaths(dirtyPaths(true));
215 String output = handler.run();
216 GitChangeUtils.parseChanges(myProject, myVcsRoot, null, GitChangeUtils.loadRevision(myProject, myVcsRoot, "HEAD"), output, myChanges,
219 catch (VcsException ex) {
220 if (!GitChangeUtils.isHeadMissing(ex)) {
223 handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.LS_FILES);
224 handler.addParameters("--cached");
225 handler.setNoSSH(true);
226 handler.setSilent(true);
227 handler.setStdoutSuppressed(true);
228 // During init diff does not works because HEAD
229 // will appear only after the first commit.
230 // In that case added files are cached in index.
231 String output = handler.run();
232 if (output.length() > 0) {
233 StringTokenizer tokenizer = new StringTokenizer(output, "\n\r");
234 while (tokenizer.hasMoreTokens()) {
235 final String s = tokenizer.nextToken();
236 Change ch = new Change(null, GitContentRevision.createRevision(myVcsRoot, s, null, myProject, false), FileStatus.ADDED);
244 * Collect unversioned and unmerged files
246 * @throws VcsException if there is a problem with running git
248 private void collectUnmergedAndUnversioned() throws VcsException {
250 GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitHandler.LS_FILES);
251 handler.addParameters("-v", "--others", "--unmerged", "--exclude-standard");
252 handler.setSilent(true);
253 handler.setNoSSH(true);
254 handler.setStdoutSuppressed(true);
255 handler.addRelativePaths(dirtyPaths(false));
256 // run handler and collect changes
257 String list = handler.run();
258 for (StringScanner sc = new StringScanner(list); sc.hasMoreData();) {
263 char status = sc.peek();
266 VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
267 if (GitUtil.gitRootOrNull(file) == myVcsRoot) {
268 myUnversioned.add(file);
271 else { //noinspection HardCodedStringLiteral
273 sc.boundedToken('\t');
274 String file = GitUtil.unescapePath(sc.line());
275 VirtualFile vFile = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
276 if (GitUtil.gitRootOrNull(vFile) != myVcsRoot) {
279 if (!myUnmergedNames.add(file)) {
282 // TODO handle conflict rename-modify
283 // TODO handle conflict copy-modify
284 // TODO handle conflict delete-modify
285 // TODO handle conflict rename-delete
286 // assume modify-modify conflict
287 ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, false);
288 ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false);
289 myChanges.add(new Change(before, after, FileStatus.MERGED_WITH_CONFLICTS));
292 throw new VcsException("Unsupported type of the merge conflict detected: " + status);