From 2badedcbe0f87c0ac9eab37630d2582b411fee5e Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Fri, 8 Aug 2014 17:53:49 -0700 Subject: [PATCH] Process most in-core merges without local temp files Instead of always writing to disk use TemporaryBuffer.LocalFile to store up to 10 MiB of merge result in RAM. Most source code will fit into this limit, avoiding local disk IO for simple merges. Larger files will automatically spool to a temporary file that can be cleaned up in the finally, reducing the risk of leaving them on disk and consuming space in /tmp. Change-Id: Ieccbd9b354d4dd3d2bc1304857325ae7a9f34ec6 --- .../org/eclipse/jgit/merge/ResolveMerger.java | 129 +++++++++--------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index d18e31aef..f8ad490c7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -44,6 +44,9 @@ */ package org.eclipse.jgit.merge; +import static org.eclipse.jgit.lib.Constants.CHARACTER_ENCODING; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -76,7 +79,6 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ConfigConstants; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; @@ -89,6 +91,7 @@ import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.TemporaryBuffer; /** * A three-way merger performing a content-merge if necessary @@ -756,90 +759,92 @@ public class ResolveMerger extends ThreeWayMerger { CanonicalTreeParser ours, CanonicalTreeParser theirs, MergeResult result) throws FileNotFoundException, IOException { - File of = writeMergedFile(result); + File mergedFile = !inCore ? writeMergedFile(result) : null; if (result.containsConflicts()) { - // a conflict occurred, the file will contain conflict markers - // the index will be populated with the three stages and only the - // workdir (if used) contains the halfways merged content + // A conflict occurred, the file will contain conflict markers + // the index will be populated with the three stages and the + // workdir (if used) contains the halfway merged content. add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); mergeResults.put(tw.getPathString(), result); - } else { - // no conflict occurred, the file will contain fully merged content. - // the index will be populated with the new merged version - DirCacheEntry dce = new DirCacheEntry(tw.getPathString()); - int newMode = mergeFileModes(tw.getRawMode(0), tw.getRawMode(1), - tw.getRawMode(2)); - // set the mode for the new content. Fall back to REGULAR_FILE if - // you can't merge modes of OURS and THEIRS - dce.setFileMode((newMode == FileMode.MISSING.getBits()) ? FileMode.REGULAR_FILE - : FileMode.fromBits(newMode)); - dce.setLastModified(of.lastModified()); - dce.setLength((int) of.length()); - InputStream is = new FileInputStream(of); + return; + } + + // No conflict occurred, the file will contain fully merged content. + // The index will be populated with the new merged version. + DirCacheEntry dce = new DirCacheEntry(tw.getPathString()); + + // Set the mode for the new content. Fall back to REGULAR_FILE if + // we can't merge modes of OURS and THEIRS. + int newMode = mergeFileModes( + tw.getRawMode(0), + tw.getRawMode(1), + tw.getRawMode(2)); + dce.setFileMode(newMode == FileMode.MISSING.getBits() + ? FileMode.REGULAR_FILE + : FileMode.fromBits(newMode)); + if (mergedFile != null) { + long len = mergedFile.length(); + dce.setLastModified(mergedFile.lastModified()); + dce.setLength((int) len); + InputStream is = new FileInputStream(mergedFile); try { - dce.setObjectId(getObjectInserter().insert( - Constants.OBJ_BLOB, of.length(), is)); + dce.setObjectId(getObjectInserter().insert(OBJ_BLOB, len, is)); } finally { is.close(); - if (inCore) - FileUtils.delete(of); } - builder.add(dce); - } + } else + dce.setObjectId(insertMergeResult(result)); + builder.add(dce); } /** - * Writes merged file content to the working tree. In case {@link #inCore} - * is set and we don't have a working tree the content is written to a - * temporary file + * Writes merged file content to the working tree. * * @param result * the result of the content merge - * @return the file to which the merged content was written + * @return the working tree file to which the merged content was written. * @throws FileNotFoundException * @throws IOException */ private File writeMergedFile(MergeResult result) throws FileNotFoundException, IOException { - MergeFormatter fmt = new MergeFormatter(); - File of = null; - FileOutputStream fos; - if (!inCore) { - File workTree = db.getWorkTree(); - if (workTree == null) - // TODO: This should be handled by WorkingTreeIterators which - // support write operations - throw new UnsupportedOperationException(); - - FS fs = db.getFS(); - of = new File(workTree, tw.getPathString()); - File parentFolder = of.getParentFile(); - if (!fs.exists(parentFolder)) - parentFolder.mkdirs(); - fos = new FileOutputStream(of); - try { - fmt.formatMerge(fos, result, Arrays.asList(commitNames), - Constants.CHARACTER_ENCODING); - } finally { - fos.close(); - } - } else if (!result.containsConflicts()) { - // When working inCore, only trivial merges can be handled, - // so we generate objects only in conflict free cases - of = File.createTempFile("merge_", "_temp", null); //$NON-NLS-1$ //$NON-NLS-2$ - fos = new FileOutputStream(of); - try { - fmt.formatMerge(fos, result, Arrays.asList(commitNames), - Constants.CHARACTER_ENCODING); - } finally { - fos.close(); - } + File workTree = db.getWorkTree(); + if (workTree == null) + // TODO: This should be handled by WorkingTreeIterators which + // support write operations + throw new UnsupportedOperationException(); + + FS fs = db.getFS(); + File of = new File(workTree, tw.getPathString()); + File parentFolder = of.getParentFile(); + if (!fs.exists(parentFolder)) + parentFolder.mkdirs(); + FileOutputStream fos = new FileOutputStream(of); + try { + new MergeFormatter().formatMerge(fos, result, + Arrays.asList(commitNames), CHARACTER_ENCODING); + } finally { + fos.close(); } return of; } + private ObjectId insertMergeResult(MergeResult result) + throws IOException { + TemporaryBuffer.LocalFile buf = new TemporaryBuffer.LocalFile(10 << 20); + try { + new MergeFormatter().formatMerge(buf, result, + Arrays.asList(commitNames), CHARACTER_ENCODING); + buf.close(); + return getObjectInserter().insert(OBJ_BLOB, buf.length(), + buf.openInputStream()); + } finally { + buf.destroy(); + } + } + /** * Try to merge filemodes. If only ours or theirs have changed the mode * (compared to base) we choose that one. If ours and theirs have equal @@ -872,7 +877,7 @@ public class ResolveMerger extends ThreeWayMerger { throws IOException { if (id.equals(ObjectId.zeroId())) return new RawText(new byte[] {}); - return new RawText(db.open(id, Constants.OBJ_BLOB).getCachedBytes()); + return new RawText(db.open(id, OBJ_BLOB).getCachedBytes()); } private static boolean nonTree(final int mode) {