View Javadoc
1   /*
2    *  Copyright 2014 The Kuali Foundation Licensed under the
3    *	Educational Community License, Version 2.0 (the "License"); you may
4    *	not use this file except in compliance with the License. You may
5    *	obtain a copy of the License at
6    *
7    *	http://www.osedu.org/licenses/ECL-2.0
8    *
9    *	Unless required by applicable law or agreed to in writing,
10   *	software distributed under the License is distributed on an "AS IS"
11   *	BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   *	or implied. See the License for the specific language governing
13   *	permissions and limitations under the License.
14   */
15  package org.kuali.student.git.model;
16  
17  import java.io.BufferedReader;
18  import java.io.ByteArrayInputStream;
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.OutputStream;
27  import java.io.PrintWriter;
28  import java.io.RandomAccessFile;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Comparator;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.TreeMap;
39  
40  import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
41  import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
42  import org.apache.commons.io.FileUtils;
43  import org.apache.commons.io.IOUtils;
44  import org.apache.commons.io.output.ByteArrayOutputStream;
45  import org.apache.commons.lang3.StringUtils;
46  import org.eclipse.jgit.lib.Constants;
47  import org.eclipse.jgit.lib.ObjectId;
48  import org.eclipse.jgit.lib.Ref;
49  import org.eclipse.jgit.lib.Repository;
50  import org.kuali.student.git.model.branch.utils.GitBranchUtils;
51  import org.kuali.student.git.model.branch.utils.GitBranchUtils.ILargeBranchNameProvider;
52  import org.kuali.student.git.model.tree.utils.GitTreeDataUtils;
53  import org.kuali.student.git.model.tree.utils.GitTreeProcessor;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  /**
58   * 
59   * @author Kuali Student Team
60   * 
61   */
62  public class SvnRevisionMapper implements ILargeBranchNameProvider {
63  
64  	private static final Logger log = LoggerFactory
65  			.getLogger(SvnRevisionMapper.class);
66  
67  	private static final String REVISION_MAP_FILE_NAME = "revisions.map";
68  
69  	private static final String REVISION_MAP_INDEX_FILE_NAME = "revisions.idx";
70  
71  	private static final String REVISION_BRANCH_MERGE_FILE_NAME = "merge.map";
72  
73  	private static final String REVISION_BRANCH_MERGE_INDEX_FILE_NAME = "merge.idx";
74  
75  	public static class SvnRevisionMap {
76  		private long revision;
77  		private String branchName;
78  		private String branchPath;
79  		private String commitId;
80  
81  		/**
82  		 * @param branchName
83  		 * @param commitId
84  		 */
85  		public SvnRevisionMap(long revision, String branchName,
86  				String branchPath, String commitId) {
87  			super();
88  			this.revision = revision;
89  			this.branchName = branchName;
90  			this.branchPath = branchPath;
91  			this.commitId = commitId;
92  		}
93  
94  		/**
95  		 * @return the branchPath
96  		 */
97  		public String getBranchPath() {
98  			return branchPath;
99  		}
100 
101 		/**
102 		 * @return the branchName
103 		 */
104 		public String getBranchName() {
105 			return branchName;
106 		}
107 
108 		/**
109 		 * @return the commitId
110 		 */
111 		public String getCommitId() {
112 			return commitId;
113 		}
114 
115 		/**
116 		 * @return the revision
117 		 */
118 		public long getRevision() {
119 			return revision;
120 		}
121 
122 		/**
123 		 * @param revision
124 		 *            the revision to set
125 		 */
126 		public void setRevision(long revision) {
127 			this.revision = revision;
128 		}
129 
130 	}
131 
132 	private static class RevisionMapOffset {
133 		private long revision;
134 		private long startBtyeOffset;
135 		private long totalBytes;
136 
137 		/**
138 		 * @param revision
139 		 * @param startBtyeOffset
140 		 * @param totalBytes
141 		 */
142 		public RevisionMapOffset(long revision, long startBtyeOffset,
143 				long totalBytes) {
144 			super();
145 			this.revision = revision;
146 			this.startBtyeOffset = startBtyeOffset;
147 			this.totalBytes = totalBytes;
148 		}
149 
150 		/**
151 		 * @return the revision
152 		 */
153 		public long getRevision() {
154 			return revision;
155 		}
156 
157 		/**
158 		 * @return the startBtyeOffset
159 		 */
160 		public long getStartBtyeOffset() {
161 			return startBtyeOffset;
162 		}
163 
164 		/**
165 		 * @return the totalBytes
166 		 */
167 		public long getTotalBytes() {
168 			return totalBytes;
169 		}
170 
171 	}
172 
173 	private File revisonMappings;
174 
175 	private TreeMap<String, RevisionMapOffset> revisionMap = new TreeMap<>();
176 
177 	private File revisionMapDataFile;
178 
179 	private File revisionMapIndexFile;
180 
181 	private PrintWriter revisionMapIndexWriter;
182 
183 	private RandomAccessFile revisionMapDataRandomAccessFile;
184 
185 	private long endOfRevisionMapDataFileInBytes;
186 
187 	private File revisionBranchMergeDataFile;
188 	private File revisionBranchMergeIndexFile;
189 
190 	private PrintWriter revisionBranchMergeIndexWriter;
191 
192 	private RandomAccessFile revisionBranchMergeDataRandomAccessFile;
193 
194 	private long endOfRevisionBranchMergeDataFileInBytes;
195 
196 	private TreeMap<String, Map<String, RevisionMapOffset>> revisionMergeMap = new TreeMap<>();
197 
198 	private GitTreeProcessor treeProcessor;
199 
200 	private static Comparator<? super String> STRING_LONG_VALUE_COMPARATOR = new Comparator<String>() {
201 
202 		@Override
203 		public int compare(String o1, String o2) {
204 			Long l1 = Long.valueOf(o1);
205 			Long l2 = Long.valueOf(o2);
206 
207 			return l1.compareTo(l2);
208 		}
209 
210 	};
211 
212 	private class MergeDataOffsetProvider implements RevisionMapOffsetProvider {
213 
214 		private long revision;
215 		private String targetBranch;
216 
217 		/**
218 		 * 
219 		 */
220 		public MergeDataOffsetProvider(long revision, String targetBranch) {
221 			super();
222 			this.revision = revision;
223 			this.targetBranch = targetBranch;
224 		}
225 
226 		@Override
227 		public RevisionMapOffset getRevisionMapOffset() {
228 
229 			Map<String, RevisionMapOffset> map = revisionMergeMap.get(String
230 					.valueOf(revision));
231 
232 			if (map == null)
233 				return null;
234 
235 			return map.get(targetBranch);
236 
237 		}
238 
239 	}
240 
241 	/**
242 	 * 
243 	 */
244 	public SvnRevisionMapper(Repository repo) {
245 
246 		treeProcessor = new GitTreeProcessor(repo);
247 
248 		revisonMappings = new File(repo.getDirectory(), "jsvn");
249 
250 		revisonMappings.mkdirs();
251 
252 		revisionMapDataFile = new File(revisonMappings, REVISION_MAP_FILE_NAME);
253 
254 		revisionMapIndexFile = new File(revisonMappings,
255 				REVISION_MAP_INDEX_FILE_NAME);
256 
257 		revisionBranchMergeDataFile = new File(revisonMappings,
258 				REVISION_BRANCH_MERGE_FILE_NAME);
259 
260 		revisionBranchMergeIndexFile = new File(revisonMappings,
261 				REVISION_BRANCH_MERGE_INDEX_FILE_NAME);
262 
263 	}
264 
265 	public void initialize() throws IOException {
266 
267 		// tracks the branch heads at each revision
268 		revisionMapDataRandomAccessFile = new RandomAccessFile(
269 				revisionMapDataFile, "rw");
270 
271 		if (revisionMapIndexFile.exists()) {
272 			// load in any existing data.
273 			loadRevisionMapIndexData();
274 		}
275 
276 		endOfRevisionMapDataFileInBytes = revisionMapDataFile.length();
277 
278 		revisionMapIndexWriter = new PrintWriter(new FileOutputStream(
279 				revisionMapIndexFile, true));
280 
281 		// tracks the merge info of each branch at each revision
282 		// used so we can compute the delta.
283 		revisionBranchMergeDataRandomAccessFile = new RandomAccessFile(
284 				revisionBranchMergeDataFile, "rwd");
285 
286 		if (revisionBranchMergeIndexFile.exists()) {
287 			// load in any existing data.
288 			loadRevisionMergeIndexData();
289 		}
290 
291 		endOfRevisionBranchMergeDataFileInBytes = revisionBranchMergeDataFile
292 				.length();
293 
294 		revisionBranchMergeIndexWriter = new PrintWriter(new FileOutputStream(
295 				revisionBranchMergeIndexFile, true));
296 
297 	}
298 
299 	public void shutdown() throws IOException {
300 
301 		revisionMapIndexWriter.flush();
302 		revisionMapIndexWriter.close();
303 
304 		revisionMapDataRandomAccessFile.close();
305 
306 		revisionBranchMergeIndexWriter.flush();
307 		revisionBranchMergeIndexWriter.close();
308 
309 		revisionBranchMergeDataRandomAccessFile.close();
310 
311 	}
312 
313 	private void loadRevisionMergeIndexData() throws IOException {
314 
315 		BufferedReader indexReader = new BufferedReader(new InputStreamReader(
316 				new FileInputStream(revisionBranchMergeIndexFile)));
317 
318 		while (true) {
319 
320 			String line = indexReader.readLine();
321 
322 			if (line == null)
323 				break;
324 
325 			String parts[] = line.split("::");
326 
327 			if (parts.length != 3)
328 				continue;
329 
330 			long revision = Long.parseLong(parts[0]);
331 			String targetBranch = parts[1];
332 			long byteStartOffset = Long.parseLong(parts[2]);
333 			long totalbytes = Long.parseLong(parts[3]);
334 
335 			Map<String, RevisionMapOffset> targetBranchOffsetMap = getRevisionMergeDataByTargetBranch(
336 					parts[0], true);
337 
338 			targetBranchOffsetMap.put(targetBranch, new RevisionMapOffset(
339 					revision, byteStartOffset, totalbytes));
340 
341 		}
342 
343 		indexReader.close();
344 	}
345 
346 	private Map<String, RevisionMapOffset> getRevisionMergeDataByTargetBranch(
347 			String revisionString, boolean createIfDoesNotExist) {
348 
349 		Map<String, RevisionMapOffset> targetBranchOffsetMap = revisionMergeMap
350 				.get(revisionString);
351 
352 		if (targetBranchOffsetMap == null && createIfDoesNotExist) {
353 			targetBranchOffsetMap = new HashMap<String, SvnRevisionMapper.RevisionMapOffset>();
354 			revisionMergeMap.put(revisionString, targetBranchOffsetMap);
355 		}
356 
357 		return targetBranchOffsetMap;
358 	}
359 
360 	private void loadRevisionMapIndexData() throws IOException {
361 
362 		BufferedReader indexReader = new BufferedReader(new InputStreamReader(
363 				new FileInputStream(revisionMapIndexFile)));
364 
365 		while (true) {
366 
367 			String line = indexReader.readLine();
368 
369 			if (line == null)
370 				break;
371 
372 			String parts[] = line.split("::");
373 
374 			if (parts.length != 3)
375 				continue;
376 
377 			long revision = Long.parseLong(parts[0]);
378 			long byteStartOffset = Long.parseLong(parts[1]);
379 			long totalbytes = Long.parseLong(parts[2]);
380 
381 			revisionMap.put(parts[0], new RevisionMapOffset(revision,
382 					byteStartOffset, totalbytes));
383 
384 		}
385 
386 		indexReader.close();
387 
388 	}
389 
390 	/*
391 	 * returns the total number of bytes written to the data file
392 	 */
393 	private long createRevisionEntry(RandomAccessFile dataFile,
394 			long endOfDataFileOffset, long revision, List<String> revisionLines)
395 			throws IOException {
396 
397 		OutputStream revisionMappingStream = null;
398 
399 		ByteArrayOutputStream bytesOut;
400 
401 		revisionMappingStream = new BZip2CompressorOutputStream(
402 				bytesOut = new ByteArrayOutputStream());
403 
404 		PrintWriter pw = new PrintWriter(revisionMappingStream);
405 
406 		IOUtils.writeLines(revisionLines, "\n", pw);
407 
408 		pw.flush();
409 
410 		pw.close();
411 
412 		byte[] data = bytesOut.toByteArray();
413 
414 		dataFile.seek(endOfDataFileOffset);
415 
416 		dataFile.write(data);
417 
418 		return data.length;
419 	}
420 
421 	private void createRevisionMapEntry(long revision,
422 			List<String> branchHeadLines) throws IOException {
423 
424 		long bytesWritten = createRevisionEntry(
425 				revisionMapDataRandomAccessFile,
426 				endOfRevisionMapDataFileInBytes, revision, branchHeadLines);
427 
428 		/*
429 		 * Write the number of bytes written for this revision.
430 		 */
431 
432 		updateRevisionMapIndex(revision, endOfRevisionMapDataFileInBytes,
433 				bytesWritten);
434 
435 		endOfRevisionMapDataFileInBytes += bytesWritten;
436 
437 	}
438 
439 	public void createRevisionMap(long revision, List<Ref> branchHeads)
440 			throws IOException {
441 
442 		List<String> branchHeadLines = new ArrayList<>(branchHeads.size());
443 
444 		for (Ref branchHead : branchHeads) {
445 			/*
446 			 * Only archive active branches. skip those containing @
447 			 */
448 			if (!branchHead.getName().contains("@"))
449 				branchHeadLines.add(revision + "::" + branchHead.getName()
450 						+ "::" + branchHead.getObjectId().name());
451 		}
452 
453 		createRevisionMapEntry(revision, branchHeadLines);
454 
455 	}
456 
457 	private void updateRevisionMapIndex(long revision,
458 			long revisionStartByteIndex, long bytesWritten) {
459 
460 		revisionMap.put(String.valueOf(revision), new RevisionMapOffset(
461 				revision, revisionStartByteIndex, bytesWritten));
462 
463 		revisionMapIndexWriter.println(revision + "::" + revisionStartByteIndex
464 				+ "::" + bytesWritten);
465 
466 		revisionMapIndexWriter.flush();
467 	}
468 
469 	private void updateMergeDataIndex(long revision, String targetBranchName,
470 			List<BranchMergeInfo> mergeInfo, long revisionStartByteIndex,
471 			long bytesWritten) {
472 
473 		String revisionString = String.valueOf(revision);
474 
475 		Map<String, RevisionMapOffset> targetRevisionMap = getRevisionMergeDataByTargetBranch(
476 				revisionString, true);
477 
478 		targetRevisionMap.put(targetBranchName, new RevisionMapOffset(revision,
479 				revisionStartByteIndex, bytesWritten));
480 
481 		revisionBranchMergeIndexWriter.println(revision + "::"
482 				+ targetBranchName + "::" + revisionStartByteIndex + "::"
483 				+ bytesWritten);
484 
485 		revisionBranchMergeIndexWriter.flush();
486 	}
487 
488 	private void updateIndex(Map<String, RevisionMapOffset> revisionMap,
489 			PrintWriter indexWriter, long revision,
490 			long revisionStartByteIndex, long bytesWritten) {
491 		revisionMap.put(String.valueOf(revision), new RevisionMapOffset(
492 				revision, revisionStartByteIndex, bytesWritten));
493 
494 		indexWriter.println(revision + "::" + revisionStartByteIndex + "::"
495 				+ bytesWritten);
496 
497 		indexWriter.flush();
498 
499 	}
500 
501 	/**
502 	 * Get the list of all references at the svn revision number given.
503 	 * 
504 	 * @param revision
505 	 * @return
506 	 * @throws IOException
507 	 */
508 	public List<SvnRevisionMap> getRevisionHeads(long revision)
509 			throws IOException {
510 
511 		InputStream inputStream = getRevisionInputStream(revision);
512 
513 		if (inputStream == null)
514 			return null;
515 
516 		List<String> lines = IOUtils.readLines(inputStream, "UTF-8");
517 
518 		inputStream.close();
519 
520 		String revisionString = String.valueOf(revision);
521 
522 		List<SvnRevisionMap> revisionHeads = new ArrayList<SvnRevisionMap>();
523 
524 		for (String line : lines) {
525 
526 			String[] parts = line.split("::");
527 
528 			if (!parts[0].equals(revisionString)) {
529 				log.warn(parts[0] + " is not a line for " + revisionString);
530 				continue;
531 			}
532 
533 			String branchName = parts[1];
534 
535 			String commitId = parts[2];
536 
537 			String branchPath = GitBranchUtils.getBranchPath(branchName,
538 					revision, this);
539 
540 			revisionHeads.add(new SvnRevisionMap(revision, branchName,
541 					branchPath, commitId));
542 
543 		}
544 
545 		return revisionHeads;
546 
547 	}
548 
549 	private InputStream getRevisionInputStream(final long revision)
550 			throws IOException {
551 
552 		return getInputStream(new RevisionMapOffsetProvider() {
553 
554 			@Override
555 			public RevisionMapOffset getRevisionMapOffset() {
556 
557 				return revisionMap.get(String.valueOf(revision));
558 			}
559 		}, revisionMapDataRandomAccessFile);
560 
561 	}
562 
563 	private InputStream getMergeDataInputStream(final long revision,
564 			final String targetBranch) throws IOException {
565 		return getInputStream(new MergeDataOffsetProvider(revision,
566 				targetBranch), revisionBranchMergeDataRandomAccessFile);
567 	}
568 
569 	private static interface RevisionMapOffsetProvider {
570 		public RevisionMapOffset getRevisionMapOffset();
571 	};
572 
573 	private InputStream getInputStream(
574 			RevisionMapOffsetProvider offsetProvider, RandomAccessFile dataFile)
575 			throws IOException {
576 
577 		RevisionMapOffset revisionOffset = offsetProvider
578 				.getRevisionMapOffset();
579 
580 		if (revisionOffset == null)
581 			return null;
582 
583 		byte[] data = new byte[(int) revisionOffset.getTotalBytes()];
584 
585 		dataFile.seek(revisionOffset.getStartBtyeOffset());
586 
587 		dataFile.readFully(data);
588 
589 		return new BZip2CompressorInputStream(new ByteArrayInputStream(data));
590 
591 	}
592 
593 	/**
594 	 * Get the object id of the commit refered to by the branch at the revision
595 	 * given.
596 	 * 
597 	 * @param revision
598 	 * @param branchName
599 	 * @return
600 	 * @throws IOException
601 	 */
602 	public ObjectId getRevisionBranchHead(long revision, String branchName)
603 			throws IOException {
604 
605 		InputStream inputStream = getRevisionInputStream(revision);
606 
607 		if (inputStream == null)
608 			return null;
609 
610 		List<String> lines = IOUtils.readLines(inputStream, "UTF-8");
611 
612 		inputStream.close();
613 
614 		String revisionString = String.valueOf(revision);
615 		
616 		String adjustedBranchName = branchName;
617 		
618 		if (!adjustedBranchName.startsWith(Constants.R_HEADS))
619 			adjustedBranchName = Constants.R_HEADS + branchName;
620 
621 		for (String line : lines) {
622 
623 			String[] parts = line.split("::");
624 
625 			if (!parts[0].equals(revisionString)) {
626 				log.warn("incorrect version");
627 				continue;
628 			}
629 
630 			if (parts[1].equals(adjustedBranchName)) {
631 				ObjectId id = ObjectId.fromString(parts[2]);
632 
633 				return id;
634 
635 			}
636 
637 		}
638 
639 		// this is actually an exceptional case
640 		// if not found it means that the reference can't be found.
641 		
642 		return null;
643 	}
644 
645 	/*
646 	 * When we compute the list of revisions for a path its useful to know what
647 	 * the matched subpath was.
648 	 */
649 	public static class SvnRevisionMapResults {
650 
651 		private String copyFromPath;
652 
653 		private final SvnRevisionMap revMap;
654 
655 		private final String subPath;
656 
657 		public SvnRevisionMapResults(SvnRevisionMap revMap,
658 				String copyFromPath, String subPath) {
659 			this.revMap = revMap;
660 			this.copyFromPath = copyFromPath;
661 			this.subPath = subPath;
662 		}
663 
664 		public SvnRevisionMapResults(SvnRevisionMap revMap, String copyFromPath) {
665 			this(revMap, copyFromPath, "");
666 		}
667 
668 		/**
669 		 * @return the revMap
670 		 */
671 		public SvnRevisionMap getRevMap() {
672 			return revMap;
673 		}
674 
675 		/**
676 		 * @return the subPath
677 		 */
678 		public String getSubPath() {
679 			return subPath;
680 		}
681 
682 		/**
683 		 * @return the copyFromPath
684 		 */
685 		public String getCopyFromPath() {
686 			return copyFromPath;
687 		}
688 
689 		/**
690 		 * @param copyFromPath
691 		 *            the copyFromPath to set
692 		 */
693 		public void setCopyFromPath(String copyFromPath) {
694 			this.copyFromPath = copyFromPath;
695 		}
696 
697 	}
698 
699 	public List<SvnRevisionMapResults> getRevisionBranches(long targetRevision,
700 			String targetPath) throws IOException {
701 
702 		ArrayList<SvnRevisionMapResults> branches = new ArrayList<>();
703 
704 		List<SvnRevisionMap> heads = this.getRevisionHeads(targetRevision);
705 
706 		if (heads == null)
707 			return branches;
708 
709 		for (SvnRevisionMap revMap : heads) {
710 
711 			SvnRevisionMapResults results = findResults(revMap, targetPath);
712 
713 			if (results != null)
714 				branches.add(results);
715 
716 		}
717 
718 		return branches;
719 	}
720 
721 	private SvnRevisionMapResults findResults(SvnRevisionMap revMap,
722 			String copyFromPath) {
723 
724 		/*
725 		 * In most cases the match is because the copyFromPath is an actual
726 		 * branch.
727 		 * 
728 		 * In other cases it is a prefix that can match several branches
729 		 * 
730 		 * In a few cases it will refer to a branch and then a subpath within it
731 		 * it.
732 		 */
733 		String candidateBranchPath = revMap.getBranchPath().substring(
734 				Constants.R_HEADS.length());
735 
736 		String candidateBranchParts[] = candidateBranchPath.split("\\/");
737 
738 		String copyFromPathParts[] = copyFromPath.split("\\/");
739 
740 		int smallestLength = Math.min(candidateBranchParts.length,
741 				copyFromPathParts.length);
742 
743 		boolean allEquals = true;
744 
745 		for (int i = 0; i < smallestLength; i++) {
746 
747 			String candidatePart = candidateBranchParts[i];
748 			String copyFromPart = copyFromPathParts[i];
749 
750 			if (!copyFromPart.equals(candidatePart)) {
751 				allEquals = false;
752 				break;
753 			}
754 
755 		}
756 
757 		if (allEquals) {
758 
759 			if (copyFromPathParts.length > smallestLength) {
760 				// check inside of the branch for the rest of the path
761 				ObjectId commitId = ObjectId.fromString(revMap.getCommitId());
762 
763 				String insidePath = StringUtils.join(copyFromPathParts, "/",
764 						smallestLength, copyFromPathParts.length);
765 
766 				try {
767 					if (treeProcessor.treeContainsPath(commitId, insidePath)) {
768 						return new SvnRevisionMapResults(revMap, copyFromPath,
769 								insidePath);
770 					}
771 					// fall through
772 				} catch (Exception e) {
773 					log.error("Failed to find paths for commit {}", commitId);
774 					// fall through
775 				}
776 			} else {
777 				return new SvnRevisionMapResults(revMap, copyFromPath);
778 			}
779 		}
780 
781 		return null;
782 	}
783 
784 	/*
785 	 * (non-Javadoc)
786 	 * 
787 	 * @see org.kuali.student.git.utils.GitBranchUtils.ILargeBranchNameProvider#
788 	 * getBranchName(java.lang.String, long)
789 	 */
790 	@Override
791 	public String getBranchName(String longBranchId, long revision) {
792 
793 		try {
794 			File revisionFile = new File(revisonMappings, "r" + revision
795 					+ "-large-branches");
796 
797 			List<String> lines = FileUtils.readLines(revisionFile, "UTF-8");
798 
799 			for (String line : lines) {
800 
801 				String[] parts = line.split("::");
802 
803 				if (parts.length != 2) {
804 					continue;
805 				}
806 
807 				if (parts[0].equals(longBranchId)) {
808 					return parts[1].trim();
809 				}
810 
811 			}
812 
813 			// not found
814 			return null;
815 		} catch (IOException e) {
816 			log.debug("failed to find longbranch for id = {}", longBranchId);
817 			return null;
818 		}
819 
820 	}
821 
822 	/*
823 	 * (non-Javadoc)
824 	 * 
825 	 * @see org.kuali.student.git.utils.GitBranchUtils.ILargeBranchNameProvider#
826 	 * storeLargeBranchName(java.lang.String, java.lang.String, long)
827 	 */
828 	@Override
829 	public String storeLargeBranchName(String branchName, long revision) {
830 
831 		try {
832 			ObjectId largeBranchNameId = GitBranchUtils
833 					.getBranchNameObjectId(branchName);
834 
835 			String existingBranchName = getBranchName(largeBranchNameId.name(),
836 					revision);
837 
838 			if (existingBranchName != null)
839 				return largeBranchNameId.getName();
840 
841 			File revisionFile = new File(revisonMappings, "r" + revision
842 					+ "-large-branches");
843 
844 			PrintWriter pw = new PrintWriter(new FileOutputStream(revisionFile,
845 					true));
846 
847 			pw.println(largeBranchNameId.name() + "::" + branchName);
848 
849 			pw.flush();
850 			pw.close();
851 
852 			return largeBranchNameId.name();
853 		} catch (FileNotFoundException e) {
854 			log.warn("storeLargeBranchName: failed to open r" + revision
855 					+ "-large-branches");
856 			return null;
857 		}
858 	}
859 
860 	public void repackMapFile() throws IOException {
861 
862 		// close the data file
863 		revisionMapDataRandomAccessFile.close();
864 
865 		// close the index file
866 		revisionMapIndexWriter.close();
867 
868 		revisionMapIndexFile.delete();
869 
870 		endOfRevisionMapDataFileInBytes = 0L;
871 
872 		revisionMapIndexWriter = new PrintWriter(new FileOutputStream(new File(
873 				revisonMappings, REVISION_MAP_INDEX_FILE_NAME), true));
874 
875 		// clear the in memory index
876 		revisionMap.clear();
877 
878 		File copy = new File(revisonMappings, "repack-source.dat");
879 
880 		FileUtils.copyFile(revisionMapDataFile, copy);
881 
882 		revisionMapDataFile.delete();
883 
884 		revisionMapDataRandomAccessFile = new RandomAccessFile(
885 				revisionMapDataFile, "rwd");
886 
887 		BufferedReader reader = new BufferedReader(
888 				new InputStreamReader(new BZip2CompressorInputStream(
889 						new FileInputStream(copy), true)));
890 
891 		String currentRevision = null;
892 
893 		List<String> currentRevisionHeads = new ArrayList<String>();
894 
895 		while (true) {
896 
897 			String line = reader.readLine();
898 
899 			if (line == null) {
900 				if (currentRevision != null) {
901 					// archive the last revision
902 					createRevisionMapEntry(Long.parseLong(currentRevision),
903 							currentRevisionHeads);
904 
905 				}
906 				break;
907 			}
908 
909 			String parts[] = line.split("::");
910 
911 			String revisionString = parts[0];
912 
913 			if (currentRevision == null)
914 				currentRevision = revisionString;
915 
916 			if (!currentRevision.equals(revisionString)) {
917 
918 				// write the revision data and update the index file
919 				createRevisionMapEntry(Long.parseLong(currentRevision),
920 						currentRevisionHeads);
921 
922 				currentRevision = revisionString;
923 
924 				currentRevisionHeads.clear();
925 
926 			}
927 
928 			currentRevisionHeads.add(line);
929 
930 		}
931 
932 		reader.close();
933 
934 		copy.delete();
935 
936 	}
937 
938 	public void createMergeData(long revision, String targetBranch,
939 			List<BranchMergeInfo> mergeInfo) throws IOException {
940 
941 		List<String> dataLines = new LinkedList<>();
942 		/*
943 		 * Format: revision :: target branch name :: merge branch name ::
944 		 * revision_1 , revision_2, .. revision_n.
945 		 */
946 		for (BranchMergeInfo bmi : mergeInfo) {
947 
948 			List<String> lineParts = new LinkedList<>();
949 
950 			lineParts.add(String.valueOf(revision));
951 
952 			lineParts.add(targetBranch);
953 
954 			lineParts.add(bmi.getBranchName());
955 
956 			lineParts.add(StringUtils.join(bmi.getMergedRevisions().iterator(),
957 					","));
958 
959 			dataLines.add(StringUtils.join(lineParts, "::"));
960 
961 		}
962 
963 		long bytesWritten = createRevisionEntry(
964 				revisionBranchMergeDataRandomAccessFile,
965 				endOfRevisionBranchMergeDataFileInBytes, revision, dataLines);
966 
967 		/*
968 		 * Write the number of bytes written for this revision.
969 		 */
970 
971 		updateMergeDataIndex(revision, targetBranch, mergeInfo,
972 				endOfRevisionBranchMergeDataFileInBytes, bytesWritten);
973 
974 		endOfRevisionBranchMergeDataFileInBytes += bytesWritten;
975 
976 	}
977 
978 	private BranchMergeInfo extractBranchMergeInfoFromLine(String branchName,
979 			String revisionParts[]) {
980 
981 		BranchMergeInfo bmi = new BranchMergeInfo(branchName);
982 
983 		for (String revisionString : revisionParts) {
984 
985 			bmi.addMergeRevision(Long.valueOf(revisionString));
986 		}
987 
988 		return bmi;
989 
990 	}
991 
992 	/**
993 	 * Get the list of branch merge info for the revision and target branch
994 	 * given.
995 	 * 
996 	 * @param revision
997 	 * @param targetBranch
998 	 * @return
999 	 * @throws IOException
1000 	 */
1001 	public List<BranchMergeInfo> getMergeBranches(long revision,
1002 			String targetBranch) throws IOException {
1003 
1004 		List<BranchMergeInfo> bmiList = new LinkedList<>();
1005 
1006 		InputStream inputStream = getMergeDataInputStream(revision,
1007 				targetBranch);
1008 
1009 		if (inputStream == null)
1010 			return null;
1011 
1012 		List<String> lines = IOUtils.readLines(inputStream, "UTF-8");
1013 
1014 		inputStream.close();
1015 
1016 		String revisionString = String.valueOf(revision);
1017 
1018 		for (String line : lines) {
1019 
1020 			String[] parts = line.split("::");
1021 
1022 			if (!parts[0].equals(revisionString)) {
1023 				log.warn(parts[0] + " is not a line for " + revisionString);
1024 				continue;
1025 			}
1026 
1027 			String targetBranchName = parts[1];
1028 
1029 			if (targetBranch.equals(targetBranchName)) {
1030 
1031 				String mergeBranchName = parts[2];
1032 
1033 				String mergedRevisionStrings[] = parts[3].split(",");
1034 
1035 				BranchMergeInfo bmi = extractBranchMergeInfoFromLine(
1036 						mergeBranchName, mergedRevisionStrings);
1037 
1038 				bmiList.add(bmi);
1039 
1040 			} else {
1041 				log.warn(
1042 						line
1043 								+ " is not a valid line for revision {} and target branch {}",
1044 						revision, targetBranch);
1045 			}
1046 
1047 		}
1048 
1049 		return bmiList;
1050 
1051 	}
1052 
1053 	public Set<Long> getMergeBranchRevisions(long revision,
1054 			String targetBranch, String mergeBranch) throws IOException {
1055 
1056 		List<BranchMergeInfo> bmiList = getMergeBranches(revision, targetBranch);
1057 
1058 		for (BranchMergeInfo bmi : bmiList) {
1059 
1060 			if (bmi.getBranchName().equals(mergeBranch)) {
1061 				return bmi.getMergedRevisions();
1062 			}
1063 		}
1064 
1065 		// no matches found.
1066 		return new HashSet<>();
1067 
1068 	}
1069 
1070 	public void truncateTo(long longRevision) throws IOException {
1071 
1072 		Map<String, RevisionMapOffset> branchOffsets = revisionMergeMap
1073 				.get(longRevision);
1074 
1075 		long maxEndOfFile = endOfRevisionBranchMergeDataFileInBytes;
1076 
1077 		if (branchOffsets != null) {
1078 			for (RevisionMapOffset candidateOffset : branchOffsets.values()) {
1079 
1080 				long candidateEndOfFile = candidateOffset.getStartBtyeOffset()
1081 						+ candidateOffset.getTotalBytes();
1082 
1083 				if (candidateEndOfFile > maxEndOfFile)
1084 					maxEndOfFile = candidateEndOfFile;
1085 			}
1086 		}
1087 
1088 		RevisionMapOffset revisionMapOffset = revisionMap.get(String
1089 				.valueOf(longRevision));
1090 
1091 		long revMapEndOfFile = revisionMapOffset.getStartBtyeOffset()
1092 				+ revisionMapOffset.getTotalBytes();
1093 
1094 		// now do the truncate
1095 
1096 		endOfRevisionBranchMergeDataFileInBytes = maxEndOfFile;
1097 		endOfRevisionMapDataFileInBytes = revMapEndOfFile;
1098 
1099 		revisionBranchMergeDataRandomAccessFile
1100 				.setLength(endOfRevisionBranchMergeDataFileInBytes);
1101 
1102 		revisionMapDataRandomAccessFile
1103 				.setLength(endOfRevisionMapDataFileInBytes);
1104 
1105 		// reset the indices
1106 
1107 		List<String> revisions = new ArrayList<>();
1108 
1109 		revisions.addAll(this.revisionMergeMap.keySet());
1110 
1111 		Collections.sort(revisions, STRING_LONG_VALUE_COMPARATOR);
1112 
1113 		int targetRevisionIndex = revisions.indexOf(String
1114 				.valueOf(longRevision));
1115 
1116 		Set<String> keysToRemove = new HashSet<>(revisions.subList(
1117 				targetRevisionIndex + 1, revisions.size()));
1118 
1119 		for (String key : keysToRemove) {
1120 
1121 			this.revisionMergeMap.remove(key);
1122 
1123 		}
1124 
1125 		revisions = new ArrayList<>(this.revisionMap.keySet());
1126 
1127 		Collections.sort(revisions, STRING_LONG_VALUE_COMPARATOR);
1128 
1129 		targetRevisionIndex = revisions.indexOf(String.valueOf(longRevision));
1130 
1131 		keysToRemove = new HashSet<>(revisions.subList(targetRevisionIndex + 1,
1132 				revisions.size()));
1133 
1134 		for (String key : keysToRemove) {
1135 
1136 			this.revisionMap.remove(key);
1137 
1138 		}
1139 
1140 	}
1141 
1142 }