package org.nibor.git_merge_repos; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.RawParseUtils; /** * Merges the passed commit trees into one tree, adjusting directory structure * if necessary (depends on options from user). */ public class SubtreeMerger { private final Repository repository; public SubtreeMerger(Repository repository) { this.repository = repository; } public ObjectId createMergeCommit(Map<SubtreeConfig, RevCommit> parentCommits, String message) throws IOException { PersonIdent latestIdent = getLatestPersonIdent(parentCommits.values()); DirCache treeDirCache = createTreeDirCache(parentCommits, message); List<? extends ObjectId> parentIds = new ArrayList<>(parentCommits.values()); try (ObjectInserter inserter = repository.newObjectInserter()) { ObjectId treeId = treeDirCache.writeTree(inserter); PersonIdent repositoryUser = new PersonIdent(repository); PersonIdent ident = new PersonIdent(repositoryUser, latestIdent.getWhen().getTime(), latestIdent.getTimeZoneOffset()); CommitBuilder commitBuilder = new CommitBuilder(); commitBuilder.setTreeId(treeId); commitBuilder.setAuthor(ident); commitBuilder.setCommitter(ident); commitBuilder.setMessage(message); commitBuilder.setParentIds(parentIds); ObjectId mergeCommit = inserter.insert(commitBuilder); inserter.flush(); return mergeCommit; } } private PersonIdent getLatestPersonIdent(Collection<RevCommit> commits) { PersonIdent latest = null; for (RevCommit commit : commits) { PersonIdent ident = commit.getCommitterIdent(); Date when = ident.getWhen(); if (latest == null || when.after(latest.getWhen())) { latest = ident; } } return latest; } private DirCache createTreeDirCache(Map<SubtreeConfig, RevCommit> parentCommits, String commitMessage) throws IOException { try (TreeWalk treeWalk = new TreeWalk(repository)) { treeWalk.setRecursive(true); addTrees(parentCommits, treeWalk); DirCacheBuilder builder = DirCache.newInCore().builder(); while (treeWalk.next()) { AbstractTreeIterator iterator = getSingleTreeIterator(treeWalk, commitMessage); if (iterator == null) { throw new IllegalStateException( "Tree walker did not return a single tree (should not happen): " + treeWalk.getPathString()); } byte[] path = Arrays.copyOf(iterator.getEntryPathBuffer(), iterator.getEntryPathLength()); DirCacheEntry entry = new DirCacheEntry(path); entry.setFileMode(iterator.getEntryFileMode()); entry.setObjectId(iterator.getEntryObjectId()); builder.add(entry); } builder.finish(); return builder.getDirCache(); } } private void addTrees(Map<SubtreeConfig, RevCommit> parentCommits, TreeWalk treeWalk) throws IOException { for (Map.Entry<SubtreeConfig, RevCommit> entry : parentCommits.entrySet()) { String directory = entry.getKey().getSubtreeDirectory(); RevCommit parentCommit = entry.getValue(); if (".".equals(directory)) { treeWalk.addTree(parentCommit.getTree()); } else { byte[] prefix = directory.getBytes(RawParseUtils.UTF8_CHARSET); CanonicalTreeParser treeParser = new CanonicalTreeParser(prefix, treeWalk.getObjectReader(), parentCommit.getTree()); treeWalk.addTree(treeParser); } } } private AbstractTreeIterator getSingleTreeIterator(TreeWalk treeWalk, String commitMessage) { AbstractTreeIterator result = null; int treeCount = treeWalk.getTreeCount(); for (int i = 0; i < treeCount; i++) { AbstractTreeIterator it = treeWalk.getTree(i, AbstractTreeIterator.class); if (it != null) { if (result != null) { String msg = "Trees of repositories overlap in path '" + it.getEntryPathString() + "'. " + "We can only merge non-overlapping trees, " + "so make sure the repositories have been prepared for that. " + "One possible way is to process each repository to move the root to a subdirectory first.\n" + "Current commit:\n" + commitMessage; throw new IllegalStateException(msg); } else { result = it; } } } return result; } }