From: Robin Rosenberg Date: Mon, 24 May 2010 19:19:59 +0000 (+0200) Subject: Add support for computing a Change-Id à la Gerrit X-Git-Tag: v0.9.1~211 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=920d89d6af01a668e8fada15e7ba3f6f83efc10c;p=jgit.git Add support for computing a Change-Id à la Gerrit A Change-Id helps tools like Gerrit Code Review to keeps different versions of a patch together. The Change-Id is computed as a SHA-1 hash of some of the same basic information as a commit id on the first commit intended to solve a particular problem and then reused for updated solutions. Change-Id: I04334f84e76e83a4185283cb72ea0308b1cb4182 Signed-off-by: Robin Rosenberg --- diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java new file mode 100644 index 0000000000..3b1584612d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2010, Robin Rosenberg + * Copyright (C) 2009, Google, Inc. + * 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.io.IOException; +import java.util.concurrent.TimeUnit; + +import junit.framework.TestCase; + +import org.eclipse.jgit.junit.MockSystemReader; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; + +/** + * Portions of this test is from CommitMsgHookTest in the Android project Gerrit + */ +public class ChangeIdUtilTest extends TestCase { + + private final String SOB1 = "Signed-off-by: J Author \n"; + + private final String SOB2 = "Signed-off-by: J Committer \n"; + + final PersonIdent p = new PersonIdent( + "A U Thor 1142878501 -0500"); + + final PersonIdent q = new PersonIdent( + "W Riter 1142878502 -0500"); + + ObjectId treeId = ObjectId + .fromString("f51de923607cd51cf872b928a6b523ba823f7f35"); + + ObjectId treeId1 = ObjectId + .fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"); + + final ObjectId treeId2 = ObjectId + .fromString("617601c79811cbbae338512798318b4e5b70c9ac"); + + ObjectId parentId = ObjectId + .fromString("91fea719aaf9447feb9580477eb3dd08b62b5eca"); + + ObjectId parentId1 = null; + + final ObjectId parentId2 = ObjectId + .fromString("485c91e0600b165c301c278bfbae3e492413980c"); + + MockSystemReader mockSystemReader = new MockSystemReader(); + + final long when = mockSystemReader.getCurrentTime(); + + final int tz = new MockSystemReader().getTimezone(when); + + PersonIdent author = new PersonIdent("J. Author", "ja@example.com"); + { + author = new PersonIdent(author, when, tz); + } + + PersonIdent committer = new PersonIdent("J. Committer", "jc@example.com"); + { + committer = new PersonIdent(committer, when, tz); + } + + public void testClean() { + assertEquals("hej", ChangeIdUtil.clean("hej\n\n")); + assertEquals("hej\n\nsan", ChangeIdUtil.clean("hej\n\nsan\n\n")); + assertEquals("hej\nsan", ChangeIdUtil.clean("hej\n#men\nsan\n\n#men")); + assertEquals("hej\nsan", ChangeIdUtil.clean("hej\nsan\n\n#men")); + assertEquals("hej\nsan", ChangeIdUtil.clean("#no\nhej\nsan\n\n#men")); + assertEquals("hej\nsan", ChangeIdUtil + .clean("#no\nhej\nsan\nSigned-off-by: me \n#men")); + } + + public void testId() throws IOException { + String msg = "A\nMessage\n"; + ObjectId id = ChangeIdUtil.computeChangeId(treeId, parentId, p, q, msg); + assertEquals("73f3751208ac92cbb76f9a26ac4a0d9d472e381b", ObjectId + .toString(id)); + } + + public void testHasChangeid() throws Exception { + assertEquals( + "has changeid\n\nBug: 33\nmore text\nSigned-off-by: me@you.too\nChange-Id: I0123456789012345678901234567890123456789\nAnd then some\n", + call("has changeid\n\nBug: 33\nmore text\nSigned-off-by: me@you.too\nChange-Id: I0123456789012345678901234567890123456789\nAnd then some\n")); + } + + public void testOneliner() throws Exception { + assertEquals( + "oneliner\n\nChange-Id: I3a98091ce4470de88d52ae317fcd297e2339f063\n", + call("oneliner\n")); + } + + public void testOnelinerFollowedByBlank() throws Exception { + assertEquals( + "oneliner followed by blank\n\nChange-Id: I3a12c21ef342a18498f95c62efbc186cd782b743\n", + call("oneliner followed by blank\n")); + } + + public void testATwoLines() throws Exception { + assertEquals( + "a two lines\nwith text withour break after subject line\n\nChange-Id: I549a0fed3d69b7876c54b4f5a35637135fd43fac\n", + call("a two lines\nwith text withour break after subject line\n")); + } + + public void testRegularCommit() throws Exception { + assertEquals( + "regular commit\n\nwith header and body\n\nChange-Id: I62d8749d3c3a888c11e3fadc3924220a19389766\n", + call("regular commit\n\nwith header and body\n")); + } + + public void testRegularCommitWithSob_ButNoBody() throws Exception { + assertEquals( + "regular commit with sob, but no body\n\nChange-Id: I0f0b4307e9944ecbd5a9f6b9489e25cfaede43c4\nSigned-off-by: me@you.too\n", + call("regular commit with sob, but no body\n\nSigned-off-by: me@you.too\n")); + } + + public void testACommitWithBug_SubButNoBody() throws Exception { + assertEquals( + "a commit with bug, sub but no body\n\nBug: 33\nChange-Id: I337e264868613dab6d1e11a34f394db369487412\nSigned-off-by: me@you.too\n", + call("a commit with bug, sub but no body\n\nBug: 33\nSigned-off-by: me@you.too\n")); + } + + public void testACommitWithSubject_NoBodySobAndBug() throws Exception { + assertEquals( + "a commit with subject, no body sob and bug\n\nChange-Id: Ib3616d4bf77707a3215a6cb0602c004ee119a445\nSigned-off-by: me@you.too\nBug: 33\n", + call("a commit with subject, no body sob and bug\n\nSigned-off-by: me@you.too\nBug: 33\n")); + } + + public void testACommitWithSubjectBug_NonFooterLineAndSob() + throws Exception { + assertEquals( + "a commit with subject bug, non-footer line and sob\n\nBug: 33\nmore text\nSigned-off-by: me@you.too\n\nChange-Id: Ia8500eab2304e6e5eac6ae488ff44d5d850d118a\n", + call("a commit with subject bug, non-footer line and sob\n\nBug: 33\nmore text\nSigned-off-by: me@you.too\n")); + } + + public void testACommitWithSubject_NonFooterAndBugAndSob() throws Exception { + assertEquals( + "a commit with subject, non-footer and bug and sob\n\nmore text (two empty lines after bug)\nBug: 33\n\n\nChange-Id: Idac75ccbad2ab6727b8612e344df5190d87891dd\nSigned-off-by: me@you.too\n", + call("a commit with subject, non-footer and bug and sob\n\nmore text (two empty lines after bug)\nBug: 33\n\n\nSigned-off-by: me@you.too\n")); + } + + public void testACommitWithSubjectBodyBugBrackersAndSob() throws Exception { + assertEquals( + "a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\nChange-Id: I90ecb589bef766302532c3e00915e10114b00f62\n[bracket]\nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n")); + } + + public void testACommitWithSubjectBodyBugLineWithASpaceAndSob() + throws Exception { + assertEquals( + "a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\nChange-Id: I864e2218bdee033c8ce9a7f923af9e0d5dc16863\n \nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n")); + } + + public void testACommitWithSubjectBodyBugEmptyLineAndSob() throws Exception { + assertEquals( + "a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\nChange-Id: I33f119f533313883e6ada3df600c4f0d4db23a76\n \nSigned-off-by: me@you.too\n", + call("a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n")); + } + + public void testEmptyMessages() throws Exception { + // Empty input must not produce a change id. + hookDoesNotModify(""); + hookDoesNotModify(" "); + hookDoesNotModify("\n"); + hookDoesNotModify("\n\n"); + hookDoesNotModify(" \n "); + + hookDoesNotModify("#"); + hookDoesNotModify("#\n"); + hookDoesNotModify("# on branch master\n# Untracked files:\n"); + hookDoesNotModify("\n# on branch master\n# Untracked files:\n"); + hookDoesNotModify("\n\n# on branch master\n# Untracked files:\n"); + + hookDoesNotModify("\n# on branch master\ndiff --git a/src b/src\n" + + "new file mode 100644\nindex 0000000..c78b7f0\n"); + } + + public void testChangeIdAlreadySet() throws Exception { + // If a Change-Id is already present in the footer, the hook must + // not modify the message but instead must leave the identity alone. + // + hookDoesNotModify("a\n" + // + "\n" + // + "Change-Id: Iaeac9b4149291060228ef0154db2985a31111335\n"); + hookDoesNotModify("fix: this thing\n" + // + "\n" + // + "Change-Id: I388bdaf52ed05b55e62a22d0a20d2c1ae0d33e7e\n"); + hookDoesNotModify("fix-a-widget: this thing\n" + // + "\n" + // + "Change-Id: Id3bc5359d768a6400450283e12bdfb6cd135ea4b\n"); + hookDoesNotModify("FIX: this thing\n" + // + "\n" + // + "Change-Id: I1b55098b5a2cce0b3f3da783dda50d5f79f873fa\n"); + hookDoesNotModify("Fix-A-Widget: this thing\n" + // + "\n" + // + "Change-Id: I4f4e2e1e8568ddc1509baecb8c1270a1fb4b6da7\n"); + } + + public void testTimeAltersId() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// + call("a\n")); + + tick(); + assertEquals("a\n" + // + "\n" + // + "Change-Id: I3251906b99dda598a58a6346d8126237ee1ea800\n",// + call("a\n")); + + tick(); + assertEquals("a\n" + // + "\n" + // + "Change-Id: I69adf9208d828f41a3d7e41afbca63aff37c0c5c\n",// + call("a\n")); + } + + /** Increment the {@link #author} and {@link #committer} times. */ + protected void tick() { + final long delta = TimeUnit.MILLISECONDS.convert(5 * 60, + TimeUnit.SECONDS); + final long now = author.getWhen().getTime() + delta; + + author = new PersonIdent(author, now, tz); + committer = new PersonIdent(committer, now, tz); + } + + public void testFirstParentAltersId() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// + call("a\n")); + + parentId1 = parentId2; + assertEquals("a\n" + // + "\n" + // + "Change-Id: I51e86482bde7f92028541aaf724d3a3f996e7ea2\n",// + call("a\n")); + } + + public void testDirCacheAltersId() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// + call("a\n")); + + treeId1 = treeId2; + assertEquals("a\n" + // + "\n" + // + "Change-Id: If56597ea9759f23b070677ea6f064c60c38da631\n",// + call("a\n")); + } + + public void testSingleLineMessages() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// + call("a\n")); + + assertEquals("fix: this thing\n" + // + "\n" + // + "Change-Id: I0f13d0e6c739ca3ae399a05a93792e80feb97f37\n",// + call("fix: this thing\n")); + assertEquals("fix-a-widget: this thing\n" + // + "\n" + // + "Change-Id: I1a1a0c751e4273d532e4046a501a612b9b8a775e\n",// + call("fix-a-widget: this thing\n")); + + assertEquals("FIX: this thing\n" + // + "\n" + // + "Change-Id: If816d944c57d3893b60cf10c65931fead1290d97\n",// + call("FIX: this thing\n")); + assertEquals("Fix-A-Widget: this thing\n" + // + "\n" + // + "Change-Id: I3e18d00cbda2ba1f73aeb63ed8c7d57d7fd16c76\n",// + call("Fix-A-Widget: this thing\n")); + } + + public void testMultiLineMessagesWithoutFooter() throws Exception { + assertEquals("a\n" + // + "\n" + // + "b\n" + // + "\n" + // + "Change-Id: Id0b4f42d3d6fc1569595c9b97cb665e738486f5d\n",// + call("a\n" + "\n" + "b\n")); + + assertEquals("a\n" + // + "\n" + // + "b\nc\nd\ne\n" + // + "\n" + // + "Change-Id: I7d237b20058a0f46cc3f5fabc4a0476877289d75\n",// + call("a\n" + "\n" + "b\nc\nd\ne\n")); + + assertEquals("a\n" + // + "\n" + // + "b\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + "Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n",// + call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n")); + } + + public void testSingleLineMessagesWithSignedOffBy() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + // + SOB1,// + call("a\n" + "\n" + SOB1)); + + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + // + SOB1 + // + SOB2,// + call("a\n" + "\n" + SOB1 + SOB2)); + } + + public void testMultiLineMessagesWithSignedOffBy() throws Exception { + assertEquals("a\n" + // + "\n" + // + "b\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + "Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n" + // + SOB1,// + call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n" + "\n" + + SOB1)); + + assertEquals("a\n" + // + "\n" + // + "b\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + "Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n" + // + SOB1 + // + SOB2,// + call("a\n" + // + "\n" + // + "b\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + SOB1 + // + SOB2)); + + assertEquals("a\n" + // + "\n" + // + "b: not a footer\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + "Change-Id: I8869aabd44b3017cd55d2d7e0d546a03e3931ee2\n" + // + SOB1 + // + SOB2,// + call("a\n" + // + "\n" + // + "b: not a footer\nc\nd\ne\n" + // + "\n" + // + "f\ng\nh\n" + // + "\n" + // + SOB1 + // + SOB2)); + } + + public void testNoteInMiddle() throws Exception { + assertEquals("a\n" + // + "\n" + // + "NOTE: This\n" + // + "does not fix it.\n" + // + "\n" + // + "Change-Id: I988a127969a6ee5e58db546aab74fc46e66847f8\n", // + call("a\n" + // + "\n" + // + "NOTE: This\n" + // + "does not fix it.\n")); + } + + public void testKernelStyleFooter() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I1bd787f9e7590a2ac82b02c404c955ffb21877c4\n" + // + SOB1 + // + "[ja: Fixed\n" + // + " the indentation]\n" + // + SOB2, // + call("a\n" + // + "\n" + // + SOB1 + // + "[ja: Fixed\n" + // + " the indentation]\n" + // + SOB2)); + } + + public void testChangeIdAfterBugOrIssue() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Bug: 42\n" + // + "Change-Id: I8c0321227c4324e670b9ae8cf40eccc87af21b1b\n" + // + SOB1,// + call("a\n" + // + "\n" + // + "Bug: 42\n" + // + SOB1)); + + assertEquals("a\n" + // + "\n" + // + "Issue: 42\n" + // + "Change-Id: Ie66e07d89ae5b114c0975b49cf326e90331dd822\n" + // + SOB1,// + call("a\n" + // + "\n" + // + "Issue: 42\n" + // + SOB1)); + } + + public void notestCommitDashV() throws Exception { + assertEquals("a\n" + // + "\n" + // + "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + // + SOB1 + // + SOB2, // + call("a\n" + // + "\n" + // + SOB1 + // + SOB2 + // + "\n" + // + "# on branch master\n" + // + "diff --git a/src b/src\n" + // + "new file mode 100644\n" + // + "index 0000000..c78b7f0\n")); + } + + public void testWithEndingURL() throws Exception { + assertEquals("a\n" + // + "\n" + // + "http://example.com/ fixes this\n" + // + "\n" + // + "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n", // + call("a\n" + // + "\n" + // + "http://example.com/ fixes this\n")); + assertEquals("a\n" + // + "\n" + // + "https://example.com/ fixes this\n" + // + "\n" + // + "Change-Id: I62b9039e2fc0dce274af55e8f99312a8a80a805d\n", // + call("a\n" + // + "\n" + // + "https://example.com/ fixes this\n")); + assertEquals("a\n" + // + "\n" + // + "ftp://example.com/ fixes this\n" + // + "\n" + // + "Change-Id: I71b05dc1f6b9a5540a53a693e64d58b65a8910e8\n", // + call("a\n" + // + "\n" + // + "ftp://example.com/ fixes this\n")); + assertEquals("a\n" + // + "\n" + // + "git://example.com/ fixes this\n" + // + "\n" + // + "Change-Id: Id34e942baa68d790633737d815ddf11bac9183e5\n", // + call("a\n" + // + "\n" + // + "git://example.com/ fixes this\n")); + } + + private void hookDoesNotModify(final String in) throws Exception { + assertEquals(in, call(in)); + } + + private String call(final String body) throws Exception { + ObjectId computeChangeId = ChangeIdUtil.computeChangeId(treeId1, + parentId1, author, committer, body); + if (computeChangeId == null) + return body; + return ChangeIdUtil.insertId(body, computeChangeId); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java index ea57a0215e..20147ed6ce 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java @@ -300,6 +300,24 @@ public class ObjectWriter { return writeObject(Constants.OBJ_BLOB, len, is, false); } + /** + * Compute the SHA-1 of an object without actually creating an object in the + * database + * + * @param type + * kind of object + * @param len + * number of bytes to consume + * @param is + * stream for read data from + * @return SHA-1 of data combined with type information + * @throws IOException + */ + public ObjectId computeObjectSha1(final int type, final long len, final InputStream is) + throws IOException { + return writeObject(type, len, is, false); + } + ObjectId writeObject(final int type, long len, final InputStream is, boolean store) throws IOException { final File t; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java new file mode 100644 index 0000000000..6c146f79f0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -0,0 +1,190 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.regex.Pattern; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectWriter; +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. + */ +public class ChangeIdUtil { + + // package-private so the unit test can test this part only + static String clean(String msg) { + return msg.// + replaceAll("(?i)(?m)^Signed-off-by:.*$\n?", "").// + replaceAll("(?m)^#.*$\n?", "").// + replaceAll("(?m)\n\n\n+", "\\\n").// + replaceAll("\\n*$", "").// + replaceAll("(?s)\ndiff --git.*", "").// + 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 PersonIdent} for the presumed author and time + * @param committer + * the {@link 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 + * @throws IOException + */ + public static ObjectId computeChangeId(final ObjectId treeId, + final ObjectId firstParentId, final PersonIdent author, + final PersonIdent committer, final String message) + throws IOException { + String cleanMessage = clean(message); + if (cleanMessage.length() == 0) + return null; + StringBuilder b = new StringBuilder(); + b.append("tree "); + b.append(ObjectId.toString(treeId)); + b.append("\n"); + if (firstParentId != null) { + b.append("parent "); + b.append(ObjectId.toString(firstParentId)); + b.append("\n"); + } + b.append("author "); + b.append(author.toExternalString()); + b.append("\n"); + b.append("committer "); + b.append(committer.toExternalString()); + b.append("\n\n"); + b.append(cleanMessage); + ObjectWriter w = new ObjectWriter(null); + byte[] bytes = b.toString().getBytes(Constants.CHARACTER_ENCODING); + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + ObjectId sha1 = w.computeObjectSha1(Constants.OBJ_COMMIT, bytes.length, + is); + return sha1; + } + + private static final Pattern issuePattern = Pattern + .compile("^(Bug|Issue)[a-zA-Z0-9-]*:.*$"); + + private static final Pattern footerPattern = Pattern + .compile("(^[a-zA-Z0-9-]+:(?!//).*$)"); + + private static final Pattern includeInFooterPattern = Pattern + .compile("^[ \\[].*$"); + + /** + * 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. + * + * @param message + * @param changeId + * @return a commit message with an inserted Change-Id line + */ + public static String insertId(String message, ObjectId changeId) { + if (message.indexOf("\nChange-Id:") > 0) + return message; + + String[] lines = message.split("\n"); + 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; + } + 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"); + } + if (insertAfter == lines.length && insertAfter == footerFirstLine) + ret.append("\n"); + ret.append("Change-Id: I"); + ret.append(ObjectId.toString(changeId)); + ret.append("\n"); + for (; i < lines.length; ++i) { + ret.append(lines[i]); + ret.append("\n"); + } + return ret.toString(); + } +}