]> source.dussan.org Git - jgit.git/commitdiff
Reapply "Create util class for work tree updating in both filesystem and index." 99/195099/5
authorNitzan Gur-Furman <nitzan@google.com>
Mon, 8 Aug 2022 15:50:47 +0000 (17:50 +0200)
committerHan-Wen Nienhuys <hanwen@google.com>
Mon, 8 Aug 2022 16:38:23 +0000 (18:38 +0200)
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 <nitzan@google.com>
Co-authored-by: Han-Wen Nienhuys <hanwen@google.com>
Change-Id: Ideaefd51789a382a8b499d1ca7ae0146d032f48b

org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java [new file with mode: 0644]

index 583767af3f7d98dcc63f5e8f586dcdbebf534646..88bc7ddd2df2b05536ea24e7f2a5289531a99112 100644 (file)
@@ -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<ApplyResult> {
                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<ApplyResult> {
        }
 
        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<ApplyResult> {
                                                                                                                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<ApplyResult> {
                                                        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<ApplyResult> {
                        }
                        try (OutputStream output = new FileOutputStream(f)) {
                                DirCacheCheckout.getContent(repository, path, checkOut,
-                                               new StreamLoader(buffer::openInputStream,
+                                               WorkTreeUpdater.createStreamLoader(buffer::openInputStream,
                                                                buffer.length()),
                                                null, output);
                        }
index bf2a78f6b309f2a4028ca0286adfecbe6ec3dcfd..df6068925b84e8590840c759fba990259c5e223c 100644 (file)
@@ -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();
index b9ab1d1b7af537804faa8ce5431376271e700830..949ab5f449a156484892b686c75316f5e7aafab0 100644 (file)
@@ -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<String> unmergedPaths = new ArrayList<>();
-
-       /**
-        * Files modified during this merge operation.
-        *
-        * @since 3.4
-        */
-       protected List<String> 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<String, DirCacheEntry> toBeCheckedOut = new HashMap<>();
+       protected List<String> 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<String> toBeDeleted = new ArrayList<>();
+       protected List<String> 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<String, CheckoutMetadata> checkoutMetadata;
-
-       /**
-        * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}.
-        */
-       private Map<String, CheckoutMetadata> 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<String, DirCacheEntry> 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<String> 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<String, CheckoutMetadata> 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 <code>false</code> 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<RawText> 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<RawText> 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<String> getModifiedFiles() {
-               return modifiedFiles;
+               return workTreeUpdater != null ? workTreeUpdater.getModifiedFiles() : modifiedFiles;
        }
 
        /**
@@ -1247,7 +1049,7 @@ public class ResolveMerger extends ThreeWayMerger {
         *         for this path.
         */
        public Map<String, DirCacheEntry> 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 (file)
index 0000000..f3531a6
--- /dev/null
@@ -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.
+ * <p>
+ * 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<String> modifiedFiles = new LinkedList<>();
+
+               /**
+                * Files in this list were failed to be deleted.
+                */
+               public List<String> 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<String, DirCacheEntry> toBeCheckedOut = new HashMap<>();
+
+       /**
+        * Files in this list will be deleted from the local copy at the end of the operation.
+        */
+       private final TreeMap<String, File> toBeDeleted = new TreeMap<>();
+
+       /**
+        * Keeps {@link CheckoutMetadata} for {@link #checkout()}.
+        */
+       private Map<String, CheckoutMetadata> checkoutMetadata;
+
+       /**
+        * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}.
+        */
+       private Map<String, CheckoutMetadata> 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.
+        *
+        * <p>This means: the operation wrote a new stage 0 entry for this path.</p>
+        *
+        * @return the map
+        */
+       public Map<String, DirCacheEntry> getToBeCheckedOut() {
+               return toBeCheckedOut;
+       }
+
+       /**
+        * Deletes the given file
+        * <p>
+        * 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<String, CheckoutMetadata> 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.
+        * <p></p>
+        * 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<String> 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<String, DirCacheEntry> 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