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