/*
 * Copyright (C) 2010, Robin Rosenberg
 * 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.util;

import java.util.regex.Pattern;

import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;

Utilities for creating and working with Change-Id's, like the one used by Gerrit Code Review.

A Change-Id is a SHA-1 computed from the content of a commit, in a similar fashion to how the commit id is computed. Unlike the commit id a Change-Id is retained in the commit and subsequent revised commits in the footer of the commit text.

/** * Utilities for creating and working with Change-Id's, like the one used by * Gerrit Code Review. * <p> * A Change-Id is a SHA-1 computed from the content of a commit, in a similar * fashion to how the commit id is computed. Unlike the commit id a Change-Id is * retained in the commit and subsequent revised commits in the footer of the * commit text. */
public class ChangeIdUtil { static final String CHANGE_ID = "Change-Id:"; //$NON-NLS-1$ // package-private so the unit test can test this part only @SuppressWarnings("nls") static String clean(String msg) { return msg.// replaceAll("(?i)(?m)^Signed-off-by:.*$\n?", "").// //$NON-NLS-1$ replaceAll("(?m)^#.*$\n?", "").// //$NON-NLS-1$ replaceAll("(?m)\n\n\n+", "\\\n").// //$NON-NLS-1$ replaceAll("\\n*$", "").// //$NON-NLS-1$ replaceAll("(?s)\ndiff --git.*", "").// //$NON-NLS-1$ trim(); }
Compute a Change-Id.
Params:
  • treeId – The id of the tree that would be committed
  • firstParentId – parent id of previous commit or null
  • author – the PersonIdent for the presumed author and time
  • committer – the PersonIdent for the presumed committer and time
  • message – The commit message
Returns:the change id SHA1 string (without the 'I') or null if the message is not complete enough
/** * Compute a Change-Id. * * @param treeId * The id of the tree that would be committed * @param firstParentId * parent id of previous commit or null * @param author * the {@link org.eclipse.jgit.lib.PersonIdent} for the presumed * author and time * @param committer * the {@link org.eclipse.jgit.lib.PersonIdent} for the presumed * committer and time * @param message * The commit message * @return the change id SHA1 string (without the 'I') or null if the * message is not complete enough */
public static ObjectId computeChangeId(final ObjectId treeId, final ObjectId firstParentId, final PersonIdent author, final PersonIdent committer, final String message) { String cleanMessage = clean(message); if (cleanMessage.length() == 0) return null; StringBuilder b = new StringBuilder(); b.append("tree "); //$NON-NLS-1$ b.append(ObjectId.toString(treeId)); b.append("\n"); //$NON-NLS-1$ if (firstParentId != null) { b.append("parent "); //$NON-NLS-1$ b.append(ObjectId.toString(firstParentId)); b.append("\n"); //$NON-NLS-1$ } b.append("author "); //$NON-NLS-1$ b.append(author.toExternalString()); b.append("\n"); //$NON-NLS-1$ b.append("committer "); //$NON-NLS-1$ b.append(committer.toExternalString()); b.append("\n\n"); //$NON-NLS-1$ b.append(cleanMessage); try (ObjectInserter f = new ObjectInserter.Formatter()) { return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString())); } } private static final Pattern issuePattern = Pattern .compile("^(Bug|Issue)[a-zA-Z0-9-]*:.*$"); //$NON-NLS-1$ private static final Pattern footerPattern = Pattern .compile("(^[a-zA-Z0-9-]+:(?!//).*$)"); //$NON-NLS-1$ private static final Pattern changeIdPattern = Pattern .compile("(^" + CHANGE_ID + " *I[a-f0-9]{40}$)"); //$NON-NLS-1$ //$NON-NLS-2$ private static final Pattern includeInFooterPattern = Pattern .compile("^[ \\[].*$"); //$NON-NLS-1$ private static final Pattern trailingWhitespace = Pattern.compile("\\s+$"); //$NON-NLS-1$
Find the right place to insert a Change-Id and return it.

The Change-Id is inserted before the first footer line but after a Bug line.

Params:
  • message – a message.
  • changeId – a Change-Id.
Returns:a commit message with an inserted Change-Id line
/** * Find the right place to insert a Change-Id and return it. * <p> * The Change-Id is inserted before the first footer line but after a Bug * line. * * @param message * a message. * @param changeId * a Change-Id. * @return a commit message with an inserted Change-Id line */
public static String insertId(String message, ObjectId changeId) { return insertId(message, changeId, false); }
Find the right place to insert a Change-Id and return it.

If no Change-Id is found the Change-Id is inserted before the first footer line but after a Bug line. If Change-Id is found and replaceExisting is set to false, the message is unchanged. If Change-Id is found and replaceExisting is set to true, the Change-Id is replaced with changeId.

Params:
  • message – a message.
  • changeId – a Change-Id.
  • replaceExisting – a boolean.
Returns:a commit message with an inserted Change-Id line
/** * Find the right place to insert a Change-Id and return it. * <p> * If no Change-Id is found the Change-Id is inserted before the first * footer line but after a Bug line. * * If Change-Id is found and replaceExisting is set to false, the message is * unchanged. * * If Change-Id is found and replaceExisting is set to true, the Change-Id * is replaced with {@code changeId}. * * @param message * a message. * @param changeId * a Change-Id. * @param replaceExisting * a boolean. * @return a commit message with an inserted Change-Id line */
public static String insertId(String message, ObjectId changeId, boolean replaceExisting) { int indexOfChangeId = indexOfChangeId(message, "\n"); //$NON-NLS-1$ if (indexOfChangeId > 0) { if (!replaceExisting) return message; else { StringBuilder ret = new StringBuilder(message.substring(0, indexOfChangeId)); ret.append(CHANGE_ID); ret.append(" I"); //$NON-NLS-1$ ret.append(ObjectId.toString(changeId)); int indexOfNextLineBreak = message.indexOf("\n", //$NON-NLS-1$ indexOfChangeId); if (indexOfNextLineBreak > 0) ret.append(message.substring(indexOfNextLineBreak)); return ret.toString(); } } String[] lines = message.split("\n"); //$NON-NLS-1$ int footerFirstLine = indexOfFirstFooterLine(lines); int insertAfter = footerFirstLine; for (int i = footerFirstLine; i < lines.length; ++i) { if (issuePattern.matcher(lines[i]).matches()) { insertAfter = i + 1; continue; } break; } StringBuilder ret = new StringBuilder(); int i = 0; for (; i < insertAfter; ++i) { ret.append(lines[i]); ret.append("\n"); //$NON-NLS-1$ } if (insertAfter == lines.length && insertAfter == footerFirstLine) ret.append("\n"); //$NON-NLS-1$ ret.append(CHANGE_ID); ret.append(" I"); //$NON-NLS-1$ ret.append(ObjectId.toString(changeId)); ret.append("\n"); //$NON-NLS-1$ for (; i < lines.length; ++i) { ret.append(lines[i]); ret.append("\n"); //$NON-NLS-1$ } return ret.toString(); }
Return the index in the String message where the Change-Id entry in the footer begins. If there are more than one entries matching the pattern, return the index of the last one in the last section. Because of Bug: 400818 we release the constraint here that a footer must contain only lines matching footerPattern.
Params:
  • message – a message.
  • delimiter – the line delimiter, like "\n" or "\r\n", needed to find the footer
Returns:the index of the ChangeId footer in the message, or -1 if no ChangeId footer available
/** * Return the index in the String {@code message} where the Change-Id entry * in the footer begins. If there are more than one entries matching the * pattern, return the index of the last one in the last section. Because of * Bug: 400818 we release the constraint here that a footer must contain * only lines matching {@code footerPattern}. * * @param message * a message. * @param delimiter * the line delimiter, like "\n" or "\r\n", needed to find the * footer * @return the index of the ChangeId footer in the message, or -1 if no * ChangeId footer available */
public static int indexOfChangeId(String message, String delimiter) { String[] lines = message.split(delimiter); if (lines.length == 0) return -1; int indexOfChangeIdLine = 0; boolean inFooter = false; for (int i = lines.length - 1; i >= 0; --i) { if (!inFooter && isEmptyLine(lines[i])) continue; inFooter = true; if (changeIdPattern.matcher(trimRight(lines[i])).matches()) { indexOfChangeIdLine = i; break; } else if (isEmptyLine(lines[i]) || i == 0) return -1; } int indexOfChangeIdLineinString = 0; for (int i = 0; i < indexOfChangeIdLine; ++i) indexOfChangeIdLineinString += lines[i].length() + delimiter.length(); return indexOfChangeIdLineinString + lines[indexOfChangeIdLine].indexOf(CHANGE_ID); } private static boolean isEmptyLine(String line) { return line.trim().length() == 0; } private static String trimRight(String s) { return trailingWhitespace.matcher(s).replaceAll(""); //$NON-NLS-1$ }
Find the index of the first line of the footer paragraph in an array of the lines, or lines.length if no footer is available
Params:
  • lines – the commit message split into lines and the line delimiters stripped off
Returns:the index of the first line of the footer paragraph, or lines.length if no footer is available
/** * Find the index of the first line of the footer paragraph in an array of * the lines, or lines.length if no footer is available * * @param lines * the commit message split into lines and the line delimiters * stripped off * @return the index of the first line of the footer paragraph, or * lines.length if no footer is available */
public static int indexOfFirstFooterLine(String[] lines) { int footerFirstLine = lines.length; for (int i = lines.length - 1; i > 1; --i) { if (footerPattern.matcher(lines[i]).matches()) { footerFirstLine = i; continue; } if (footerFirstLine != lines.length && lines[i].length() == 0) break; if (footerFirstLine != lines.length && includeInFooterPattern.matcher(lines[i]).matches()) { footerFirstLine = i + 1; continue; } footerFirstLine = lines.length; break; } return footerFirstLine; } }