View Javadoc
1   /*
2    * Copyright (C) 2010, Robin Rosenberg
3    * Copyright (C) 2009, Google, Inc.
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  package org.eclipse.jgit.util;
45  
46  import static org.junit.Assert.assertEquals;
47  
48  import java.io.IOException;
49  import java.util.concurrent.TimeUnit;
50  
51  import org.eclipse.jgit.junit.MockSystemReader;
52  import org.eclipse.jgit.lib.ObjectId;
53  import org.eclipse.jgit.lib.PersonIdent;
54  import org.junit.Test;
55  
56  /**
57   * Portions of this test is from CommitMsgHookTest in the Android project Gerrit
58   */
59  public class ChangeIdUtilTest {
60  
61  	private final String SOB1 = "Signed-off-by: J Author <ja@example.com>\n";
62  
63  	private final String SOB2 = "Signed-off-by: J Committer <jc@example.com>\n";
64  
65  	final PersonIdent p = RawParseUtils.parsePersonIdent(
66  			"A U Thor <author@example.com> 1142878501 -0500");
67  
68  	final PersonIdent q = RawParseUtils.parsePersonIdent(
69  			"W Riter <writer@example.com> 1142878502 -0500");
70  
71  	ObjectId treeId = ObjectId
72  			.fromString("f51de923607cd51cf872b928a6b523ba823f7f35");
73  
74  	ObjectId treeId1 = ObjectId
75  			.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
76  
77  	final ObjectId treeId2 = ObjectId
78  			.fromString("617601c79811cbbae338512798318b4e5b70c9ac");
79  
80  	ObjectId parentId = ObjectId
81  			.fromString("91fea719aaf9447feb9580477eb3dd08b62b5eca");
82  
83  	ObjectId parentId1 = null;
84  
85  	final ObjectId parentId2 = ObjectId
86  			.fromString("485c91e0600b165c301c278bfbae3e492413980c");
87  
88  	MockSystemReader mockSystemReader = new MockSystemReader();
89  
90  	final long when = mockSystemReader.getCurrentTime();
91  
92  	final int tz = new MockSystemReader().getTimezone(when);
93  
94  	PersonIdent author = new PersonIdent("J. Author", "ja@example.com");
95  	{
96  		author = new PersonIdent(author, when, tz);
97  	}
98  
99  	PersonIdent committer = new PersonIdent("J. Committer", "jc@example.com");
100 	{
101 		committer = new PersonIdent(committer, when, tz);
102 	}
103 
104 	@Test
105 	public void testClean() {
106 		assertEquals("hej", ChangeIdUtil.clean("hej\n\n"));
107 		assertEquals("hej\n\nsan", ChangeIdUtil.clean("hej\n\nsan\n\n"));
108 		assertEquals("hej\nsan", ChangeIdUtil.clean("hej\n#men\nsan\n\n#men"));
109 		assertEquals("hej\nsan", ChangeIdUtil.clean("hej\nsan\n\n#men"));
110 		assertEquals("hej\nsan", ChangeIdUtil.clean("#no\nhej\nsan\n\n#men"));
111 		assertEquals("hej\nsan", ChangeIdUtil
112 				.clean("#no\nhej\nsan\nSigned-off-by: me \n#men"));
113 	}
114 
115 	@Test
116 	public void testId() throws IOException {
117 		String msg = "A\nMessage\n";
118 		ObjectId id = ChangeIdUtil.computeChangeId(treeId, parentId, p, q, msg);
119 		assertEquals("73f3751208ac92cbb76f9a26ac4a0d9d472e381b", ObjectId
120 				.toString(id));
121 	}
122 
123 	@Test
124 	public void testHasChangeid() throws Exception {
125 		assertEquals(
126 				"has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
127 						+ "Change-Id: I0123456789012345678901234567890123456789\n",
128 				call("has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
129 						+ "Change-Id: I0123456789012345678901234567890123456789\n"));
130 	}
131 
132 	@Test
133 	public void testHasChangeidWithReplacement() throws Exception {
134 		assertEquals(
135 				"has changeid\nmore text\n\nSigned-off-by: me@you.too\n"
136 						+ "Change-Id: I2178563fada5edb2c99a8d8c0d619471b050ec24\nBug: 33\n",
137 				call("has changeid\nmore text\n\nSigned-off-by: me@you.too\n"
138 						+ "Change-Id: I0123456789012345678901234567890123456789\nBug: 33\n",
139 						true));
140 	}
141 
142 	@Test
143 	public void testHasChangeidWithReplacementInLastLine() throws Exception {
144 		assertEquals(
145 				"has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
146 						+ "Change-Id: I1d6578f4c96e3db4dd707705fe3d17bf658c4758\n",
147 				call("has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
148 						+ "Change-Id: I0123456789012345678901234567890123456789\n",
149 						true));
150 	}
151 
152 	@Test
153 	public void testHasChangeidWithReplacementInLastLineNoLineBreak()
154 			throws Exception {
155 		assertEquals(
156 				"has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
157 						+ "Change-Id: I1d6578f4c96e3db4dd707705fe3d17bf658c4758",
158 				call("has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
159 						+ "Change-Id: I0123456789012345678901234567890123456789",
160 						true));
161 	}
162 
163 	@Test
164 	public void testHasChangeidWithSpacesBeforeId() throws Exception {
165 		assertEquals(
166 				"has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
167 						+ "Change-Id: Ie7575eaf450fdd0002df2e642426faf251de3ad9\n",
168 				call("has changeid\nmore text\n\nBug: 33\nSigned-off-by: me@you.too\n"
169 						+ "Change-Id:    I0123456789012345678901234567890123456789\n",
170 						true));
171 	}
172 
173 	@Test
174 	public void testHasChangeidWithReplacementWithChangeIdInCommitMessage()
175 			throws Exception {
176 		assertEquals(
177 				"has changeid\nmore text\n"
178 						+ "Change-Id: I0123456789012345678901234567890123456789\n\n"
179 						+ "Bug: 33\nSigned-off-by: me@you.too\n"
180 						+ "Change-Id: Ie48d10d59ef67995ca89688ac0171b88f10dd520\n",
181 				call("has changeid\nmore text\n"
182 						+ "Change-Id: I0123456789012345678901234567890123456789\n\n"
183 						+ "Bug: 33\nSigned-off-by: me@you.too\n"
184 						+ "Change-Id: I0123456789012345678901234567890123456789\n",
185 						true));
186 	}
187 
188 	@Test
189 	public void testOneliner() throws Exception {
190 		assertEquals(
191 				"oneliner\n\nChange-Id: I3a98091ce4470de88d52ae317fcd297e2339f063\n",
192 				call("oneliner\n"));
193 	}
194 
195 	@Test
196 	public void testOnelinerFollowedByBlank() throws Exception {
197 		assertEquals(
198 				"oneliner followed by blank\n\nChange-Id: I3a12c21ef342a18498f95c62efbc186cd782b743\n",
199 				call("oneliner followed by blank\n"));
200 	}
201 
202 	@Test
203 	public void testATwoLines() throws Exception {
204 		assertEquals(
205 				"a two lines\nwith text withour break after subject line\n\nChange-Id: I549a0fed3d69b7876c54b4f5a35637135fd43fac\n",
206 				call("a two lines\nwith text withour break after subject line\n"));
207 	}
208 
209 	@Test
210 	public void testRegularCommit() throws Exception {
211 		assertEquals(
212 				"regular commit\n\nwith header and body\n\nChange-Id: I62d8749d3c3a888c11e3fadc3924220a19389766\n",
213 				call("regular commit\n\nwith header and body\n"));
214 	}
215 
216 	@Test
217 	public void testRegularCommitWithSob_ButNoBody() throws Exception {
218 		assertEquals(
219 				"regular commit with sob, but no body\n\nChange-Id: I0f0b4307e9944ecbd5a9f6b9489e25cfaede43c4\nSigned-off-by: me@you.too\n",
220 				call("regular commit with sob, but no body\n\nSigned-off-by: me@you.too\n"));
221 	}
222 
223 	@Test
224 	public void testACommitWithBug_SubButNoBody() throws Exception {
225 		assertEquals(
226 				"a commit with bug, sub but no body\n\nBug: 33\nChange-Id: I337e264868613dab6d1e11a34f394db369487412\nSigned-off-by: me@you.too\n",
227 				call("a commit with bug, sub but no body\n\nBug: 33\nSigned-off-by: me@you.too\n"));
228 	}
229 
230 	@Test
231 	public void testACommitWithSubject_NoBodySobAndBug() throws Exception {
232 		assertEquals(
233 				"a commit with subject, no body sob and bug\n\nChange-Id: Ib3616d4bf77707a3215a6cb0602c004ee119a445\nSigned-off-by: me@you.too\nBug: 33\n",
234 				call("a commit with subject, no body sob and bug\n\nSigned-off-by: me@you.too\nBug: 33\n"));
235 	}
236 
237 	@Test
238 	public void testACommitWithSubjectBug_NonFooterLineAndSob()
239 			throws Exception {
240 		assertEquals(
241 				"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",
242 				call("a commit with subject bug, non-footer line and sob\n\nBug: 33\nmore text\nSigned-off-by: me@you.too\n"));
243 	}
244 
245 	@Test
246 	public void testACommitWithSubject_NonFooterAndBugAndSob() throws Exception {
247 		assertEquals(
248 				"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",
249 				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"));
250 	}
251 
252 	@Test
253 	public void testACommitWithSubjectBodyBugBrackersAndSob() throws Exception {
254 		assertEquals(
255 				"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",
256 				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"));
257 	}
258 
259 	@Test
260 	public void testACommitWithSubjectBodyBugLineWithASpaceAndSob()
261 			throws Exception {
262 		assertEquals(
263 				"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",
264 				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"));
265 	}
266 
267 	@Test
268 	public void testACommitWithSubjectBodyBugEmptyLineAndSob() throws Exception {
269 		assertEquals(
270 				"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",
271 				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"));
272 	}
273 
274 	@Test
275 	public void testEmptyMessages() throws Exception {
276 		// Empty input must not produce a change id.
277 		hookDoesNotModify("");
278 		hookDoesNotModify(" ");
279 		hookDoesNotModify("\n");
280 		hookDoesNotModify("\n\n");
281 		hookDoesNotModify("  \n  ");
282 
283 		hookDoesNotModify("#");
284 		hookDoesNotModify("#\n");
285 		hookDoesNotModify("# on branch master\n# Untracked files:\n");
286 		hookDoesNotModify("\n# on branch master\n# Untracked files:\n");
287 		hookDoesNotModify("\n\n# on branch master\n# Untracked files:\n");
288 
289 		hookDoesNotModify("\n# on branch master\ndiff --git a/src b/src\n"
290 				+ "new file mode 100644\nindex 0000000..c78b7f0\n");
291 	}
292 
293 	@Test
294 	public void testChangeIdAlreadySet() throws Exception {
295 		// If a Change-Id is already present in the footer, the hook must
296 		// not modify the message but instead must leave the identity alone.
297 		//
298 		hookDoesNotModify("a\n" + //
299 				"\n" + //
300 				"Change-Id: Iaeac9b4149291060228ef0154db2985a31111335\n");
301 		hookDoesNotModify("fix: this thing\n" + //
302 				"\n" + //
303 				"Change-Id: I388bdaf52ed05b55e62a22d0a20d2c1ae0d33e7e\n");
304 		hookDoesNotModify("fix-a-widget: this thing\n" + //
305 				"\n" + //
306 				"Change-Id: Id3bc5359d768a6400450283e12bdfb6cd135ea4b\n");
307 		hookDoesNotModify("FIX: this thing\n" + //
308 				"\n" + //
309 				"Change-Id: I1b55098b5a2cce0b3f3da783dda50d5f79f873fa\n");
310 		hookDoesNotModify("Fix-A-Widget: this thing\n" + //
311 				"\n" + //
312 				"Change-Id: I4f4e2e1e8568ddc1509baecb8c1270a1fb4b6da7\n");
313 	}
314 
315 	@Test
316 	public void testChangeIdAlreadySetWithReplacement() throws Exception {
317 		// If a Change-Id is already present in the footer, the hook
318 		// replaces the Change-Id with the new value..
319 		//
320 		assertEquals("a\n" + //
321 				"\n" + //
322 				"Change-Id: Ifa324efa85bfb3c8696a46a0f67fa70c35be5f5f\n",
323 				call("a\n" + //
324 						"\n" + //
325 						"Change-Id: Iaeac9b4149291060228ef0154db2985a31111335\n",
326 						true));
327 		assertEquals("fix: this thing\n" + //
328 				"\n" + //
329 				"Change-Id: Ib63e4990a06412a3f24bd93bb160e98ac1bd412b\n",
330 				call("fix: this thing\n" + //
331 						"\n" + //
332 						"Change-Id: I388bdaf52ed05b55e62a22d0a20d2c1ae0d33e7e\n",
333 						true));
334 		assertEquals("fix-a-widget: this thing\n" + //
335 				"\n" + //
336 				"Change-Id: If0444e4d0cabcf41b3d3b46b7e9a7a64a82117af\n",
337 				call("fix-a-widget: this thing\n" + //
338 						"\n" + //
339 						"Change-Id: Id3bc5359d768a6400450283e12bdfb6cd135ea4b\n",
340 						true));
341 		assertEquals("FIX: this thing\n" + //
342 				"\n" + //
343 				"Change-Id: Iba5a3b2d5e5df46448f6daf362b6bfa775c6491d\n",
344 				call("FIX: this thing\n" + //
345 						"\n" + //
346 						"Change-Id: I1b55098b5a2cce0b3f3da783dda50d5f79f873fa\n",
347 						true));
348 		assertEquals("Fix-A-Widget: this thing\n" + //
349 				"\n" + //
350 				"Change-Id: I2573d47c62c42429fbe424d70cfba931f8f87848\n",
351 				call("Fix-A-Widget: this thing\n" + //
352 				"\n" + //
353 				"Change-Id: I4f4e2e1e8568ddc1509baecb8c1270a1fb4b6da7\n",
354 				true));
355 	}
356 
357 	@Test
358 	public void testTimeAltersId() throws Exception {
359 		assertEquals("a\n" + //
360 				"\n" + //
361 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",//
362 				call("a\n"));
363 
364 		tick();
365 		assertEquals("a\n" + //
366 				"\n" + //
367 				"Change-Id: I3251906b99dda598a58a6346d8126237ee1ea800\n",//
368 				call("a\n"));
369 
370 		tick();
371 		assertEquals("a\n" + //
372 				"\n" + //
373 				"Change-Id: I69adf9208d828f41a3d7e41afbca63aff37c0c5c\n",//
374 				call("a\n"));
375 	}
376 
377 	/** Increment the {@link #author} and {@link #committer} times. */
378 	protected void tick() {
379 		final long delta = TimeUnit.MILLISECONDS.convert(5 * 60,
380 				TimeUnit.SECONDS);
381 		final long now = author.getWhen().getTime() + delta;
382 
383 		author = new PersonIdent(author, now, tz);
384 		committer = new PersonIdent(committer, now, tz);
385 	}
386 
387 	@Test
388 	public void testFirstParentAltersId() throws Exception {
389 		assertEquals("a\n" + //
390 				"\n" + //
391 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",//
392 				call("a\n"));
393 
394 		parentId1 = parentId2;
395 		assertEquals("a\n" + //
396 				"\n" + //
397 				"Change-Id: I51e86482bde7f92028541aaf724d3a3f996e7ea2\n",//
398 				call("a\n"));
399 	}
400 
401 	@Test
402 	public void testDirCacheAltersId() throws Exception {
403 		assertEquals("a\n" + //
404 				"\n" + //
405 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",//
406 				call("a\n"));
407 
408 		treeId1 = treeId2;
409 		assertEquals("a\n" + //
410 				"\n" + //
411 				"Change-Id: If56597ea9759f23b070677ea6f064c60c38da631\n",//
412 				call("a\n"));
413 	}
414 
415 	@Test
416 	public void testSingleLineMessages() throws Exception {
417 		assertEquals("a\n" + //
418 				"\n" + //
419 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",//
420 				call("a\n"));
421 
422 		assertEquals("fix: this thing\n" + //
423 				"\n" + //
424 				"Change-Id: I0f13d0e6c739ca3ae399a05a93792e80feb97f37\n",//
425 				call("fix: this thing\n"));
426 		assertEquals("fix-a-widget: this thing\n" + //
427 				"\n" + //
428 				"Change-Id: I1a1a0c751e4273d532e4046a501a612b9b8a775e\n",//
429 				call("fix-a-widget: this thing\n"));
430 
431 		assertEquals("FIX: this thing\n" + //
432 				"\n" + //
433 				"Change-Id: If816d944c57d3893b60cf10c65931fead1290d97\n",//
434 				call("FIX: this thing\n"));
435 		assertEquals("Fix-A-Widget: this thing\n" + //
436 				"\n" + //
437 				"Change-Id: I3e18d00cbda2ba1f73aeb63ed8c7d57d7fd16c76\n",//
438 				call("Fix-A-Widget: this thing\n"));
439 	}
440 
441 	@Test
442 	public void testMultiLineMessagesWithoutFooter() throws Exception {
443 		assertEquals("a\n" + //
444 				"\n" + //
445 				"b\n" + //
446 				"\n" + //
447 				"Change-Id: Id0b4f42d3d6fc1569595c9b97cb665e738486f5d\n",//
448 				call("a\n" + "\n" + "b\n"));
449 
450 		assertEquals("a\n" + //
451 				"\n" + //
452 				"b\nc\nd\ne\n" + //
453 				"\n" + //
454 				"Change-Id: I7d237b20058a0f46cc3f5fabc4a0476877289d75\n",//
455 				call("a\n" + "\n" + "b\nc\nd\ne\n"));
456 
457 		assertEquals("a\n" + //
458 				"\n" + //
459 				"b\nc\nd\ne\n" + //
460 				"\n" + //
461 				"f\ng\nh\n" + //
462 				"\n" + //
463 				"Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n",//
464 				call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n"));
465 	}
466 
467 	@Test
468 	public void testSingleLineMessagesWithSignedOffBy() throws Exception {
469 		assertEquals("a\n" + //
470 				"\n" + //
471 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + //
472 				SOB1,//
473 				call("a\n" + "\n" + SOB1));
474 
475 		assertEquals("a\n" + //
476 				"\n" + //
477 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + //
478 				SOB1 + //
479 				SOB2,//
480 				call("a\n" + "\n" + SOB1 + SOB2));
481 	}
482 
483 	@Test
484 	public void testMultiLineMessagesWithSignedOffBy() throws Exception {
485 		assertEquals("a\n" + //
486 				"\n" + //
487 				"b\nc\nd\ne\n" + //
488 				"\n" + //
489 				"f\ng\nh\n" + //
490 				"\n" + //
491 				"Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n" + //
492 				SOB1,//
493 				call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n" + "\n"
494 						+ SOB1));
495 
496 		assertEquals("a\n" + //
497 				"\n" + //
498 				"b\nc\nd\ne\n" + //
499 				"\n" + //
500 				"f\ng\nh\n" + //
501 				"\n" + //
502 				"Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n" + //
503 				SOB1 + //
504 				SOB2,//
505 				call("a\n" + //
506 						"\n" + //
507 						"b\nc\nd\ne\n" + //
508 						"\n" + //
509 						"f\ng\nh\n" + //
510 						"\n" + //
511 						SOB1 + //
512 						SOB2));
513 
514 		assertEquals("a\n" + //
515 				"\n" + //
516 				"b: not a footer\nc\nd\ne\n" + //
517 				"\n" + //
518 				"f\ng\nh\n" + //
519 				"\n" + //
520 				"Change-Id: I8869aabd44b3017cd55d2d7e0d546a03e3931ee2\n" + //
521 				SOB1 + //
522 				SOB2,//
523 				call("a\n" + //
524 						"\n" + //
525 						"b: not a footer\nc\nd\ne\n" + //
526 						"\n" + //
527 						"f\ng\nh\n" + //
528 						"\n" + //
529 						SOB1 + //
530 						SOB2));
531 	}
532 
533 	@Test
534 	public void testNoteInMiddle() throws Exception {
535 		assertEquals("a\n" + //
536 				"\n" + //
537 				"NOTE: This\n" + //
538 				"does not fix it.\n" + //
539 				"\n" + //
540 				"Change-Id: I988a127969a6ee5e58db546aab74fc46e66847f8\n", //
541 				call("a\n" + //
542 						"\n" + //
543 						"NOTE: This\n" + //
544 						"does not fix it.\n"));
545 	}
546 
547 	@Test
548 	public void testKernelStyleFooter() throws Exception {
549 		assertEquals("a\n" + //
550 				"\n" + //
551 				"Change-Id: I1bd787f9e7590a2ac82b02c404c955ffb21877c4\n" + //
552 				SOB1 + //
553 				"[ja: Fixed\n" + //
554 				"     the indentation]\n" + //
555 				SOB2, //
556 				call("a\n" + //
557 						"\n" + //
558 						SOB1 + //
559 						"[ja: Fixed\n" + //
560 						"     the indentation]\n" + //
561 						SOB2));
562 	}
563 
564 	@Test
565 	public void testChangeIdAfterBugOrIssue() throws Exception {
566 		assertEquals("a\n" + //
567 				"\n" + //
568 				"Bug: 42\n" + //
569 				"Change-Id: I8c0321227c4324e670b9ae8cf40eccc87af21b1b\n" + //
570 				SOB1,//
571 				call("a\n" + //
572 						"\n" + //
573 						"Bug: 42\n" + //
574 						SOB1));
575 
576 		assertEquals("a\n" + //
577 				"\n" + //
578 				"Issue: 42\n" + //
579 				"Change-Id: Ie66e07d89ae5b114c0975b49cf326e90331dd822\n" + //
580 				SOB1,//
581 				call("a\n" + //
582 						"\n" + //
583 						"Issue: 42\n" + //
584 						SOB1));
585 	}
586 
587 	public void notestCommitDashV() throws Exception {
588 		assertEquals("a\n" + //
589 				"\n" + //
590 				"Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n" + //
591 				SOB1 + //
592 				SOB2, //
593 				call("a\n" + //
594 						"\n" + //
595 						SOB1 + //
596 						SOB2 + //
597 						"\n" + //
598 						"# on branch master\n" + //
599 						"diff --git a/src b/src\n" + //
600 						"new file mode 100644\n" + //
601 						"index 0000000..c78b7f0\n"));
602 	}
603 
604 	@Test
605 	public void testWithEndingURL() throws Exception {
606 		assertEquals("a\n" + //
607 				"\n" + //
608 				"http://example.com/ fixes this\n" + //
609 				"\n" + //
610 				"Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n", //
611 				call("a\n" + //
612 						"\n" + //
613 						"http://example.com/ fixes this\n"));
614 		assertEquals("a\n" + //
615 				"\n" + //
616 				"https://example.com/ fixes this\n" + //
617 				"\n" + //
618 				"Change-Id: I62b9039e2fc0dce274af55e8f99312a8a80a805d\n", //
619 				call("a\n" + //
620 						"\n" + //
621 						"https://example.com/ fixes this\n"));
622 		assertEquals("a\n" + //
623 				"\n" + //
624 				"ftp://example.com/ fixes this\n" + //
625 				"\n" + //
626 				"Change-Id: I71b05dc1f6b9a5540a53a693e64d58b65a8910e8\n", //
627 				call("a\n" + //
628 						"\n" + //
629 						"ftp://example.com/ fixes this\n"));
630 		assertEquals("a\n" + //
631 				"\n" + //
632 				"git://example.com/ fixes this\n" + //
633 				"\n" + //
634 				"Change-Id: Id34e942baa68d790633737d815ddf11bac9183e5\n", //
635 				call("a\n" + //
636 						"\n" + //
637 						"git://example.com/ fixes this\n"));
638 	}
639 
640 	@Test
641 	public void testIndexOfChangeId() {
642 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("", "\n"));
643 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("\n", "\n"));
644 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("\r\n", "\r\n"));
645 
646 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
647 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
648 				"\n"));
649 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
650 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n\n\n",
651 				"\n"));
652 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
653 										+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n \n \n",
654 								"\n"));
655 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
656 				+ "Change-Id:  I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
657 				"\n"));
658 
659 		// leading whitespace is rejected by Gerrit
660 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
661 				+ " Change-Id:  I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
662 				"\n"));
663 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
664 				+ "\t Change-Id:  I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
665 				"\n"));
666 
667 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
668 				+ "Change-Id: \n", "\n"));
669 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
670 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701 \n",
671 				"\n"));
672 		assertEquals(12, ChangeIdUtil.indexOfChangeId("x\n" + "\n"
673 				+ "Bug 4711\n"
674 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
675 				"\n"));
676 		assertEquals(56, ChangeIdUtil.indexOfChangeId("x\n"
677 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n"
678 				+ "\n"
679 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
680 				"\n"));
681 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("x\n"
682 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n"
683 				+ "\n" + "x\n", "\n"));
684 		assertEquals(-1, ChangeIdUtil.indexOfChangeId("x\n\n"
685 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n"
686 				+ "\n" + "x\n", "\n"));
687 		assertEquals(5, ChangeIdUtil.indexOfChangeId("x\r\n" + "\r\n"
688 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\r\n",
689 				"\r\n"));
690 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\r" + "\r"
691 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\r",
692 				"\r"));
693 		assertEquals(3, ChangeIdUtil.indexOfChangeId("x\r" + "\r"
694 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\r",
695 				"\r"));
696 		assertEquals(8, ChangeIdUtil.indexOfChangeId("x\ny\n\nz\n" + "\n"
697 				+ "Change-Id: I3b7e4e16b503ce00f07ba6ad01d97a356dad7701\n",
698 				"\n"));
699 	}
700 
701 	private void hookDoesNotModify(final String in) throws Exception {
702 		assertEquals(in, call(in));
703 	}
704 
705 	private String call(final String body) throws Exception {
706 		return call(body, false);
707 	}
708 
709 	private String call(final String body, boolean replaceExisting) throws Exception {
710 		ObjectId computeChangeId = ChangeIdUtil.computeChangeId(treeId1,
711 				parentId1, author, committer, body);
712 		if (computeChangeId == null)
713 			return body;
714 		return ChangeIdUtil.insertId(body, computeChangeId, replaceExisting);
715 	}
716 
717 }