2 * Copyright 2000-2011 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.commands;
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;
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;
37 import java.util.concurrent.atomic.AtomicBoolean;
38 import java.util.concurrent.atomic.AtomicInteger;
39 import java.util.concurrent.atomic.AtomicReference;
42 * Easy-to-use wrapper of common native Git commands.
43 * Most of them return result as {@link GitCommandResult}.
45 * @author Kirill Likhodedov
47 public class GitImpl implements Git {
49 private final Logger LOG = Logger.getInstance(Git.class);
55 * Calls 'git init' on the specified directory.
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);
65 h.setStdoutSuppressed(false);
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>
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.
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>();
85 untrackedFiles.addAll(untrackedFilesNoChunk(project, root, null));
88 for (List<String> relativePaths : VcsFileUtil.chunkFiles(root, files)) {
89 untrackedFiles.addAll(untrackedFilesNoChunk(project, root, relativePaths));
93 return untrackedFiles;
96 // relativePaths are guaranteed to fit into command line length limitations.
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);
106 h.addParameters("--exclude-standard", "--others", "-z");
108 if (relativePaths != null) {
109 h.addParameters(relativePaths);
112 final String output = h.run();
113 if (StringUtil.isEmptyOrSpaces(output)) {
114 return untrackedFiles;
117 for (String relPath : output.split("\u0000")) {
118 VirtualFile f = root.findFileByRelativePath(relPath);
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));
124 untrackedFiles.add(f);
128 return untrackedFiles;
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>() {
137 public GitLineHandler compute() {
138 GitLineHandler handler = new GitLineHandler(project, parentDirectory, GitCommand.CLONE);
139 handler.setStdoutSuppressed(false);
141 handler.addParameters("--progress");
142 handler.addParameters(url);
143 handler.addParameters(clonedDirectoryName);
144 addListeners(handler, listeners);
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);
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);
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));
177 h.addRelativeFiles(files);
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);
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);
201 public List<GitCommit> history(@NotNull GitRepository repository, @NotNull String range) {
203 return GitHistoryUtils.history(repository.getProject(), repository.getRoot(), range);
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);
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);
221 for (GitLineHandlerListener listener : listeners) {
222 mergeHandler.addLineListener(listener);
224 return run(mergeHandler);
229 * {@code git checkout <reference>} <br/>
230 * {@code git checkout -b <newBranch> <reference>}
234 public GitCommandResult checkout(@NotNull GitRepository repository,
235 @NotNull String reference,
236 @Nullable String newBranch,
238 @NotNull GitLineHandlerListener... listeners) {
239 final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHECKOUT);
241 h.setStdoutSuppressed(false);
243 h.addParameters("--force");
245 if (newBranch == null) { // simply checkout
246 h.addParameters(reference);
248 else { // checkout reference as new branch
249 h.addParameters("-b", newBranch, reference);
251 for (GitLineHandlerListener listener : listeners) {
252 h.addLineListener(listener);
258 * {@code git checkout -b <branchName>}
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());
266 h.setStdoutSuppressed(false);
267 h.addParameters("-b");
268 h.addParameters(branchName);
269 if (listener != null) {
270 h.addLineListener(listener);
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);
281 h.addParameters(tagName);
282 if (!reference.isEmpty()) {
283 h.addParameters(reference);
285 if (listener != null) {
286 h.addLineListener(listener);
292 * {@code git branch -d <reference>} or {@code git branch -D <reference>}
296 public GitCommandResult branchDelete(@NotNull GitRepository repository,
297 @NotNull String branchName,
299 @NotNull GitLineHandlerListener... listeners) {
300 final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.BRANCH);
302 h.setStdoutSuppressed(false);
303 h.addParameters(force ? "-D" : "-d");
304 h.addParameters(branchName);
305 for (GitLineHandlerListener listener : listeners) {
306 h.addLineListener(listener);
312 * Get branches containing the commit.
313 * {@code git branch --contains <commit>}
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);
324 * Create branch without checking it out.
325 * {@code git branch <branchName>}
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);
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);
345 public GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) {
346 return reset(repository, "--merge", revision);
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);
357 addListeners(handler, listeners);
362 * Returns the last (tip) commit on the given branch.<br/>
363 * {@code git rev-list -1 <branchName>}
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);
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);
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>() {
388 public GitLineHandler compute() {
389 final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.PUSH);
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");
404 h.addParameters("--force");
406 if (tagMode != null) {
407 h.addParameters(tagMode);
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);
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
428 if (pushUrls.isEmpty()) {
429 LOG.error("No urls or pushUrls are defined for " + remote);
433 url = pushUrls.iterator().next();
435 String spec = source.getFullName() + ":" + target.getNameForRemoteOperations();
436 return doPush(repository, remote.getName(), url, spec, force, updateTracking, tagMode, listeners);
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);
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");
454 handler.addParameters("-n");
456 handler.addParameters(hash);
457 addListeners(handler, listeners);
458 handler.setSilent(false);
459 handler.setStdoutSuppressed(false);
465 public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) {
466 GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.LS_FILES);
467 h.addParameters("--unmerged");
473 * Fetch remote branch
474 * {@code git fetch <remote> <params>}
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>() {
482 public GitLineHandler compute() {
483 final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.FETCH);
485 h.setStdoutSuppressed(false);
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");
494 addListeners(h, listeners);
500 private static void addListeners(@NotNull GitLineHandler handler, @NotNull GitLineHandlerListener... listeners) {
501 addListeners(handler, Arrays.asList(listeners));
504 private static void addListeners(@NotNull GitLineHandler handler, @NotNull List<GitLineHandlerListener> listeners) {
505 for (GitLineHandlerListener listener : listeners) {
506 handler.addLineListener(listener);
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>();
525 startFailed.set(false);
528 GitLineHandler handler = handlerConstructor.compute();
529 handler.addLineListener(new GitLineHandlerListener() {
530 @Override public void onLineAvailable(String line, Key outputType) {
532 errorOutput.add(line);
538 @Override public void processTerminated(int code) {
542 @Override public void startFailed(Throwable t) {
543 startFailed.set(true);
544 errorOutput.add("Failed to start Git process");
549 handler.runInCurrentThread(null);
550 authFailed = handler.hasHttpAuthFailed();
551 success = !startFailed.get() && errorOutput.isEmpty() && (handler.isIgnoredErrorCode(exitCode.get()) || exitCode.get() == 0);
553 while (authFailed && authAttempt++ < 2);
554 return new GitCommandResult(success, exitCode.get(), errorOutput, output, null);
558 * Runs the given {@link GitLineHandler} in the current thread and returns the {@link GitCommandResult}.
561 private static GitCommandResult run(@NotNull GitLineHandler handler) {
562 return run(new Computable.PredefinedValueComputable<GitLineHandler>(handler));
567 public GitCommandResult runCommand(@NotNull Computable<GitLineHandler> handlerConstructor) {
568 return run(handlerConstructor);
572 * Check if the line looks line an error message
574 private static boolean isError(String text) {
575 for (String indicator : ERROR_INDICATORS) {
576 if (text.startsWith(indicator.toLowerCase())) {
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",