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