2 * Copyright 2000-2009 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.util.io.FileUtil;
20 import com.intellij.openapi.vcs.FilePath;
21 import com.intellij.openapi.vcs.FilePathImpl;
22 import com.intellij.openapi.vcs.FileStatus;
23 import com.intellij.openapi.vcs.VcsException;
24 import com.intellij.openapi.vcs.changes.Change;
25 import com.intellij.openapi.vcs.changes.ChangeListManager;
26 import com.intellij.openapi.vcs.changes.ContentRevision;
27 import com.intellij.openapi.vcs.changes.VcsDirtyScope;
28 import com.intellij.openapi.vfs.VirtualFile;
29 import git4idea.GitContentRevision;
30 import git4idea.GitRevisionNumber;
31 import git4idea.GitUtil;
32 import git4idea.commands.GitCommand;
33 import git4idea.commands.GitSimpleHandler;
34 import git4idea.commands.StringScanner;
40 * A collector for changes in the Git. It is introduced because changes are not
41 * cannot be got as a sum of stateless operations.
43 class ChangeCollector {
44 private final Project myProject;
45 private final ChangeListManager myChangeListManager;
46 private final VcsDirtyScope myDirtyScope;
47 private final VirtualFile myVcsRoot;
49 private final List<VirtualFile> myUnversioned = new ArrayList<VirtualFile>(); // Unversioned files
50 private final Set<String> myUnmergedNames = new HashSet<String>(); // Names of unmerged files
51 private final List<Change> myChanges = new ArrayList<Change>(); // all changes
52 private boolean myIsCollected = false; // indicates that collecting changes has been started
53 private boolean myIsFailed = true; // indicates that collecting changes has been failed.
55 public ChangeCollector(final Project project, ChangeListManager changeListManager, VcsDirtyScope dirtyScope, final VirtualFile vcsRoot) {
56 myChangeListManager = changeListManager;
57 myDirtyScope = dirtyScope;
63 * Get unversioned files
65 public Collection<VirtualFile> unversioned() throws VcsException {
73 public Collection<Change> changes() throws VcsException {
80 * Ensure that changes has been collected.
82 private void ensureCollected() throws VcsException {
85 throw new IllegalStateException("The method should not be called after after exception has been thrown.");
93 collectUnmergedAndUnversioned();
98 private void updateIndex() throws VcsException {
99 GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.UPDATE_INDEX);
100 handler.addParameters("--refresh", "--ignore-missing");
101 handler.setSilent(true);
102 handler.setNoSSH(true);
103 handler.setStdoutSuppressed(true);
104 handler.ignoreErrorCode(1);
109 * Collect dirty file paths
111 * @param includeChanges if true, previous changes are included in collection
112 * @return the set of dirty paths to check, the paths are automatically collapsed if the summary length more than limit
114 private Collection<FilePath> dirtyPaths(boolean includeChanges) {
115 final List<String> allPaths = new ArrayList<String>();
117 for (FilePath p : myDirtyScope.getRecursivelyDirtyDirectories()) {
118 addToPaths(p, allPaths);
120 for (FilePath p : myDirtyScope.getDirtyFilesNoExpand()) {
121 addToPaths(p, allPaths);
124 if (includeChanges) {
126 for (Change c : myChangeListManager.getChangesIn(myVcsRoot)) {
127 switch (c.getType()) {
131 if (c.getAfterRevision() != null) {
132 addToPaths(c.getAfterRevision().getFile(), allPaths);
134 if (c.getBeforeRevision() != null) {
135 addToPaths(c.getBeforeRevision().getFile(), allPaths);
143 catch (Exception t) {
148 removeCommonParents(allPaths);
150 final List<FilePath> paths = new ArrayList<FilePath>(allPaths.size());
151 for (String p : allPaths) {
152 final File file = new File(p);
153 paths.add(new FilePathImpl(file, file.isDirectory()));
158 private void addToPaths(FilePath pathToAdd, List<String> paths) {
159 if (myVcsRoot.equals(GitUtil.getGitRootOrNull(pathToAdd))) {
160 paths.add(pathToAdd.getPath());
164 private static void removeCommonParents(List<String> allPaths) {
165 Collections.sort(allPaths);
167 String prevPath = null;
168 Iterator<String> it = allPaths.iterator();
169 while (it.hasNext()) {
170 String path = it.next();
171 if (prevPath != null && FileUtil.startsWith(path, prevPath)) { // the file is under previous file, so enough to check the parent
181 * Collect diff with head
183 * @throws VcsException if there is a problem with running git
185 private void collectDiffChanges() throws VcsException {
186 Collection<FilePath> dirtyPaths = dirtyPaths(true);
187 if (dirtyPaths.isEmpty()) {
190 GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.DIFF);
191 handler.addParameters("--name-status", "--diff-filter=ADCMRUX", "-M", "HEAD");
192 handler.setNoSSH(true);
193 handler.setSilent(true);
194 handler.setStdoutSuppressed(true);
195 handler.endOptions();
196 handler.addRelativePaths(dirtyPaths);
197 if (handler.isLargeCommandLine()) {
198 // if there are too much files, just get all changes for the project
199 handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.DIFF);
200 handler.addParameters("--name-status", "--diff-filter=ADCMRUX", "-M", "HEAD");
201 handler.setNoSSH(true);
202 handler.setSilent(true);
203 handler.setStdoutSuppressed(true);
204 handler.endOptions();
207 String output = handler.run();
208 GitChangeUtils.parseChanges(myProject, myVcsRoot, null, GitChangeUtils.loadRevision(myProject, myVcsRoot, "HEAD"), output, myChanges,
211 catch (VcsException ex) {
212 if (!GitChangeUtils.isHeadMissing(ex)) {
215 handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
216 handler.addParameters("--cached");
217 handler.setNoSSH(true);
218 handler.setSilent(true);
219 handler.setStdoutSuppressed(true);
220 // During init diff does not works because HEAD
221 // will appear only after the first commit.
222 // In that case added files are cached in index.
223 String output = handler.run();
224 if (output.length() > 0) {
225 StringTokenizer tokenizer = new StringTokenizer(output, "\n\r");
226 while (tokenizer.hasMoreTokens()) {
227 final String s = tokenizer.nextToken();
228 Change ch = new Change(null, GitContentRevision.createRevision(myVcsRoot, s, null, myProject, false, false), FileStatus.ADDED);
236 * Collect unversioned and unmerged files
238 * @throws VcsException if there is a problem with running git
240 private void collectUnmergedAndUnversioned() throws VcsException {
241 Collection<FilePath> dirtyPaths = dirtyPaths(false);
242 if (dirtyPaths.isEmpty()) {
246 GitSimpleHandler handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
247 handler.addParameters("-v", "--unmerged");
248 handler.setSilent(true);
249 handler.setNoSSH(true);
250 handler.setStdoutSuppressed(true);
251 // run handler and collect changes
252 parseFiles(handler.run());
254 handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
255 handler.addParameters("-v", "--others", "--exclude-standard");
256 handler.setSilent(true);
257 handler.setNoSSH(true);
258 handler.setStdoutSuppressed(true);
259 handler.endOptions();
260 handler.addRelativePaths(dirtyPaths);
261 if(handler.isLargeCommandLine()) {
262 handler = new GitSimpleHandler(myProject, myVcsRoot, GitCommand.LS_FILES);
263 handler.addParameters("-v", "--others", "--exclude-standard");
264 handler.setSilent(true);
265 handler.setNoSSH(true);
266 handler.setStdoutSuppressed(true);
267 handler.endOptions();
269 // run handler and collect changes
270 parseFiles(handler.run());
273 private void parseFiles(String list) throws VcsException {
274 for (StringScanner sc = new StringScanner(list); sc.hasMoreData();) {
279 char status = sc.peek();
282 VirtualFile file = myVcsRoot.findFileByRelativePath(GitUtil.unescapePath(sc.line()));
283 if (GitUtil.gitRootOrNull(file) == myVcsRoot) {
284 myUnversioned.add(file);
287 else { //noinspection HardCodedStringLiteral
289 sc.boundedToken('\t');
290 String file = GitUtil.unescapePath(sc.line());
291 VirtualFile vFile = myVcsRoot.findFileByRelativePath(file);
292 if (GitUtil.gitRootOrNull(vFile) != myVcsRoot) {
295 if (!myUnmergedNames.add(file)) {
298 // TODO handle conflict rename-modify
299 // TODO handle conflict copy-modify
300 // TODO handle conflict delete-modify
301 // TODO handle conflict rename-delete
302 // assume modify-modify conflict
303 ContentRevision before = GitContentRevision.createRevision(myVcsRoot, file, new GitRevisionNumber("orig_head"), myProject, false, true);
304 ContentRevision after = GitContentRevision.createRevision(myVcsRoot, file, null, myProject, false, false);
305 myChanges.add(new Change(before, after, FileStatus.MERGED_WITH_CONFLICTS));
308 throw new VcsException("Unsupported type of the merge conflict detected: " + status);