c52978d918dd98ca26e6bf1bb36753750cb7b75c
[idea/community.git] / plugins / git4idea / src / git4idea / commands / Git.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.Key;
21 import com.intellij.openapi.util.text.StringUtil;
22 import com.intellij.openapi.vcs.VcsException;
23 import com.intellij.openapi.vfs.VirtualFile;
24 import com.intellij.util.ExceptionUtil;
25 import com.intellij.vcsUtil.VcsFileUtil;
26 import git4idea.GitBranch;
27 import git4idea.push.GitPushSpec;
28 import git4idea.repo.GitRemote;
29 import git4idea.repo.GitRepository;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.io.File;
34 import java.util.*;
35 import java.util.concurrent.atomic.AtomicBoolean;
36 import java.util.concurrent.atomic.AtomicInteger;
37
38 /**
39  * Collection of common native Git commands.
40  *
41  * @author Kirill Likhodedov
42  */
43 public class Git {
44
45   private static final Logger LOG = Logger.getInstance(Git.class);
46
47   private Git() {
48   }
49
50   /**
51    * Calls 'git init' on the specified directory.
52    * // TODO use common format
53    */
54   public static void init(Project project, VirtualFile root) throws VcsException {
55     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.INIT);
56     h.setSilent(false);
57     h.setNoSSH(true);
58     h.run();
59     if (!h.errors().isEmpty()) {
60       throw h.errors().get(0);
61     }
62   }
63
64   /**
65    * <p>Queries Git for the unversioned files in the given paths.</p>
66    * <p>
67    *   <b>Note:</b> this method doesn't check for ignored files. You have to check if the file is ignored afterwards, if needed.
68    * </p>
69    *
70    * @param files files that are to be checked for the unversioned files among them.
71    *              <b>Pass <code>null</code> to query the whole repository.</b>
72    * @return Unversioned files from the given scope.
73    */
74   @NotNull
75   public static Set<VirtualFile> untrackedFiles(@NotNull Project project,
76                                                 @NotNull VirtualFile root,
77                                                 @Nullable Collection<VirtualFile> files) throws VcsException {
78     final Set<VirtualFile> untrackedFiles = new HashSet<VirtualFile>();
79
80     if (files == null) {
81       untrackedFiles.addAll(untrackedFilesNoChunk(project, root, null));
82     } else {
83       for (List<String> relativePaths : VcsFileUtil.chunkFiles(root, files)) {
84         untrackedFiles.addAll(untrackedFilesNoChunk(project, root, relativePaths));
85       }
86     }
87
88     return untrackedFiles;
89   }
90
91   // relativePaths are guaranteed to fit into command line length limitations.
92   @NotNull
93   private static Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project,
94                                                                @NotNull VirtualFile root,
95                                                                @Nullable List<String> relativePaths)
96     throws VcsException {
97     final Set<VirtualFile> untrackedFiles = new HashSet<VirtualFile>();
98     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LS_FILES);
99     h.setNoSSH(true);
100     h.setSilent(true);
101     h.addParameters("--exclude-standard", "--others", "-z");
102     h.endOptions();
103     if (relativePaths != null) {
104       h.addParameters(relativePaths);
105     }
106
107     final String output = h.run();
108     if (StringUtil.isEmptyOrSpaces(output)) {
109       return untrackedFiles;
110     }
111
112     for (String relPath : output.split("\u0000")) {
113       VirtualFile f = root.findFileByRelativePath(relPath);
114       if (f == null) {
115         // files was created on disk, but VirtualFile hasn't yet been created,
116         // when the GitChangeProvider has already been requested about changes.
117         LOG.info(String.format("VirtualFile for path [%s] is null", relPath));
118       } else {
119         untrackedFiles.add(f);
120       }
121     }
122
123     return untrackedFiles;
124   }
125   
126   @NotNull
127   public static GitCommandResult clone(@NotNull Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName) {
128     GitLineHandlerPasswordRequestAware handler = new GitLineHandlerPasswordRequestAware(project, parentDirectory, GitCommand.CLONE);
129     handler.addParameters(url);
130     handler.addParameters(clonedDirectoryName);
131     return run(handler, true);
132   }
133
134   /**
135    * {@code git checkout &lt;reference&gt;} <br/>
136    * {@code git checkout -b &lt;newBranch&gt; &lt;reference&gt;}
137    */
138   public static GitCommandResult checkout(@NotNull GitRepository repository,
139                                           @NotNull String reference,
140                                           @Nullable String newBranch,
141                                           boolean force,
142                                           @NotNull GitLineHandlerListener... listeners) {
143     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
144     h.setSilent(false);
145     if (force) {
146       h.addParameters("--force");
147     }
148     if (newBranch == null) { // simply checkout
149       h.addParameters(reference);
150     } 
151     else { // checkout reference as new branch
152       h.addParameters("-b", newBranch, reference);
153     }
154     for (GitLineHandlerListener listener : listeners) {
155       h.addLineListener(listener);
156     }
157     return run(h);
158   }
159
160   /**
161    * {@code git checkout -b &lt;branchName&gt;}
162    */
163   public static GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName,
164                                                    @Nullable GitLineHandlerListener listener) {
165     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
166     h.setSilent(false);
167     h.addParameters("-b");
168     h.addParameters(branchName);
169     if (listener != null) {
170       h.addLineListener(listener);
171     }
172     return run(h);
173   }
174
175   public static GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName,
176                                                      @Nullable GitLineHandlerListener listener, String reference) {
177     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.TAG);
178     h.setSilent(false);
179     h.addParameters(tagName);
180     if (reference != null && ! reference.isEmpty()) {
181       h.addParameters(reference);
182     }
183     if (listener != null) {
184       h.addLineListener(listener);
185     }
186     return run(h);
187   }
188
189   /**
190    * {@code git branch -d <reference>} or {@code git branch -D <reference>}
191    */
192   public static GitCommandResult branchDelete(@NotNull GitRepository repository,
193                                               @NotNull String branchName,
194                                               boolean force,
195                                               @NotNull GitLineHandlerListener... listeners) {
196     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
197     h.setSilent(false);
198     h.addParameters(force ? "-D" : "-d");
199     h.addParameters(branchName);
200     for (GitLineHandlerListener listener : listeners) {
201       h.addLineListener(listener);
202     }
203     return run(h);
204   }
205
206   /**
207    * Get branches containing the commit.
208    * {@code git branch --contains <commit>}
209    */
210   @NotNull
211   public static GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) {
212     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
213     h.addParameters("--contains", commit);
214     return run(h);
215   }
216
217   /**
218    * Create branch without checking it out.
219    * {@code git branch <branchName>}
220    */
221   @NotNull
222   public static GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) {
223     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
224     h.addParameters(branchName);
225     return run(h);
226   }
227
228   /**
229    * Returns the last (tip) commit on the given branch.<br/>
230    * {@code git rev-list -1 <branchName>}
231    */
232   public static GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) {
233     final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REV_LIST);
234     h.addParameters("-1");
235     h.addParameters(branchName);
236     return run(h);
237   }
238
239   public static GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull GitLineHandlerListener... listeners) {
240     final GitLineHandlerPasswordRequestAware h = new GitLineHandlerPasswordRequestAware(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
241     h.setSilent(false);
242
243     for (GitLineHandlerListener listener : listeners) {
244       h.addLineListener(listener);
245     }
246     GitRemote remote = pushSpec.getRemote();
247     h.addParameters(remote.getName());
248     GitBranch remoteBranch = pushSpec.getDest();
249     String destination = remoteBranch.getName().replaceFirst(remote.getName() + "/", "");
250     h.addParameters(pushSpec.getSource().getName() + ":" + destination);
251     return run(h, true);
252   }
253
254   private static GitCommandResult run(@NotNull GitLineHandler handler) {
255     return run(handler, false);
256   } 
257
258   /**
259    * Runs the given {@link GitLineHandler} in the current thread and returns the {@link GitCommandResult}.
260    */
261   private static GitCommandResult run(@NotNull GitLineHandler handler, boolean remote) {
262     handler.setNoSSH(!remote);
263
264     final List<String> errorOutput = new ArrayList<String>();
265     final List<String> output = new ArrayList<String>();
266     final AtomicInteger exitCode = new AtomicInteger();
267     final AtomicBoolean startFailed = new AtomicBoolean();
268     
269     handler.addLineListener(new GitLineHandlerListener() {
270       @Override public void onLineAvailable(String line, Key outputType) {
271         if (isError(line)) {
272           errorOutput.add(line);
273         } else {
274           output.add(line);
275         }
276       }
277
278       @Override public void processTerminated(int code) {
279         exitCode.set(code);
280       }
281
282       @Override public void startFailed(Throwable exception) {
283         startFailed.set(true);
284         errorOutput.add("Failed to start Git process");
285         errorOutput.add(ExceptionUtil.getThrowableText(exception));
286       }
287     });
288     
289     handler.runInCurrentThread(null);
290
291     if (handler instanceof GitLineHandlerPasswordRequestAware && ((GitLineHandlerPasswordRequestAware)handler).hadAuthRequest()) {
292       errorOutput.add("Authentication failed");
293     }
294
295     final boolean success = !startFailed.get() && errorOutput.isEmpty() && (handler.isIgnoredErrorCode(exitCode.get()) || exitCode.get() == 0);
296     return new GitCommandResult(success, exitCode.get(), errorOutput, output);
297   }
298   
299   /**
300    * Check if the line looks line an error message
301    */
302   private static boolean isError(String text) {
303     for (String indicator : ERROR_INDICATORS) {
304       if (text.startsWith(indicator.toLowerCase())) {
305         return true;
306       }
307     }
308     return false;
309   }
310
311   // could be upper-cased, so should check case-insensitively
312   private static final String[] ERROR_INDICATORS = {
313     "error", "fatal", "Cannot apply", "Could not", "Interactive rebase already started", "refusing to pull", "cannot rebase:", "conflict"
314   };
315
316 }