From 23a71696cd9785e99dad93e54c3884edfdac1157 Mon Sep 17 00:00:00 2001 From: Nitzan Gur-Furman Date: Mon, 8 Aug 2022 17:50:47 +0200 Subject: [PATCH] Reapply "Create util class for work tree updating in both filesystem and index." This reverts commit 5709317f71ccaf26eceaa896150f203879b634b8. Add a bugfix for deletions in ResolveMergers instantiated with just an ObjectInserter as argument. Original change description: Create util class for work tree updating in both filesystem and index. This class intends to make future support in index updating easier. This class currently extracts some logic from ResolveMerger. Logic related to StreamSupplier was copied from ApplyCommand, which will be integrated in a following change. Co-authored-by: Nitzan Gur-Furman Co-authored-by: Han-Wen Nienhuys Change-Id: Ideaefd51789a382a8b499d1ca7ae0146d032f48b --- .../org/eclipse/jgit/api/ApplyCommand.java | 72 +- .../eclipse/jgit/merge/RecursiveMerger.java | 3 - .../org/eclipse/jgit/merge/ResolveMerger.java | 515 ++++--------- .../eclipse/jgit/util/WorkTreeUpdater.java | 693 ++++++++++++++++++ 4 files changed, 856 insertions(+), 427 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java index 583767af3f..88bc7ddd2d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -9,7 +9,6 @@ */ package org.eclipse.jgit.api; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -25,7 +24,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.zip.InflaterInputStream; - import org.eclipse.jgit.api.errors.FilterFailedException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.PatchApplyException; @@ -38,15 +36,11 @@ import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheIterator; -import org.eclipse.jgit.errors.LargeObjectException; -import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectLoader; -import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.patch.BinaryHunk; import org.eclipse.jgit.patch.FileHeader; @@ -64,6 +58,7 @@ import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.WorkTreeUpdater; import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; @@ -355,60 +350,6 @@ public class ApplyCommand extends GitCommand { return result.getStdout().openInputStreamWithAutoDestroy(); } - /** - * Something that can supply an {@link InputStream}. - */ - private interface StreamSupplier { - InputStream load() throws IOException; - } - - /** - * We write the patch result to a {@link TemporaryBuffer} and then use - * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF - * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a - * TemporaryBuffer, so this class bridges between the two, making any Stream - * provided by a {@link StreamSupplier} look like an ordinary git blob to - * DirCacheCheckout. - */ - private static class StreamLoader extends ObjectLoader { - - private StreamSupplier data; - - private long size; - - StreamLoader(StreamSupplier data, long length) { - this.data = data; - this.size = length; - } - - @Override - public int getType() { - return Constants.OBJ_BLOB; - } - - @Override - public long getSize() { - return size; - } - - @Override - public boolean isLarge() { - return true; - } - - @Override - public byte[] getCachedBytes() throws LargeObjectException { - throw new LargeObjectException(); - } - - @Override - public ObjectStream openStream() - throws MissingObjectException, IOException { - return new ObjectStream.Filter(getType(), getSize(), - new BufferedInputStream(data.load())); - } - } - private void initHash(SHA1 hash, long size) { hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB)); hash.update((byte) ' '); @@ -456,7 +397,7 @@ public class ApplyCommand extends GitCommand { } private void applyBinary(Repository repository, String path, File f, - FileHeader fh, StreamSupplier loader, ObjectId id, + FileHeader fh, WorkTreeUpdater.StreamSupplier loader, ObjectId id, CheckoutMetadata checkOut) throws PatchApplyException, IOException { if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) { @@ -488,7 +429,8 @@ public class ApplyCommand extends GitCommand { hunk.getBuffer(), start, length))))) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(() -> inflated, hunk.getSize()), + WorkTreeUpdater.createStreamLoader(() -> inflated, + hunk.getSize()), null, out); if (!fh.getNewId().toObjectId().equals(hash.toObjectId())) { throw new PatchApplyException(MessageFormat.format( @@ -520,8 +462,8 @@ public class ApplyCommand extends GitCommand { SHA1InputStream hashed = new SHA1InputStream(hash, input)) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(() -> hashed, finalSize), null, - out); + WorkTreeUpdater.createStreamLoader(() -> hashed, finalSize), + null, out); if (!fh.getNewId().toObjectId() .equals(hash.toObjectId())) { throw new PatchApplyException(MessageFormat.format( @@ -689,7 +631,7 @@ public class ApplyCommand extends GitCommand { } try (OutputStream output = new FileOutputStream(f)) { DirCacheCheckout.getContent(repository, path, checkOut, - new StreamLoader(buffer::openInputStream, + WorkTreeUpdater.createStreamLoader(buffer::openInputStream, buffer.length()), null, output); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java index bf2a78f6b3..df6068925b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -195,9 +195,6 @@ public class RecursiveMerger extends ResolveMerger { inCore = oldIncore; dircache = oldDircache; workingTreeIterator = oldWTreeIt; - toBeCheckedOut.clear(); - toBeDeleted.clear(); - modifiedFiles.clear(); unmergedPaths.clear(); mergeResults.clear(); failingPaths.clear(); 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 b9ab1d1b7a..949ab5f449 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -20,23 +20,15 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_ALGORITHM; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; -import java.io.BufferedOutputStream; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; - import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.diff.DiffAlgorithm; @@ -46,18 +38,10 @@ import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.Sequence; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuildIterator; -import org.eclipse.jgit.dircache.DirCacheBuilder; -import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.BinaryBlobException; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.IndexWriteException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; @@ -72,20 +56,19 @@ import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.NameConflictTreeWalk; import org.eclipse.jgit.treewalk.TreeWalk; -import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; -import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.LfsFactory; -import org.eclipse.jgit.util.LfsFactory.LfsInputStream; +import org.eclipse.jgit.util.WorkTreeUpdater; +import org.eclipse.jgit.util.WorkTreeUpdater.StreamLoader; import org.eclipse.jgit.util.TemporaryBuffer; -import org.eclipse.jgit.util.io.EolStreamTypeUtil; /** * A three-way merger performing a content-merge if necessary */ public class ResolveMerger extends ThreeWayMerger { + /** * If the merge fails (means: not stopped because of unresolved conflicts) * this enum is used to explain why it failed @@ -149,11 +132,9 @@ public class ResolveMerger extends ThreeWayMerger { protected static final int T_FILE = 4; /** - * Builder to update the cache during this merge. - * - * @since 3.4 + * Handler for repository I/O actions. */ - protected DirCacheBuilder builder; + protected WorkTreeUpdater workTreeUpdater; /** * merge result as tree @@ -163,35 +144,17 @@ public class ResolveMerger extends ThreeWayMerger { protected ObjectId resultTree; /** - * Paths that could not be merged by this merger because of an unsolvable - * conflict. - * - * @since 3.4 - */ - protected List unmergedPaths = new ArrayList<>(); - - /** - * Files modified during this merge operation. - * - * @since 3.4 - */ - protected List modifiedFiles = new LinkedList<>(); - - /** - * If the merger has nothing to do for a file but check it out at the end of - * the operation, it can be added here. - * - * @since 3.4 + * Files modified during this operation. Note this list is only updated after a successful write. */ - protected Map toBeCheckedOut = new HashMap<>(); + protected List modifiedFiles = new ArrayList<>(); /** - * Paths in this list will be deleted from the local copy at the end of the - * operation. + * Paths that could not be merged by this merger because of an unsolvable + * conflict. * * @since 3.4 */ - protected List toBeDeleted = new ArrayList<>(); + protected List unmergedPaths = new ArrayList<>(); /** * Low-level textual merge results. Will be passed on to the callers in case @@ -226,15 +189,6 @@ public class ResolveMerger extends ThreeWayMerger { */ protected boolean inCore; - /** - * Set to true if this merger should use the default dircache of the - * repository and should handle locking and unlocking of the dircache. If - * this merger should work in-core or if an explicit dircache was specified - * during construction then this field is set to false. - * @since 3.0 - */ - protected boolean implicitDirCache; - /** * Directory cache * @since 3.0 @@ -254,20 +208,6 @@ public class ResolveMerger extends ThreeWayMerger { */ protected MergeAlgorithm mergeAlgorithm; - /** - * The {@link WorkingTreeOptions} are needed to determine line endings for - * merged files. - * - * @since 4.11 - */ - protected WorkingTreeOptions workingTreeOptions; - - /** - * The size limit (bytes) which controls a file to be stored in {@code Heap} - * or {@code LocalFile} during the merge. - */ - private int inCoreLimit; - /** * The {@link ContentMergeStrategy} to use for "resolve" and "recursive" * merges. @@ -275,16 +215,6 @@ public class ResolveMerger extends ThreeWayMerger { @NonNull private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; - /** - * Keeps {@link CheckoutMetadata} for {@link #checkout()}. - */ - private Map checkoutMetadata; - - /** - * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}. - */ - private Map cleanupMetadata; - private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -292,13 +222,8 @@ public class ResolveMerger extends ThreeWayMerger { return new MergeAlgorithm(DiffAlgorithm.getAlgorithm(diffAlg)); } - private static int getInCoreLimit(Config config) { - return config.getInt( - ConfigConstants.CONFIG_MERGE_SECTION, ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); - } - private static String[] defaultCommitNames() { - return new String[] { "BASE", "OURS", "THEIRS" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + return new String[]{"BASE", "OURS", "THEIRS"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } private static final Attributes NO_ATTRIBUTES = new Attributes(); @@ -315,17 +240,8 @@ public class ResolveMerger extends ThreeWayMerger { super(local); Config config = local.getConfig(); mergeAlgorithm = getMergeAlgorithm(config); - inCoreLimit = getInCoreLimit(config); commitNames = defaultCommitNames(); this.inCore = inCore; - - if (inCore) { - implicitDirCache = false; - dircache = DirCache.newInCore(); - } else { - implicitDirCache = true; - workingTreeOptions = local.getConfig().get(WorkingTreeOptions.KEY); - } } /** @@ -352,8 +268,6 @@ public class ResolveMerger extends ThreeWayMerger { mergeAlgorithm = getMergeAlgorithm(config); commitNames = defaultCommitNames(); inCore = true; - implicitDirCache = false; - dircache = DirCache.newInCore(); } /** @@ -382,81 +296,8 @@ public class ResolveMerger extends ThreeWayMerger { /** {@inheritDoc} */ @Override protected boolean mergeImpl() throws IOException { - if (implicitDirCache) { - dircache = nonNullRepo().lockDirCache(); - } - if (!inCore) { - checkoutMetadata = new HashMap<>(); - cleanupMetadata = new HashMap<>(); - } - try { - return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], - false); - } finally { - checkoutMetadata = null; - cleanupMetadata = null; - if (implicitDirCache) { - dircache.unlock(); - } - } - } - - private void checkout() throws NoWorkTreeException, IOException { - // Iterate in reverse so that "folder/file" is deleted before - // "folder". Otherwise this could result in a failing path because - // of a non-empty directory, for which delete() would fail. - for (int i = toBeDeleted.size() - 1; i >= 0; i--) { - String fileName = toBeDeleted.get(i); - File f = new File(nonNullRepo().getWorkTree(), fileName); - if (!f.delete()) - if (!f.isDirectory()) - failingPaths.put(fileName, - MergeFailureReason.COULD_NOT_DELETE); - modifiedFiles.add(fileName); - } - for (Map.Entry entry : toBeCheckedOut - .entrySet()) { - DirCacheEntry cacheEntry = entry.getValue(); - if (cacheEntry.getFileMode() == FileMode.GITLINK) { - new File(nonNullRepo().getWorkTree(), entry.getKey()).mkdirs(); - } else { - DirCacheCheckout.checkoutEntry(db, cacheEntry, reader, false, - checkoutMetadata.get(entry.getKey())); - modifiedFiles.add(entry.getKey()); - } - } - } - - /** - * Reverts the worktree after an unsuccessful merge. We know that for all - * modified files the old content was in the old index and the index - * contained only stage 0. In case if inCore operation just clear the - * history of modified files. - * - * @throws java.io.IOException - * @throws org.eclipse.jgit.errors.CorruptObjectException - * @throws org.eclipse.jgit.errors.NoWorkTreeException - * @since 3.4 - */ - protected void cleanUp() throws NoWorkTreeException, - CorruptObjectException, - IOException { - if (inCore) { - modifiedFiles.clear(); - return; - } - - DirCache dc = nonNullRepo().readDirCache(); - Iterator mpathsIt=modifiedFiles.iterator(); - while(mpathsIt.hasNext()) { - String mpath = mpathsIt.next(); - DirCacheEntry entry = dc.getEntry(mpath); - if (entry != null) { - DirCacheCheckout.checkoutEntry(db, entry, reader, false, - cleanupMetadata.get(mpath)); - } - mpathsIt.remove(); - } + return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], + false); } /** @@ -472,13 +313,9 @@ public class ResolveMerger extends ThreeWayMerger { private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, Instant lastMod, long len) { if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { - DirCacheEntry e = new DirCacheEntry(path, stage); - e.setFileMode(p.getEntryFileMode()); - e.setObjectId(p.getEntryObjectId()); - e.setLastModified(lastMod); - e.setLength(len); - builder.add(e); - return e; + return workTreeUpdater.addExistingToIndex(p.getEntryObjectId(), path, + p.getEntryFileMode(), stage, + lastMod, (int) len); } return null; } @@ -493,41 +330,8 @@ public class ResolveMerger extends ThreeWayMerger { * @return the entry which was added to the index */ private DirCacheEntry keep(DirCacheEntry e) { - DirCacheEntry newEntry = new DirCacheEntry(e.getRawPath(), - e.getStage()); - newEntry.setFileMode(e.getFileMode()); - newEntry.setObjectId(e.getObjectId()); - newEntry.setLastModified(e.getLastModifiedInstant()); - newEntry.setLength(e.getLength()); - builder.add(newEntry); - return newEntry; - } - - /** - * Remembers the {@link CheckoutMetadata} for the given path; it may be - * needed in {@link #checkout()} or in {@link #cleanUp()}. - * - * @param map - * to add the metadata to - * @param path - * of the current node - * @param attributes - * to use for determining the metadata - * @throws IOException - * if the smudge filter cannot be determined - * @since 6.1 - */ - protected void addCheckoutMetadata(Map map, - String path, Attributes attributes) - throws IOException { - if (map != null) { - EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, - attributes); - CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getSmudgeCommand(attributes)); - map.put(path, data); - } + return workTreeUpdater.addExistingToIndex(e.getObjectId(), e.getRawPath(), e.getFileMode(), + e.getStage(), e.getLastModifiedInstant(), e.getLength()); } /** @@ -547,14 +351,17 @@ public class ResolveMerger extends ThreeWayMerger { protected void addToCheckout(String path, DirCacheEntry entry, Attributes[] attributes) throws IOException { - toBeCheckedOut.put(path, entry); - addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); - addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); + EolStreamType cleanupStreamType = workTreeUpdater.detectCheckoutStreamType(attributes[T_OURS]); + String cleanupSmudgeCommand = tw.getSmudgeCommand(attributes[T_OURS]); + EolStreamType checkoutStreamType = workTreeUpdater.detectCheckoutStreamType(attributes[T_THEIRS]); + String checkoutSmudgeCommand = tw.getSmudgeCommand(attributes[T_THEIRS]); + workTreeUpdater.addToCheckout(path, entry, cleanupStreamType, cleanupSmudgeCommand, + checkoutStreamType, checkoutSmudgeCommand); } /** * Remember a path for deletion, and remember its {@link CheckoutMetadata} - * in case it has to be restored in {@link #cleanUp()}. + * in case it has to be restored in the cleanUp. * * @param path * of the entry @@ -568,10 +375,13 @@ public class ResolveMerger extends ThreeWayMerger { */ protected void addDeletion(String path, boolean isFile, Attributes attributes) throws IOException { - toBeDeleted.add(path); - if (isFile) { - addCheckoutMetadata(cleanupMetadata, path, attributes); - } + if (db == null || nonNullRepo().isBare() || !isFile) + return; + + File file = new File(nonNullRepo().getWorkTree(), path); + EolStreamType streamType = workTreeUpdater.detectCheckoutStreamType(attributes); + String smudgeCommand = tw.getSmudgeCommand(attributes); + workTreeUpdater.deleteFile(path, file, streamType, smudgeCommand); } /** @@ -615,9 +425,6 @@ public class ResolveMerger extends ThreeWayMerger { * @return false if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred - * @throws org.eclipse.jgit.errors.MissingObjectException - * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException - * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException * @since 6.1 */ @@ -625,20 +432,21 @@ public class ResolveMerger extends ThreeWayMerger { CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, boolean ignoreConflicts, Attributes[] attributes) - throws MissingObjectException, IncorrectObjectTypeException, - CorruptObjectException, IOException { + throws IOException { enterSubtree = true; final int modeO = tw.getRawMode(T_OURS); final int modeT = tw.getRawMode(T_THEIRS); final int modeB = tw.getRawMode(T_BASE); boolean gitLinkMerging = isGitLink(modeO) || isGitLink(modeT) || isGitLink(modeB); - if (modeO == 0 && modeT == 0 && modeB == 0) + if (modeO == 0 && modeT == 0 && modeB == 0) { // File is either untracked or new, staged but uncommitted return true; + } - if (isIndexDirty()) + if (isIndexDirty()) { return false; + } DirCacheEntry ourDce = null; @@ -706,8 +514,9 @@ public class ResolveMerger extends ThreeWayMerger { if (modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) { // THEIRS was not changed compared to BASE. All changes must be in // OURS. OURS is chosen. We can keep the existing entry. - if (ourDce != null) + if (ourDce != null) { keep(ourDce); + } // no checkout needed! return true; } @@ -717,8 +526,9 @@ public class ResolveMerger extends ThreeWayMerger { // THEIRS. THEIRS is chosen. // Check worktree before checking out THEIRS - if (isWorktreeDirty(work, ourDce)) + if (isWorktreeDirty(work, ourDce)) { return false; + } if (nonTree(modeT)) { // we know about length and lastMod only after we have written // the new content. @@ -759,12 +569,15 @@ public class ResolveMerger extends ThreeWayMerger { enterSubtree = false; return true; } - if (nonTree(modeB)) + if (nonTree(modeB)) { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - if (nonTree(modeO)) + } + if (nonTree(modeO)) { add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - if (nonTree(modeT)) + } + if (nonTree(modeT)) { add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + } unmergedPaths.add(tw.getPathString()); enterSubtree = false; return true; @@ -774,8 +587,9 @@ public class ResolveMerger extends ThreeWayMerger { // tells us we are in a subtree because of index or working-dir). // If they are both folders no content-merge is required - we can // return here. - if (!nonTree(modeO)) + if (!nonTree(modeO)) { return true; + } // ours and theirs are both files, just fall out of the if block // and do the content merge @@ -806,16 +620,16 @@ public class ResolveMerger extends ThreeWayMerger { } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { - case OURS: - keep(ourDce); - return true; - case THEIRS: - DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - addToCheckout(tw.getPathString(), theirEntry, attributes); - return true; - default: - break; + case OURS: + keep(ourDce); + return true; + case THEIRS: + DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + addToCheckout(tw.getPathString(), theirEntry, attributes); + return true; + default: + break; } add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); @@ -837,18 +651,18 @@ public class ResolveMerger extends ThreeWayMerger { getContentMergeStrategy()); } catch (BinaryBlobException e) { switch (getContentMergeStrategy()) { - case OURS: - keep(ourDce); - return true; - case THEIRS: - DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - addToCheckout(tw.getPathString(), theirEntry, attributes); - return true; - default: - result = new MergeResult<>(Collections.emptyList()); - result.setContainsConflicts(true); - break; + case OURS: + keep(ourDce); + return true; + case THEIRS: + DirCacheEntry theirEntry = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + addToCheckout(tw.getPathString(), theirEntry, attributes); + return true; + default: + result = new MergeResult<>(Collections.emptyList()); + result.setContainsConflicts(true); + break; } } if (ignoreConflicts) { @@ -859,11 +673,9 @@ public class ResolveMerger extends ThreeWayMerger { if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } - modifiedFiles.add(currentPath); - addCheckoutMetadata(cleanupMetadata, currentPath, - attributes[T_OURS]); - addCheckoutMetadata(checkoutMetadata, currentPath, - attributes[T_THEIRS]); + workTreeUpdater.markAsModified(currentPath); + // Entry is null - only adds the metadata. + addToCheckout(currentPath, null, attributes); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -975,8 +787,9 @@ public class ResolveMerger extends ThreeWayMerger { } private boolean isIndexDirty() { - if (inCore) + if (inCore) { return false; + } final int modeI = tw.getRawMode(T_INDEX); final int modeO = tw.getRawMode(T_OURS); @@ -984,37 +797,42 @@ public class ResolveMerger extends ThreeWayMerger { // Index entry has to match ours to be considered clean final boolean isDirty = nonTree(modeI) && !(modeO == modeI && tw.idEqual(T_INDEX, T_OURS)); - if (isDirty) + if (isDirty) { failingPaths .put(tw.getPathString(), MergeFailureReason.DIRTY_INDEX); + } return isDirty; } private boolean isWorktreeDirty(WorkingTreeIterator work, DirCacheEntry ourDce) throws IOException { - if (work == null) + if (work == null) { return false; + } final int modeF = tw.getRawMode(T_FILE); final int modeO = tw.getRawMode(T_OURS); // Worktree entry has to match ours to be considered clean boolean isDirty; - if (ourDce != null) + if (ourDce != null) { isDirty = work.isModified(ourDce, true, reader); - else { + } else { isDirty = work.isModeDifferent(modeO); - if (!isDirty && nonTree(modeF)) + if (!isDirty && nonTree(modeF)) { isDirty = !tw.idEqual(T_FILE, T_OURS); + } } // Ignore existing empty directories if (isDirty && modeF == FileMode.TYPE_TREE - && modeO == FileMode.TYPE_MISSING) + && modeO == FileMode.TYPE_MISSING) { isDirty = false; - if (isDirty) + } + if (isDirty) { failingPaths.put(tw.getPathString(), MergeFailureReason.DIRTY_WORKTREE); + } return isDirty; } @@ -1029,14 +847,12 @@ public class ResolveMerger extends ThreeWayMerger { * @param theirs * @param result * @param attributes - * @throws FileNotFoundException * @throws IOException */ private void updateIndex(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, MergeResult result, Attributes attributes) - throws FileNotFoundException, - IOException { + throws IOException { TemporaryBuffer rawMerged = null; try { rawMerged = doMerge(result); @@ -1055,21 +871,17 @@ public class ResolveMerger extends ThreeWayMerger { // 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()); - + Instant lastModified = + mergedFile == null ? null : nonNullRepo().getFS().lastModifiedInstant(mergedFile); // 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) { - dce.setLastModified( - nonNullRepo().getFS().lastModifiedInstant(mergedFile)); - dce.setLength((int) mergedFile.length()); - } - dce.setObjectId(insertMergeResult(rawMerged, attributes)); - builder.add(dce); + FileMode mode = newMode == FileMode.MISSING.getBits() + ? FileMode.REGULAR_FILE : FileMode.fromBits(newMode); + workTreeUpdater.insertToIndex(rawMerged.openInputStream(), tw.getPathString().getBytes(UTF_8), mode, + DirCacheEntry.STAGE_0, lastModified, (int) rawMerged.length(), + attributes.get(Constants.ATTR_MERGE)); } finally { if (rawMerged != null) { rawMerged.destroy(); @@ -1085,34 +897,30 @@ public class ResolveMerger extends ThreeWayMerger { * @param attributes * the files .gitattributes entries * @return the working tree file to which the merged content was written. - * @throws FileNotFoundException * @throws IOException */ private File writeMergedFile(TemporaryBuffer rawMerged, Attributes attributes) - throws FileNotFoundException, IOException { + throws IOException { File workTree = nonNullRepo().getWorkTree(); FS fs = nonNullRepo().getFS(); File of = new File(workTree, tw.getPathString()); File parentFolder = of.getParentFile(); + EolStreamType eol = workTreeUpdater.detectCheckoutStreamType(attributes); if (!fs.exists(parentFolder)) { parentFolder.mkdirs(); } - EolStreamType streamType = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, - attributes); - try (OutputStream os = EolStreamTypeUtil.wrapOutputStream( - new BufferedOutputStream(new FileOutputStream(of)), - streamType)) { - rawMerged.writeTo(os, null); - } + StreamLoader contentLoader = WorkTreeUpdater.createStreamLoader(rawMerged::openInputStream, + rawMerged.length()); + workTreeUpdater.updateFileWithContent(contentLoader, + eol, tw.getSmudgeCommand(attributes), of.getPath(), of, false); return of; } private TemporaryBuffer doMerge(MergeResult result) throws IOException { TemporaryBuffer.LocalFile buf = new TemporaryBuffer.LocalFile( - db != null ? nonNullRepo().getDirectory() : null, inCoreLimit); + db != null ? nonNullRepo().getDirectory() : null, workTreeUpdater.getInCoreFileSizeLimit()); boolean success = false; try { new MergeFormatter().formatMerge(buf, result, @@ -1127,16 +935,6 @@ public class ResolveMerger extends ThreeWayMerger { return buf; } - private ObjectId insertMergeResult(TemporaryBuffer buf, - Attributes attributes) throws IOException { - InputStream in = buf.openInputStream(); - try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter( - getRepository(), in, - buf.length(), attributes.get(Constants.ATTR_MERGE))) { - return getObjectInserter().insert(OBJ_BLOB, is.getLength(), is); - } - } - /** * 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 @@ -1154,22 +952,26 @@ public class ResolveMerger extends ThreeWayMerger { * conflict */ private int mergeFileModes(int modeB, int modeO, int modeT) { - if (modeO == modeT) + if (modeO == modeT) { return modeO; - if (modeB == modeO) + } + if (modeB == modeO) { // Base equal to Ours -> chooses Theirs if that is not missing return (modeT == FileMode.MISSING.getBits()) ? modeO : modeT; - if (modeB == modeT) + } + if (modeB == modeT) { // Base equal to Theirs -> chooses Ours if that is not missing return (modeO == FileMode.MISSING.getBits()) ? modeT : modeO; + } return FileMode.MISSING.getBits(); } private RawText getRawText(ObjectId id, Attributes attributes) throws IOException, BinaryBlobException { - if (id.equals(ObjectId.zeroId())) - return new RawText(new byte[] {}); + if (id.equals(ObjectId.zeroId())) { + return new RawText(new byte[]{}); + } ObjectLoader loader = LfsFactory.getInstance().applySmudgeFilter( getRepository(), reader.open(id, OBJ_BLOB), @@ -1233,7 +1035,7 @@ public class ResolveMerger extends ThreeWayMerger { * superset of the files listed by {@link #getUnmergedPaths()}. */ public List getModifiedFiles() { - return modifiedFiles; + return workTreeUpdater != null ? workTreeUpdater.getModifiedFiles() : modifiedFiles; } /** @@ -1247,7 +1049,7 @@ public class ResolveMerger extends ThreeWayMerger { * for this path. */ public Map getToBeCheckedOut() { - return toBeCheckedOut; + return workTreeUpdater.getToBeCheckedOut(); } /** @@ -1297,7 +1099,6 @@ public class ResolveMerger extends ThreeWayMerger { */ public void setDirCache(DirCache dc) { this.dircache = dc; - implicitDirCache = false; } /** @@ -1352,53 +1153,48 @@ public class ResolveMerger extends ThreeWayMerger { protected boolean mergeTrees(AbstractTreeIterator baseTree, RevTree headTree, RevTree mergeTree, boolean ignoreConflicts) throws IOException { + try { + workTreeUpdater = inCore ? + WorkTreeUpdater.createInCoreWorkTreeUpdater(db, dircache, getObjectInserter()) : + WorkTreeUpdater.createWorkTreeUpdater(db, dircache); + dircache = workTreeUpdater.getLockedDirCache(); + tw = new NameConflictTreeWalk(db, reader); + + tw.addTree(baseTree); + tw.setHead(tw.addTree(headTree)); + tw.addTree(mergeTree); + DirCacheBuildIterator buildIt = workTreeUpdater.createDirCacheBuildIterator(); + int dciPos = tw.addTree(buildIt); + if (workingTreeIterator != null) { + tw.addTree(workingTreeIterator); + workingTreeIterator.setDirCacheIterator(tw, dciPos); + } else { + tw.setFilter(TreeFilter.ANY_DIFF); + } - builder = dircache.builder(); - DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); - - tw = new NameConflictTreeWalk(db, reader); - tw.addTree(baseTree); - tw.setHead(tw.addTree(headTree)); - tw.addTree(mergeTree); - int dciPos = tw.addTree(buildIt); - if (workingTreeIterator != null) { - tw.addTree(workingTreeIterator); - workingTreeIterator.setDirCacheIterator(tw, dciPos); - } else { - tw.setFilter(TreeFilter.ANY_DIFF); - } + if (!mergeTreeWalk(tw, ignoreConflicts)) { + return false; + } - if (!mergeTreeWalk(tw, ignoreConflicts)) { + workTreeUpdater.writeWorkTreeChanges(true); + if (getUnmergedPaths().isEmpty() && !failed()) { + WorkTreeUpdater.Result result = workTreeUpdater.writeIndexChanges(); + resultTree = result.treeId; + modifiedFiles = result.modifiedFiles; + for (String f : result.failedToDelete) { + failingPaths.put(f, MergeFailureReason.COULD_NOT_DELETE); + } + return result.failedToDelete.isEmpty(); + } + resultTree = null; return false; - } - - if (!inCore) { - // No problem found. The only thing left to be done is to - // checkout all files from "theirs" which have been selected to - // go into the new index. - checkout(); - - // All content-merges are successfully done. If we can now write the - // new index we are on quite safe ground. Even if the checkout of - // files coming from "theirs" fails the user can work around such - // failures by checking out the index again. - if (!builder.commit()) { - cleanUp(); - throw new IndexWriteException(); + } finally { + if(modifiedFiles.isEmpty()) { + modifiedFiles = workTreeUpdater.getModifiedFiles(); } - builder = null; - - } else { - builder.finish(); - builder = null; + workTreeUpdater.close(); + workTreeUpdater = null; } - - if (getUnmergedPaths().isEmpty() && !failed()) { - resultTree = dircache.writeTree(getObjectInserter()); - return true; - } - resultTree = null; - return false; } /** @@ -1419,8 +1215,8 @@ public class ResolveMerger extends ThreeWayMerger { boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { - Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, - NO_ATTRIBUTES }; + Attributes[] attributes = {NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES}; if (hasAttributeNodeProvider) { attributes[T_BASE] = treeWalk.getAttributes(T_BASE); attributes[T_OURS] = treeWalk.getAttributes(T_OURS); @@ -1434,11 +1230,12 @@ public class ResolveMerger extends ThreeWayMerger { hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, ignoreConflicts, attributes)) { - cleanUp(); + workTreeUpdater.revertModifiedFiles(); return false; } - if (treeWalk.isSubtree() && enterSubtree) + if (treeWalk.isSubtree() && enterSubtree) { treeWalk.enterSubtree(); + } } return true; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java new file mode 100644 index 0000000000..f3531a6d1f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java @@ -0,0 +1,693 @@ +/* + * Copyright (C) 2022, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.util; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import java.io.BufferedInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.IndexWriteException; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.TreeWalk.OperationType; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.LfsFactory.LfsInputStream; +import org.eclipse.jgit.util.io.EolStreamTypeUtil; + +/** + * Handles work tree updates on both the checkout and the index. + *

+ * You should use a single instance for all of your file changes. In case of an error, make sure + * your instance is released, and initiate a new one if necessary. + */ +public class WorkTreeUpdater implements Closeable { + + /** + * The result of writing the index changes. + */ + public static class Result { + + /** + * Files modified during this operation. + */ + public List modifiedFiles = new LinkedList<>(); + + /** + * Files in this list were failed to be deleted. + */ + public List failedToDelete = new LinkedList<>(); + + /** + * Modified tree ID if any, or null otherwise. + */ + public ObjectId treeId = null; + } + + Result result = new Result(); + + /** + * The repository this handler operates on. + */ + @Nullable + private final Repository repo; + + /** + * Set to true if this operation should work in-memory. The repo's dircache and + * workingtree are not touched by this method. Eventually needed files are + * created as temporary files and a new empty, in-memory dircache will be + * used instead the repo's one. Often used for bare repos where the repo + * doesn't even have a workingtree and dircache. + */ + private final boolean inCore; + + private final ObjectInserter inserter; + private final ObjectReader reader; + private DirCache dirCache; + private boolean implicitDirCache = false; + + /** + * Builder to update the dir cache during this operation. + */ + private DirCacheBuilder builder = null; + + /** + * The {@link WorkingTreeOptions} are needed to determine line endings for affected files. + */ + private WorkingTreeOptions workingTreeOptions; + + /** + * The size limit (bytes) which controls a file to be stored in {@code Heap} or {@code LocalFile} + * during the operation. + */ + private int inCoreFileSizeLimit; + + /** + * If the operation has nothing to do for a file but check it out at the end of the operation, it + * can be added here. + */ + private final Map toBeCheckedOut = new HashMap<>(); + + /** + * Files in this list will be deleted from the local copy at the end of the operation. + */ + private final TreeMap toBeDeleted = new TreeMap<>(); + + /** + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. + */ + private Map checkoutMetadata; + + /** + * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}. + */ + private Map cleanupMetadata; + + /** + * Whether the changes were successfully written + */ + private boolean indexChangesWritten = false; + + /** + * @param repo the {@link org.eclipse.jgit.lib.Repository}. + * @param dirCache if set, use the provided dir cache. Otherwise, use the default repository one + */ + private WorkTreeUpdater( + Repository repo, + DirCache dirCache) { + this.repo = repo; + this.dirCache = dirCache; + + this.inCore = false; + this.inserter = repo.newObjectInserter(); + this.reader = inserter.newReader(); + this.workingTreeOptions = repo.getConfig().get(WorkingTreeOptions.KEY); + this.checkoutMetadata = new HashMap<>(); + this.cleanupMetadata = new HashMap<>(); + this.inCoreFileSizeLimit = setInCoreFileSizeLimit(repo.getConfig()); + } + + /** + * @param repo the {@link org.eclipse.jgit.lib.Repository}. + * @param dirCache if set, use the provided dir cache. Otherwise, use the default repository one + * @return an IO handler. + */ + public static WorkTreeUpdater createWorkTreeUpdater(Repository repo, DirCache dirCache) { + return new WorkTreeUpdater(repo, dirCache); + } + + /** + * @param repo the {@link org.eclipse.jgit.lib.Repository}. + * @param dirCache if set, use the provided dir cache. Otherwise, creates a new one + * @param oi to use for writing the modified objects with. + */ + private WorkTreeUpdater( + Repository repo, + DirCache dirCache, + ObjectInserter oi) { + this.repo = repo; + this.dirCache = dirCache; + this.inserter = oi; + + this.inCore = true; + this.reader = oi.newReader(); + if (repo != null) { + this.inCoreFileSizeLimit = setInCoreFileSizeLimit(repo.getConfig()); + } + } + + /** + * @param repo the {@link org.eclipse.jgit.lib.Repository}. + * @param dirCache if set, use the provided dir cache. Otherwise, creates a new one + * @param oi to use for writing the modified objects with. + * @return an IO handler. + */ + public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo, DirCache dirCache, + ObjectInserter oi) { + return new WorkTreeUpdater(repo, dirCache, oi); + } + + /** + * Something that can supply an {@link InputStream}. + */ + public interface StreamSupplier { + + /** + * Loads the input stream. + * + * @return the loaded stream + * @throws IOException if any reading error occurs + */ + InputStream load() throws IOException; + } + + /** + * We write the patch result to a {@link org.eclipse.jgit.util.TemporaryBuffer} and then use + * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF and smudge filters. + * DirCacheCheckout needs an ObjectLoader, not a TemporaryBuffer, so this class bridges between + * the two, making any Stream provided by a {@link StreamSupplier} look like an ordinary git blob + * to DirCacheCheckout. + */ + public static class StreamLoader extends ObjectLoader { + + private final StreamSupplier data; + + private final long size; + + private StreamLoader(StreamSupplier data, long length) { + this.data = data; + this.size = length; + } + + @Override + public int getType() { + return Constants.OBJ_BLOB; + } + + @Override + public long getSize() { + return size; + } + + @Override + public boolean isLarge() { + return true; + } + + @Override + public byte[] getCachedBytes() throws LargeObjectException { + throw new LargeObjectException(); + } + + @Override + public ObjectStream openStream() throws IOException { + return new ObjectStream.Filter(getType(), getSize(), new BufferedInputStream(data.load())); + } + } + + /** + * Creates stream loader for the given supplier. + * + * @param supplier to wrap + * @param length of the supplied content + * @return the result stream loader + */ + public static StreamLoader createStreamLoader(StreamSupplier supplier, long length) { + return new StreamLoader(supplier, length); + } + + private static int setInCoreFileSizeLimit(Config config) { + return config.getInt( + ConfigConstants.CONFIG_MERGE_SECTION, ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20); + } + + /** + * Gets the size limit for in-core files in this config. + * + * @return the size + */ + public int getInCoreFileSizeLimit() { + return inCoreFileSizeLimit; + } + + /** + * Gets dir cache for the repo. Locked if not inCore. + * + * @return the result dir cache + * @throws IOException is case the dir cache cannot be read + */ + public DirCache getLockedDirCache() throws IOException { + if (dirCache == null) { + implicitDirCache = true; + if (inCore) { + dirCache = DirCache.newInCore(); + } else { + dirCache = nonNullNonBareRepo().lockDirCache(); + } + } + if (builder == null) { + builder = dirCache.builder(); + } + return dirCache; + } + + /** + * Creates build iterator for the handler's builder. + * + * @return the iterator + */ + public DirCacheBuildIterator createDirCacheBuildIterator() { + return new DirCacheBuildIterator(builder); + } + + /** + * Writes the changes to the WorkTree (but not the index). + * + * @param shouldCheckoutTheirs before committing the changes + * @throws IOException if any of the writes fail + */ + public void writeWorkTreeChanges(boolean shouldCheckoutTheirs) throws IOException { + handleDeletedFiles(); + + if (inCore) { + builder.finish(); + return; + } + if (shouldCheckoutTheirs) { + // No problem found. The only thing left to be done is to + // check out all files from "theirs" which have been selected to + // go into the new index. + checkout(); + } + + // All content operations are successfully done. If we can now write the + // new index we are on quite safe ground. Even if the checkout of + // files coming from "theirs" fails the user can work around such + // failures by checking out the index again. + if (!builder.commit()) { + revertModifiedFiles(); + throw new IndexWriteException(); + } + } + + /** + * Writes the changes to the index. + * + * @return the Result of the operation. + * @throws IOException if any of the writes fail + */ + public Result writeIndexChanges() throws IOException { + result.treeId = getLockedDirCache().writeTree(inserter); + indexChangesWritten = true; + return result; + } + + /** + * Adds a {@link DirCacheEntry} for direct checkout and remembers its {@link CheckoutMetadata}. + * + * @param path of the entry + * @param entry to add + * @param cleanupStreamType to use for the cleanup metadata + * @param cleanupSmudgeCommand to use for the cleanup metadata + * @param checkoutStreamType to use for the checkout metadata + * @param checkoutSmudgeCommand to use for the checkout metadata + * @since 6.1 + */ + public void addToCheckout( + String path, DirCacheEntry entry, EolStreamType cleanupStreamType, + String cleanupSmudgeCommand, EolStreamType checkoutStreamType, String checkoutSmudgeCommand) { + if (entry != null) { + // In some cases, we just want to add the metadata. + toBeCheckedOut.put(path, entry); + } + addCheckoutMetadata(cleanupMetadata, path, cleanupStreamType, cleanupSmudgeCommand); + addCheckoutMetadata(checkoutMetadata, path, checkoutStreamType, checkoutSmudgeCommand); + } + + /** + * Get a map which maps the paths of files which have to be checked out because the operation + * created new fully-merged content for this file into the index. + * + *

This means: the operation wrote a new stage 0 entry for this path.

+ * + * @return the map + */ + public Map getToBeCheckedOut() { + return toBeCheckedOut; + } + + /** + * Deletes the given file + *

+ * Note the actual deletion is only done in {@link #writeWorkTreeChanges} + * + * @param path of the file to be deleted + * @param file to be deleted + * @param streamType to use for cleanup metadata + * @param smudgeCommand to use for cleanup metadata + * @throws IOException if the file cannot be deleted + */ + public void deleteFile(String path, File file, EolStreamType streamType, String smudgeCommand) + throws IOException { + toBeDeleted.put(path, file); + if (file != null && file.isFile()) { + addCheckoutMetadata(cleanupMetadata, path, streamType, smudgeCommand); + } + } + + /** + * Remembers the {@link CheckoutMetadata} for the given path; it may be needed in {@link + * #checkout()} or in {@link #revertModifiedFiles()}. + * + * @param map to add the metadata to + * @param path of the current node + * @param streamType to use for the metadata + * @param smudgeCommand to use for the metadata + * @since 6.1 + */ + private void addCheckoutMetadata( + Map map, String path, EolStreamType streamType, + String smudgeCommand) { + if (inCore || map == null) { + return; + } + map.put(path, new CheckoutMetadata(streamType, smudgeCommand)); + } + + /** + * Detects if CRLF conversion has been configured. + *

+ * See {@link EolStreamTypeUtil#detectStreamType} for more info. + * + * @param attributes of the file for which the type is to be detected + * @return the detected type + */ + public EolStreamType detectCheckoutStreamType(Attributes attributes) { + if (inCore) { + return null; + } + return EolStreamTypeUtil.detectStreamType( + OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + } + + private void handleDeletedFiles() { + // Iterate in reverse so that "folder/file" is deleted before + // "folder". Otherwise, this could result in a failing path because + // of a non-empty directory, for which delete() would fail. + for (String path : toBeDeleted.descendingKeySet()) { + File file = inCore ? null : toBeDeleted.get(path); + if (file != null && !file.delete()) { + if (!file.isDirectory()) { + result.failedToDelete.add(path); + } + } + } + } + + /** + * Marks the given path as modified in the operation. + * + * @param path to mark as modified + */ + public void markAsModified(String path) { + result.modifiedFiles.add(path); + } + + /** + * Gets the list of files which were modified in this operation. + * + * @return the list + */ + public List getModifiedFiles() { + return result.modifiedFiles; + } + + private void checkout() throws NoWorkTreeException, IOException { + // Iterate in reverse so that "folder/file" is deleted before + // "folder". Otherwise, this could result in a failing path because + // of a non-empty directory, for which delete() would fail. + for (Map.Entry entry : toBeCheckedOut.entrySet()) { + DirCacheEntry dirCacheEntry = entry.getValue(); + if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { + new File(nonNullNonBareRepo().getWorkTree(), entry.getKey()).mkdirs(); + } else { + DirCacheCheckout.checkoutEntry( + repo, dirCacheEntry, reader, false, checkoutMetadata.get(entry.getKey())); + result.modifiedFiles.add(entry.getKey()); + } + } + } + + /** + * Reverts any uncommitted changes in the worktree. We know that for all modified files the + * old content was in the old index and the index contained only stage 0. In case if inCore + * operation just clear the history of modified files. + * + * @throws java.io.IOException in case the cleaning up failed + */ + public void revertModifiedFiles() throws IOException { + if (inCore) { + result.modifiedFiles.clear(); + return; + } + if (indexChangesWritten) { + return; + } + for (String path : result.modifiedFiles) { + DirCacheEntry entry = dirCache.getEntry(path); + if (entry != null) { + DirCacheCheckout.checkoutEntry( + repo, entry, reader, false, cleanupMetadata.get(path)); + } + } + } + + @Override + public void close() throws IOException { + if (implicitDirCache) { + dirCache.unlock(); + } + } + + /** + * Updates the file in the checkout with the given content. + * + * @param resultStreamLoader with the content to be updated + * @param streamType for parsing the content + * @param smudgeCommand for formatting the content + * @param path of the file to be updated + * @param file to be updated + * @param safeWrite whether the content should be written to a buffer first + * @throws IOException if the {@link CheckoutMetadata} cannot be determined + */ + public void updateFileWithContent( + StreamLoader resultStreamLoader, + EolStreamType streamType, + String smudgeCommand, + String path, + File file, + boolean safeWrite) + throws IOException { + if (inCore) { + return; + } + CheckoutMetadata checkoutMetadata = new CheckoutMetadata(streamType, smudgeCommand); + if (safeWrite) { + try (org.eclipse.jgit.util.TemporaryBuffer buffer = + new org.eclipse.jgit.util.TemporaryBuffer.LocalFile(null)) { + // Write to a buffer and copy to the file only if everything was fine. + DirCacheCheckout.getContent( + repo, path, checkoutMetadata, resultStreamLoader, null, buffer); + InputStream bufIn = buffer.openInputStream(); + Files.copy(bufIn, file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + return; + } + OutputStream outputStream = new FileOutputStream(file); + DirCacheCheckout.getContent( + repo, path, checkoutMetadata, resultStreamLoader, null, outputStream); + + } + + /** + * Creates a path with the given content, and adds it to the specified stage to the index builder + * + * @param inputStream with the content to be updated + * @param path of the file to be updated + * @param fileMode of the modified file + * @param entryStage of the new entry + * @param lastModified instant of the modified file + * @param len of the content + * @param lfsAttribute for checking for LFS enablement + * @return the entry which was added to the index + * @throws IOException if inserting the content fails + */ + public DirCacheEntry insertToIndex( + InputStream inputStream, + byte[] path, + FileMode fileMode, + int entryStage, + Instant lastModified, + int len, + Attribute lfsAttribute) throws IOException { + StreamLoader contentLoader = createStreamLoader(() -> inputStream, len); + return insertToIndex(contentLoader, path, fileMode, entryStage, lastModified, len, + lfsAttribute); + } + + /** + * Creates a path with the given content, and adds it to the specified stage to the index builder + * + * @param resultStreamLoader with the content to be updated + * @param path of the file to be updated + * @param fileMode of the modified file + * @param entryStage of the new entry + * @param lastModified instant of the modified file + * @param len of the content + * @param lfsAttribute for checking for LFS enablement + * @return the entry which was added to the index + * @throws IOException if inserting the content fails + */ + public DirCacheEntry insertToIndex( + StreamLoader resultStreamLoader, + byte[] path, + FileMode fileMode, + int entryStage, + Instant lastModified, + int len, + Attribute lfsAttribute) throws IOException { + return addExistingToIndex(insertResult(resultStreamLoader, lfsAttribute), + path, fileMode, entryStage, lastModified, len); + } + + /** + * Adds a path with the specified stage to the index builder + * + * @param objectId of the existing object to add + * @param path of the modified file + * @param fileMode of the modified file + * @param entryStage of the new entry + * @param lastModified instant of the modified file + * @param len of the modified file content + * @return the entry which was added to the index + */ + public DirCacheEntry addExistingToIndex( + ObjectId objectId, + byte[] path, + FileMode fileMode, + int entryStage, + Instant lastModified, + int len) { + DirCacheEntry dce = new DirCacheEntry(path, entryStage); + dce.setFileMode(fileMode); + if (lastModified != null) { + dce.setLastModified(lastModified); + } + dce.setLength(inCore ? 0 : len); + + dce.setObjectId(objectId); + builder.add(dce); + return dce; + } + + private ObjectId insertResult(StreamLoader resultStreamLoader, Attribute lfsAttribute) + throws IOException { + try (LfsInputStream is = + org.eclipse.jgit.util.LfsFactory.getInstance() + .applyCleanFilter( + repo, + resultStreamLoader.data.load(), + resultStreamLoader.size, + lfsAttribute)) { + return inserter.insert(OBJ_BLOB, is.getLength(), is); + } + } + + /** + * Gets non-null repository instance + * + * @return non-null repository instance + * @throws java.lang.NullPointerException if the handler was constructed without a repository. + */ + private Repository nonNullRepo() throws NullPointerException { + if (repo == null) { + throw new NullPointerException(JGitText.get().repositoryIsRequired); + } + return repo; + } + + + /** + * Gets non-null and non-bare repository instance + * + * @return non-null and non-bare repository instance + * @throws java.lang.NullPointerException if the handler was constructed without a repository. + * @throws NoWorkTreeException if the handler was constructed with a bare repository + */ + private Repository nonNullNonBareRepo() throws NullPointerException, NoWorkTreeException { + if (nonNullRepo().isBare()) { + throw new NoWorkTreeException(); + } + return repo; + } +} \ No newline at end of file -- 2.39.5