org.eclipse.egit.ui.internal.history.CommitInfoBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.egit.ui.internal.history.CommitInfoBuilder.java

Source

/*******************************************************************************
 * 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>
 *
 * 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.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.egit.core.internal.CompareCoreUtils;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.history.CommitMessageViewer.ObjectLink;
import org.eclipse.egit.ui.internal.trace.GitTraceLocation;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawText;
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.jgit.util.io.SafeBufferedOutputStream;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.graphics.Color;

/**
 * Class to build and format commit info in History View
 */
public class CommitInfoBuilder {

    private static final String SPACE = " "; //$NON-NLS-1$

    private static final String LF = "\n"; //$NON-NLS-1$

    private static final int MAXBRANCHES = 20;

    private final DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //$NON-NLS-1$

    private PlotCommit<?> commit;

    private final Repository db;

    private final boolean fill;

    // set by selecting files in the file list
    private final List<FileDiff> currentDiffs;

    private Color linkColor;

    private Color darkGrey;

    private Color hunkheaderColor;

    private Color linesAddedColor;

    private Color linesRemovedColor;

    private final Collection<Ref> allRefs;

    /**
     * @param db the repository
     * @param commit the commit the info should be shown for
     * @param currentDiffs list of current diffs
     * @param fill whether to fill the available space
     * @param allRefs all Ref's to examine regarding marge bases
     */
    public CommitInfoBuilder(Repository db, PlotCommit commit, List<FileDiff> currentDiffs, boolean fill,
            Collection<Ref> allRefs) {
        this.db = db;
        this.commit = commit;
        this.fill = fill;
        this.allRefs = allRefs;
        this.currentDiffs = new ArrayList<FileDiff>(currentDiffs);
    }

    /**
     * set colors for formatting
     *
     * @param linkColor
     * @param darkGrey
     * @param hunkheaderColor
     * @param linesAddedColor
     * @param linesRemovedColor
     */
    public void setColors(Color linkColor, Color darkGrey, Color hunkheaderColor, Color linesAddedColor,
            Color linesRemovedColor) {
        this.linkColor = linkColor;
        this.darkGrey = darkGrey;
        this.hunkheaderColor = hunkheaderColor;
        this.linesAddedColor = linesAddedColor;
        this.linesRemovedColor = linesRemovedColor;
    }

