]> source.dussan.org Git - jgit.git/commitdiff
Add support for computing a Change-Id à la Gerrit 23/523/7
authorRobin Rosenberg <robin.rosenberg@dewire.com>
Mon, 24 May 2010 19:19:59 +0000 (21:19 +0200)
committerShawn O. Pearce <spearce@spearce.org>
Sat, 5 Jun 2010 01:42:14 +0000 (18:42 -0700)
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 <robin.rosenberg@dewire.com>
org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java
org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java [new file with mode: 0644]

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 (file)
index 0000000..3b15846
--- /dev/null
@@ -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 <ja@example.com>\n";
+
+       private final String SOB2 = "Signed-off-by: J Committer <jc@example.com>\n";
+
+       final PersonIdent p = new PersonIdent(
+                       "A U Thor <author@example.com> 1142878501 -0500");
+
+       final PersonIdent q = new PersonIdent(
+                       "W Riter <writer@example.com> 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);
+       }
+
+}
index ea57a0215ea166c155908932fa58dee8e9d00864..20147ed6ce74108c6595384a12fc9861b8caf27c 100644 (file)
@@ -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 (file)
index 0000000..6c146f7
--- /dev/null
@@ -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.
+ * <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 {
+
+       // 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.
+        * <p>
+        * 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();
+       }
+}