View Javadoc
1   /*
2    * Copyright (C) 2017, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.storage.reftable;
12  
13  import static org.eclipse.jgit.lib.Constants.HEAD;
14  import static org.eclipse.jgit.lib.Constants.MASTER;
15  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
16  import static org.eclipse.jgit.lib.Constants.R_HEADS;
17  import static org.eclipse.jgit.lib.Ref.Storage.NEW;
18  import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
19  import static org.junit.Assert.assertEquals;
20  import static org.junit.Assert.assertFalse;
21  import static org.junit.Assert.assertNull;
22  import static org.junit.Assert.assertTrue;
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.eclipse.jgit.internal.storage.io.BlockSource;
35  import org.eclipse.jgit.lib.ObjectId;
36  import org.eclipse.jgit.lib.ObjectIdRef;
37  import org.eclipse.jgit.lib.Ref;
38  import org.eclipse.jgit.lib.RefComparator;
39  import org.eclipse.jgit.lib.SymbolicRef;
40  import org.junit.Test;
41  
42  public class MergedReftableTest {
43  	@Test
44  	public void noTables() throws IOException {
45  		MergedReftable mr = merge(new byte[0][]);
46  		try (RefCursor rc = mr.allRefs()) {
47  			assertFalse(rc.next());
48  		}
49  		try (RefCursor rc = mr.seekRef(HEAD)) {
50  			assertFalse(rc.next());
51  		}
52  		try (RefCursor rc = mr.seekRefsWithPrefix(R_HEADS)) {
53  			assertFalse(rc.next());
54  		}
55  	}
56  
57  	@Test
58  	public void oneEmptyTable() throws IOException {
59  		MergedReftable mr = merge(write());
60  		try (RefCursor rc = mr.allRefs()) {
61  			assertFalse(rc.next());
62  		}
63  		try (RefCursor rc = mr.seekRef(HEAD)) {
64  			assertFalse(rc.next());
65  		}
66  		try (RefCursor rc = mr.seekRefsWithPrefix(R_HEADS)) {
67  			assertFalse(rc.next());
68  		}
69  	}
70  
71  	@Test
72  	public void twoEmptyTables() throws IOException {
73  		MergedReftable mr = merge(write(), write());
74  		try (RefCursor rc = mr.allRefs()) {
75  			assertFalse(rc.next());
76  		}
77  		try (RefCursor rc = mr.seekRef(HEAD)) {
78  			assertFalse(rc.next());
79  		}
80  		try (RefCursor rc = mr.seekRefsWithPrefix(R_HEADS)) {
81  			assertFalse(rc.next());
82  		}
83  	}
84  
85  	@SuppressWarnings("boxing")
86  	@Test
87  	public void oneTableScan() throws IOException {
88  		List<Ref> refs = new ArrayList<>();
89  		for (int i = 1; i <= 567; i++) {
90  			refs.add(ref(String.format("refs/heads/%03d", i), i));
91  		}
92  
93  		MergedReftable mr = merge(write(refs));
94  		try (RefCursor rc = mr.allRefs()) {
95  			for (Ref exp : refs) {
96  				assertTrue("has " + exp.getName(), rc.next());
97  				Ref act = rc.getRef();
98  				assertEquals(exp.getName(), act.getName());
99  				assertEquals(exp.getObjectId(), act.getObjectId());
100 				assertEquals(1, act.getUpdateIndex());
101 			}
102 			assertFalse(rc.next());
103 		}
104 	}
105 
106 	@Test
107 	public void deleteIsHidden() throws IOException {
108 		List<Ref> delta1 = Arrays.asList(
109 				ref("refs/heads/apple", 1),
110 				ref("refs/heads/master", 2));
111 		List<Ref> delta2 = Arrays.asList(delete("refs/heads/apple"));
112 
113 		MergedReftable mr = merge(write(delta1), write(delta2));
114 		try (RefCursor rc = mr.allRefs()) {
115 			assertTrue(rc.next());
116 			assertEquals("refs/heads/master", rc.getRef().getName());
117 			assertEquals(id(2), rc.getRef().getObjectId());
118 			assertEquals(1, rc.getRef().getUpdateIndex());
119 			assertFalse(rc.next());
120 		}
121 	}
122 
123 	@Test
124 	public void twoTableSeek() throws IOException {
125 		List<Ref> delta1 = Arrays.asList(
126 				ref("refs/heads/apple", 1),
127 				ref("refs/heads/master", 2));
128 		List<Ref> delta2 = Arrays.asList(ref("refs/heads/banana", 3));
129 
130 		MergedReftable mr = merge(write(delta1), write(delta2));
131 		try (RefCursor rc = mr.seekRef("refs/heads/master")) {
132 			assertTrue(rc.next());
133 			assertEquals("refs/heads/master", rc.getRef().getName());
134 			assertEquals(id(2), rc.getRef().getObjectId());
135 			assertFalse(rc.next());
136 			assertEquals(1, rc.getRef().getUpdateIndex());
137 		}
138 	}
139 
140 	@Test
141 	public void twoTableSeekPastWithRefCursor() throws IOException {
142 		List<Ref> delta1 = Arrays.asList(
143 				ref("refs/heads/apple", 1),
144 				ref("refs/heads/master", 2));
145 		List<Ref> delta2 = Arrays.asList(
146 				ref("refs/heads/banana", 3),
147 				ref("refs/heads/zzlast", 4));
148 
149 		MergedReftable mr = merge(write(delta1), write(delta2));
150 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
151 			assertTrue(rc.next());
152 			assertEquals("refs/heads/apple", rc.getRef().getName());
153 			assertEquals(id(1), rc.getRef().getObjectId());
154 
155 			rc.seekPastPrefix("refs/heads/banana/");
156 
157 			assertTrue(rc.next());
158 			assertEquals("refs/heads/master", rc.getRef().getName());
159 			assertEquals(id(2), rc.getRef().getObjectId());
160 
161 			assertTrue(rc.next());
162 			assertEquals("refs/heads/zzlast", rc.getRef().getName());
163 			assertEquals(id(4), rc.getRef().getObjectId());
164 
165 			assertEquals(1, rc.getRef().getUpdateIndex());
166 		}
167 	}
168 
169 	@Test
170 	public void oneTableSeekPastWithRefCursor() throws IOException {
171 		List<Ref> delta1 = Arrays.asList(
172 				ref("refs/heads/apple", 1),
173 				ref("refs/heads/master", 2));
174 
175 		MergedReftable mr = merge(write(delta1));
176 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
177 			rc.seekPastPrefix("refs/heads/apple");
178 
179 			assertTrue(rc.next());
180 			assertEquals("refs/heads/master", rc.getRef().getName());
181 			assertEquals(id(2), rc.getRef().getObjectId());
182 
183 			assertEquals(1, rc.getRef().getUpdateIndex());
184 		}
185 	}
186 
187 	@Test
188 	public void seekPastToNonExistentPrefixToTheMiddle() throws IOException {
189 		List<Ref> delta1 = Arrays.asList(
190 				ref("refs/heads/apple", 1),
191 				ref("refs/heads/master", 2));
192 		List<Ref> delta2 = Arrays.asList(
193 				ref("refs/heads/banana", 3),
194 				ref("refs/heads/zzlast", 4));
195 
196 		MergedReftable mr = merge(write(delta1), write(delta2));
197 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
198 			rc.seekPastPrefix("refs/heads/x");
199 
200 			assertTrue(rc.next());
201 			assertEquals("refs/heads/zzlast", rc.getRef().getName());
202 			assertEquals(id(4), rc.getRef().getObjectId());
203 
204 			assertEquals(1, rc.getRef().getUpdateIndex());
205 		}
206 	}
207 
208 	@Test
209 	public void seekPastToNonExistentPrefixToTheEnd() throws IOException {
210 		List<Ref> delta1 = Arrays.asList(
211 				ref("refs/heads/apple", 1),
212 				ref("refs/heads/master", 2));
213 		List<Ref> delta2 = Arrays.asList(
214 				ref("refs/heads/banana", 3),
215 				ref("refs/heads/zzlast", 4));
216 
217 		MergedReftable mr = merge(write(delta1), write(delta2));
218 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
219 			rc.seekPastPrefix("refs/heads/zzz");
220 			assertFalse(rc.next());
221 		}
222 	}
223 
224 	@Test
225 	public void seekPastManyTimes() throws IOException {
226 		List<Ref> delta1 = Arrays.asList(
227 				ref("refs/heads/apple", 1),
228 				ref("refs/heads/master", 2));
229 		List<Ref> delta2 = Arrays.asList(
230 				ref("refs/heads/banana", 3),
231 				ref("refs/heads/zzlast", 4));
232 
233 		MergedReftable mr = merge(write(delta1), write(delta2));
234 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
235 			rc.seekPastPrefix("refs/heads/apple");
236 			rc.seekPastPrefix("refs/heads/banana");
237 			rc.seekPastPrefix("refs/heads/master");
238 			rc.seekPastPrefix("refs/heads/zzlast");
239 			assertFalse(rc.next());
240 		}
241 	}
242 
243 	@Test
244 	public void seekPastOnEmptyTable() throws IOException {
245 		MergedReftable mr = merge(write(), write());
246 		try (RefCursor rc = mr.seekRefsWithPrefix("")) {
247 			rc.seekPastPrefix("refs/");
248 			assertFalse(rc.next());
249 		}
250 	}
251 
252 	@Test
253 	public void twoTableById() throws IOException {
254 		List<Ref> delta1 = Arrays.asList(
255 				ref("refs/heads/apple", 1),
256 				ref("refs/heads/master", 2));
257 		List<Ref> delta2 = Arrays.asList(ref("refs/heads/banana", 3));
258 
259 		MergedReftable mr = merge(write(delta1), write(delta2));
260 		try (RefCursor rc = mr.byObjectId(id(2))) {
261 			assertTrue(rc.next());
262 			assertEquals("refs/heads/master", rc.getRef().getName());
263 			assertEquals(id(2), rc.getRef().getObjectId());
264 			assertEquals(1, rc.getRef().getUpdateIndex());
265 			assertFalse(rc.next());
266 		}
267 	}
268 
269 	@Test
270 	public void tableByIDDeletion() throws IOException {
271 		List<Ref> delta1 = Arrays.asList(
272 				ref("refs/heads/apple", 1),
273 				ref("refs/heads/master", 2));
274 		List<Ref> delta2 = Arrays.asList(ref("refs/heads/master", 3));
275 
276 		MergedReftable mr = merge(write(delta1), write(delta2));
277 		try (RefCursor rc = mr.byObjectId(id(2))) {
278 			assertFalse(rc.next());
279 		}
280 	}
281 
282 	@SuppressWarnings("boxing")
283 	@Test
284 	public void fourTableScan() throws IOException {
285 		List<Ref> base = new ArrayList<>();
286 		for (int i = 1; i <= 567; i++) {
287 			base.add(ref(String.format("refs/heads/%03d", i), i));
288 		}
289 
290 		List<Ref> delta1 = Arrays.asList(
291 				ref("refs/heads/next", 4),
292 				ref(String.format("refs/heads/%03d", 55), 4096));
293 		List<Ref> delta2 = Arrays.asList(
294 				delete("refs/heads/next"),
295 				ref(String.format("refs/heads/%03d", 55), 8192));
296 		List<Ref> delta3 = Arrays.asList(
297 				ref("refs/heads/master", 4242),
298 				ref(String.format("refs/heads/%03d", 42), 5120),
299 				ref(String.format("refs/heads/%03d", 98), 6120));
300 
301 		List<Ref> expected = merge(base, delta1, delta2, delta3);
302 		MergedReftable mr = merge(
303 				write(base),
304 				write(delta1),
305 				write(delta2),
306 				write(delta3));
307 		try (RefCursor rc = mr.allRefs()) {
308 			for (Ref exp : expected) {
309 				assertTrue("has " + exp.getName(), rc.next());
310 				Ref act = rc.getRef();
311 				assertEquals(exp.getName(), act.getName());
312 				assertEquals(exp.getObjectId(), act.getObjectId());
313 				assertEquals(1, rc.getRef().getUpdateIndex());
314 			}
315 			assertFalse(rc.next());
316 		}
317 	}
318 
319 	@Test
320 	public void scanIncludeDeletes() throws IOException {
321 		List<Ref> delta1 = Arrays.asList(ref("refs/heads/next", 4));
322 		List<Ref> delta2 = Arrays.asList(delete("refs/heads/next"));
323 		List<Ref> delta3 = Arrays.asList(ref("refs/heads/master", 8));
324 
325 		MergedReftable mr = merge(write(delta1), write(delta2), write(delta3));
326 		mr.setIncludeDeletes(true);
327 		try (RefCursor rc = mr.allRefs()) {
328 			assertTrue(rc.next());
329 			Ref r = rc.getRef();
330 			assertEquals("refs/heads/master", r.getName());
331 			assertEquals(id(8), r.getObjectId());
332 			assertEquals(1, rc.getRef().getUpdateIndex());
333 
334 			assertTrue(rc.next());
335 			r = rc.getRef();
336 			assertEquals("refs/heads/next", r.getName());
337 			assertEquals(NEW, r.getStorage());
338 			assertNull(r.getObjectId());
339 			assertEquals(1, rc.getRef().getUpdateIndex());
340 
341 			assertFalse(rc.next());
342 		}
343 	}
344 
345 	@SuppressWarnings("boxing")
346 	@Test
347 	public void oneTableSeek() throws IOException {
348 		List<Ref> refs = new ArrayList<>();
349 		for (int i = 1; i <= 567; i++) {
350 			refs.add(ref(String.format("refs/heads/%03d", i), i));
351 		}
352 
353 		MergedReftable mr = merge(write(refs));
354 		for (Ref exp : refs) {
355 			try (RefCursor rc = mr.seekRef(exp.getName())) {
356 				assertTrue("has " + exp.getName(), rc.next());
357 				Ref act = rc.getRef();
358 				assertEquals(exp.getName(), act.getName());
359 				assertEquals(exp.getObjectId(), act.getObjectId());
360 				assertEquals(1, act.getUpdateIndex());
361 				assertFalse(rc.next());
362 			}
363 		}
364 	}
365 
366 	@Test
367 	public void missedUpdate() throws IOException {
368 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
369 		ReftableWriter writer = new ReftableWriter(buf)
370 				.setMinUpdateIndex(1)
371 				.setMaxUpdateIndex(3)
372 				.begin();
373 		writer.writeRef(ref("refs/heads/a", 1), 1);
374 		writer.writeRef(ref("refs/heads/c", 3), 3);
375 		writer.finish();
376 		byte[] base = buf.toByteArray();
377 
378 		byte[] delta = write(Arrays.asList(
379 				ref("refs/heads/b", 2),
380 				ref("refs/heads/c", 4)),
381 				2);
382 		MergedReftable mr = merge(base, delta);
383 		try (RefCursor rc = mr.allRefs()) {
384 			assertTrue(rc.next());
385 			assertEquals("refs/heads/a", rc.getRef().getName());
386 			assertEquals(id(1), rc.getRef().getObjectId());
387 			assertEquals(1, rc.getRef().getUpdateIndex());
388 
389 			assertTrue(rc.next());
390 			assertEquals("refs/heads/b", rc.getRef().getName());
391 			assertEquals(id(2), rc.getRef().getObjectId());
392 			assertEquals(2, rc.getRef().getUpdateIndex());
393 
394 			assertTrue(rc.next());
395 			assertEquals("refs/heads/c", rc.getRef().getName());
396 			assertEquals(id(3), rc.getRef().getObjectId());
397 			assertEquals(3, rc.getRef().getUpdateIndex());
398 		}
399 	}
400 
401 	@Test
402 	public void nonOverlappedUpdateIndices() throws IOException {
403 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
404 		ReftableWriter writer = new ReftableWriter(buf)
405 				.setMinUpdateIndex(1)
406 				.setMaxUpdateIndex(2)
407 				.begin();
408 		writer.writeRef(ref("refs/heads/a", 1), 1);
409 		writer.writeRef(ref("refs/heads/b", 2), 2);
410 		writer.finish();
411 		byte[] base = buf.toByteArray();
412 
413 		buf = new ByteArrayOutputStream();
414 		writer = new ReftableWriter(buf)
415 				.setMinUpdateIndex(3)
416 				.setMaxUpdateIndex(4)
417 				.begin();
418 		writer.writeRef(ref("refs/heads/a", 10), 3);
419 		writer.writeRef(ref("refs/heads/b", 20), 4);
420 		writer.finish();
421 		byte[] delta = buf.toByteArray();
422 
423 		MergedReftable mr = merge(base, delta);
424 		assertEquals(1, mr.minUpdateIndex());
425 		assertEquals(4, mr.maxUpdateIndex());
426 
427 		try (RefCursor rc = mr.allRefs()) {
428 			assertTrue(rc.next());
429 			assertEquals("refs/heads/a", rc.getRef().getName());
430 			assertEquals(id(10), rc.getRef().getObjectId());
431 			assertEquals(3, rc.getRef().getUpdateIndex());
432 
433 			assertTrue(rc.next());
434 			assertEquals("refs/heads/b", rc.getRef().getName());
435 			assertEquals(id(20), rc.getRef().getObjectId());
436 			assertEquals(4, rc.getRef().getUpdateIndex());
437 		}
438 	}
439 
440 	@Test
441 	public void overlappedUpdateIndices() throws IOException {
442 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
443 		ReftableWriter writer = new ReftableWriter(buf)
444 				.setMinUpdateIndex(2)
445 				.setMaxUpdateIndex(4)
446 				.begin();
447 		writer.writeRef(ref("refs/heads/a", 10), 2);
448 		writer.writeRef(ref("refs/heads/b", 20), 4);
449 		writer.finish();
450 		byte[] base = buf.toByteArray();
451 
452 		buf = new ByteArrayOutputStream();
453 		writer = new ReftableWriter(buf)
454 				.setMinUpdateIndex(1)
455 				.setMaxUpdateIndex(3)
456 				.begin();
457 		writer.writeRef(ref("refs/heads/a", 1), 1);
458 		writer.writeRef(ref("refs/heads/b", 2), 3);
459 		writer.finish();
460 		byte[] delta = buf.toByteArray();
461 
462 		MergedReftable mr = merge(base, delta);
463 		assertEquals(1, mr.minUpdateIndex());
464 		assertEquals(4, mr.maxUpdateIndex());
465 
466 		try (RefCursor rc = mr.allRefs()) {
467 			assertTrue(rc.next());
468 			assertEquals("refs/heads/a", rc.getRef().getName());
469 			assertEquals(id(10), rc.getRef().getObjectId());
470 			assertEquals(2, rc.getRef().getUpdateIndex());
471 
472 			assertTrue(rc.next());
473 			assertEquals("refs/heads/b", rc.getRef().getName());
474 			assertEquals(id(20), rc.getRef().getObjectId());
475 			assertEquals(4, rc.getRef().getUpdateIndex());
476 		}
477 	}
478 
479 	@Test
480 	public void enclosedUpdateIndices() throws IOException {
481 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
482 		ReftableWriter writer = new ReftableWriter(buf)
483 				.setMinUpdateIndex(2)
484 				.setMaxUpdateIndex(3)
485 				.begin();
486 		writer.writeRef(ref("refs/heads/a", 10), 2);
487 		writer.writeRef(ref("refs/heads/b", 2), 3);
488 		writer.finish();
489 		byte[] base = buf.toByteArray();
490 
491 		buf = new ByteArrayOutputStream();
492 		writer = new ReftableWriter(buf)
493 				.setMinUpdateIndex(1)
494 				.setMaxUpdateIndex(4)
495 				.begin();
496 		writer.writeRef(ref("refs/heads/a", 1), 1);
497 		writer.writeRef(ref("refs/heads/b", 20), 4);
498 		writer.finish();
499 		byte[] delta = buf.toByteArray();
500 
501 		MergedReftable mr = merge(base, delta);
502 		assertEquals(1, mr.minUpdateIndex());
503 		assertEquals(4, mr.maxUpdateIndex());
504 
505 		try (RefCursor rc = mr.allRefs()) {
506 			assertTrue(rc.next());
507 			assertEquals("refs/heads/a", rc.getRef().getName());
508 			assertEquals(id(10), rc.getRef().getObjectId());
509 			assertEquals(2, rc.getRef().getUpdateIndex());
510 
511 			assertTrue(rc.next());
512 			assertEquals("refs/heads/b", rc.getRef().getName());
513 			assertEquals(id(20), rc.getRef().getObjectId());
514 			assertEquals(4, rc.getRef().getUpdateIndex());
515 		}
516 	}
517 
518 	@Test
519 	public void compaction() throws IOException {
520 		List<Ref> delta1 = Arrays.asList(
521 				ref("refs/heads/next", 4),
522 				ref("refs/heads/master", 1));
523 		List<Ref> delta2 = Arrays.asList(delete("refs/heads/next"));
524 		List<Ref> delta3 = Arrays.asList(ref("refs/heads/master", 8));
525 
526 		ByteArrayOutputStream out = new ByteArrayOutputStream();
527 		ReftableCompactor compactor = new ReftableCompactor(out);
528 		compactor.addAll(Arrays.asList(
529 				read(write(delta1)),
530 				read(write(delta2)),
531 				read(write(delta3))));
532 		compactor.compact();
533 		byte[] table = out.toByteArray();
534 
535 		ReftableReader reader = read(table);
536 		try (RefCursor rc = reader.allRefs()) {
537 			assertTrue(rc.next());
538 			Ref r = rc.getRef();
539 			assertEquals("refs/heads/master", r.getName());
540 			assertEquals(id(8), r.getObjectId());
541 			assertFalse(rc.next());
542 		}
543 	}
544 
545 	@Test
546 	public void versioningSymbolicReftargetMoves() throws IOException {
547 		Ref master = ref(MASTER, 100);
548 
549 		List<Ref> delta1 = Arrays.asList(master, sym(HEAD, MASTER));
550 		List<Ref> delta2 = Arrays.asList(ref(MASTER, 200));
551 
552 		MergedReftable mr = merge(write(delta1, 1), write(delta2, 2));
553 		Ref head = mr.exactRef(HEAD);
554 		assertEquals(head.getUpdateIndex(), 1);
555 
556 		Ref masterRef = mr.exactRef(MASTER);
557 		assertEquals(masterRef.getUpdateIndex(), 2);
558 	}
559 
560 	@Test
561 	public void versioningSymbolicRefMoves() throws IOException {
562 		Ref branchX = ref("refs/heads/branchX", 200);
563 
564 		List<Ref> delta1 = Arrays.asList(ref(MASTER, 100), branchX,
565 				sym(HEAD, MASTER));
566 		List<Ref> delta2 = Arrays.asList(sym(HEAD, "refs/heads/branchX"));
567 		List<Ref> delta3 = Arrays.asList(sym(HEAD, MASTER));
568 
569 		MergedReftable mr = merge(write(delta1, 1), write(delta2, 2),
570 				write(delta3, 3));
571 		Ref head = mr.exactRef(HEAD);
572 		assertEquals(head.getUpdateIndex(), 3);
573 
574 		Ref masterRef = mr.exactRef(MASTER);
575 		assertEquals(masterRef.getUpdateIndex(), 1);
576 
577 		Ref branchRef = mr.exactRef(MASTER);
578 		assertEquals(branchRef.getUpdateIndex(), 1);
579 	}
580 
581 	@Test
582 	public void versioningResolveRef() throws IOException {
583 		List<Ref> delta1 = Arrays.asList(sym(HEAD, "refs/heads/tmp"),
584 				sym("refs/heads/tmp", MASTER), ref(MASTER, 100));
585 		List<Ref> delta2 = Arrays.asList(ref(MASTER, 200));
586 		List<Ref> delta3 = Arrays.asList(ref(MASTER, 300));
587 
588 		MergedReftable mr = merge(write(delta1, 1), write(delta2, 2),
589 				write(delta3, 3));
590 		Ref head = mr.exactRef(HEAD);
591 		Ref resolvedHead = mr.resolve(head);
592 		assertEquals(resolvedHead.getObjectId(), id(300));
593 		assertEquals("HEAD has not moved", resolvedHead.getUpdateIndex(), 1);
594 
595 		Ref master = mr.exactRef(MASTER);
596 		Ref resolvedMaster = mr.resolve(master);
597 		assertEquals(resolvedMaster.getObjectId(), id(300));
598 		assertEquals("master also has update index",
599 				resolvedMaster.getUpdateIndex(), 3);
600 	}
601 
602 	private static MergedReftable merge(byte[]... table) {
603 		List<ReftableReader> stack = new ArrayList<>(table.length);
604 		for (byte[] b : table) {
605 			stack.add(read(b));
606 		}
607 		return new MergedReftable(stack);
608 	}
609 
610 	private static ReftableReader read(byte[] table) {
611 		return new ReftableReader(BlockSource.from(table));
612 	}
613 
614 	private static Ref ref(String name, int id) {
615 		return new ObjectIdRef.PeeledNonTag(PACKED, name, id(id));
616 	}
617 
618 	private static Ref sym(String name, String target) {
619 		return new SymbolicRef(name, newRef(target));
620 	}
621 
622 	private static Ref newRef(String name) {
623 		return new ObjectIdRef.Unpeeled(NEW, name, null);
624 	}
625 
626 	private static Ref delete(String name) {
627 		return new ObjectIdRef.Unpeeled(NEW, name, null);
628 	}
629 
630 	private static ObjectId id(int i) {
631 		byte[] buf = new byte[OBJECT_ID_LENGTH];
632 		buf[0] = (byte) (i & 0xff);
633 		buf[1] = (byte) ((i >>> 8) & 0xff);
634 		buf[2] = (byte) ((i >>> 16) & 0xff);
635 		buf[3] = (byte) (i >>> 24);
636 		return ObjectId.fromRaw(buf);
637 	}
638 
639 	private byte[] write(Ref... refs) throws IOException {
640 		return write(Arrays.asList(refs));
641 	}
642 
643 	private byte[] write(Collection<Ref> refs) throws IOException {
644 		return write(refs, 1);
645 	}
646 
647 	private byte[] write(Collection<Ref> refs, long updateIndex)
648 			throws IOException {
649 		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
650 		new ReftableWriter(buffer)
651 				.setMinUpdateIndex(updateIndex)
652 				.setMaxUpdateIndex(updateIndex)
653 				.begin()
654 				.sortAndWriteRefs(refs)
655 				.finish();
656 		return buffer.toByteArray();
657 	}
658 
659 	@SafeVarargs
660 	private static List<Ref> merge(List<Ref>... tables) {
661 		Map<String, Ref> expect = new HashMap<>();
662 		for (List<Ref> t : tables) {
663 			for (Ref r : t) {
664 				if (r.getStorage() == NEW && r.getObjectId() == null) {
665 					expect.remove(r.getName());
666 				} else {
667 					expect.put(r.getName(), r);
668 				}
669 			}
670 		}
671 
672 		List<Ref> expected = new ArrayList<>(expect.values());
673 		Collections.sort(expected, RefComparator.INSTANCE);
674 		return expected;
675 	}
676 }