4fc4042e214d5b59edba97c3c4c869edd2b06d3b
[idea/community.git] / plugins / git4idea / src / git4idea / history / GitHistoryUtils.java
1 /*
2  * Copyright 2000-2014 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.history;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.components.ServiceManager;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.progress.ProcessCanceledException;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.*;
24 import com.intellij.openapi.util.registry.Registry;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vcs.FilePath;
27 import com.intellij.openapi.vcs.FileStatus;
28 import com.intellij.openapi.vcs.VcsException;
29 import com.intellij.openapi.vcs.changes.Change;
30 import com.intellij.openapi.vcs.changes.ChangeListManager;
31 import com.intellij.openapi.vcs.diff.ItemLatestState;
32 import com.intellij.openapi.vcs.history.VcsFileRevision;
33 import com.intellij.openapi.vcs.history.VcsRevisionDescription;
34 import com.intellij.openapi.vcs.history.VcsRevisionDescriptionImpl;
35 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
36 import com.intellij.openapi.vfs.VirtualFile;
37 import com.intellij.util.*;
38 import com.intellij.util.concurrency.Semaphore;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.OpenTHashSet;
41 import com.intellij.vcs.log.*;
42 import com.intellij.vcs.log.impl.HashImpl;
43 import com.intellij.vcs.log.impl.LogDataImpl;
44 import com.intellij.vcs.log.util.StopWatch;
45 import git4idea.*;
46 import git4idea.branch.GitBranchUtil;
47 import git4idea.commands.*;
48 import git4idea.config.GitVersion;
49 import git4idea.config.GitVersionSpecialty;
50 import git4idea.history.browser.GitHeavyCommit;
51 import git4idea.history.browser.SHAHash;
52 import git4idea.history.browser.SymbolicRefs;
53 import git4idea.history.browser.SymbolicRefsI;
54 import git4idea.history.wholeTree.AbstractHash;
55 import git4idea.log.GitLogProvider;
56 import git4idea.log.GitRefManager;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import java.util.*;
61 import java.util.concurrent.atomic.AtomicBoolean;
62 import java.util.concurrent.atomic.AtomicInteger;
63 import java.util.concurrent.atomic.AtomicReference;
64
65 import static git4idea.history.GitLogParser.GitLogOption.*;
66
67 /**
68  * A collection of methods for retrieving history information from native Git.
69  */
70 public class GitHistoryUtils {
71
72   /**
73    * A parameter to {@code git log} which is equivalent to {@code --all}, but doesn't show the stuff from index or stash.
74    */
75   public static final List<String> LOG_ALL = Arrays.asList("HEAD", "--branches", "--remotes", "--tags");
76
77   private static final Logger LOG = Logger.getInstance("#git4idea.history.GitHistoryUtils");
78
79   private GitHistoryUtils() {
80   }
81
82   /**
83    * Get current revision for the file under git in the current or specified branch.
84    *
85    * @param project  a project
86    * @param filePath file path to the file which revision is to be retrieved.
87    * @param branch   name of branch or null if current branch wanted.
88    * @return revision number or null if the file is unversioned or new.
89    * @throws VcsException if there is a problem with running git.
90    */
91   @Nullable
92   public static VcsRevisionNumber getCurrentRevision(@NotNull Project project, @NotNull FilePath filePath,
93                                                      @Nullable String branch) throws VcsException {
94     filePath = getLastCommitName(project, filePath);
95     GitSimpleHandler h = new GitSimpleHandler(project, GitUtil.getGitRoot(filePath), GitCommand.LOG);
96     GitLogParser parser = new GitLogParser(project, HASH, COMMIT_TIME);
97     h.setSilent(true);
98     h.addParameters("-n1", parser.getPretty());
99     h.addParameters(!StringUtil.isEmpty(branch) ? branch : "--all");
100     h.endOptions();
101     h.addRelativePaths(filePath);
102     String result = h.run();
103     if (result.length() == 0) {
104       return null;
105     }
106     final GitLogRecord record = parser.parseOneRecord(result);
107     if (record == null) {
108       return null;
109     }
110     record.setUsedHandler(h);
111     return new GitRevisionNumber(record.getHash(), record.getDate());
112   }
113
114   @Nullable
115   public static VcsRevisionDescription getCurrentRevisionDescription(final Project project, FilePath filePath)
116     throws VcsException {
117     filePath = getLastCommitName(project, filePath);
118     GitSimpleHandler h = new GitSimpleHandler(project, GitUtil.getGitRoot(filePath), GitCommand.LOG);
119     GitLogParser parser = new GitLogParser(project, HASH, COMMIT_TIME, AUTHOR_NAME, COMMITTER_NAME, SUBJECT, BODY, RAW_BODY);
120     h.setSilent(true);
121     h.addParameters("-n1", parser.getPretty());
122     h.addParameters("--all");
123     h.endOptions();
124     h.addRelativePaths(filePath);
125     String result = h.run();
126     if (result.length() == 0) {
127       return null;
128     }
129     final GitLogRecord record = parser.parseOneRecord(result);
130     if (record == null) {
131       return null;
132     }
133     record.setUsedHandler(h);
134
135     final String author = Comparing.equal(record.getAuthorName(), record.getCommitterName()) ? record.getAuthorName() :
136                           record.getAuthorName() + " (" + record.getCommitterName() + ")";
137     return new VcsRevisionDescriptionImpl(new GitRevisionNumber(record.getHash(), record.getDate()), record.getDate(), author,
138                                           record.getFullMessage());
139   }
140
141   /**
142    * Get current revision for the file under git
143    *
144    * @param project  a project
145    * @param filePath a file path
146    * @return a revision number or null if the file is unversioned or new
147    * @throws VcsException if there is problem with running git
148    */
149   @Nullable
150   public static ItemLatestState getLastRevision(final Project project, FilePath filePath) throws VcsException {
151     VirtualFile root = GitUtil.getGitRoot(filePath);
152     GitBranch c = GitBranchUtil.getCurrentBranch(project, root);
153     GitBranch t = c == null ? null : GitBranchUtil.tracked(project, root, c.getName());
154     if (t == null) {
155       return new ItemLatestState(getCurrentRevision(project, filePath, null), true, false);
156     }
157     filePath = getLastCommitName(project, filePath);
158     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
159     GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.STATUS, HASH, COMMIT_TIME, PARENTS);
160     h.setSilent(true);
161     h.addParameters("-n1", parser.getPretty(), "--name-status", t.getFullName());
162     h.endOptions();
163     h.addRelativePaths(filePath);
164     String result = h.run();
165     if (result.length() == 0) {
166       return null;
167     }
168     GitLogRecord record = parser.parseOneRecord(result);
169     if (record == null) {
170       return null;
171     }
172     final List<Change> changes = record.parseChanges(project, root);
173     boolean exists = changes.isEmpty() || !FileStatus.DELETED.equals(changes.get(0).getFileStatus());
174     record.setUsedHandler(h);
175     return new ItemLatestState(new GitRevisionNumber(record.getHash(), record.getDate()), exists, false);
176   }
177
178   /*
179    === Smart full log with renames ===
180    'git log --follow' does detect renames, but it has a bug - merge commits aren't handled properly: they just dissapear from the history.
181    See http://kerneltrap.org/mailarchive/git/2009/1/30/4861054 and the whole thread about that: --follow is buggy, but maybe it won't be fixed.
182    To get the whole history through renames we do the following:
183    1. 'git log <file>' - and we get the history since the first rename, if there was one.
184    2. 'git show -M --follow --name-status <first_commit_id> -- <file>'
185       where <first_commit_id> is the hash of the first commit in the history we got in #1.
186       With this command we get the rename-detection-friendly information about the first commit of the given file history.
187       (by specifying the <file> we filter out other changes in that commit; but in that case rename detection requires '--follow' to work,
188       that's safe for one commit though)
189       If the first commit was ADDING the file, then there were no renames with this file, we have the full history.
190       But if the first commit was RENAMING the file, we are going to query for the history before rename.
191       Now we have the previous name of the file:
192
193         ~/sandbox/git # git show --oneline --name-status -M 4185b97
194         4185b97 renamed a to b
195         R100    a       b
196
197    3. 'git log <rename_commit_id> -- <previous_file_name>' - get the history of a before the given commit.
198       We need to specify <rename_commit_id> here, because <previous_file_name> could have some new history, which has nothing common with our <file>.
199       Then we repeat 2 and 3 until the first commit is ADDING the file, not RENAMING it.
200
201     TODO: handle multiple repositories configuration: a file can be moved from one repo to another
202    */
203
204   /**
205    * Retrieves the history of the file, including renames.
206    *
207    * @param project
208    * @param path              FilePath which history is queried.
209    * @param root              Git root - optional: if this is null, then git root will be detected automatically.
210    * @param consumer          This consumer is notified ({@link Consumer#consume(Object)} when new history records are retrieved.
211    * @param exceptionConsumer This consumer is notified in case of error while executing git command.
212    * @param parameters        Optional parameters which will be added to the git log command just before the path.
213    */
214   public static void history(@NotNull Project project,
215                              @NotNull FilePath path,
216                              @Nullable VirtualFile root,
217                              @NotNull Consumer<GitFileRevision> consumer,
218                              @NotNull Consumer<VcsException> exceptionConsumer,
219                              String... parameters) {
220     history(project, path, root, GitRevisionNumber.HEAD, consumer, exceptionConsumer, parameters);
221   }
222
223   public static void history(@NotNull final Project project,
224                              @NotNull FilePath path,
225                              @Nullable VirtualFile root,
226                              @NotNull VcsRevisionNumber startingRevision,
227                              @NotNull final Consumer<GitFileRevision> consumer,
228                              @NotNull final Consumer<VcsException> exceptionConsumer,
229                              String... parameters) {
230     // adjust path using change manager
231     final FilePath filePath = getLastCommitName(project, path);
232     final VirtualFile finalRoot;
233     try {
234       finalRoot = (root == null ? GitUtil.getGitRoot(filePath) : root);
235     }
236     catch (VcsException e) {
237       exceptionConsumer.consume(e);
238       return;
239     }
240     final GitLogParser logParser = new GitLogParser(project, GitLogParser.NameStatus.STATUS,
241                                                     HASH, COMMIT_TIME, AUTHOR_NAME, AUTHOR_EMAIL, COMMITTER_NAME, COMMITTER_EMAIL, PARENTS,
242                                                     SUBJECT, BODY, RAW_BODY, AUTHOR_TIME);
243
244     final AtomicReference<String> firstCommit = new AtomicReference<>(startingRevision.asString());
245     final AtomicReference<String> firstCommitParent = new AtomicReference<>(firstCommit.get());
246     final AtomicReference<FilePath> currentPath = new AtomicReference<>(filePath);
247     final AtomicReference<GitLineHandler> logHandler = new AtomicReference<>();
248     final AtomicBoolean skipFurtherOutput = new AtomicBoolean();
249
250     final Consumer<GitLogRecord> resultAdapter = new Consumer<GitLogRecord>() {
251       public void consume(GitLogRecord record) {
252         if (skipFurtherOutput.get()) {
253           return;
254         }
255         if (record == null) {
256           exceptionConsumer.consume(new VcsException("revision details are null."));
257           return;
258         }
259         record.setUsedHandler(logHandler.get());
260         final GitRevisionNumber revision = new GitRevisionNumber(record.getHash(), record.getDate());
261         firstCommit.set(record.getHash());
262         final String[] parentHashes = record.getParentsHashes();
263         if (parentHashes.length < 1) {
264           firstCommitParent.set(null);
265         }
266         else {
267           firstCommitParent.set(parentHashes[0]);
268         }
269         final String message = record.getFullMessage();
270
271         FilePath revisionPath;
272         try {
273           final List<FilePath> paths = record.getFilePaths(finalRoot);
274           if (paths.size() > 0) {
275             revisionPath = paths.get(0);
276           }
277           else {
278             // no paths are shown for merge commits, so we're using the saved path we're inspecting now
279             revisionPath = currentPath.get();
280           }
281
282           Couple<String> authorPair = Couple.of(record.getAuthorName(), record.getAuthorEmail());
283           Couple<String> committerPair = Couple.of(record.getCommitterName(), record.getCommitterEmail());
284           Collection<String> parents = Arrays.asList(parentHashes);
285           consumer.consume(new GitFileRevision(project, finalRoot, revisionPath, revision, Couple.of(authorPair, committerPair), message,
286                                                null, new Date(record.getAuthorTimeStamp()), parents));
287           List<GitLogStatusInfo> statusInfos = record.getStatusInfos();
288           if (statusInfos.isEmpty()) {
289             // can safely be empty, for example, for simple merge commits that don't change anything.
290             return;
291           }
292           if (statusInfos.get(0).getType() == GitChangeType.ADDED && !filePath.isDirectory()) {
293             skipFurtherOutput.set(true);
294           }
295         }
296         catch (VcsException e) {
297           exceptionConsumer.consume(e);
298         }
299       }
300     };
301
302     GitVcs vcs = GitVcs.getInstance(project);
303     GitVersion version = vcs != null ? vcs.getVersion() : GitVersion.NULL;
304     final AtomicBoolean criticalFailure = new AtomicBoolean();
305     while (currentPath.get() != null && firstCommitParent.get() != null) {
306       logHandler.set(getLogHandler(project, version, finalRoot, logParser, currentPath.get(), firstCommitParent.get(), parameters));
307       final MyTokenAccumulator accumulator = new MyTokenAccumulator(logParser);
308       final Semaphore semaphore = new Semaphore();
309
310       logHandler.get().addLineListener(new GitLineHandlerAdapter() {
311         @Override
312         public void onLineAvailable(String line, Key outputType) {
313           final GitLogRecord record = accumulator.acceptLine(line);
314           if (record != null) {
315             resultAdapter.consume(record);
316           }
317         }
318
319         @Override
320         public void startFailed(Throwable exception) {
321           //noinspection ThrowableInstanceNeverThrown
322           try {
323             exceptionConsumer.consume(new VcsException(exception));
324           }
325           finally {
326             criticalFailure.set(true);
327             semaphore.up();
328           }
329         }
330
331         @Override
332         public void processTerminated(int exitCode) {
333           try {
334             super.processTerminated(exitCode);
335             final GitLogRecord record = accumulator.processLast();
336             if (record != null) {
337               resultAdapter.consume(record);
338             }
339           }
340           catch (Throwable t) {
341             LOG.error(t);
342             exceptionConsumer.consume(new VcsException("Internal error " + t.getMessage(), t));
343             criticalFailure.set(true);
344           }
345           finally {
346             semaphore.up();
347           }
348         }
349       });
350       semaphore.down();
351       logHandler.get().start();
352       semaphore.waitFor();
353       if (criticalFailure.get()) {
354         return;
355       }
356
357       try {
358         Pair<String, FilePath> firstCommitParentAndPath = getFirstCommitParentAndPathIfRename(project, finalRoot, firstCommit.get(),
359                                                                                               currentPath.get(), version);
360         currentPath.set(firstCommitParentAndPath == null ? null : firstCommitParentAndPath.second);
361         firstCommitParent.set(firstCommitParentAndPath == null ? null : firstCommitParentAndPath.first);
362         skipFurtherOutput.set(false);
363       }
364       catch (VcsException e) {
365         LOG.warn("Tried to get first commit rename path", e);
366         exceptionConsumer.consume(e);
367         return;
368       }
369     }
370   }
371
372   private static GitLineHandler getLogHandler(Project project,
373                                               @NotNull GitVersion version,
374                                               VirtualFile root,
375                                               GitLogParser parser,
376                                               FilePath path,
377                                               String lastCommit,
378                                               String... parameters) {
379     final GitLineHandler h = new GitLineHandler(project, root, GitCommand.LOG);
380     h.setStdoutSuppressed(true);
381     h.addParameters("--name-status", parser.getPretty(), "--encoding=UTF-8", lastCommit);
382     if (GitVersionSpecialty.FULL_HISTORY_SIMPLIFY_MERGES_WORKS_CORRECTLY.existsIn(version) && Registry.is("git.file.history.full")) {
383       h.addParameters("--full-history", "--simplify-merges");
384     }
385     if (parameters != null && parameters.length > 0) {
386       h.addParameters(parameters);
387     }
388     h.endOptions();
389     h.addRelativePaths(path);
390     return h;
391   }
392
393   /**
394    * Gets info of the given commit and checks if it was a RENAME.
395    * If yes, returns the older file path, which file was renamed from.
396    * If it's not a rename, returns null.
397    */
398   @Nullable
399   private static Pair<String, FilePath> getFirstCommitParentAndPathIfRename(Project project,
400                                                                             VirtualFile root,
401                                                                             String commit,
402                                                                             FilePath filePath,
403                                                                             @NotNull GitVersion version) throws VcsException {
404     // 'git show -M --name-status <commit hash>' returns the information about commit and detects renames.
405     // NB: we can't specify the filepath, because then rename detection will work only with the '--follow' option, which we don't wanna use.
406     final GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.SHOW);
407     final GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.STATUS, HASH, COMMIT_TIME, PARENTS);
408     h.setStdoutSuppressed(true);
409     h.addParameters("-M", "--name-status", parser.getPretty(), "--encoding=UTF-8", commit);
410     if (!GitVersionSpecialty.FOLLOW_IS_BUGGY_IN_THE_LOG.existsIn(version)) {
411       h.addParameters("--follow");
412       h.endOptions();
413       h.addRelativePaths(filePath);
414     }
415     else {
416       h.endOptions();
417     }
418     final String output = h.run();
419     final List<GitLogRecord> records = parser.parse(output);
420
421     if (records.isEmpty()) return null;
422     // we have information about all changed files of the commit. Extracting information about the file we need.
423     GitLogRecord record = records.get(0);
424     final List<Change> changes = record.parseChanges(project, root);
425     for (Change change : changes) {
426       if ((change.isMoved() || change.isRenamed()) && filePath.equals(change.getAfterRevision().getFile())) {
427         final String[] parents = record.getParentsHashes();
428         String parent = parents.length > 0 ? parents[0] : null;
429         return Pair.create(parent, change.getBeforeRevision().getFile());
430       }
431     }
432     return null;
433   }
434
435   public static List<? extends VcsShortCommitDetails> readMiniDetails(final Project project, final VirtualFile root, List<String> hashes)
436     throws VcsException {
437     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
438     if (factory == null) {
439       return Collections.emptyList();
440     }
441
442     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
443     GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.NONE, HASH, PARENTS, AUTHOR_NAME,
444                                            AUTHOR_EMAIL, COMMIT_TIME, SUBJECT, COMMITTER_NAME, COMMITTER_EMAIL, AUTHOR_TIME);
445     h.setSilent(true);
446     // git show can show either -p, or --name-status, or --name-only, but we need nothing, just details => using git log --no-walk
447     h.addParameters("--no-walk");
448     h.addParameters(parser.getPretty(), "--encoding=UTF-8");
449     h.addParameters(new ArrayList<>(hashes));
450     h.endOptions();
451
452     String output = h.run();
453     List<GitLogRecord> records = parser.parse(output);
454
455     return ContainerUtil.map(records, new Function<GitLogRecord, VcsShortCommitDetails>() {
456       @Override
457       public VcsShortCommitDetails fun(GitLogRecord record) {
458         List<Hash> parents = new SmartList<>();
459         for (String parent : record.getParentsHashes()) {
460           parents.add(HashImpl.build(parent));
461         }
462         return factory.createShortDetails(HashImpl.build(record.getHash()), parents, record.getCommitTime(), root,
463                                           record.getSubject(), record.getAuthorName(), record.getAuthorEmail(), record.getCommitterName(),
464                                           record.getCommitterEmail(),
465                                           record.getAuthorTimeStamp());
466       }
467     });
468   }
469
470   @Nullable
471   public static List<VcsCommitMetadata> readLastCommits(@NotNull Project project,
472                                                         @NotNull final VirtualFile root,
473                                                         @NotNull String... refs)
474     throws VcsException {
475     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
476     if (factory == null) {
477       return null;
478     }
479
480     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
481     GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.NONE, HASH, PARENTS, COMMIT_TIME, SUBJECT, AUTHOR_NAME,
482                                            AUTHOR_EMAIL, RAW_BODY, COMMITTER_NAME, COMMITTER_EMAIL, AUTHOR_TIME);
483
484     h.setSilent(true);
485     // git show can show either -p, or --name-status, or --name-only, but we need nothing, just details => using git log --no-walk
486     h.addParameters("--no-walk");
487     h.addParameters(parser.getPretty(), "--encoding=UTF-8");
488     h.addParameters(refs);
489     h.endOptions();
490
491     String output = h.run();
492     List<GitLogRecord> records = parser.parse(output);
493     if (records.size() != refs.length) return null;
494
495     return ContainerUtil.map(records, new Function<GitLogRecord, VcsCommitMetadata>() {
496       @Override
497       public VcsCommitMetadata fun(GitLogRecord record) {
498         return factory.createCommitMetadata(factory.createHash(record.getHash()), getParentHashes(factory, record), record.getCommitTime(),
499                                             root, record.getSubject(), record.getAuthorName(), record.getAuthorEmail(),
500                                             record.getFullMessage(), record.getCommitterName(), record.getCommitterEmail(),
501                                             record.getAuthorTimeStamp());
502       }
503     });
504   }
505
506   private static void processHandlerOutputByLine(@NotNull GitLineHandler handler,
507                                                  @NotNull Consumer<StringBuilder> recordConsumer,
508                                                  int bufferSize)
509     throws VcsException {
510     final StringBuilder buffer = new StringBuilder();
511     final Ref<VcsException> ex = new Ref<>();
512     final AtomicInteger records = new AtomicInteger();
513     handler.addLineListener(new GitLineHandlerListener() {
514       @Override
515       public void onLineAvailable(String line, Key outputType) {
516         try {
517           String tail = null;
518           int nextRecordStart = line.indexOf(GitLogParser.RECORD_START);
519           if (nextRecordStart == -1) {
520             buffer.append(line).append("\n");
521           }
522           else if (nextRecordStart == 0) {
523             tail = line + "\n";
524           }
525           else {
526             buffer.append(line.substring(0, nextRecordStart));
527             tail = line.substring(nextRecordStart) + "\n";
528           }
529
530           if (tail != null) {
531             if (records.incrementAndGet() > bufferSize) {
532               recordConsumer.consume(buffer);
533               buffer.setLength(0);
534             }
535             buffer.append(tail);
536           }
537         }
538         catch (Exception e) {
539           ex.set(new VcsException(e));
540         }
541       }
542
543       @Override
544       public void processTerminated(int exitCode) {
545         try {
546           recordConsumer.consume(buffer);
547         }
548         catch (Exception e) {
549           ex.set(new VcsException(e));
550         }
551       }
552
553       @Override
554       public void startFailed(Throwable exception) {
555         ex.set(new VcsException(exception));
556       }
557     });
558     handler.runInCurrentThread(null);
559     if (!ex.isNull()) {
560       if (ex.get().getCause() instanceof ProcessCanceledException) {
561         throw (ProcessCanceledException)ex.get().getCause();
562       }
563       throw ex.get();
564     }
565   }
566
567   /*
568   Unlike loadDetails, which accepts list of hashes in parameters, loads details for all commits in the repository.
569   To optimize memory consumption, git log command output is parsed on-the-fly and resulting commits are immediately fed to the consumer
570   and not stored in memory.
571    */
572   public static void loadAllDetails(@NotNull Project project,
573                                     @NotNull VirtualFile root,
574                                     @NotNull Consumer<VcsFullCommitDetails> commitConsumer) throws VcsException {
575     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
576     if (factory == null) {
577       return;
578     }
579
580     GitLineHandler h = new GitLineHandler(project, root, GitCommand.LOG);
581     GitLogParser parser = createParserForDetails(h, project, false, true, ArrayUtil.toStringArray(LOG_ALL));
582
583     Ref<Throwable> parseError = new Ref<>();
584     Consumer<StringBuilder> recordConsumer = builder -> {
585       try {
586         GitLogRecord record = parser.parseOneRecord(builder.toString());
587         if (record != null) {
588           commitConsumer.consume(createCommit(project, root, record, factory));
589         }
590       }
591       catch (ProcessCanceledException ignored) {
592       }
593       catch (Throwable t) {
594         if (parseError.get() == null) {
595           parseError.set(t);
596           LOG.error("Could not parse \" " + builder.toString() + "\"", t);
597         }
598       }
599     };
600     processHandlerOutputByLine(h, recordConsumer, 0);
601   }
602
603   public static void readCommits(@NotNull final Project project,
604                                  @NotNull final VirtualFile root,
605                                  @NotNull List<String> parameters,
606                                  @NotNull final Consumer<VcsUser> userConsumer,
607                                  @NotNull final Consumer<VcsRef> refConsumer,
608                                  @NotNull final Consumer<TimedVcsCommit> commitConsumer) throws VcsException {
609     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
610     if (factory == null) {
611       return;
612     }
613
614     GitLineHandler h = new GitLineHandler(project, root, GitCommand.LOG);
615     final GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.NONE, HASH, PARENTS, COMMIT_TIME,
616                                                  AUTHOR_NAME, AUTHOR_EMAIL, REF_NAMES);
617     h.setStdoutSuppressed(true);
618     h.addParameters(parser.getPretty(), "--encoding=UTF-8");
619     h.addParameters("--decorate=full");
620     h.addParameters(parameters);
621     h.endOptions();
622
623     final int COMMIT_BUFFER = 1000;
624     processHandlerOutputByLine(h, buffer -> {
625       List<TimedVcsCommit> commits = parseCommit(parser, buffer, userConsumer, refConsumer, factory, root);
626       for (TimedVcsCommit commit : commits) {
627         commitConsumer.consume(commit);
628       }
629     }, COMMIT_BUFFER);
630   }
631
632   @NotNull
633   private static List<TimedVcsCommit> parseCommit(@NotNull GitLogParser parser,
634                                                   @NotNull StringBuilder record,
635                                                   @NotNull final Consumer<VcsUser> userRegistry,
636                                                   @NotNull final Consumer<VcsRef> refConsumer,
637                                                   @NotNull final VcsLogObjectsFactory factory,
638                                                   @NotNull final VirtualFile root) {
639     List<GitLogRecord> rec = parser.parse(record.toString());
640     return ContainerUtil.mapNotNull(rec, new Function<GitLogRecord, TimedVcsCommit>() {
641       @Override
642       public TimedVcsCommit fun(GitLogRecord record) {
643         if (record == null) {
644           return null;
645         }
646         Pair<TimedVcsCommit, Collection<VcsRef>> pair = convert(record, factory, root);
647         TimedVcsCommit commit = pair.first;
648         for (VcsRef ref : pair.second) {
649           refConsumer.consume(ref);
650         }
651         userRegistry.consume(factory.createUser(record.getAuthorName(), record.getAuthorEmail()));
652         return commit;
653       }
654     });
655   }
656
657   @NotNull
658   private static Pair<TimedVcsCommit, Collection<VcsRef>> convert(@NotNull GitLogRecord rec,
659                                                                   @NotNull VcsLogObjectsFactory factory,
660                                                                   @NotNull VirtualFile root) {
661     Hash hash = HashImpl.build(rec.getHash());
662     List<Hash> parents = getParentHashes(factory, rec);
663     TimedVcsCommit commit = factory.createTimedCommit(hash, parents, rec.getCommitTime());
664     return Pair.create(commit, parseRefs(rec.getRefs(), hash, factory, root));
665   }
666
667   @NotNull
668   private static Collection<VcsRef> parseRefs(@NotNull Collection<String> refs,
669                                               @NotNull final Hash hash,
670                                               @NotNull final VcsLogObjectsFactory factory,
671                                               @NotNull final VirtualFile root) {
672     return ContainerUtil.mapNotNull(refs, new Function<String, VcsRef>() {
673       @Override
674       public VcsRef fun(String refName) {
675         VcsRefType type = GitRefManager.getRefType(refName);
676         refName = GitBranchUtil.stripRefsPrefix(refName);
677         return refName.equals(GitUtil.ORIGIN_HEAD) ? null : factory.createRef(hash, refName, type, root);
678       }
679     });
680   }
681
682   @Nullable
683   private static VcsLogObjectsFactory getObjectsFactoryWithDisposeCheck(@NotNull final Project project) {
684     return ApplicationManager.getApplication().runReadAction(new Computable<VcsLogObjectsFactory>() {
685       @Override
686       public VcsLogObjectsFactory compute() {
687         if (!project.isDisposed()) {
688           return ServiceManager.getService(project, VcsLogObjectsFactory.class);
689         }
690         return null;
691       }
692     });
693   }
694
695   private static class MyTokenAccumulator {
696     private final StringBuilder myBuffer = new StringBuilder();
697
698     private boolean myNotStarted = true;
699     private GitLogParser myParser;
700
701     public MyTokenAccumulator(GitLogParser parser) {
702       myParser = parser;
703     }
704
705     @Nullable
706     public GitLogRecord acceptLine(String s) {
707       final boolean recordStart = s.startsWith(GitLogParser.RECORD_START);
708       if (recordStart) {
709         s = s.substring(GitLogParser.RECORD_START.length());
710       }
711
712       if (myNotStarted) {
713         myBuffer.append(s);
714         myBuffer.append("\n");
715
716         myNotStarted = false;
717         return null;
718       }
719       else if (recordStart) {
720         final String line = myBuffer.toString();
721         myBuffer.setLength(0);
722
723         myBuffer.append(s);
724         myBuffer.append("\n");
725
726         return processResult(line);
727       }
728       else {
729         myBuffer.append(s);
730         myBuffer.append("\n");
731         return null;
732       }
733     }
734
735     public GitLogRecord processLast() {
736       return processResult(myBuffer.toString());
737     }
738
739     private GitLogRecord processResult(final String line) {
740       return myParser.parseOneRecord(line);
741     }
742   }
743
744   /**
745    * Get history for the file
746    *
747    * @param project the context project
748    * @param path    the file path
749    * @return the list of the revisions
750    * @throws VcsException if there is problem with running git
751    */
752   public static List<VcsFileRevision> history(final Project project, final FilePath path, String... parameters) throws VcsException {
753     final VirtualFile root = GitUtil.getGitRoot(path);
754     return history(project, path, root, parameters);
755   }
756
757   public static List<VcsFileRevision> history(@NotNull Project project,
758                                               @NotNull FilePath path,
759                                               @Nullable VirtualFile root,
760                                               String... parameters) throws VcsException {
761     return history(project, path, root, GitRevisionNumber.HEAD, parameters);
762   }
763
764   public static List<VcsFileRevision> history(@NotNull Project project,
765                                               @NotNull FilePath path,
766                                               @Nullable VirtualFile root,
767                                               @NotNull VcsRevisionNumber startingFrom,
768                                               String... parameters) throws VcsException {
769     final List<VcsFileRevision> rc = new ArrayList<>();
770     final List<VcsException> exceptions = new ArrayList<>();
771
772     history(project, path, root, startingFrom, new Consumer<GitFileRevision>() {
773       @Override
774       public void consume(GitFileRevision gitFileRevision) {
775         rc.add(gitFileRevision);
776       }
777     }, new Consumer<VcsException>() {
778       @Override
779       public void consume(VcsException e) {
780         exceptions.add(e);
781       }
782     }, parameters);
783     if (!exceptions.isEmpty()) {
784       throw exceptions.get(0);
785     }
786     return rc;
787   }
788
789   /**
790    * @deprecated To remove in IDEA 17
791    */
792   @Deprecated
793   @SuppressWarnings("unused")
794   public static List<Pair<SHAHash, Date>> onlyHashesHistory(Project project, FilePath path, final String... parameters)
795     throws VcsException {
796     final VirtualFile root = GitUtil.getGitRoot(path);
797     return onlyHashesHistory(project, path, root, parameters);
798   }
799
800   /**
801    * @deprecated To remove in IDEA 17
802    */
803   @Deprecated
804   public static List<Pair<SHAHash, Date>> onlyHashesHistory(Project project,
805                                                             FilePath path,
806                                                             final VirtualFile root,
807                                                             final String... parameters)
808     throws VcsException {
809     // adjust path using change manager
810     path = getLastCommitName(project, path);
811     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
812     GitLogParser parser = new GitLogParser(project, HASH, COMMIT_TIME);
813     h.setStdoutSuppressed(true);
814     h.addParameters(parameters);
815     h.addParameters(parser.getPretty(), "--encoding=UTF-8");
816     h.endOptions();
817     h.addRelativePaths(path);
818     String output = h.run();
819
820     final List<Pair<SHAHash, Date>> rc = new ArrayList<>();
821     for (GitLogRecord record : parser.parse(output)) {
822       record.setUsedHandler(h);
823       rc.add(Pair.create(new SHAHash(record.getHash()), record.getDate()));
824     }
825     return rc;
826   }
827
828   @NotNull
829   public static VcsLogProvider.DetailedLogData loadMetadata(@NotNull final Project project,
830                                                             @NotNull final VirtualFile root,
831                                                             final boolean withRefs,
832                                                             String... params) throws VcsException {
833     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
834     if (factory == null) {
835       return LogDataImpl.empty();
836     }
837     final Set<VcsRef> refs = new OpenTHashSet<>(GitLogProvider.DONT_CONSIDER_SHA);
838     final List<VcsCommitMetadata> commits =
839       loadDetails(project, root, withRefs, false, new NullableFunction<GitLogRecord, VcsCommitMetadata>() {
840         @Nullable
841         @Override
842         public VcsCommitMetadata fun(GitLogRecord record) {
843           GitCommit commit = createCommit(project, root, record, factory);
844           if (withRefs) {
845             Collection<VcsRef> refsInRecord = parseRefs(record.getRefs(), commit.getId(), factory, root);
846             for (VcsRef ref : refsInRecord) {
847               if (!refs.add(ref)) {
848                 LOG.error("Adding duplicate element to the set");
849               }
850             }
851           }
852           return commit;
853         }
854       }, params);
855     return new LogDataImpl(refs, commits);
856   }
857
858   /**
859    * <p>Get & parse git log detailed output with commits, their parents and their changes.</p>
860    * <p>
861    * <p>Warning: this is method is efficient by speed, but don't query too much, because the whole log output is retrieved at once,
862    * and it can occupy too much memory. The estimate is ~600Kb for 1000 commits.</p>
863    */
864   @NotNull
865   public static List<GitCommit> history(@NotNull final Project project, @NotNull final VirtualFile root, String... parameters)
866     throws VcsException {
867     final VcsLogObjectsFactory factory = getObjectsFactoryWithDisposeCheck(project);
868     if (factory == null) {
869       return Collections.emptyList();
870     }
871     return loadDetails(project, root, false, true, new NullableFunction<GitLogRecord, GitCommit>() {
872       @Override
873       @Nullable
874       public GitCommit fun(GitLogRecord record) {
875         return createCommit(project, root, record, factory);
876       }
877     }, parameters);
878   }
879
880   private static GitLogParser createParserForDetails(@NotNull GitTextHandler h,
881                                                      @NotNull Project project,
882                                                      boolean withRefs,
883                                                      boolean withChanges,
884                                                      String... parameters) {
885     GitLogParser.NameStatus status = withChanges ? GitLogParser.NameStatus.STATUS : GitLogParser.NameStatus.NONE;
886     GitLogParser.GitLogOption[] options = {HASH, COMMIT_TIME, AUTHOR_NAME, AUTHOR_TIME, AUTHOR_EMAIL, COMMITTER_NAME, COMMITTER_EMAIL,
887       PARENTS, SUBJECT, BODY, RAW_BODY};
888     if (withRefs) {
889       options = ArrayUtil.append(options, REF_NAMES);
890     }
891     GitLogParser parser = new GitLogParser(project, status, options);
892     h.setStdoutSuppressed(true);
893     h.addParameters(parameters);
894     h.addParameters(parser.getPretty(), "--encoding=UTF-8");
895     if (withRefs) {
896       h.addParameters("--decorate=full");
897     }
898     if (withChanges) {
899       h.addParameters("-M", /*find and report renames*/
900                       "--name-status",
901                       "-c" /*single diff for merge commits, only showing files that were modified from both parents*/);
902     }
903     h.endOptions();
904
905     return parser;
906   }
907
908   @NotNull
909   public static <T> List<T> loadDetails(@NotNull final Project project,
910                                         @NotNull final VirtualFile root,
911                                         boolean withRefs,
912                                         boolean withChanges,
913                                         @NotNull NullableFunction<GitLogRecord, T> converter,
914                                         String... parameters)
915     throws VcsException {
916     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
917     GitLogParser parser = createParserForDetails(h, project, withRefs, withChanges, parameters);
918
919     StopWatch sw = StopWatch.start("loading details");
920     String output = h.run();
921     sw.report();
922
923     sw = StopWatch.start("parsing");
924     List<GitLogRecord> records = parser.parse(output);
925     sw.report();
926
927     sw = StopWatch.start("Creating objects");
928     List<T> commits = ContainerUtil.mapNotNull(records, converter);
929     sw.report();
930     return commits;
931   }
932
933   private static GitCommit createCommit(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLogRecord record,
934                                         @NotNull VcsLogObjectsFactory factory) {
935     List<Hash> parents = getParentHashes(factory, record);
936     return new GitCommit(project, HashImpl.build(record.getHash()), parents, record.getCommitTime(), root, record.getSubject(),
937                          factory.createUser(record.getAuthorName(), record.getAuthorEmail()), record.getFullMessage(),
938                          factory.createUser(record.getCommitterName(), record.getCommitterEmail()), record.getAuthorTimeStamp(),
939                          record.getStatusInfos());
940   }
941
942   @NotNull
943   private static List<Hash> getParentHashes(@NotNull final VcsLogObjectsFactory factory, @NotNull GitLogRecord record) {
944     return ContainerUtil.map(record.getParentsHashes(), new Function<String, Hash>() {
945       @Override
946       public Hash fun(String hash) {
947         return factory.createHash(hash);
948       }
949     });
950   }
951
952   @NotNull
953   private static GitHeavyCommit createCommit(@NotNull Project project, @Nullable SymbolicRefsI refs, @NotNull VirtualFile root,
954                                              @NotNull GitLogRecord record) throws VcsException {
955     final Collection<String> currentRefs = record.getRefs();
956     List<String> locals = new ArrayList<>();
957     List<String> remotes = new ArrayList<>();
958     List<String> tags = new ArrayList<>();
959     final String s = parseRefs(refs, currentRefs, locals, remotes, tags);
960
961     GitHeavyCommit
962       gitCommit = new GitHeavyCommit(root, AbstractHash.create(record.getHash()), new SHAHash(record.getHash()), record.getAuthorName(),
963                                      record.getCommitterName(),
964                                      record.getDate(), record.getSubject(), record.getFullMessage(),
965                                      new HashSet<>(Arrays.asList(record.getParentsHashes())), record.getFilePaths(root),
966                                      record.getAuthorEmail(),
967                                      record.getCommitterEmail(), tags, locals, remotes,
968                                      record.parseChanges(project, root), record.getAuthorTimeStamp());
969     gitCommit.setCurrentBranch(s);
970     return gitCommit;
971   }
972
973   @Nullable
974   private static String parseRefs(@Nullable SymbolicRefsI refs, Collection<String> currentRefs, List<String> locals,
975                                   List<String> remotes, List<String> tags) {
976     if (refs == null) {
977       return null;
978     }
979     for (String ref : currentRefs) {
980       final SymbolicRefs.Kind kind = refs.getKind(ref);
981       if (SymbolicRefs.Kind.LOCAL.equals(kind)) {
982         locals.add(ref);
983       }
984       else if (SymbolicRefs.Kind.REMOTE.equals(kind)) {
985         remotes.add(ref);
986       }
987       else {
988         tags.add(ref);
989       }
990     }
991     if (refs.getCurrent() != null && currentRefs.contains(refs.getCurrent().getName())) {
992       return refs.getCurrent().getName();
993     }
994     return null;
995   }
996
997   @Deprecated
998   @NotNull
999   public static List<GitHeavyCommit> commitsDetails(@NotNull Project project, @NotNull FilePath path, @Nullable SymbolicRefsI refs,
1000                                                     @NotNull final Collection<String> commitsIds) throws VcsException {
1001     path = getLastCommitName(project, path);     // adjust path using change manager
1002     VirtualFile root = GitUtil.getGitRoot(path);
1003     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.SHOW);
1004     GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.STATUS,
1005                                            HASH, HASH, COMMIT_TIME, AUTHOR_NAME, AUTHOR_TIME, AUTHOR_EMAIL, COMMITTER_NAME,
1006                                            COMMITTER_EMAIL, PARENTS, REF_NAMES, SUBJECT, BODY, RAW_BODY);
1007     h.setSilent(true);
1008     h.addParameters("--name-status", "-M", parser.getPretty(), "--encoding=UTF-8");
1009     h.addParameters(new ArrayList<>(commitsIds));
1010
1011     String output = h.run();
1012     final List<GitHeavyCommit> rc = new ArrayList<>();
1013     for (GitLogRecord record : parser.parse(output)) {
1014       final GitHeavyCommit gitCommit = createCommit(project, refs, root, record);
1015       rc.add(gitCommit);
1016     }
1017     return rc;
1018   }
1019
1020   public static long getAuthorTime(Project project, FilePath path, final String commitsId) throws VcsException {
1021     // adjust path using change manager
1022     path = getLastCommitName(project, path);
1023     final VirtualFile root = GitUtil.getGitRoot(path);
1024     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.SHOW);
1025     GitLogParser parser = new GitLogParser(project, GitLogParser.NameStatus.STATUS, AUTHOR_TIME);
1026     h.setSilent(true);
1027     h.addParameters("--name-status", parser.getPretty(), "--encoding=UTF-8");
1028     h.addParameters(commitsId);
1029
1030     String output = h.run();
1031     GitLogRecord logRecord = parser.parseOneRecord(output);
1032     return logRecord.getAuthorTimeStamp();
1033   }
1034
1035   /**
1036    * Get name of the file in the last commit. If file was renamed, returns the previous name.
1037    *
1038    * @param project the context project
1039    * @param path    the path to check
1040    * @return the name of file in the last commit or argument
1041    */
1042   public static FilePath getLastCommitName(@NotNull Project project, FilePath path) {
1043     if (project.isDefault()) return path;
1044     final ChangeListManager changeManager = ChangeListManager.getInstance(project);
1045     final Change change = changeManager.getChange(path);
1046     if (change != null && change.getType() == Change.Type.MOVED) {
1047       // GitContentRevision r = (GitContentRevision)change.getBeforeRevision();
1048       assert change.getBeforeRevision() != null : "Move change always have beforeRevision";
1049       path = change.getBeforeRevision().getFile();
1050     }
1051     return path;
1052   }
1053
1054   @Nullable
1055   public static GitRevisionNumber getMergeBase(final Project project, final VirtualFile root, @NotNull final String first,
1056                                                @NotNull final String second)
1057     throws VcsException {
1058     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.MERGE_BASE);
1059     h.setSilent(true);
1060     h.addParameters(first, second);
1061     String output = h.run().trim();
1062     if (output.length() == 0) {
1063       return null;
1064     }
1065     else {
1066       return GitRevisionNumber.resolve(project, root, output);
1067     }
1068   }
1069 }