Java tutorial
/* * 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. * <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. * * @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. * <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. * <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; } 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 {@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 * * @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; } }