/*******************************************************************************
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2011, Jens Baumgart <jens.baumgart@sap.com>
* Copyright (C) 2011, Stefan Lay <stefan.lay@sap.com>
* Copyright (C) 2015, Thomas Wolf <thomas.wolf@paranor.ch>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.ui.internal.history;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.PreferenceBasedDateFormatter;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.history.FormatJob.FormatResult;
import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
import org.eclipse.jface.text.Region;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.RevWalkUtils;
import org.eclipse.osgi.util.NLS;
/**
* Class to build and format commit info in History View
*/
public class CommitInfoBuilder {
private static final String LF = "\n"; //$NON-NLS-1$
private static final int MAXBRANCHES = 20;
private PlotCommit<?> commit;
private final Repository db;
private final boolean fill;
private final Collection<Ref> allRefs;
private final PreferenceBasedDateFormatter dateFormatter;
/**
* @param db the repository
* @param commit the commit the info should be shown for
* @param fill whether to fill the available space
* @param allRefs all Ref's to examine regarding marge bases
*/
public CommitInfoBuilder(Repository db, PlotCommit commit, boolean fill,
Collection<Ref> allRefs) {
this.db = db;
this.commit = commit;
this.fill = fill;
this.allRefs = allRefs;
this.dateFormatter = PreferenceBasedDateFormatter.create();
}
/**
* Retrieves and formats the commit info.
*
* @param monitor
* for progress reporting and cancellation
* @return formatted commit info
* @throws IOException
*/
public FormatResult format(IProgressMonitor monitor) throws IOException {
boolean trace = GitTraceLocation.HISTORYVIEW.isActive();
if (trace)
GitTraceLocation.getTrace().traceEntry(
GitTraceLocation.HISTORYVIEW.getLocation());
monitor.setTaskName(UIText.CommitMessageViewer_FormattingMessageTaskName);
final StringBuilder d = new StringBuilder();
final PersonIdent author = commit.getAuthorIdent();
final PersonIdent committer = commit.getCommitterIdent();
List<GitCommitReference> hyperlinks = new ArrayList<>();
d.append(UIText.CommitMessageViewer_commit);
d.append(' ');
d.append(commit.getId().name());
d.append(LF);
addPersonIdent(d, author, UIText.CommitMessageViewer_author);
addPersonIdent(d, committer, UIText.CommitMessageViewer_committer);
for (int i = 0; i < commit.getParentCount(); i++) {
addCommit(d, (SWTCommit) commit.getParent(i),
UIText.CommitMessageViewer_parent, hyperlinks);
}
for (int i = 0; i < commit.getChildCount(); i++) {
addCommit(d, (SWTCommit) commit.getChild(i),
UIText.CommitMessageViewer_child, hyperlinks);
}
if(Activator.getDefault().getPreferenceStore().getBoolean(
UIPreferences.HISTORY_SHOW_BRANCH_SEQUENCE)) {
try (RevWalk rw = new RevWalk(db)) {
List<Ref> branches = getBranches(commit, allRefs, db);
if (!branches.isEmpty()) {
d.append(UIText.CommitMessageViewer_branches);
d.append(": "); //$NON-NLS-1$
int count = 0;
for (Iterator<Ref> i = branches.iterator(); i.hasNext();) {
Ref head = i.next();
RevCommit p;
p = rw.parseCommit(head.getObjectId());
addLink(d, formatHeadRef(head), hyperlinks, p);
if (i.hasNext()) {
if (count++ <= MAXBRANCHES) {
d.append(", "); //$NON-NLS-1$
} else {
d.append(NLS.bind(UIText.CommitMessageViewer_MoreBranches, Integer.valueOf(branches.size() - MAXBRANCHES)));
break;
}
}
}
d.append(LF);
}
} catch (IOException e) {
Activator.logError(e.getMessage(), e);
}
}
String tagsString = getTagsString();
if (tagsString.length() > 0) {
d.append(UIText.CommitMessageViewer_tags);
d.append(": "); //$NON-NLS-1$
d.append(tagsString);
d.append(LF);
}
if (Activator.getDefault().getPreferenceStore().getBoolean(
UIPreferences.HISTORY_SHOW_TAG_SEQUENCE)) {
try (RevWalk rw = new RevWalk(db)) {
monitor.setTaskName(UIText.CommitMessageViewer_GettingPreviousTagTaskName);
addTag(d, UIText.CommitMessageViewer_follows, rw,
getNextTag(false, monitor), hyperlinks);
} catch (IOException e) {
Activator.logError(e.getMessage(), e);
}
try (RevWalk rw = new RevWalk(db)) {
monitor.setTaskName(UIText.CommitMessageViewer_GettingNextTagTaskName);
addTag(d, UIText.CommitMessageViewer_precedes, rw,
getNextTag(true, monitor), hyperlinks);
} catch (IOException e) {
Activator.logError(e.getMessage(), e);
}
}
d.append(LF);
int headerEnd = d.length();
String msg = commit.getFullMessage().trim();
// Find start of footer:
int footerStart = CommonUtils.getFooterOffset(msg);
if (footerStart >= 0) {
if (fill) {
String footer = msg.substring(footerStart);
msg = msg.substring(0, footerStart);
msg = msg.replaceAll("([\\w.,; \t])\n(\\w)", "$1 $2") //$NON-NLS-1$ //$NON-NLS-2$
+ footer;
footerStart = headerEnd + msg.length() - footer.length();
} else {
footerStart = headerEnd + footerStart;
}
} else if (fill) {
msg = msg.replaceAll("([\\w.,; \t])\n(\\w)", "$1 $2"); //$NON-NLS-1$ //$NON-NLS-2$
}
d.append(msg);
if (!msg.endsWith(LF))
d.append(LF);
if (trace)
GitTraceLocation.getTrace().traceExit(
GitTraceLocation.HISTORYVIEW.getLocation());
return new FormatResult(d.toString(), hyperlinks, headerEnd,
footerStart >= 0 ? footerStart : d.length());
}
private void addLink(StringBuilder d, String linkLabel,
Collection<GitCommitReference> hyperlinks, RevCommit to) {
if (to != null) {
hyperlinks.add(new GitCommitReference(to,
new Region(d.length(), linkLabel.length())));
}
d.append(linkLabel);
}
private void addLink(StringBuilder d, Collection<GitCommitReference> hyperlinks,
RevCommit to) {
addLink(d, to.getId().name(), hyperlinks, to);
}
private void addPersonIdent(StringBuilder d, PersonIdent ident,
String label) {
if (ident != null) {
d.append(label).append(": "); //$NON-NLS-1$
d.append(ident.getName().trim());
d.append(" <").append(ident.getEmailAddress().trim()).append("> "); //$NON-NLS-1$ //$NON-NLS-2$
d.append(dateFormatter.formatDate(ident));
d.append(LF);
}
}
private void addCommit(StringBuilder d, SWTCommit gitcommit, String label,
List<GitCommitReference> hyperlinks) throws IOException {
if (gitcommit != null) {
d.append(label).append(": "); //$NON-NLS-1$
gitcommit.parseBody();
addLink(d, hyperlinks, gitcommit);
d.append(" (").append(gitcommit.getShortMessage()).append(')'); //$NON-NLS-1$
d.append(LF);
}
}
private void addTag(StringBuilder d, String label, RevWalk walk, Ref tag,
List<GitCommitReference> hyperlinks) throws IOException {
if (tag != null) {
d.append(label).append(": "); //$NON-NLS-1$
RevCommit p = walk.parseCommit(tag.getObjectId());
addLink(d, formatTagRef(tag), hyperlinks, p);
d.append(LF);
}
}
/**
* @param commit
* @param allRefs
* @param db
* @return List of heads from those current commit is reachable
* @throws MissingObjectException
* @throws IncorrectObjectTypeException
* @throws IOException
*/
private static List<Ref> getBranches(RevCommit commit,
Collection<Ref> allRefs, Repository db)
throws MissingObjectException, IncorrectObjectTypeException,
IOException {
try (RevWalk revWalk = new RevWalk(db)) {
revWalk.setRetainBody(false);
return RevWalkUtils.findBranchesReachableFrom(commit, revWalk, allRefs);
}
}
private String formatHeadRef(Ref ref) {
final String name = ref.getName();
if (name.startsWith(Constants.R_HEADS))
return name.substring(Constants.R_HEADS.length());
else if (name.startsWith(Constants.R_REMOTES))
return name.substring(Constants.R_REMOTES.length());
return name;
}
private String formatTagRef(Ref ref) {
final String name = ref.getName();
if (name.startsWith(Constants.R_TAGS))
return name.substring(Constants.R_TAGS.length());
return name;
}
private String getTagsString() {
StringBuilder sb = new StringBuilder();
Map<String, Ref> tagsMap = db.getTags();
for (Entry<String, Ref> tagEntry : tagsMap.entrySet()) {
ObjectId target = tagEntry.getValue().getPeeledObjectId();
if (target == null)
target = tagEntry.getValue().getObjectId();
if (target != null && target.equals(commit)) {
if (sb.length() > 0)
sb.append(", "); //$NON-NLS-1$
sb.append(tagEntry.getKey());
}
}
return sb.toString();
}
/**
* Finds next door tagged revision. Searches forwards (in descendants) or
* backwards (in ancestors)
*
* @param searchDescendant
* if <code>false</code>, will search for tagged revision in
* ancestors
* @param monitor
* @return {@link Ref} or <code>null</code> if no tag found
* @throws IOException
* @throws OperationCanceledException
*/
private Ref getNextTag(boolean searchDescendant, IProgressMonitor monitor)
throws IOException, OperationCanceledException {
if (monitor.isCanceled())
throw new OperationCanceledException();
try (RevWalk revWalk = new RevWalk(db)) {
revWalk.setRetainBody(false);
Map<String, Ref> tagsMap = db.getTags();
Ref tagRef = null;
for (Ref ref : tagsMap.values()) {
if (monitor.isCanceled())
throw new OperationCanceledException();
// both RevCommits must be allocated using same RevWalk
// instance,
// otherwise isMergedInto returns wrong result!
RevCommit current = revWalk.parseCommit(commit);
// tags can point to any object, we only want tags pointing at
// commits
RevObject any = revWalk
.peel(revWalk.parseAny(ref.getObjectId()));
if (!(any instanceof RevCommit))
continue;
RevCommit newTag = (RevCommit) any;
if (newTag.getId().equals(commit))
continue;
// check if newTag matches our criteria
if (isMergedInto(revWalk, newTag, current, searchDescendant)) {
if (monitor.isCanceled())
throw new OperationCanceledException();
if (tagRef != null) {
RevCommit oldTag = revWalk
.parseCommit(tagRef.getObjectId());
// both oldTag and newTag satisfy search criteria, so
// taking
// the closest one
if (isMergedInto(revWalk, oldTag, newTag,
searchDescendant))
tagRef = ref;
} else
tagRef = ref;
}
}
return tagRef;
}
}
/**
* @param rw
* @param base
* @param tip
* @param swap
* if <code>true</code>, base and tip arguments are swapped
* @return <code>true</code> if there is a path directly from tip to base
* (and thus base is fully merged into tip); <code>false</code>
* otherwise.
* @throws IOException
*/
private boolean isMergedInto(final RevWalk rw, final RevCommit base,
final RevCommit tip, boolean swap) throws IOException {
return !swap ? rw.isMergedInto(base, tip) : rw.isMergedInto(tip, base);
}
}