3fdaa5bd8b7731d1cef043e43463a7ea127e7bb1
[idea/community.git] / plugins / git4idea / src / git4idea / commands / GitImpl.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.commands;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.util.Computable;
21 import com.intellij.openapi.util.Key;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.openapi.vcs.VcsException;
24 import com.intellij.openapi.vfs.VirtualFile;
25 import com.intellij.vcsUtil.VcsFileUtil;
26 import git4idea.*;
27 import git4idea.config.GitVersionSpecialty;
28 import git4idea.history.GitHistoryUtils;
29 import git4idea.repo.GitRemote;
30 import git4idea.repo.GitRepository;
31 import git4idea.reset.GitResetMode;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import java.io.File;
36 import java.util.*;
37 import java.util.concurrent.atomic.AtomicBoolean;
38 import java.util.concurrent.atomic.AtomicInteger;
39 import java.util.concurrent.atomic.AtomicReference;
40
41 /**
42  * Easy-to-use wrapper of common native Git commands.
43  * Most of them return result as {@link GitCommandResult}.
44  *
45  * @author Kirill Likhodedov
46  */
47 public class GitImpl implements Git {
48
49   private final Logger LOG = Logger.getInstance(Git.class);
50
51   public GitImpl() {
52   }
53
54   /**
55    * Calls 'git init' on the specified directory.
56    */
57   @NotNull
58   @Override
59   public GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners) {
60     GitLineHandler h = new GitLineHandler(project, root, GitCommand.INIT);
61     for (GitLineHandlerListener listener : listeners) {
62       h.addLineListener(listener);
63     }
64     h.setSilent(false);
65     h.setStdoutSuppressed(false);
66     return run(h);
67   }
68
69   /**
70    * <p>Queries Git for the unversioned files in the given paths. </p>
71    * <p>Ignored files are left ignored, i. e. no information is returned about them (thus this method may also be used as a
72    *    ignored files checker.</p>
73    *
74    * @param files files that are to be checked for the unversioned files among them.
75    *              <b>Pass <code>null</code> to query the whole repository.</b>
76    * @return Unversioned not ignored files from the given scope.
77    */
78   @Override
79   @NotNull
80   public Set<VirtualFile> untrackedFiles(@NotNull Project project, @NotNull VirtualFile root,
81                                          @Nullable Collection<VirtualFile> files) throws VcsException {
82     final Set<VirtualFile> untrackedFiles = new HashSet<VirtualFile>();
83
84     if (files == null) {
85       untrackedFiles.addAll(untrackedFilesNoChunk(project, root, null));
86     }
87     else {
88       for (List<String> relativePaths : VcsFileUtil.chunkFiles(root, files)) {
89         untrackedFiles.addAll(untrackedFilesNoChunk(project, root, relativePaths));
90       }
91     }
92
93     return untrackedFiles;
94   }
95
96   // relativePaths are guaranteed to fit into command line length limitations.
97   @Override
98   @NotNull
99   public Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project,
100                                                        @NotNull VirtualFile root,
101                                                        @Nullable List<String> relativePaths)
102     throws VcsException {
103     final Set<VirtualFile> untrackedFiles = new HashSet<VirtualFile>();
104     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LS_FILES);
105     h.setSilent(true);
106     h.addParameters("--exclude-standard", "--others", "-z");
107     h.endOptions();
108     if (relativePaths != null) {
109       h.addParameters(relativePaths);
110     }
111
112     final String output = h.run();
113     if (StringUtil.isEmptyOrSpaces(output)) {
114       return untrackedFiles;
115     }
116
117     for (String relPath : output.split("\u0000")) {
118       VirtualFile f = root.findFileByRelativePath(relPath);
119       if (f == null) {
120         // files was created on disk, but VirtualFile hasn't yet been created,
121         // when the GitChangeProvider has already been requested about changes.
122         LOG.info(String.format("VirtualFile for path [%s] is null", relPath));
123       } else {
124         untrackedFiles.add(f);
125       }
126     }
127
128     return untrackedFiles;
129   }
130   
131   @Override
132   @NotNull
133   public GitCommandResult clone(@NotNull final Project project, @NotNull final File parentDirectory, @NotNull final String url,
134                                 @NotNull final String clonedDirectoryName, @NotNull final GitLineHandlerListener... listeners) {
135     return run(new Computable<GitLineHandler>() {
136       @Override
137       public GitLineHandler compute() {
138         GitLineHandler handler = new GitLineHandler(project, parentDirectory, GitCommand.CLONE);
139         handler.setStdoutSuppressed(false);
140         handler.setUrl(url);
141         handler.addParameters("--progress");
142         handler.addParameters(url);
143         handler.addParameters(clonedDirectoryName);
144         addListeners(handler, listeners);
145         return handler;
146       }
147     });
148   }
149
150   @NotNull
151   @Override
152   public GitCommandResult config(@NotNull GitRepository repository, String... params) {
153     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CONFIG);
154     h.addParameters(params);
155     return run(h);
156   }
157
158   @NotNull
159   @Override
160   public GitCommandResult diff(@NotNull GitRepository repository, @NotNull List<String> parameters, @NotNull String range) {
161     final GitLineHandler diff = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.DIFF);
162     diff.addParameters(parameters);
163     diff.addParameters(range);
164     diff.setStdoutSuppressed(true);
165     diff.setStderrSuppressed(true);
166     diff.setSilent(true);
167     return run(diff);
168   }
169
170   @NotNull
171   @Override
172   public GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection<String> attributes,
173                                     @NotNull Collection<VirtualFile> files) {
174     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECK_ATTR);
175     h.addParameters(new ArrayList<String>(attributes));
176     h.endOptions();
177     h.addRelativeFiles(files);
178     return run(h);
179   }
180
181   @NotNull
182   @Override
183   public GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message) {
184     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH);
185     h.addParameters("save");
186     h.addParameters(message);
187     return run(h);
188   }
189
190   @NotNull
191   @Override
192   public GitCommandResult stashPop(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) {
193     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH);
194     handler.addParameters("pop");
195     addListeners(handler, listeners);
196     return run(handler);
197   }
198
199   @NotNull
200   @Override
201   public List<GitCommit> history(@NotNull GitRepository repository, @NotNull String range) {
202     try {
203       return GitHistoryUtils.history(repository.getProject(), repository.getRoot(), range);
204     }
205     catch (VcsException e) {
206       // this is critical, because we need to show the list of unmerged commits, and it shouldn't happen => inform user and developer
207       throw new GitExecutionException("Couldn't get [git log " + range + "] on repository [" + repository.getRoot() + "]", e);
208     }
209   }
210
211   @Override
212   @NotNull
213   public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge,
214                                 @Nullable List<String> additionalParams, @NotNull GitLineHandlerListener... listeners) {
215     final GitLineHandler mergeHandler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.MERGE);
216     mergeHandler.setSilent(false);
217     mergeHandler.addParameters(branchToMerge);
218     if (additionalParams != null) {
219       mergeHandler.addParameters(additionalParams);
220     }
221     for (GitLineHandlerListener listener : listeners) {
222       mergeHandler.addLineListener(listener);
223     }
224     return run(mergeHandler);
225   }
226
227
228   /**
229    * {@code git checkout &lt;reference&gt;} <br/>
230    * {@code git checkout -b &lt;newBranch&gt; &lt;reference&gt;}
231    */
232   @NotNull
233   @Override
234   public GitCommandResult checkout(@NotNull GitRepository repository,
235                                           @NotNull String reference,
236                                           @Nullable String newBranch,
237                                           boolean force,
238                                           @NotNull GitLineHandlerListener... listeners) {
239     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
240     h.setSilent(false);
241     h.setStdoutSuppressed(false);
242     if (force) {
243       h.addParameters("--force");
244     }
245     if (newBranch == null) { // simply checkout
246       h.addParameters(reference);
247     } 
248     else { // checkout reference as new branch
249       h.addParameters("-b", newBranch, reference);
250     }
251     for (GitLineHandlerListener listener : listeners) {
252       h.addLineListener(listener);
253     }
254     return run(h);
255   }
256
257   /**
258    * {@code git checkout -b &lt;branchName&gt;}
259    */
260   @NotNull
261   @Override
262   public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName,
263                                                    @Nullable GitLineHandlerListener listener) {
264     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT.readLockingCommand());
265     h.setSilent(false);
266     h.setStdoutSuppressed(false);
267     h.addParameters("-b");
268     h.addParameters(branchName);
269     if (listener != null) {
270       h.addLineListener(listener);
271     }
272     return run(h);
273   }
274
275   @NotNull
276   @Override
277   public GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName,
278                                        @Nullable GitLineHandlerListener listener, @NotNull String reference) {
279     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.TAG);
280     h.setSilent(false);
281     h.addParameters(tagName);
282     if (!reference.isEmpty()) {
283       h.addParameters(reference);
284     }
285     if (listener != null) {
286       h.addLineListener(listener);
287     }
288     return run(h);
289   }
290
291   /**
292    * {@code git branch -d <reference>} or {@code git branch -D <reference>}
293    */
294   @NotNull
295   @Override
296   public GitCommandResult branchDelete(@NotNull GitRepository repository,
297                                               @NotNull String branchName,
298                                               boolean force,
299                                               @NotNull GitLineHandlerListener... listeners) {
300     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
301     h.setSilent(false);
302     h.setStdoutSuppressed(false);
303     h.addParameters(force ? "-D" : "-d");
304     h.addParameters(branchName);
305     for (GitLineHandlerListener listener : listeners) {
306       h.addLineListener(listener);
307     }
308     return run(h);
309   }
310
311   /**
312    * Get branches containing the commit.
313    * {@code git branch --contains <commit>}
314    */
315   @Override
316   @NotNull
317   public GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) {
318     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
319     h.addParameters("--contains", commit);
320     return run(h);
321   }
322
323   /**
324    * Create branch without checking it out.
325    * {@code git branch <branchName>}
326    */
327   @Override
328   @NotNull
329   public GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) {
330     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
331     h.setStdoutSuppressed(false);
332     h.addParameters(branchName);
333     return run(h);
334   }
335
336   @Override
337   @NotNull
338   public GitCommandResult reset(@NotNull GitRepository repository, @NotNull GitResetMode mode, @NotNull String target,
339                                 @NotNull GitLineHandlerListener... listeners) {
340     return reset(repository, mode.getArgument(), target, listeners);
341   }
342
343   @Override
344   @NotNull
345   public GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) {
346     return reset(repository, "--merge", revision);
347   }
348
349   @NotNull
350   private static GitCommandResult reset(@NotNull GitRepository repository, @NotNull String argument, @Nullable String target,
351                                         @NotNull GitLineHandlerListener... listeners) {
352     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.RESET);
353     handler.addParameters(argument);
354     if (target != null) {
355       handler.addParameters(target);
356     }
357     addListeners(handler, listeners);
358     return run(handler);
359   }
360
361   /**
362    * Returns the last (tip) commit on the given branch.<br/>
363    * {@code git rev-list -1 <branchName>}
364    */
365   @NotNull
366   @Override
367   public GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) {
368     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REV_LIST);
369     h.addParameters("-1");
370     h.addParameters(branchName);
371     return run(h);
372   }
373
374   @Override
375   @NotNull
376   public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
377                                boolean updateTracking, @NotNull GitLineHandlerListener... listeners) {
378     return doPush(repository, remote, url, spec, false, updateTracking, null, listeners);
379   }
380
381   @NotNull
382   private GitCommandResult doPush(@NotNull final GitRepository repository, @NotNull final String remote, @Nullable final String url,
383                                @NotNull final String spec, final boolean force, final boolean updateTracking,
384                                @Nullable final String tagMode,
385                                @NotNull final GitLineHandlerListener... listeners) {
386     return runCommand(new Computable<GitLineHandler>() {
387       @Override
388       public GitLineHandler compute() {
389         final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
390         if (url != null) {
391           h.setUrl(url);
392         }
393         h.setSilent(false);
394         h.setStdoutSuppressed(false);
395         addListeners(h, listeners);
396         h.addProgressParameter();
397         h.addParameters("--porcelain");
398         h.addParameters(remote);
399         h.addParameters(spec);
400         if (updateTracking) {
401           h.addParameters("--set-upstream");
402         }
403         if (force) {
404           h.addParameters("--force");
405         }
406         if (tagMode != null) {
407           h.addParameters(tagMode);
408         }
409         return h;
410       }
411     });
412   }
413
414   @Override
415   @NotNull
416   public GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @Nullable String url, @NotNull String spec,
417                                @NotNull GitLineHandlerListener... listeners) {
418     return push(repository, remote, url, spec, false, listeners);
419   }
420
421   @Override
422   @NotNull
423   public GitCommandResult push(@NotNull GitRepository repository, @NotNull GitLocalBranch source, @NotNull GitRemoteBranch target,
424                                boolean force, boolean updateTracking, @Nullable String tagMode, GitLineHandlerListener... listeners) {
425     GitRemote remote = target.getRemote();
426     Collection<String> pushUrls = remote.getPushUrls(); // TODO handle the case with multiple pushurls with different protocols
427     String url;
428     if (pushUrls.isEmpty()) {
429       LOG.error("No urls or pushUrls are defined for " + remote);
430       url = null;
431     }
432     else {
433       url = pushUrls.iterator().next();
434     }
435     String spec = source.getFullName() + ":" + target.getNameForRemoteOperations();
436     return doPush(repository, remote.getName(), url, spec, force, updateTracking, tagMode, listeners);
437   }
438
439   @NotNull
440   @Override
441   public GitCommandResult show(@NotNull GitRepository repository, @NotNull String... params) {
442     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.SHOW);
443     handler.addParameters(params);
444     return run(handler);
445   }
446
447   @Override
448   @NotNull
449   public GitCommandResult cherryPick(@NotNull GitRepository repository, @NotNull String hash, boolean autoCommit,
450                                      @NotNull GitLineHandlerListener... listeners) {
451     final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHERRY_PICK);
452     handler.addParameters("-x");
453     if (!autoCommit) {
454       handler.addParameters("-n");
455     }
456     handler.addParameters(hash);
457     addListeners(handler, listeners);
458     handler.setSilent(false);
459     handler.setStdoutSuppressed(false);
460     return run(handler);
461   }
462
463   @NotNull
464   @Override
465   public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) {
466     GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.LS_FILES);
467     h.addParameters("--unmerged");
468     h.setSilent(true);
469     return run(h);
470   }
471
472   /**
473    * Fetch remote branch
474    * {@code git fetch <remote> <params>}
475    */
476   @Override
477   @NotNull
478   public GitCommandResult fetch(@NotNull final GitRepository repository, @NotNull final String url, @NotNull final String remote,
479                                 @NotNull final List<GitLineHandlerListener> listeners, final String... params) {
480     return runCommand(new Computable<GitLineHandler>() {
481       @Override
482       public GitLineHandler compute() {
483         final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.FETCH);
484         h.setSilent(false);
485         h.setStdoutSuppressed(false);
486         h.setUrl(url);
487         h.addParameters(remote);
488         h.addParameters(params);
489         h.addProgressParameter();
490         GitVcs vcs = GitVcs.getInstance(repository.getProject());
491         if (vcs != null && GitVersionSpecialty.SUPPORTS_FETCH_PRUNE.existsIn(vcs.getVersion())) {
492           h.addParameters("--prune");
493         }
494         addListeners(h, listeners);
495         return h;
496       }
497     });
498   }
499
500   private static void addListeners(@NotNull GitLineHandler handler, @NotNull GitLineHandlerListener... listeners) {
501     addListeners(handler, Arrays.asList(listeners));
502   }
503
504   private static void addListeners(@NotNull GitLineHandler handler, @NotNull List<GitLineHandlerListener> listeners) {
505     for (GitLineHandlerListener listener : listeners) {
506       handler.addLineListener(listener);
507     }
508   }
509
510   @NotNull
511   private static GitCommandResult run(@NotNull Computable<GitLineHandler> handlerConstructor) {
512     final List<String> errorOutput = new ArrayList<String>();
513     final List<String> output = new ArrayList<String>();
514     final AtomicInteger exitCode = new AtomicInteger();
515     final AtomicBoolean startFailed = new AtomicBoolean();
516     final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
517
518     int authAttempt = 0;
519     boolean authFailed;
520     boolean success;
521     do {
522       errorOutput.clear();
523       output.clear();
524       exitCode.set(0);
525       startFailed.set(false);
526       exception.set(null);
527
528       GitLineHandler handler = handlerConstructor.compute();
529       handler.addLineListener(new GitLineHandlerListener() {
530         @Override public void onLineAvailable(String line, Key outputType) {
531           if (isError(line)) {
532             errorOutput.add(line);
533           } else {
534             output.add(line);
535           }
536         }
537
538         @Override public void processTerminated(int code) {
539           exitCode.set(code);
540         }
541
542         @Override public void startFailed(Throwable t) {
543           startFailed.set(true);
544           errorOutput.add("Failed to start Git process");
545           exception.set(t);
546         }
547       });
548
549       handler.runInCurrentThread(null);
550       authFailed = handler.hasHttpAuthFailed();
551       success = !startFailed.get() && errorOutput.isEmpty() && (handler.isIgnoredErrorCode(exitCode.get()) || exitCode.get() == 0);
552     }
553     while (authFailed && authAttempt++ < 2);
554     return new GitCommandResult(success, exitCode.get(), errorOutput, output, null);
555   }
556
557   /**
558    * Runs the given {@link GitLineHandler} in the current thread and returns the {@link GitCommandResult}.
559    */
560   @NotNull
561   private static GitCommandResult run(@NotNull GitLineHandler handler) {
562     return run(new Computable.PredefinedValueComputable<GitLineHandler>(handler));
563   }
564
565   @Override
566   @NotNull
567   public GitCommandResult runCommand(@NotNull Computable<GitLineHandler> handlerConstructor) {
568     return run(handlerConstructor);
569   }
570   
571   /**
572    * Check if the line looks line an error message
573    */
574   private static boolean isError(String text) {
575     for (String indicator : ERROR_INDICATORS) {
576       if (text.startsWith(indicator.toLowerCase())) {
577         return true;
578       }
579     }
580     return false;
581   }
582
583   // could be upper-cased, so should check case-insensitively
584   public static final String[] ERROR_INDICATORS = {
585     "error", "remote: error", "fatal",
586     "Cannot apply", "Could not", "Interactive rebase already started", "refusing to pull", "cannot rebase:", "conflict",
587     "unable"
588   };
589 }