IDEA-212693 vcs: show navigatable links in annotations popup
[idea/community.git] / plugins / git4idea / src / git4idea / annotate / GitFileAnnotation.java
1 /*
2  * Copyright 2000-2016 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.annotate;
17
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.util.Pair;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.openapi.vcs.CommittedChangesProvider;
22 import com.intellij.openapi.vcs.FilePath;
23 import com.intellij.openapi.vcs.VcsException;
24 import com.intellij.openapi.vcs.VcsKey;
25 import com.intellij.openapi.vcs.annotate.FileAnnotation;
26 import com.intellij.openapi.vcs.annotate.LineAnnotationAspect;
27 import com.intellij.openapi.vcs.annotate.LineAnnotationAspectAdapter;
28 import com.intellij.openapi.vcs.changes.ContentRevision;
29 import com.intellij.openapi.vcs.history.VcsFileRevision;
30 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
31 import com.intellij.openapi.vcs.impl.AbstractVcsHelperImpl;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.util.containers.ContainerUtil;
34 import com.intellij.util.text.DateFormatUtil;
35 import com.intellij.vcs.log.VcsUser;
36 import com.intellij.vcsUtil.VcsUtil;
37 import git4idea.GitContentRevision;
38 import git4idea.GitFileRevision;
39 import git4idea.GitRevisionNumber;
40 import git4idea.GitVcs;
41 import git4idea.changes.GitCommittedChangeList;
42 import git4idea.changes.GitCommittedChangeListProvider;
43 import git4idea.log.GitCommitTooltipLinkHandler;
44 import git4idea.repo.GitRepository;
45 import git4idea.repo.GitRepositoryManager;
46 import gnu.trove.TObjectIntHashMap;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import java.util.*;
51
52 public class GitFileAnnotation extends FileAnnotation {
53   private final Project myProject;
54   @NotNull private final VirtualFile myFile;
55   @NotNull private final FilePath myFilePath;
56   @NotNull private final GitVcs myVcs;
57   @Nullable private final VcsRevisionNumber myBaseRevision;
58
59   @NotNull private final List<LineInfo> myLines;
60   @Nullable private List<VcsFileRevision> myRevisions;
61   @Nullable private TObjectIntHashMap<VcsRevisionNumber> myRevisionMap;
62   @NotNull private final Map<VcsRevisionNumber, String> myCommitMessageMap = new HashMap<>();
63
64   private final LineAnnotationAspect DATE_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.DATE, true) {
65     @Override
66     public String doGetValue(LineInfo info) {
67       return DateFormatUtil.formatPrettyDate(info.getAuthorDate());
68     }
69   };
70
71   private final LineAnnotationAspect REVISION_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.REVISION, false) {
72     @Override
73     protected String doGetValue(LineInfo lineInfo) {
74       return lineInfo.getRevisionNumber().getShortRev();
75     }
76   };
77
78   private final LineAnnotationAspect AUTHOR_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.AUTHOR, true) {
79     @Override
80     protected String doGetValue(LineInfo lineInfo) {
81       return lineInfo.getAuthor();
82     }
83   };
84
85   public GitFileAnnotation(@NotNull Project project,
86                            @NotNull VirtualFile file,
87                            @Nullable VcsRevisionNumber revision,
88                            @NotNull List<LineInfo> lines) {
89     super(project);
90     myProject = project;
91     myFile = file;
92     myFilePath = VcsUtil.getFilePath(file);
93     myVcs = GitVcs.getInstance(myProject);
94     myBaseRevision = revision;
95     myLines = lines;
96   }
97
98   public GitFileAnnotation(@NotNull GitFileAnnotation annotation) {
99     this(annotation.getProject(), annotation.getFile(), annotation.getCurrentRevision(), annotation.getLines());
100   }
101
102   @Override
103   public void dispose() {
104   }
105
106   @Override
107   public LineAnnotationAspect[] getAspects() {
108     return new LineAnnotationAspect[]{REVISION_ASPECT, DATE_ASPECT, AUTHOR_ASPECT};
109   }
110
111   @Nullable
112   @Override
113   public String getAnnotatedContent() {
114     try {
115       ContentRevision revision = GitContentRevision.createRevision(myFilePath, myBaseRevision, myProject);
116       return revision.getContent();
117     }
118     catch (VcsException e) {
119       return null;
120     }
121   }
122
123   @Override
124   public List<VcsFileRevision> getRevisions() {
125     return myRevisions;
126   }
127
128   public void setRevisions(@NotNull List<VcsFileRevision> revisions) {
129     myRevisions = revisions;
130
131     myRevisionMap = new TObjectIntHashMap<>();
132     for (int i = 0; i < myRevisions.size(); i++) {
133       myRevisionMap.put(myRevisions.get(i).getRevisionNumber(), i);
134     }
135   }
136
137   public void setCommitMessage(@NotNull VcsRevisionNumber revisionNumber, @NotNull String message) {
138     myCommitMessageMap.put(revisionNumber, message);
139   }
140
141   @Override
142   public int getLineCount() {
143     return myLines.size();
144   }
145
146   @Nullable
147   public LineInfo getLineInfo(int lineNumber) {
148     if (lineNumberCheck(lineNumber)) return null;
149     return myLines.get(lineNumber);
150   }
151
152   @Nullable
153   @Override
154   public String getToolTip(int lineNumber) {
155     return getToolTip(lineNumber, false);
156   }
157
158   @Nullable
159   @Override
160   public String getHtmlToolTip(int lineNumber) {
161     return getToolTip(lineNumber, true);
162   }
163
164   @Nullable
165   private String getToolTip(int lineNumber, boolean asHtml) {
166     LineInfo lineInfo = getLineInfo(lineNumber);
167     if (lineInfo == null) return null;
168
169     AnnotationTooltipBuilder atb = new AnnotationTooltipBuilder(myProject, asHtml);
170     GitRevisionNumber revisionNumber = lineInfo.getRevisionNumber();
171
172     atb.appendRevisionLine(revisionNumber, it -> GitCommitTooltipLinkHandler.createLink(it.asString(), it));
173     atb.appendLine("Author: " + lineInfo.getAuthor());
174     atb.appendLine("Date: " + DateFormatUtil.formatDateTime(lineInfo.getAuthorDate()));
175
176     if (!myFilePath.equals(lineInfo.myFilePath)) {
177       String path = FileUtil.getLocationRelativeToUserHome(lineInfo.myFilePath.getPresentableUrl());
178       atb.appendLine("Path: " + path);
179     }
180
181     String commitMessage = getCommitMessage(revisionNumber);
182     if (commitMessage == null) commitMessage = lineInfo.getSubject() + "\n...";
183     atb.appendCommitMessageBlock(commitMessage);
184
185     return atb.toString();
186   }
187
188   @Nullable
189   public String getCommitMessage(@NotNull VcsRevisionNumber revisionNumber) {
190     if (myRevisions != null && myRevisionMap != null &&
191         myRevisionMap.contains(revisionNumber)) {
192       VcsFileRevision fileRevision = myRevisions.get(myRevisionMap.get(revisionNumber));
193       return fileRevision.getCommitMessage();
194     }
195     return myCommitMessageMap.get(revisionNumber);
196   }
197
198   @Nullable
199   @Override
200   public VcsRevisionNumber getLineRevisionNumber(int lineNumber) {
201     LineInfo lineInfo = getLineInfo(lineNumber);
202     return lineInfo != null ? lineInfo.getRevisionNumber() : null;
203   }
204
205   @Nullable
206   @Override
207   public Date getLineDate(int lineNumber) {
208     LineInfo lineInfo = getLineInfo(lineNumber);
209     return lineInfo != null ? lineInfo.getAuthorDate() : null;
210   }
211
212   private boolean lineNumberCheck(int lineNumber) {
213     return myLines.size() <= lineNumber || lineNumber < 0;
214   }
215
216   @NotNull
217   public List<LineInfo> getLines() {
218     return myLines;
219   }
220
221   /**
222    * Revision annotation aspect implementation
223    */
224   private abstract class GitAnnotationAspect extends LineAnnotationAspectAdapter {
225     GitAnnotationAspect(String id, boolean showByDefault) {
226       super(id, showByDefault);
227     }
228
229     @Override
230     public String getValue(int lineNumber) {
231       if (lineNumberCheck(lineNumber)) {
232         return "";
233       }
234       else {
235         return doGetValue(myLines.get(lineNumber));
236       }
237     }
238
239     protected abstract String doGetValue(LineInfo lineInfo);
240
241     @Override
242     protected void showAffectedPaths(int lineNum) {
243       if (lineNum >= 0 && lineNum < myLines.size()) {
244         LineInfo info = myLines.get(lineNum);
245
246         AbstractVcsHelperImpl.loadAndShowCommittedChangesDetails(myProject, info.getRevisionNumber(), myFilePath, () -> getRevisionsChangesProvider().getChangesIn(lineNum));
247       }
248     }
249   }
250
251   static class LineInfo {
252     @NotNull private final Project myProject;
253     @NotNull private final GitRevisionNumber myRevision;
254     @NotNull private final FilePath myFilePath;
255     @Nullable private final GitRevisionNumber myPreviousRevision;
256     @Nullable private final FilePath myPreviousFilePath;
257     @NotNull private final Date myCommitterDate;
258     @NotNull private final Date myAuthorDate;
259     @NotNull private final VcsUser myAuthor;
260     @NotNull private final String mySubject;
261
262     LineInfo(@NotNull Project project,
263                     @NotNull GitRevisionNumber revision,
264                     @NotNull FilePath path,
265                     @NotNull Date committerDate,
266                     @NotNull Date authorDate,
267                     @NotNull VcsUser author,
268                     @NotNull String subject,
269                     @Nullable GitRevisionNumber previousRevision,
270                     @Nullable FilePath previousPath) {
271       myProject = project;
272       myRevision = revision;
273       myFilePath = path;
274       myPreviousRevision = previousRevision;
275       myPreviousFilePath = previousPath;
276       myCommitterDate = committerDate;
277       myAuthorDate = authorDate;
278       myAuthor = author;
279       mySubject = subject;
280     }
281
282     @NotNull
283     public GitRevisionNumber getRevisionNumber() {
284       return myRevision;
285     }
286
287     @NotNull
288     public FilePath getFilePath() {
289       return myFilePath;
290     }
291
292     @NotNull
293     public VcsFileRevision getFileRevision() {
294       return new GitFileRevision(myProject, myFilePath, myRevision);
295     }
296
297     @Nullable
298     public VcsFileRevision getPreviousFileRevision() {
299       if (myPreviousRevision == null || myPreviousFilePath == null) return null;
300       return new GitFileRevision(myProject, myPreviousFilePath, myPreviousRevision);
301     }
302
303     @NotNull
304     public Date getCommitterDate() {
305       return myCommitterDate;
306     }
307
308     @NotNull
309     public Date getAuthorDate() {
310       return myAuthorDate;
311     }
312
313     @NotNull
314     public String getAuthor() {
315       return myAuthor.getName();
316     }
317
318     @NotNull
319     public String getSubject() {
320       return mySubject;
321     }
322   }
323
324   @NotNull
325   @Override
326   public VirtualFile getFile() {
327     return myFile;
328   }
329
330   @Nullable
331   @Override
332   public VcsRevisionNumber getCurrentRevision() {
333     return myBaseRevision;
334   }
335
336   @Override
337   public VcsKey getVcsKey() {
338     return GitVcs.getKey();
339   }
340
341   @Override
342   public boolean isBaseRevisionChanged(@NotNull VcsRevisionNumber number) {
343     if (!myFile.isInLocalFileSystem()) return false;
344     final VcsRevisionNumber currentCurrentRevision = myVcs.getDiffProvider().getCurrentRevision(myFile);
345     return myBaseRevision != null && ! myBaseRevision.equals(currentCurrentRevision);
346   }
347
348
349   @Nullable
350   @Override
351   public CurrentFileRevisionProvider getCurrentFileRevisionProvider() {
352     return (lineNumber) -> {
353       LineInfo lineInfo = getLineInfo(lineNumber);
354       return lineInfo != null ? lineInfo.getFileRevision() : null;
355     };
356   }
357
358   @Nullable
359   @Override
360   public PreviousFileRevisionProvider getPreviousFileRevisionProvider() {
361     return new PreviousFileRevisionProvider() {
362       @Nullable
363       @Override
364       public VcsFileRevision getPreviousRevision(int lineNumber) {
365         LineInfo lineInfo = getLineInfo(lineNumber);
366         if (lineInfo == null) return null;
367
368         VcsFileRevision previousFileRevision = lineInfo.getPreviousFileRevision();
369         if (previousFileRevision != null) return previousFileRevision;
370
371         GitRevisionNumber revisionNumber = lineInfo.getRevisionNumber();
372         if (myRevisions != null && myRevisionMap != null &&
373             myRevisionMap.contains(revisionNumber)) {
374           int index = myRevisionMap.get(revisionNumber);
375           if (index + 1 < myRevisions.size()) {
376             return myRevisions.get(index + 1);
377           }
378         }
379
380         return null;
381       }
382
383       @Nullable
384       @Override
385       public VcsFileRevision getLastRevision() {
386         if (myBaseRevision instanceof GitRevisionNumber) {
387           return new GitFileRevision(myProject, myFilePath, (GitRevisionNumber)myBaseRevision);
388         }
389         else {
390           return ContainerUtil.getFirstItem(getRevisions());
391         }
392       }
393     };
394   }
395
396   @Nullable
397   @Override
398   public AuthorsMappingProvider getAuthorsMappingProvider() {
399     Map<VcsRevisionNumber, String> authorsMap = new HashMap<>();
400     for (int i = 0; i < getLineCount(); i++) {
401       LineInfo lineInfo = getLineInfo(i);
402       if (lineInfo == null) continue;
403
404       if (!authorsMap.containsKey(lineInfo.getRevisionNumber())) {
405         authorsMap.put(lineInfo.getRevisionNumber(), lineInfo.getAuthor());
406       }
407     }
408
409     return () -> authorsMap;
410   }
411
412   @Nullable
413   @Override
414   public RevisionsOrderProvider getRevisionsOrderProvider() {
415     ContainerUtil.KeyOrderedMultiMap<Date, VcsRevisionNumber> dates = new ContainerUtil.KeyOrderedMultiMap<>();
416
417     for (int i = 0; i < getLineCount(); i++) {
418       LineInfo lineInfo = getLineInfo(i);
419       if (lineInfo == null) continue;
420
421       VcsRevisionNumber number = lineInfo.getRevisionNumber();
422       Date date = lineInfo.getCommitterDate();
423
424       dates.putValue(date, number);
425     }
426
427     List<List<VcsRevisionNumber>> orderedRevisions = new ArrayList<>();
428     NavigableSet<Date> orderedDates = dates.navigableKeySet();
429     for (Date date : orderedDates.descendingSet()) {
430       Collection<VcsRevisionNumber> revisionNumbers = dates.get(date);
431       orderedRevisions.add(new ArrayList<>(revisionNumbers));
432     }
433
434     return () -> orderedRevisions;
435   }
436
437   /**
438    * Do not use {@link CommittedChangesProvider#getOneList} to avoid unnecessary rename detections (as we know FilePath already)
439    */
440   @NotNull
441   @Override
442   public RevisionChangesProvider getRevisionsChangesProvider() {
443     return (lineNumber) -> {
444       LineInfo lineInfo = getLineInfo(lineNumber);
445       if (lineInfo == null) return null;
446
447       GitRepository repository = GitRepositoryManager.getInstance(myProject).getRepositoryForFile(lineInfo.getFilePath());
448       if (repository == null) return null;
449
450       GitCommittedChangeList changeList =
451         GitCommittedChangeListProvider.getCommittedChangeList(myProject, repository.getRoot(), lineInfo.getRevisionNumber());
452       return Pair.create(changeList, lineInfo.getFilePath());
453     };
454   }
455 }