org.eclipse.jgit.revwalk.RevCommit.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.revwalk.RevCommit.java

Source

/*
 * Copyright (C) 2008-2009, Google Inc.
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.revwalk;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.StringUtils;

/**
 * A commit reference to a commit in the DAG.
 */
public class RevCommit extends RevObject {
    private static final int STACK_DEPTH = 500;

    /**
     * Parse a commit from its canonical format.
     *
     * This method constructs a temporary revision pool, parses the commit as
     * supplied, and returns it to the caller. Since the commit was built inside
     * of a private revision pool its parent pointers will be initialized, but
     * will not have their headers loaded.
     *
     * Applications are discouraged from using this API. Callers usually need
     * more than one commit. Use
     * {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)} to
     * obtain a RevCommit from an existing repository.
     *
     * @param raw
     *            the canonical formatted commit to be parsed.
     * @return the parsed commit, in an isolated revision pool that is not
     *         available to the caller.
     */
    public static RevCommit parse(byte[] raw) {
        try {
            return parse(new RevWalk((ObjectReader) null), raw);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Parse a commit from its canonical format.
     * <p>
     * This method inserts the commit directly into the caller supplied revision
     * pool, making it appear as though the commit exists in the repository,
     * even if it doesn't. The repository under the pool is not affected.
     * <p>
     * The body of the commit (message, author, committer) is always retained in
     * the returned {@code RevCommit}, even if the supplied {@code RevWalk} has
     * been configured with {@code setRetainBody(false)}.
     *
     * @param rw
     *            the revision pool to allocate the commit within. The commit's
     *            tree and parent pointers will be obtained from this pool.
     * @param raw
     *            the canonical formatted commit to be parsed. This buffer will
     *            be retained by the returned {@code RevCommit} and must not be
     *            modified by the caller.
     * @return the parsed commit, in an isolated revision pool that is not
     *         available to the caller.
     * @throws java.io.IOException
     *             in case of RevWalk initialization fails
     */
    public static RevCommit parse(RevWalk rw, byte[] raw) throws IOException {
        try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
            RevCommit r = rw.lookupCommit(fmt.idFor(Constants.OBJ_COMMIT, raw));
            r.parseCanonical(rw, raw);
            r.buffer = raw;
            return r;
        }
    }

    static final RevCommit[] NO_PARENTS = {};

    private RevTree tree;

    RevCommit[] parents;

    int commitTime; // An int here for performance, overflows in 2038

    int inDegree;

    private byte[] buffer;

    /**
     * Create a new commit reference.
     *
     * @param id
     *            object name for the commit.
     */
    protected RevCommit(AnyObjectId id) {
        super(id);
    }

    @Override
    void parseHeaders(RevWalk walk) throws MissingObjectException, IncorrectObjectTypeException, IOException {
        parseCanonical(walk, walk.getCachedBytes(this));
    }

    @Override
    void parseBody(RevWalk walk) throws MissingObjectException, IncorrectObjectTypeException, IOException {
        if (buffer == null) {
            buffer = walk.getCachedBytes(this);
            if ((flags & PARSED) == 0)
                parseCanonical(walk, buffer);
        }
    }

    void parseCanonical(RevWalk walk, byte[] raw) throws IOException {
        if (!walk.shallowCommitsInitialized) {
            walk.initializeShallowCommits(this);
        }

        final MutableObjectId idBuffer = walk.idBuffer;
        idBuffer.fromString(raw, 5);
        tree = walk.lookupTree(idBuffer);

        int ptr = 46;
        if (parents == null) {
            RevCommit[] pList = new RevCommit[1];
            int nParents = 0;
            for (;;) {
                if (raw[ptr] != 'p') {
                    break;
                }
                idBuffer.fromString(raw, ptr + 7);
                final RevCommit p = walk.lookupCommit(idBuffer);
                switch (nParents) {
                case 0:
                    pList[nParents++] = p;
                    break;
                case 1:
                    pList = new RevCommit[] { pList[0], p };
                    nParents = 2;
                    break;
                default:
                    if (pList.length <= nParents) {
                        RevCommit[] old = pList;
                        pList = new RevCommit[pList.length + 32];
                        System.arraycopy(old, 0, pList, 0, nParents);
                    }
                    pList[nParents++] = p;
                    break;
                }
                ptr += 48;
            }
            if (nParents != pList.length) {
                RevCommit[] old = pList;
                pList = new RevCommit[nParents];
                System.arraycopy(old, 0, pList, 0, nParents);
            }
            parents = pList;
        }

        // extract time from "committer "
        ptr = RawParseUtils.committer(raw, ptr);
        if (ptr > 0) {
            ptr = RawParseUtils.nextLF(raw, ptr, '>');

            // In 2038 commitTime will overflow unless it is changed to long.
            commitTime = RawParseUtils.parseBase10(raw, ptr, null);
        }

        if (walk.isRetainBody()) {
            buffer = raw;
        }
        flags |= PARSED;
    }

    /** {@inheritDoc} */
    @Override
    public final int getType() {
        return Constants.OBJ_COMMIT;
    }

    static void carryFlags(RevCommit c, int carry) {
        FIFORevQueue q = carryFlags1(c, carry, 0);
        if (q != null)
            slowCarryFlags(q, carry);
    }

    private static FIFORevQueue carryFlags1(RevCommit c, int carry, int depth) {
        for (;;) {
            RevCommit[] pList = c.parents;
            if (pList == null || pList.length == 0)
                return null;
            if (pList.length != 1) {
                if (depth == STACK_DEPTH)
                    return defer(c);
                for (int i = 1; i < pList.length; i++) {
                    RevCommit p = pList[i];
                    if ((p.flags & carry) == carry)
                        continue;
                    p.flags |= carry;
                    FIFORevQueue q = carryFlags1(p, carry, depth + 1);
                    if (q != null)
                        return defer(q, carry, pList, i + 1);
                }
            }

            c = pList[0];
            if ((c.flags & carry) == carry)
                return null;
            c.flags |= carry;
        }
    }

    private static FIFORevQueue defer(RevCommit c) {
        FIFORevQueue q = new FIFORevQueue();
        q.add(c);
        return q;
    }

    private static FIFORevQueue defer(FIFORevQueue q, int carry, RevCommit[] pList, int i) {
        // In normal case the caller will run pList[0] in a tail recursive
        // fashion by updating the variable. However the caller is unwinding
        // the stack and will skip that pList[0] execution step.
        carryOneStep(q, carry, pList[0]);

        // Remaining parents (if any) need to have flags checked and be
        // enqueued if they have ancestors.
        for (; i < pList.length; i++)
            carryOneStep(q, carry, pList[i]);
        return q;
    }

    private static void slowCarryFlags(FIFORevQueue q, int carry) {
        // Commits in q have non-null parent arrays and have set all
        // flags in carry. This loop finishes copying over the graph.
        for (RevCommit c; (c = q.next()) != null;) {
            for (RevCommit p : c.parents)
                carryOneStep(q, carry, p);
        }
    }

    private static void carryOneStep(FIFORevQueue q, int carry, RevCommit c) {
        if ((c.flags & carry) != carry) {
            c.flags |= carry;
            if (c.parents != null)
                q.add(c);
        }
    }

    /**
     * Carry a RevFlag set on this commit to its parents.
     * <p>
     * If this commit is parsed, has parents, and has the supplied flag set on
     * it we automatically add it to the parents, grand-parents, and so on until
     * an unparsed commit or a commit with no parents is discovered. This
     * permits applications to force a flag through the history chain when
     * necessary.
     *
     * @param flag
     *            the single flag value to carry back onto parents.
     */
    public void carry(RevFlag flag) {
        final int carry = flags & flag.mask;
        if (carry != 0)
            carryFlags(this, carry);
    }

    /**
     * Time from the "committer " line of the buffer.
     *
     * @return commit time
     */
    public final int getCommitTime() {
        return commitTime;
    }

    /**
     * Get a reference to this commit's tree.
     *
     * @return tree of this commit.
     */
    public final RevTree getTree() {
        return tree;
    }

    /**
     * Get the number of parent commits listed in this commit.
     *
     * @return number of parents; always a positive value but can be 0.
     */
    public final int getParentCount() {
        return parents.length;
    }

    /**
     * Get the nth parent from this commit's parent list.
     *
     * @param nth
     *            parent index to obtain. Must be in the range 0 through
     *            {@link #getParentCount()}-1.
     * @return the specified parent.
     * @throws java.lang.ArrayIndexOutOfBoundsException
     *             an invalid parent index was specified.
     */
    public final RevCommit getParent(int nth) {
        return parents[nth];
    }

    /**
     * Obtain an array of all parents (<b>NOTE - THIS IS NOT A COPY</b>).
     * <p>
     * This method is exposed only to provide very fast, efficient access to
     * this commit's parent list. Applications relying on this list should be
     * very careful to ensure they do not modify its contents during their use
     * of it.
     *
     * @return the array of parents.
     */
    public final RevCommit[] getParents() {
        return parents;
    }

    /**
     * Obtain the raw unparsed commit body (<b>NOTE - THIS IS NOT A COPY</b>).
     * <p>
     * This method is exposed only to provide very fast, efficient access to
     * this commit's message buffer within a RevFilter. Applications relying on
     * this buffer should be very careful to ensure they do not modify its
     * contents during their use of it.
     *
     * @return the raw unparsed commit body. This is <b>NOT A COPY</b>.
     *         Altering the contents of this buffer may alter the walker's
     *         knowledge of this commit, and the results it produces.
     */
    public final byte[] getRawBuffer() {
        return buffer;
    }

    /**
     * Parse the gpg signature from the raw buffer.
     * <p>
     * This method parses and returns the raw content of the gpgsig lines. This
     * method is fairly expensive and produces a new byte[] instance on each
     * invocation. Callers should invoke this method only if they are certain
     * they will need, and should cache the return value for as long as
     * necessary to use all information from it.
     * <p>
     * RevFilter implementations should try to use
     * {@link org.eclipse.jgit.util.RawParseUtils} to scan the
     * {@link #getRawBuffer()} instead, as this will allow faster evaluation of
     * commits.
     *
     * @return contents of the gpg signature; null if the commit was not signed.
     * @since 5.1
     */
    public final byte[] getRawGpgSignature() {
        final byte[] raw = buffer;
        final byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
        final int start = RawParseUtils.headerStart(header, raw, 0);
        if (start < 0) {
            return null;
        }
        final int end = RawParseUtils.headerEnd(raw, start);
        return Arrays.copyOfRange(raw, start, end);
    }

    /**
     * Parse the author identity from the raw buffer.
     * <p>
     * This method parses and returns the content of the author line, after
     * taking the commit's character set into account and decoding the author
     * name and email address. This method is fairly expensive and produces a
     * new PersonIdent instance on each invocation. Callers should invoke this
     * method only if they are certain they will be outputting the result, and
     * should cache the return value for as long as necessary to use all
     * information from it.
     * <p>
     * RevFilter implementations should try to use
     * {@link org.eclipse.jgit.util.RawParseUtils} to scan the
     * {@link #getRawBuffer()} instead, as this will allow faster evaluation of
     * commits.
     *
     * @return identity of the author (name, email) and the time the commit was
     *         made by the author; null if no author line was found.
     */
    public final PersonIdent getAuthorIdent() {
        final byte[] raw = buffer;
        final int nameB = RawParseUtils.author(raw, 0);
        if (nameB < 0)
            return null;
        return RawParseUtils.parsePersonIdent(raw, nameB);
    }

    /**
     * Parse the committer identity from the raw buffer.
     * <p>
     * This method parses and returns the content of the committer line, after
     * taking the commit's character set into account and decoding the committer
     * name and email address. This method is fairly expensive and produces a
     * new PersonIdent instance on each invocation. Callers should invoke this
     * method only if they are certain they will be outputting the result, and
     * should cache the return value for as long as necessary to use all
     * information from it.
     * <p>
     * RevFilter implementations should try to use
     * {@link org.eclipse.jgit.util.RawParseUtils} to scan the
     * {@link #getRawBuffer()} instead, as this will allow faster evaluation of
     * commits.
     *
     * @return identity of the committer (name, email) and the time the commit
     *         was made by the committer; null if no committer line was found.
     */
    public final PersonIdent getCommitterIdent() {
        final byte[] raw = buffer;
        final int nameB = RawParseUtils.committer(raw, 0);
        if (nameB < 0)
            return null;
        return RawParseUtils.parsePersonIdent(raw, nameB);
    }

    /**
     * Parse the complete commit message and decode it to a string.
     * <p>
     * This method parses and returns the message portion of the commit buffer,
     * after taking the commit's character set into account and decoding the
     * buffer using that character set. This method is a fairly expensive
     * operation and produces a new string on each invocation.
     *
     * @return decoded commit message as a string. Never null.
     */
    public final String getFullMessage() {
        byte[] raw = buffer;
        int msgB = RawParseUtils.commitMessage(raw, 0);
        if (msgB < 0) {
            return ""; //$NON-NLS-1$
        }
        return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
    }

    /**
     * Parse the commit message and return the first "line" of it.
     * <p>
     * The first line is everything up to the first pair of LFs. This is the
     * "oneline" format, suitable for output in a single line display.
     * <p>
     * This method parses and returns the message portion of the commit buffer,
     * after taking the commit's character set into account and decoding the
     * buffer using that character set. This method is a fairly expensive
     * operation and produces a new string on each invocation.
     *
     * @return decoded commit message as a string. Never null. The returned
     *         string does not contain any LFs, even if the first paragraph
     *         spanned multiple lines. Embedded LFs are converted to spaces.
     */
    public final String getShortMessage() {
        byte[] raw = buffer;
        int msgB = RawParseUtils.commitMessage(raw, 0);
        if (msgB < 0) {
            return ""; //$NON-NLS-1$
        }

        int msgE = RawParseUtils.endOfParagraph(raw, msgB);
        String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
        if (hasLF(raw, msgB, msgE)) {
            str = StringUtils.replaceLineBreaksWithSpace(str);
        }
        return str;
    }

    static boolean hasLF(byte[] r, int b, int e) {
        while (b < e)
            if (r[b++] == '\n')
                return true;
        return false;
    }

    /**
     * Determine the encoding of the commit message buffer.
     * <p>
     * Locates the "encoding" header (if present) and returns its value. Due to
     * corruption in the wild this may be an invalid encoding name that is not
     * recognized by any character encoding library.
     * <p>
     * If no encoding header is present, null.
     *
     * @return the preferred encoding of {@link #getRawBuffer()}; or null.
     * @since 4.2
     */
    @Nullable
    public final String getEncodingName() {
        return RawParseUtils.parseEncodingName(buffer);
    }

    /**
     * Determine the encoding of the commit message buffer.
     * <p>
     * Locates the "encoding" header (if present) and then returns the proper
     * character set to apply to this buffer to evaluate its contents as
     * character data.
     * <p>
     * If no encoding header is present {@code UTF-8} is assumed.
     *
     * @return the preferred encoding of {@link #getRawBuffer()}.
     * @throws IllegalCharsetNameException
     *             if the character set requested by the encoding header is
     *             malformed and unsupportable.
     * @throws UnsupportedCharsetException
     *             if the JRE does not support the character set requested by
     *             the encoding header.
     */
    public final Charset getEncoding() {
        return RawParseUtils.parseEncoding(buffer);
    }

    private Charset guessEncoding() {
        try {
            return getEncoding();
        } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
            return UTF_8;
        }
    }