    /**
     * Format the commit info
     *
     * @param styles styles for text formatting
     * @param monitor
     * @return formatted commit info
     * @throws IOException
     */
    public String format(final List<StyleRange> styles, 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();
        d.append(UIText.CommitMessageViewer_commit);
        d.append(SPACE);
        d.append(commit.getId().name());
        d.append(LF);

        if (author != null) {
            d.append(UIText.CommitMessageViewer_author);
            d.append(": "); //$NON-NLS-1$
            d.append(author.getName());
            d.append(" <"); //$NON-NLS-1$
            d.append(author.getEmailAddress());
            d.append("> "); //$NON-NLS-1$
            d.append(fmt.format(author.getWhen()));
            d.append(LF);
        }

        if (committer != null) {
            d.append(UIText.CommitMessageViewer_committer);
            d.append(": "); //$NON-NLS-1$
            d.append(committer.getName());
            d.append(" <"); //$NON-NLS-1$
            d.append(committer.getEmailAddress());
            d.append("> "); //$NON-NLS-1$
            d.append(fmt.format(committer.getWhen()));
            d.append(LF);
        }

        for (int i = 0; i < commit.getParentCount(); i++) {
            final SWTCommit p = (SWTCommit) commit.getParent(i);
            p.parseBody();
            d.append(UIText.CommitMessageViewer_parent);
            d.append(": "); //$NON-NLS-1$
            addLink(d, styles, p);
            d.append(" ("); //$NON-NLS-1$
            d.append(p.getShortMessage());
            d.append(")"); //$NON-NLS-1$
            d.append(LF);
        }

        for (int i = 0; i < commit.getChildCount(); i++) {
            final SWTCommit p = (SWTCommit) commit.getChild(i);
            p.parseBody();
            d.append(UIText.CommitMessageViewer_child);
            d.append(": "); //$NON-NLS-1$
            addLink(d, styles, p);
            d.append(" ("); //$NON-NLS-1$
            d.append(p.getShortMessage());
            d.append(")"); //$NON-NLS-1$
            d.append(LF);
        }

        try {
            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 = new RevWalk(db).parseCommit(head.getObjectId());
                    addLink(d, formatHeadRef(head), styles, 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 {
                monitor.setTaskName(UIText.CommitMessageViewer_GettingPreviousTagTaskName);
                Ref followingTag = getNextTag(false, monitor);
                if (followingTag != null) {
                    d.append(UIText.CommitMessageViewer_follows);
                    d.append(": "); //$NON-NLS-1$
                    RevCommit p = new RevWalk(db).parseCommit(followingTag.getObjectId());
                    addLink(d, formatTagRef(followingTag), styles, p);
                    d.append(LF);
                }
            } catch (IOException e) {
                Activator.logError(e.getMessage(), e);
            }

            try {
                monitor.setTaskName(UIText.CommitMessageViewer_GettingNextTagTaskName);
                Ref precedingTag = getNextTag(true, monitor);
                if (precedingTag != null) {
                    d.append(UIText.CommitMessageViewer_precedes);
                    d.append(": "); //$NON-NLS-1$
                    RevCommit p = new RevWalk(db).parseCommit(precedingTag.getObjectId());
                    addLink(d, formatTagRef(precedingTag), styles, p);
                    d.append(LF);
                }
            } catch (IOException e) {
                Activator.logError(e.getMessage(), e);
            }
        }

        makeGrayText(d, styles);
        d.append(LF);
        String msg = commit.getFullMessage();
        Pattern p = Pattern.compile("\n([A-Z](?:[A-Za-z]+-)+by: [^\n]+)"); //$NON-NLS-1$
        if (fill) {
            Matcher spm = p.matcher(msg);
            if (spm.find()) {
                String subMsg = msg.substring(0, spm.end());
                msg = subMsg.replaceAll("([\\w.,; \t])\n(\\w)", "$1 $2") //$NON-NLS-1$ //$NON-NLS-2$
                        + msg.substring(spm.end());
            }
        }
        int h0 = d.length();
        d.append(msg);
        d.append(LF);

        Matcher matcher = p.matcher(msg);
        while (matcher.find()) {
            styles.add(
                    new StyleRange(h0 + matcher.start(), matcher.end() - matcher.start(), null, null, SWT.ITALIC));
        }

        if (!currentDiffs.isEmpty())
            buildDiffs(d, styles, monitor, trace);

        if (trace)
            GitTraceLocation.getTrace().traceExit(GitTraceLocation.HISTORYVIEW.getLocation());
        return d.toString();
    }

    private void addLink(final StringBuilder d, String linkLabel, final List<StyleRange> styles,
            final RevCommit to) {
        final ObjectLink sr = new ObjectLink();
        sr.targetCommit = to;
        sr.foreground = linkColor;
        sr.underline = true;
        sr.start = d.length();
        d.append(linkLabel);
        sr.length = d.length() - sr.start;
        styles.add(sr);
    }

    private void addLink(final StringBuilder d, final List<StyleRange> styles, final RevCommit to) {
        addLink(d, to.getId().name(), styles, to);
    }

    /**
     * @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 {
        RevWalk revWalk = new RevWalk(db);
        try {
            revWalk.setRetainBody(false);
            return RevWalkUtils.findBranchesReachableFrom(commit, revWalk, allRefs);
        } finally {
            revWalk.dispose();
        }
    }

    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 void makeGrayText(StringBuilder d, List<StyleRange> styles) {
        int p0 = 0;
        for (int i = 0; i < styles.size(); ++i) {
            StyleRange r = styles.get(i);
            if (p0 < r.start) {
                StyleRange nr = new StyleRange(p0, r.start - p0, darkGrey, null);
                styles.add(i, nr);
                p0 = r.start;
            } else {
                if (r.foreground == null)
                    r.foreground = darkGrey;
                p0 = r.start + r.length;
            }
        }
        if (d.length() - 1 > p0) {
            StyleRange nr = new StyleRange(p0, d.length() - p0, darkGrey, null);
            styles.add(nr);
        }
    }

    private void buildDiffs(final StringBuilder d, final List<StyleRange> styles, IProgressMonitor monitor,
            boolean trace) throws OperationCanceledException, IOException {

        // the encoding for the currently processed file
        final String[] currentEncoding = new String[1];

        if (trace)
            GitTraceLocation.getTrace().traceEntry(GitTraceLocation.HISTORYVIEW.getLocation());
        if (commit.getParentCount() > 1) {
            d.append(UIText.CommitMessageViewer_CanNotRenderDiffMessage);
            return;
        }

        try {
            monitor.beginTask(UIText.CommitMessageViewer_BuildDiffListTaskName, currentDiffs.size());
            BufferedOutputStream bos = new SafeBufferedOutputStream(new ByteArrayOutputStream() {
                @Override
                public synchronized void write(byte[] b, int off, int len) {
                    super.write(b, off, len);
                    if (currentEncoding[0] == null)
                        d.append(toString());
                    else
                        try {
                            d.append(toString(currentEncoding[0]));
                        } catch (UnsupportedEncodingException e) {
                            d.append(toString());
                        }
                    reset();
                }

            });
            final DiffFormatter diffFmt = new MessageViewerFormatter(bos, styles, d, hunkheaderColor,
                    linesAddedColor, linesRemovedColor);

            for (FileDiff currentDiff : currentDiffs) {
                if (monitor.isCanceled())
                    throw new OperationCanceledException();
                if (currentDiff.getBlobs().length == 2) {
                    String path = currentDiff.getPath();
                    monitor.setTaskName(NLS.bind(UIText.CommitMessageViewer_BuildDiffTaskName, path));
                    currentEncoding[0] = CompareCoreUtils.getResourceEncoding(db, path);
                    d.append(formatPathLine(path)).append(LF);
                    currentDiff.outputDiff(d, db, diffFmt, true);
                    diffFmt.flush();
                }
                monitor.worked(1);
            }

        } finally {
            monitor.done();
            if (trace)
                GitTraceLocation.getTrace().traceExit(GitTraceLocation.HISTORYVIEW.getLocation());
        }
    }

    private String formatPathLine(String path) {
        int n = 80 - path.length() - 2;
        if (n < 0)
            return path;
        final StringBuilder d = new StringBuilder();
        int i = 0;
        for (; i < n / 2; i++)
            d.append("-"); //$NON-NLS-1$
        d.append(SPACE).append(path).append(SPACE);
        for (; i < n - 1; i++)
            d.append("-"); //$NON-NLS-1$
        return d.toString();
    }

    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();
    }

    private static final class MessageViewerFormatter extends DiffFormatter {
        private final List<StyleRange> styles;

        private final StringBuilder d;

        private final Color hunkheaderColor;
        private final Color linesAddedColor;
        private final Color linesRemovedColor;

        private MessageViewerFormatter(OutputStream out, List<StyleRange> styles, StringBuilder d,
                Color hunkheaderColor, Color linesAddedColor, Color linesRemovedColor) {
            super(out);
            this.styles = styles;
            this.hunkheaderColor = hunkheaderColor;
            this.linesAddedColor = linesAddedColor;
            this.linesRemovedColor = linesRemovedColor;
            this.d = d;
        }

        @Override
        protected void writeHunkHeader(int aCur, int aEnd, int bCur, int bEnd) throws IOException {
            flush();
            int start = d.length();
            super.writeHunkHeader(aCur, aEnd, bCur, bEnd);
            flush();
            int end = d.length();
            styles.add(new StyleRange(start, end - start, hunkheaderColor, null));
        }

        @Override
        protected void writeAddedLine(RawText b, int bCur) throws IOException {
            flush();
            int start = d.length();
            super.writeAddedLine(b, bCur);
            flush();
            int end = d.length();
            styles.add(new StyleRange(start, end - start, linesAddedColor, null));
        }

        @Override
        protected void writeRemovedLine(RawText b, int bCur) throws IOException {
            flush();
            int start = d.length();
            super.writeRemovedLine(b, bCur);
            flush();
            int end = d.length();
            styles.add(new StyleRange(start, end - start, linesRemovedColor, null));
        }
    }

    /**
     * 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();
        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);
    }

}