]> source.dussan.org Git - jgit.git/commitdiff
Move WorkTreeUpdater to merge package 48/195448/7
authorHan-Wen Nienhuys <hanwen@google.com>
Tue, 30 Aug 2022 13:08:48 +0000 (15:08 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Tue, 6 Sep 2022 12:48:13 +0000 (14:48 +0200)
This avoids making it public with the associated costs for backward
compatibility guarantees.

Change-Id: I888f313f3854deace8d4cd92f354a6ef0d3b5460

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

index b134cdcf590b11b0e867cdc05f5a014360407287..23f8e4a5d57c1ce0b2995f9966f7a07d67a5f1ab 100644 (file)
@@ -60,7 +60,6 @@ import org.eclipse.jgit.treewalk.WorkingTreeIterator;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.LfsFactory;
-import org.eclipse.jgit.util.WorkTreeUpdater;
 import org.eclipse.jgit.util.TemporaryBuffer;
 
 /**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/WorkTreeUpdater.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/WorkTreeUpdater.java
new file mode 100644 (file)
index 0000000..1db9bc6
--- /dev/null
@@ -0,0 +1,635 @@
+/*
+ * 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.merge;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+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.time.Instant;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+
+import org.eclipse.jgit.annotations.NonNull;
+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.StreamSupplier;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.IndexWriteException;
+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.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.ObjectReader;
+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;
+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.
+ */
+class WorkTreeUpdater implements Closeable {
+
+       /**
+        * The result of writing the index changes.
+        */
+       public static class Result {
+
+               private final List<String> modifiedFiles = new LinkedList<>();
+
+
+               private final List<String> failedToDelete = new LinkedList<>();
+
+               private ObjectId treeId = null;
+
+               /**
+                * @return Modified tree ID if any, or null otherwise.
+                */
+               public ObjectId getTreeId() {
+                       return treeId;
+               }
+
+               /**
+                * @return Files that couldn't be deleted.
+                */
+               public List<String> getFailedToDelete() {
+                       return failedToDelete;
+               }
+
+               /**
+                * @return Files modified during this operation.
+                */
+               public List<String> getModifiedFiles() {
+                       return modifiedFiles;
+               }
+       }
+
+       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;
+
+       /**
+        * 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> checkoutMetadataByPath;
+
+       /**
+        * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}.
+        */
+       private Map<String, CheckoutMetadata> cleanupMetadataByPath;
+
+       /**
+        * Whether the changes were successfully written.
+        */
+       private boolean indexChangesWritten;
+
+       /**
+        * @param repo
+        *            the {@link 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();
+               Config config = repo.getConfig();
+               this.workingTreeOptions = config.get(WorkingTreeOptions.KEY);
+               this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config);
+               this.checkoutMetadataByPath = new HashMap<>();
+               this.cleanupMetadataByPath = new HashMap<>();
+       }
+
+       /**
+        * Creates a new {@link WorkTreeUpdater} for the given repository.
+        *
+        * @param repo
+        *            the {@link Repository}.
+        * @param dirCache
+        *            if set, use the provided dir cache. Otherwise, use the default
+        *            repository one
+        * @return the {@link WorkTreeUpdater}.
+        */
+       public static WorkTreeUpdater createWorkTreeUpdater(Repository repo,
+                       DirCache dirCache) {
+               return new WorkTreeUpdater(repo, dirCache);
+       }
+
+       /**
+        * @param repo
+        *            the {@link 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 = getInCoreFileSizeLimit(repo.getConfig());
+               }
+       }
+
+       /**
+        * Creates a new {@link WorkTreeUpdater} that works in memory only.
+        *
+        * @param repo
+        *            the {@link 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 the {@link WorkTreeUpdater}
+        */
+       public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo,
+                       DirCache dirCache, ObjectInserter oi) {
+               return new WorkTreeUpdater(repo, dirCache, oi);
+       }
+
+       private static int getInCoreFileSizeLimit(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 = nonNullRepo().lockDirCache();
+                       }
+               }
+               if (builder == null) {
+                       builder = dirCache.builder();
+               }
+               return dirCache;
+       }
+
+       /**
+        * Creates a {@link DirCacheBuildIterator} for the builder of this
+        * {@link WorkTreeUpdater}.
+        *
+        * @return the {@link DirCacheBuildIterator}
+        */
+       public DirCacheBuildIterator createDirCacheBuildIterator() {
+               return new DirCacheBuildIterator(builder);
+       }
+
+       /**
+        * Writes the changes to the working tree (but not to 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 {@link 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
+        */
+       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(cleanupMetadataByPath, path, cleanupStreamType,
+                               cleanupSmudgeCommand);
+               addCheckoutMetadata(checkoutMetadataByPath, path, checkoutStreamType,
+                               checkoutSmudgeCommand);
+       }
+
+       /**
+        * Gets 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;
+       }
+
+       /**
+        * Remembers the given file to be deleted.
+        * <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
+        */
+       public void deleteFile(String path, File file, EolStreamType streamType,
+                       String smudgeCommand) {
+               toBeDeleted.put(path, file);
+               if (file != null && file.isFile()) {
+                       addCheckoutMetadata(cleanupMetadataByPath, 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
+        */
+       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 {
+               for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
+                               .entrySet()) {
+                       DirCacheEntry dirCacheEntry = entry.getValue();
+                       if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
+                               new File(nonNullRepo().getWorkTree(), entry.getKey())
+                                               .mkdirs();
+                       } else {
+                               DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader,
+                                               false, checkoutMetadataByPath.get(entry.getKey()),
+                                               workingTreeOptions);
+                               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 of inCore operation just clear the
+        * history of modified files.
+        *
+        * @throws 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,
+                                               cleanupMetadataByPath.get(path), workingTreeOptions);
+                       }
+               }
+       }
+
+       @Override
+       public void close() throws IOException {
+               if (implicitDirCache) {
+                       dirCache.unlock();
+               }
+       }
+
+       /**
+        * Updates the file in the checkout with the given content.
+        *
+        * @param inputStream
+        *            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
+        * @throws IOException
+        *             if the file cannot be updated
+        */
+       public void updateFileWithContent(StreamSupplier inputStream,
+                       EolStreamType streamType, String smudgeCommand, String path,
+                       File file) throws IOException {
+               if (inCore) {
+                       return;
+               }
+               CheckoutMetadata metadata = new CheckoutMetadata(streamType,
+                               smudgeCommand);
+
+               try (OutputStream outputStream = new FileOutputStream(file)) {
+                       DirCacheCheckout.getContent(repo, path, metadata,
+                                       inputStream, workingTreeOptions, outputStream);
+               }
+       }
+
+       /**
+        * Creates a path with the given content, and adds it to the specified stage
+        * to the index builder.
+        *
+        * @param input
+        *            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 input,
+                       byte[] path, FileMode fileMode, int entryStage,
+                       Instant lastModified, int len, Attribute lfsAttribute)
+                       throws IOException {
+               return addExistingToIndex(insertResult(input, lfsAttribute, len), 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(InputStream input,
+                       Attribute lfsAttribute, long length) throws IOException {
+               try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter(repo,
+                               input, length,
+                               lfsAttribute)) {
+                       return inserter.insert(OBJ_BLOB, is.getLength(), is);
+               }
+       }
+
+       /**
+        * Gets the non-null repository instance of this {@link WorkTreeUpdater}.
+        *
+        * @return non-null repository instance
+        * @throws NullPointerException
+        *             if the handler was constructed without a repository.
+        */
+       @NonNull
+       private Repository nonNullRepo() throws NullPointerException {
+               return Objects.requireNonNull(repo,
+                               () -> JGitText.get().repositoryIsRequired);
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/WorkTreeUpdater.java
deleted file mode 100644 (file)
index 16cd7f8..0000000
+++ /dev/null
@@ -1,636 +0,0 @@
-/*
- * 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.Closeable;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.TreeMap;
-
-import org.eclipse.jgit.annotations.NonNull;
-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.StreamSupplier;
-import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
-import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.IndexWriteException;
-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.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.ObjectReader;
-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.
- *
- * @since 6.3
- */
-public class WorkTreeUpdater implements Closeable {
-
-       /**
-        * The result of writing the index changes.
-        */
-       public static class Result {
-
-               private final List<String> modifiedFiles = new LinkedList<>();
-
-
-               private final List<String> failedToDelete = new LinkedList<>();
-
-               private ObjectId treeId = null;
-
-               /**
-                * @return Modified tree ID if any, or null otherwise.
-                */
-               public ObjectId getTreeId() {
-                       return treeId;
-               }
-
-               /**
-                * @return Files that couldn't be deleted.
-                */
-               public List<String> getFailedToDelete() {
-                       return failedToDelete;
-               }
-
-               /**
-                * @return Files modified during this operation.
-                */
-               public List<String> getModifiedFiles() {
-                       return modifiedFiles;
-               }
-       }
-
-       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;
-
-       /**
-        * 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> checkoutMetadataByPath;
-
-       /**
-        * Keeps {@link CheckoutMetadata} for {@link #revertModifiedFiles()}.
-        */
-       private Map<String, CheckoutMetadata> cleanupMetadataByPath;
-
-       /**
-        * Whether the changes were successfully written.
-        */
-       private boolean indexChangesWritten;
-
-       /**
-        * @param repo
-        *            the {@link 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();
-               Config config = repo.getConfig();
-               this.workingTreeOptions = config.get(WorkingTreeOptions.KEY);
-               this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config);
-               this.checkoutMetadataByPath = new HashMap<>();
-               this.cleanupMetadataByPath = new HashMap<>();
-       }
-
-       /**
-        * Creates a new {@link WorkTreeUpdater} for the given repository.
-        *
-        * @param repo
-        *            the {@link Repository}.
-        * @param dirCache
-        *            if set, use the provided dir cache. Otherwise, use the default
-        *            repository one
-        * @return the {@link WorkTreeUpdater}.
-        */
-       public static WorkTreeUpdater createWorkTreeUpdater(Repository repo,
-                       DirCache dirCache) {
-               return new WorkTreeUpdater(repo, dirCache);
-       }
-
-       /**
-        * @param repo
-        *            the {@link 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 = getInCoreFileSizeLimit(repo.getConfig());
-               }
-       }
-
-       /**
-        * Creates a new {@link WorkTreeUpdater} that works in memory only.
-        *
-        * @param repo
-        *            the {@link 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 the {@link WorkTreeUpdater}
-        */
-       public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo,
-                       DirCache dirCache, ObjectInserter oi) {
-               return new WorkTreeUpdater(repo, dirCache, oi);
-       }
-
-       private static int getInCoreFileSizeLimit(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 = nonNullRepo().lockDirCache();
-                       }
-               }
-               if (builder == null) {
-                       builder = dirCache.builder();
-               }
-               return dirCache;
-       }
-
-       /**
-        * Creates a {@link DirCacheBuildIterator} for the builder of this
-        * {@link WorkTreeUpdater}.
-        *
-        * @return the {@link DirCacheBuildIterator}
-        */
-       public DirCacheBuildIterator createDirCacheBuildIterator() {
-               return new DirCacheBuildIterator(builder);
-       }
-
-       /**
-        * Writes the changes to the working tree (but not to 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 {@link 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
-        */
-       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(cleanupMetadataByPath, path, cleanupStreamType,
-                               cleanupSmudgeCommand);
-               addCheckoutMetadata(checkoutMetadataByPath, path, checkoutStreamType,
-                               checkoutSmudgeCommand);
-       }
-
-       /**
-        * Gets 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;
-       }
-
-       /**
-        * Remembers the given file to be deleted.
-        * <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
-        */
-       public void deleteFile(String path, File file, EolStreamType streamType,
-                       String smudgeCommand) {
-               toBeDeleted.put(path, file);
-               if (file != null && file.isFile()) {
-                       addCheckoutMetadata(cleanupMetadataByPath, 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
-        */
-       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 {
-               for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
-                               .entrySet()) {
-                       DirCacheEntry dirCacheEntry = entry.getValue();
-                       if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
-                               new File(nonNullRepo().getWorkTree(), entry.getKey())
-                                               .mkdirs();
-                       } else {
-                               DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader,
-                                               false, checkoutMetadataByPath.get(entry.getKey()),
-                                               workingTreeOptions);
-                               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 of inCore operation just clear the
-        * history of modified files.
-        *
-        * @throws 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,
-                                               cleanupMetadataByPath.get(path), workingTreeOptions);
-                       }
-               }
-       }
-
-       @Override
-       public void close() throws IOException {
-               if (implicitDirCache) {
-                       dirCache.unlock();
-               }
-       }
-
-       /**
-        * Updates the file in the checkout with the given content.
-        *
-        * @param inputStream
-        *            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
-        * @throws IOException
-        *             if the file cannot be updated
-        */
-       public void updateFileWithContent(StreamSupplier inputStream,
-                       EolStreamType streamType, String smudgeCommand, String path,
-                       File file) throws IOException {
-               if (inCore) {
-                       return;
-               }
-               CheckoutMetadata metadata = new CheckoutMetadata(streamType,
-                               smudgeCommand);
-
-               try (OutputStream outputStream = new FileOutputStream(file)) {
-                       DirCacheCheckout.getContent(repo, path, metadata,
-                                       inputStream, workingTreeOptions, outputStream);
-               }
-       }
-
-       /**
-        * Creates a path with the given content, and adds it to the specified stage
-        * to the index builder.
-        *
-        * @param input
-        *            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 input,
-                       byte[] path, FileMode fileMode, int entryStage,
-                       Instant lastModified, int len, Attribute lfsAttribute)
-                       throws IOException {
-               return addExistingToIndex(insertResult(input, lfsAttribute, len), 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(InputStream input,
-                       Attribute lfsAttribute, long length) throws IOException {
-               try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter(repo,
-                               input, length,
-                               lfsAttribute)) {
-                       return inserter.insert(OBJ_BLOB, is.getLength(), is);
-               }
-       }
-
-       /**
-        * Gets the non-null repository instance of this {@link WorkTreeUpdater}.
-        *
-        * @return non-null repository instance
-        * @throws NullPointerException
-        *             if the handler was constructed without a repository.
-        */
-       @NonNull
-       private Repository nonNullRepo() throws NullPointerException {
-               return Objects.requireNonNull(repo,
-                               () -> JGitText.get().repositoryIsRequired);
-       }
-}