    /**
     * Parse the footer lines (e.g. "Signed-off-by") for machine processing.
     * <p>
     * This method splits all of the footer lines out of the last paragraph of
     * the commit message, providing each line as a key-value pair, ordered by
     * the order of the line's appearance in the commit message itself.
     * <p>
     * A footer line's key must match the pattern {@code ^[A-Za-z0-9-]+:}, while
     * the value is free-form, but must not contain an LF. Very common keys seen
     * in the wild are:
     * <ul>
     * <li>{@code Signed-off-by} (agrees to Developer Certificate of Origin)
     * <li>{@code Acked-by} (thinks change looks sane in context)
     * <li>{@code Reported-by} (originally found the issue this change fixes)
     * <li>{@code Tested-by} (validated change fixes the issue for them)
     * <li>{@code CC}, {@code Cc} (copy on all email related to this change)
     * <li>{@code Bug} (link to project's bug tracking system)
     * </ul>
     *
     * @return ordered list of footer lines; empty list if no footers found.
     */
    public final List<FooterLine> getFooterLines() {
        final byte[] raw = buffer;
        int ptr = raw.length - 1;
        while (raw[ptr] == '\n') // trim any trailing LFs, not interesting
            ptr--;

        final int msgB = RawParseUtils.commitMessage(raw, 0);
        final ArrayList<FooterLine> r = new ArrayList<>(4);
        final Charset enc = guessEncoding();
        for (;;) {
            ptr = RawParseUtils.prevLF(raw, ptr);
            if (ptr <= msgB)
                break; // Don't parse commit headers as footer lines.

            final int keyStart = ptr + 2;
            if (raw[keyStart] == '\n')
                break; // Stop at first paragraph break, no footers above it.

            final int keyEnd = RawParseUtils.endOfFooterLineKey(raw, keyStart);
            if (keyEnd < 0)
                continue; // Not a well formed footer line, skip it.

            // Skip over the ': *' at the end of the key before the value.
            //
            int valStart = keyEnd + 1;
            while (valStart < raw.length && raw[valStart] == ' ')
                valStart++;

            // Value ends at the LF, and does not include it.
            //
            int valEnd = RawParseUtils.nextLF(raw, valStart);
            if (raw[valEnd - 1] == '\n')
                valEnd--;

            r.add(new FooterLine(raw, enc, keyStart, keyEnd, valStart, valEnd));
        }
        Collections.reverse(r);
        return r;
    }

