2 * Copyright 2000-2014 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.history;
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;
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;
64 import java.util.concurrent.atomic.AtomicBoolean;
65 import java.util.concurrent.atomic.AtomicInteger;
66 import java.util.concurrent.atomic.AtomicReference;
68 import static git4idea.history.GitLogParser.GitLogOption.*;
71 * A collection of methods for retrieving history information from native Git.
73 public class GitHistoryUtils {
76 * A parameter to {@code git log} which is equivalent to {@code --all}, but doesn't show the stuff from index or stash.
78 public static final List<String> LOG_ALL = Arrays.asList("HEAD", "--branches", "--remotes", "--tags");
80 private static final Logger LOG = Logger.getInstance("#git4idea.history.GitHistoryUtils");
82 private GitHistoryUtils() {
86 * Get current revision for the file under git in the current or specified branch.
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.
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);
101 h.addParameters("-n1", parser.getPretty());
102 h.addParameters(!StringUtil.isEmpty(branch) ? branch : "--all");
104 h.addRelativePaths(filePath);
105 String result = h.run();
106 if (result.length() == 0) {
109 final GitLogRecord record = parser.parseOneRecord(result);
110 if (record == null) {
113 record.setUsedHandler(h);
114 return new GitRevisionNumber(record.getHash(), record.getDate());
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);
124 h.addParameters("-n1", parser.getPretty());
125 h.addParameters("--all");
127 h.addRelativePaths(filePath);
128 String result = h.run();
129 if (result.length() == 0) {
132 final GitLogRecord record = parser.parseOneRecord(result);
133 if (record == null) {
136 record.setUsedHandler(h);
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());
145 * Get current revision for the file under git
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
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());
158 return new ItemLatestState(getCurrentRevision(project, filePath, null), true, false);
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);
164 h.addParameters("-n1", parser.getPretty(), "--name-status", t.getFullName());
166 h.addRelativePaths(filePath);
167 String result = h.run();
168 if (result.length() == 0) {
171 GitLogRecord record = parser.parseOneRecord(result);
172 if (record == null) {
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);
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:
196 ~/sandbox/git # git show --oneline --name-status -M 4185b97
197 4185b97 renamed a to b
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.
204 TODO: handle multiple repositories configuration: a file can be moved from one repo to another
208 * Retrieves the history of the file, including renames.
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.
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);
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;
237 finalRoot = (root == null ? GitUtil.getGitRoot(filePath) : root);
239 catch (VcsException e) {
240 exceptionConsumer.consume(e);
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);
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();
253 final Consumer<GitLogRecord> resultAdapter = record -> {
254 if (skipFurtherOutput.get()) {
257 if (record == null) {
258 exceptionConsumer.consume(new VcsException("revision details are null."));
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);
269 firstCommitParent.set(parentHashes[0]);
271 final String message = record.getFullMessage();
273 FilePath revisionPath;
275 final List<FilePath> paths = record.getFilePaths(finalRoot);
276 if (paths.size() > 0) {
277 revisionPath = paths.get(0);
280 // no paths are shown for merge commits, so we're using the saved path we're inspecting now
281 revisionPath = currentPath.get();
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.
294 if (statusInfos.get(0).getType() == GitChangeType.ADDED && !filePath.isDirectory()) {
295 skipFurtherOutput.set(true);
298 catch (VcsException e) {
299 exceptionConsumer.consume(e);
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();
311 logHandler.get().addLineListener(new GitLineHandlerAdapter() {
313 public void onLineAvailable(String line, Key outputType) {
314 final GitLogRecord record = accumulator.acceptLine(line);
315 if (record != null) {
316 resultAdapter.consume(record);
321 public void startFailed(Throwable exception) {
322 //noinspection ThrowableInstanceNeverThrown
324 exceptionConsumer.consume(new VcsException(exception));
327 criticalFailure.set(true);
333 public void processTerminated(int exitCode) {
335 super.processTerminated(exitCode);
336 final GitLogRecord record = accumulator.processLast();
337 if (record != null) {
338 resultAdapter.consume(record);
341 catch (Throwable t) {
343 exceptionConsumer.consume(new VcsException("Internal error " + t.getMessage(), t));
344 criticalFailure.set(true);
352 logHandler.get().start();
354 if (criticalFailure.get()) {
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);
365 catch (VcsException e) {
366 LOG.warn("Tried to get first commit rename path", e);
367 exceptionConsumer.consume(e);
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");
387 if (parameters != null && parameters.length > 0) {
388 h.addParameters(parameters);
391 h.addRelativePaths(path);
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.
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");
415 h.addRelativePaths(filePath);
420 final String output = h.run();
421 final List<GitLogRecord> records = parser.parse(output);
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());
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();
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);
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));
457 String output = h.run();
458 List<GitLogRecord> records = parser.parse(output);
460 return ContainerUtil.map(records, record -> {
461 List<Hash> parents = new SmartList<>();
462 for (String parent : record.getParentsHashes()) {
463 parents.add(HashImpl.build(parent));
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());
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) {
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);
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);
493 String output = h.run();
494 List<GitLogRecord> records = parser.parse(output);
495 if (records.size() != refs.length) return null;
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()));
507 private static void processHandlerOutputByLine(@NotNull GitLineHandler handler,
508 @NotNull Consumer<StringBuilder> recordConsumer,
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() {
516 public void onLineAvailable(String line, Key outputType) {
519 int nextRecordStart = line.indexOf(GitLogParser.RECORD_START);
520 if (nextRecordStart == -1) {
521 buffer.append(line).append("\n");
523 else if (nextRecordStart == 0) {
527 buffer.append(line.substring(0, nextRecordStart));
528 tail = line.substring(nextRecordStart) + "\n";
532 if (records.incrementAndGet() > bufferSize) {
533 recordConsumer.consume(buffer);
539 catch (Exception e) {
540 ex.set(new VcsException(e));
545 public void processTerminated(int exitCode) {
547 recordConsumer.consume(buffer);
549 catch (Exception e) {
550 ex.set(new VcsException(e));
555 public void startFailed(Throwable exception) {
556 ex.set(new VcsException(exception));
559 handler.runInCurrentThread(null);
561 if (ex.get().getCause() instanceof ProcessCanceledException) {
562 throw (ProcessCanceledException)ex.get().getCause();
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.
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) {
581 GitLineHandler h = new GitLineHandler(project, root, GitCommand.LOG);
582 GitLogParser parser = createParserForDetails(h, project, false, true, ArrayUtil.toStringArray(LOG_ALL));
584 Ref<Throwable> parseError = new Ref<>();
585 Consumer<StringBuilder> recordConsumer = builder -> {
587 GitLogRecord record = parser.parseOneRecord(builder.toString());
588 if (record != null) {
589 commitConsumer.consume(createCommit(project, root, record, factory));
592 catch (ProcessCanceledException ignored) {
594 catch (Throwable t) {
595 if (parseError.get() == null) {
597 LOG.error("Could not parse \" " + builder.toString() + "\"", t);
601 processHandlerOutputByLine(h, recordConsumer, 0);
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) {
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);
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);
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) {
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);
650 userRegistry.consume(factory.createUser(gitLogRecord.getAuthorName(), gitLogRecord.getAuthorEmail()));
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));
666 private static Collection<VcsRef> parseRefs(@NotNull Collection<String> refs,
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);
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);
687 private static class MyTokenAccumulator {
688 @NotNull private final StringBuilder myBuffer = new StringBuilder();
689 @NotNull private final GitLogParser myParser;
691 private boolean myNotStarted = true;
693 public MyTokenAccumulator(@NotNull GitLogParser parser) {
698 public GitLogRecord acceptLine(String s) {
699 final boolean recordStart = s.startsWith(GitLogParser.RECORD_START);
701 s = s.substring(GitLogParser.RECORD_START.length());
706 myBuffer.append("\n");
708 myNotStarted = false;
711 else if (recordStart) {
712 final String line = myBuffer.toString();
713 myBuffer.setLength(0);
716 myBuffer.append("\n");
718 return processResult(line);
722 myBuffer.append("\n");
728 public GitLogRecord processLast() {
729 return processResult(myBuffer.toString());
733 private GitLogRecord processResult(@NotNull String line) {
734 return myParser.parseOneRecord(line);
739 * Get history for the file
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
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);
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);
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<>();
769 history(project, path, root, startingFrom, gitFileRevision -> rc.add(gitFileRevision), e -> exceptions.add(e), parameters);
770 if (!exceptions.isEmpty()) {
771 throw exceptions.get(0);
777 * @deprecated To remove in IDEA 17
780 @SuppressWarnings("unused")
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);
789 * @deprecated To remove in IDEA 17
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");
806 h.addRelativePaths(path);
807 String output = h.run();
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()));
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();
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);
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");
840 return new LogDataImpl(refs, commits);
844 * <p>Get & parse git log detailed output with commits, their parents and their changes.</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>
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();
856 return loadDetails(project, root, false, true, record -> createCommit(project, root, record, factory), parameters);
860 private static GitLogParser createParserForDetails(@NotNull GitTextHandler h,
861 @NotNull Project project,
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};
869 options = ArrayUtil.append(options, REF_NAMES);
871 GitLogParser parser = new GitLogParser(project, status, options);
872 h.setStdoutSuppressed(true);
873 h.addParameters(parameters);
874 h.addParameters(parser.getPretty(), "--encoding=UTF-8");
876 h.addParameters("--decorate=full");
879 h.addParameters("-M", /*find and report renames*/
881 "-c" /*single diff for merge commits, only showing files that were modified from both parents*/);
889 public static <T> List<T> loadDetails(@NotNull final Project project,
890 @NotNull final VirtualFile root,
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);
899 StopWatch sw = StopWatch.start("loading details");
900 String output = h.run();
903 sw = StopWatch.start("parsing");
904 List<GitLogRecord> records = parser.parse(output);
907 sw = StopWatch.start("Creating objects");
908 List<T> commits = ContainerUtil.mapNotNull(records, converter);
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());
924 private static List<Hash> getParentHashes(@NotNull VcsLogObjectsFactory factory, @NotNull GitLogRecord record) {
925 return ContainerUtil.map(record.getParentsHashes(), hash -> factory.createHash(hash));
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);
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);
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) {
955 for (String ref : currentRefs) {
956 final SymbolicRefs.Kind kind = refs.getKind(ref);
957 if (SymbolicRefs.Kind.LOCAL.equals(kind)) {
960 else if (SymbolicRefs.Kind.REMOTE.equals(kind)) {
967 if (refs.getCurrent() != null && currentRefs.contains(refs.getCurrent().getName())) {
968 return refs.getCurrent().getName();
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);
984 h.addParameters("--name-status", "-M", parser.getPretty(), "--encoding=UTF-8");
985 h.addParameters(new ArrayList<>(commitsIds));
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);
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);
1003 h.addParameters("--name-status", parser.getPretty(), "--encoding=UTF-8");
1004 h.addParameters(commitsId);
1006 String output = h.run();
1007 GitLogRecord logRecord = parser.parseOneRecord(output);
1008 return logRecord.getAuthorTimeStamp();
1012 * Get name of the file in the last commit. If file was renamed, returns the previous name.
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
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();
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);
1036 h.addParameters(first, second);
1037 String output = h.run().trim();
1038 if (output.length() == 0) {
1042 return GitRevisionNumber.resolve(project, root, output);