    /**
     * Get the values of all footer lines with the given key.
     *
     * @param keyName
     *            footer key to find values of, case insensitive.
     * @return values of footers with key of {@code keyName}, ordered by their
     *         order of appearance. Duplicates may be returned if the same
     *         footer appeared more than once. Empty list if no footers appear
     *         with the specified key, or there are no footers at all.
     * @see #getFooterLines()
     */
    public final List<String> getFooterLines(String keyName) {
        return getFooterLines(new FooterKey(keyName));
    }

    /**
     * Get the values of all footer lines with the given key.
     *
     * @param keyName
     *            footer key to find values of, case insensitive.
     * @return values of footers with key of {@code keyName}, ordered by their
     *         order of appearance. Duplicates may be returned if the same
     *         footer appeared more than once. Empty list if no footers appear
     *         with the specified key, or there are no footers at all.
     * @see #getFooterLines()
     */
    public final List<String> getFooterLines(FooterKey keyName) {
        final List<FooterLine> src = getFooterLines();
        if (src.isEmpty())
            return Collections.emptyList();
        final ArrayList<String> r = new ArrayList<>(src.size());
        for (FooterLine f : src) {
            if (f.matches(keyName))
                r.add(f.getValue());
        }
        return r;
    }

    /**
     * Reset this commit to allow another RevWalk with the same instances.
     * <p>
     * Subclasses <b>must</b> call <code>super.reset()</code> to ensure the
     * basic information can be correctly cleared out.
     */
    public void reset() {
        inDegree = 0;
    }

    /**
     * Discard the message buffer to reduce memory usage.
     * <p>
     * After discarding the memory usage of the {@code RevCommit} is reduced to
     * only the {@link #getTree()} and {@link #getParents()} pointers and the
     * time in {@link #getCommitTime()}. Accessing other properties such as
     * {@link #getAuthorIdent()}, {@link #getCommitterIdent()} or either message
     * function requires reloading the buffer by invoking
     * {@link org.eclipse.jgit.revwalk.RevWalk#parseBody(RevObject)}.
     *
     * @since 4.0
     */
    public final void disposeBody() {
        buffer = null;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        final StringBuilder s = new StringBuilder();
        s.append(Constants.typeString(getType()));
        s.append(' ');
        s.append(name());
        s.append(' ');
        s.append(commitTime);
        s.append(' ');
        appendCoreFlags(s);
        return s.toString();
    }
}