/*
 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
 * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
 * Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com>
 * Copyright (C) 2019-2020, Andre Bossert <andre.bossert@siemens.com>
 *
 * 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.dircache;

import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.FilterFailedException;
import org.eclipse.jgit.attributes.FilterCommand;
import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.errors.CheckoutConflictException;
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.events.WorkingTreeModifiedEvent;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.CoreConfig.SymLinks;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectChecker;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.WorkingTreeOptions;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.IntList;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.EolStreamTypeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

This class handles checking out one or two trees merging with the index.
/** * This class handles checking out one or two trees merging with the index. */
public class DirCacheCheckout { private static final Logger LOG = LoggerFactory .getLogger(DirCacheCheckout.class); private static final int MAX_EXCEPTION_TEXT_SIZE = 10 * 1024;
Metadata used in checkout process
Since:4.3
/** * Metadata used in checkout process * * @since 4.3 */
public static class CheckoutMetadata {
git attributes
/** git attributes */
public final EolStreamType eolStreamType;
filter command to apply
/** filter command to apply */
public final String smudgeFilterCommand;
Params:
  • eolStreamType –
  • smudgeFilterCommand –
/** * @param eolStreamType * @param smudgeFilterCommand */
public CheckoutMetadata(EolStreamType eolStreamType, String smudgeFilterCommand) { this.eolStreamType = eolStreamType; this.smudgeFilterCommand = smudgeFilterCommand; } static CheckoutMetadata EMPTY = new CheckoutMetadata( EolStreamType.DIRECT, null); } private Repository repo; private HashMap<String, CheckoutMetadata> updated = new HashMap<>(); private ArrayList<String> conflicts = new ArrayList<>(); private ArrayList<String> removed = new ArrayList<>(); private ArrayList<String> kept = new ArrayList<>(); private ObjectId mergeCommitTree; private DirCache dc; private DirCacheBuilder builder; private NameConflictTreeWalk walk; private ObjectId headCommitTree; private WorkingTreeIterator workingTree; private boolean failOnConflict = true; private boolean force = false; private ArrayList<String> toBeDeleted = new ArrayList<>(); private boolean initialCheckout; private boolean performingCheckout; private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
Get list of updated paths and smudgeFilterCommands
Returns:a list of updated paths and smudgeFilterCommands
/** * Get list of updated paths and smudgeFilterCommands * * @return a list of updated paths and smudgeFilterCommands */
public Map<String, CheckoutMetadata> getUpdated() { return updated; }
Get a list of conflicts created by this checkout
Returns:a list of conflicts created by this checkout
/** * Get a list of conflicts created by this checkout * * @return a list of conflicts created by this checkout */
public List<String> getConflicts() { return conflicts; }
Get list of paths of files which couldn't be deleted during last call to checkout()
Returns:a list of paths (relative to the start of the working tree) of files which couldn't be deleted during last call to checkout() . checkout() detected that these files should be deleted but the deletion in the filesystem failed (e.g. because a file was locked). To have a consistent state of the working tree these files have to be deleted by the callers of DirCacheCheckout.
/** * Get list of paths of files which couldn't be deleted during last call to * {@link #checkout()} * * @return a list of paths (relative to the start of the working tree) of * files which couldn't be deleted during last call to * {@link #checkout()} . {@link #checkout()} detected that these * files should be deleted but the deletion in the filesystem failed * (e.g. because a file was locked). To have a consistent state of * the working tree these files have to be deleted by the callers of * {@link org.eclipse.jgit.dircache.DirCacheCheckout}. */
public List<String> getToBeDeleted() { return toBeDeleted; }
Get list of all files removed by this checkout
Returns:a list of all files removed by this checkout
/** * Get list of all files removed by this checkout * * @return a list of all files removed by this checkout */
public List<String> getRemoved() { return removed; }
Constructs a DirCacheCeckout for merging and checking out two trees (HEAD and mergeCommitTree) and the index.
Params:
  • repo – the repository in which we do the checkout
  • headCommitTree – the id of the tree of the head commit
  • dc – the (already locked) Dircache for this repo
  • mergeCommitTree – the id of the tree we want to fast-forward to
  • workingTree – an iterator over the repositories Working Tree
Throws:
/** * Constructs a DirCacheCeckout for merging and checking out two trees (HEAD * and mergeCommitTree) and the index. * * @param repo * the repository in which we do the checkout * @param headCommitTree * the id of the tree of the head commit * @param dc * the (already locked) Dircache for this repo * @param mergeCommitTree * the id of the tree we want to fast-forward to * @param workingTree * an iterator over the repositories Working Tree * @throws java.io.IOException */
public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, ObjectId mergeCommitTree, WorkingTreeIterator workingTree) throws IOException { this.repo = repo; this.dc = dc; this.headCommitTree = headCommitTree; this.mergeCommitTree = mergeCommitTree; this.workingTree = workingTree; this.initialCheckout = !repo.isBare() && !repo.getIndexFile().exists(); }
Constructs a DirCacheCeckout for merging and checking out two trees (HEAD and mergeCommitTree) and the index. As iterator over the working tree this constructor creates a standard FileTreeIterator
Params:
  • repo – the repository in which we do the checkout
  • headCommitTree – the id of the tree of the head commit
  • dc – the (already locked) Dircache for this repo
  • mergeCommitTree – the id of the tree we want to fast-forward to
Throws:
/** * Constructs a DirCacheCeckout for merging and checking out two trees (HEAD * and mergeCommitTree) and the index. As iterator over the working tree * this constructor creates a standard * {@link org.eclipse.jgit.treewalk.FileTreeIterator} * * @param repo * the repository in which we do the checkout * @param headCommitTree * the id of the tree of the head commit * @param dc * the (already locked) Dircache for this repo * @param mergeCommitTree * the id of the tree we want to fast-forward to * @throws java.io.IOException */
public DirCacheCheckout(Repository repo, ObjectId headCommitTree, DirCache dc, ObjectId mergeCommitTree) throws IOException { this(repo, headCommitTree, dc, mergeCommitTree, new FileTreeIterator(repo)); }
Constructs a DirCacheCeckout for checking out one tree, merging with the index.
Params:
  • repo – the repository in which we do the checkout
  • dc – the (already locked) Dircache for this repo
  • mergeCommitTree – the id of the tree we want to fast-forward to
  • workingTree – an iterator over the repositories Working Tree
Throws:
/** * Constructs a DirCacheCeckout for checking out one tree, merging with the * index. * * @param repo * the repository in which we do the checkout * @param dc * the (already locked) Dircache for this repo * @param mergeCommitTree * the id of the tree we want to fast-forward to * @param workingTree * an iterator over the repositories Working Tree * @throws java.io.IOException */
public DirCacheCheckout(Repository repo, DirCache dc, ObjectId mergeCommitTree, WorkingTreeIterator workingTree) throws IOException { this(repo, null, dc, mergeCommitTree, workingTree); }
Constructs a DirCacheCeckout for checking out one tree, merging with the index. As iterator over the working tree this constructor creates a standard FileTreeIterator
Params:
  • repo – the repository in which we do the checkout
  • dc – the (already locked) Dircache for this repo
  • mergeCommitTree – the id of the tree of the
Throws:
/** * Constructs a DirCacheCeckout for checking out one tree, merging with the * index. As iterator over the working tree this constructor creates a * standard {@link org.eclipse.jgit.treewalk.FileTreeIterator} * * @param repo * the repository in which we do the checkout * @param dc * the (already locked) Dircache for this repo * @param mergeCommitTree * the id of the tree of the * @throws java.io.IOException */
public DirCacheCheckout(Repository repo, DirCache dc, ObjectId mergeCommitTree) throws IOException { this(repo, null, dc, mergeCommitTree, new FileTreeIterator(repo)); }
Set a progress monitor which can be passed to built-in filter commands, providing progress information for long running tasks.
Params:
Since:4.11
/** * Set a progress monitor which can be passed to built-in filter commands, * providing progress information for long running tasks. * * @param monitor * the {@link ProgressMonitor} * @since 4.11 */
public void setProgressMonitor(ProgressMonitor monitor) { this.monitor = monitor != null ? monitor : NullProgressMonitor.INSTANCE; }
Scan head, index and merge tree. Used during normal checkout or merge operations.
Throws:
/** * Scan head, index and merge tree. Used during normal checkout or merge * operations. * * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException */
public void preScanTwoTrees() throws CorruptObjectException, IOException { removed.clear(); updated.clear(); conflicts.clear(); walk = new NameConflictTreeWalk(repo); builder = dc.builder(); addTree(walk, headCommitTree); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); while (walk.next()) { processEntry(walk.getTree(0, CanonicalTreeParser.class), walk.getTree(1, CanonicalTreeParser.class), walk.getTree(2, DirCacheBuildIterator.class), walk.getTree(3, WorkingTreeIterator.class)); if (walk.isSubtree()) walk.enterSubtree(); } } private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException { if (id == null) tw.addTree(new EmptyTreeIterator()); else tw.addTree(id); }
Scan index and merge tree (no HEAD). Used e.g. for initial checkout when there is no head yet.
Throws:
/** * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when * there is no head yet. * * @throws org.eclipse.jgit.errors.MissingObjectException * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException */
public void prescanOneTree() throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { removed.clear(); updated.clear(); conflicts.clear(); builder = dc.builder(); walk = new NameConflictTreeWalk(repo); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); while (walk.next()) { processEntry(walk.getTree(0, CanonicalTreeParser.class), walk.getTree(1, DirCacheBuildIterator.class), walk.getTree(2, WorkingTreeIterator.class)); if (walk.isSubtree()) walk.enterSubtree(); } conflicts.removeAll(removed); }
Processing an entry in the context of prescanOneTree() when only one tree is given
Params:
  • m – the tree to merge
  • i – the index
  • f – the working tree
Throws:
/** * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given * * @param m the tree to merge * @param i the index * @param f the working tree * @throws IOException */
void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, WorkingTreeIterator f) throws IOException { if (m != null) { checkValidPath(m); // There is an entry in the merge commit. Means: we want to update // what's currently in the index and working-tree to that one if (i == null) { // The index entry is missing if (f != null && !FileMode.TREE.equals(f.getEntryFileMode()) && !f.isEntryIgnored()) { if (failOnConflict) { // don't overwrite an untracked and not ignored file conflicts.add(walk.getPathString()); } else { // failOnConflict is false. Putting something to conflicts // would mean we delete it. Instead we want the mergeCommit // content to be checked out. update(m.getEntryPathString(), m.getEntryObjectId(), m.getEntryFileMode()); } } else update(m.getEntryPathString(), m.getEntryObjectId(), m.getEntryFileMode()); } else if (f == null || !m.idEqual(i)) { // The working tree file is missing or the merge content differs // from index content update(m.getEntryPathString(), m.getEntryObjectId(), m.getEntryFileMode()); } else if (i.getDirCacheEntry() != null) { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, this.walk.getObjectReader()) || i.getDirCacheEntry().getStage() != 0) // The working tree file is dirty or the index contains a // conflict update(m.getEntryPathString(), m.getEntryObjectId(), m.getEntryFileMode()); else { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. DirCacheEntry entry = i.getDirCacheEntry(); Instant mtime = entry.getLastModifiedInstant(); if (mtime == null || mtime.equals(Instant.EPOCH)) { entry.setLastModified(f.getEntryLastModifiedInstant()); } keep(i.getEntryPathString(), entry, f); } } else // The index contains a folder keep(i.getEntryPathString(), i.getDirCacheEntry(), f); } else { // There is no entry in the merge commit. Means: we want to delete // what's currently in the index and working tree if (f != null) { // There is a file/folder for that path in the working tree if (walk.isDirectoryFileConflict()) { // We put it in conflicts. Even if failOnConflict is false // this would cause the path to be deleted. Thats exactly what // we want in this situation conflicts.add(walk.getPathString()); } else { // No file/folder conflict exists. All entries are files or // all entries are folders if (i != null) { // ... and the working tree contained a file or folder // -> add it to the removed set and remove it from // conflicts set remove(i.getEntryPathString()); conflicts.remove(i.getEntryPathString()); } else { // untracked file, neither contained in tree to merge // nor in index } } } else { // There is no file/folder for that path in the working tree, // nor in the merge head. // The only entry we have is the index entry. Like the case // where there is a file with the same name, remove it, } } }
Execute this checkout. A WorkingTreeModifiedEvent is fired if the working tree was modified; even if the checkout fails.
Throws:
Returns:false if this method could not delete all the files which should be deleted (e.g. because one of the files was locked). In this case getToBeDeleted() lists the files which should be tried to be deleted outside of this method. Although false is returned the checkout was successful and the working tree was updated for all other files. true is returned when no such problem occurred
/** * Execute this checkout. A * {@link org.eclipse.jgit.events.WorkingTreeModifiedEvent} is fired if the * working tree was modified; even if the checkout fails. * * @return <code>false</code> if this method could not delete all the files * which should be deleted (e.g. because one of the files was * locked). In this case {@link #getToBeDeleted()} lists the files * which should be tried to be deleted outside of this method. * Although <code>false</code> is returned the checkout was * successful and the working tree was updated for all other files. * <code>true</code> is returned when no such problem occurred * @throws java.io.IOException */
public boolean checkout() throws IOException { try { return doCheckout(); } catch (CanceledException ce) { // should actually be propagated, but this would change a LOT of // APIs throw new IOException(ce); } finally { try { dc.unlock(); } finally { if (performingCheckout) { Set<String> touched = new HashSet<>(conflicts); touched.addAll(getUpdated().keySet()); touched.addAll(kept); WorkingTreeModifiedEvent event = new WorkingTreeModifiedEvent( touched, getRemoved()); if (!event.isEmpty()) { repo.fireEvent(event); } } } } } private boolean doCheckout() throws CorruptObjectException, IOException, MissingObjectException, IncorrectObjectTypeException, CheckoutConflictException, IndexWriteException, CanceledException { toBeDeleted.clear(); try (ObjectReader objectReader = repo.getObjectDatabase().newReader()) { if (headCommitTree != null) preScanTwoTrees(); else prescanOneTree(); if (!conflicts.isEmpty()) { if (failOnConflict) { throw new CheckoutConflictException(conflicts.toArray(new String[0])); } cleanUpConflicts(); } // update our index builder.finish(); // init progress reporting int numTotal = removed.size() + updated.size() + conflicts.size(); monitor.beginTask(JGitText.get().checkingOutFiles, numTotal); performingCheckout = true; File file = null; String last = null; // when deleting files process them in the opposite order as they have // been reported. This ensures the files are deleted before we delete // their parent folders IntList nonDeleted = new IntList(); for (int i = removed.size() - 1; i >= 0; i--) { String r = removed.get(i); file = new File(repo.getWorkTree(), r); if (!file.delete() && repo.getFS().exists(file)) { // The list of stuff to delete comes from the index // which will only contain a directory if it is // a submodule, in which case we shall not attempt // to delete it. A submodule is not empty, so it // is safe to check this after a failed delete. if (!repo.getFS().isDirectory(file)) { nonDeleted.add(i); toBeDeleted.add(r); } } else { if (last != null && !isSamePrefix(r, last)) removeEmptyParents(new File(repo.getWorkTree(), last)); last = r; } monitor.update(1); if (monitor.isCancelled()) { throw new CanceledException(MessageFormat.format( JGitText.get().operationCanceled, JGitText.get().checkingOutFiles)); } } if (file != null) { removeEmptyParents(file); } removed = filterOut(removed, nonDeleted); nonDeleted = null; Iterator<Map.Entry<String, CheckoutMetadata>> toUpdate = updated .entrySet().iterator(); Map.Entry<String, CheckoutMetadata> e = null; try { while (toUpdate.hasNext()) { e = toUpdate.next(); String path = e.getKey(); CheckoutMetadata meta = e.getValue(); DirCacheEntry entry = dc.getEntry(path); if (FileMode.GITLINK.equals(entry.getRawMode())) { checkoutGitlink(path, entry); } else { checkoutEntry(repo, entry, objectReader, false, meta); } e = null; monitor.update(1); if (monitor.isCancelled()) { throw new CanceledException(MessageFormat.format( JGitText.get().operationCanceled, JGitText.get().checkingOutFiles)); } } } catch (Exception ex) { // We didn't actually modify the current entry nor any that // might follow. if (e != null) { toUpdate.remove(); } while (toUpdate.hasNext()) { e = toUpdate.next(); toUpdate.remove(); } throw ex; } for (String conflict : conflicts) { // the conflicts are likely to have multiple entries in the // dircache, we only want to check out the one for the "theirs" // tree int entryIdx = dc.findEntry(conflict); if (entryIdx >= 0) { while (entryIdx < dc.getEntryCount()) { DirCacheEntry entry = dc.getEntry(entryIdx); if (!entry.getPathString().equals(conflict)) { break; } if (entry.getStage() == DirCacheEntry.STAGE_3) { checkoutEntry(repo, entry, objectReader, false, null); break; } ++entryIdx; } } monitor.update(1); if (monitor.isCancelled()) { throw new CanceledException(MessageFormat.format( JGitText.get().operationCanceled, JGitText.get().checkingOutFiles)); } } monitor.endTask(); // commit the index builder - a new index is persisted if (!builder.commit()) throw new IndexWriteException(); } return toBeDeleted.isEmpty(); } private void checkoutGitlink(String path, DirCacheEntry entry) throws IOException { File gitlinkDir = new File(repo.getWorkTree(), path); FileUtils.mkdirs(gitlinkDir, true); FS fs = repo.getFS(); entry.setLastModified(fs.lastModifiedInstant(gitlinkDir)); } private static ArrayList<String> filterOut(ArrayList<String> strings, IntList indicesToRemove) { int n = indicesToRemove.size(); if (n == strings.size()) { return new ArrayList<>(0); } switch (n) { case 0: return strings; case 1: strings.remove(indicesToRemove.get(0)); return strings; default: int length = strings.size(); ArrayList<String> result = new ArrayList<>(length - n); // Process indicesToRemove from the back; we know that it // contains indices in descending order. int j = n - 1; int idx = indicesToRemove.get(j); for (int i = 0; i < length; i++) { if (i == idx) { idx = (--j >= 0) ? indicesToRemove.get(j) : -1; } else { result.add(strings.get(i)); } } return result; } } private static boolean isSamePrefix(String a, String b) { int as = a.lastIndexOf('/'); int bs = b.lastIndexOf('/'); return a.substring(0, as + 1).equals(b.substring(0, bs + 1)); } private void removeEmptyParents(File f) { File parentFile = f.getParentFile(); while (parentFile != null && !parentFile.equals(repo.getWorkTree())) { if (!parentFile.delete()) break; parentFile = parentFile.getParentFile(); } }
Compares whether two pairs of ObjectId and FileMode are equal.
Params:
  • id1 –
  • mode1 –
  • id2 –
  • mode2 –
Returns:true if FileModes and ObjectIds are equal. false otherwise
/** * Compares whether two pairs of ObjectId and FileMode are equal. * * @param id1 * @param mode1 * @param id2 * @param mode2 * @return <code>true</code> if FileModes and ObjectIds are equal. * <code>false</code> otherwise */
private boolean equalIdAndMode(ObjectId id1, FileMode mode1, ObjectId id2, FileMode mode2) { if (!mode1.equals(mode2)) return false; return id1 != null ? id1.equals(id2) : id2 == null; }
Here the main work is done. This method is called for each existing path in head, index and merge. This method decides what to do with the corresponding index entry: keep it, update it, remove it or mark a conflict.
Params:
  • h – the entry for the head
  • m – the entry for the merge
  • i – the entry for the index
  • f – the file in the working tree
Throws:
/** * Here the main work is done. This method is called for each existing path * in head, index and merge. This method decides what to do with the * corresponding index entry: keep it, update it, remove it or mark a * conflict. * * @param h * the entry for the head * @param m * the entry for the merge * @param i * the entry for the index * @param f * the file in the working tree * @throws IOException */
void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, DirCacheBuildIterator i, WorkingTreeIterator f) throws IOException { DirCacheEntry dce = i != null ? i.getDirCacheEntry() : null; String name = walk.getPathString(); if (m != null) checkValidPath(m); if (i == null && m == null && h == null) { // File/Directory conflict case #20 if (walk.isDirectoryFileConflict()) // TODO: check whether it is always correct to report a conflict here conflict(name, null, null, null); // file only exists in working tree -> ignore it return; } ObjectId iId = (i == null ? null : i.getEntryObjectId()); ObjectId mId = (m == null ? null : m.getEntryObjectId()); ObjectId hId = (h == null ? null : h.getEntryObjectId()); FileMode iMode = (i == null ? null : i.getEntryFileMode()); FileMode mMode = (m == null ? null : m.getEntryFileMode()); FileMode hMode = (h == null ? null : h.getEntryFileMode()); /** * <pre> * File/Directory conflicts: * the following table from ReadTreeTest tells what to do in case of directory/file * conflicts. I give comments here * * H I M Clean H==M H==I I==M Result * ------------------------------------------------------------------ * 1 D D F Y N Y N Update * 2 D D F N N Y N Conflict * 3 D F D Y N N Keep * 4 D F D N N N Conflict * 5 D F F Y N N Y Keep * 5b D F F Y N N N Conflict * 6 D F F N N N Y Keep * 6b D F F N N N N Conflict * 7 F D F Y Y N N Update * 8 F D F N Y N N Conflict * 9 F D F N N N Conflict * 10 F D D N N Y Keep * 11 F D D N N N Conflict * 12 F F D Y N Y N Update * 13 F F D N N Y N Conflict * 14 F F D N N N Conflict * 15 0 F D N N N Conflict * 16 0 D F Y N N N Update * 17 0 D F N N N Conflict * 18 F 0 D Update * 19 D 0 F Update * 20 0 0 F N (worktree=dir) Conflict * </pre> */ // The information whether head,index,merge iterators are currently // pointing to file/folder/non-existing is encoded into this variable. // // To decode write down ffMask in hexadecimal form. The last digit // represents the state for the merge iterator, the second last the // state for the index iterator and the third last represents the state // for the head iterator. The hexadecimal constant "F" stands for // "file", a "D" stands for "directory" (tree), and a "0" stands for // non-existing. Symbolic links and git links are treated as File here. // // Examples: // ffMask == 0xFFD -> Head=File, Index=File, Merge=Tree // ffMask == 0xDD0 -> Head=Tree, Index=Tree, Merge=Non-Existing int ffMask = 0; if (h != null) ffMask = FileMode.TREE.equals(hMode) ? 0xD00 : 0xF00; if (i != null) ffMask |= FileMode.TREE.equals(iMode) ? 0x0D0 : 0x0F0; if (m != null) ffMask |= FileMode.TREE.equals(mMode) ? 0x00D : 0x00F; // Check whether we have a possible file/folder conflict. Therefore we // need a least one file and one folder. if (((ffMask & 0x222) != 0x000) && (((ffMask & 0x00F) == 0x00D) || ((ffMask & 0x0F0) == 0x0D0) || ((ffMask & 0xF00) == 0xD00))) { // There are 3*3*3=27 possible combinations of file/folder // conflicts. Some of them are not-relevant because // they represent no conflict, e.g. 0xFFF, 0xDDD, ... The following // switch processes all relevant cases. switch (ffMask) { case 0xDDF: // 1 2 if (f != null && isModifiedSubtree_IndexWorkingtree(name)) { conflict(name, dce, h, m); // 1 } else { update(name, mId, mMode); // 2 } break; case 0xDFD: // 3 4 keep(name, dce, f); break; case 0xF0D: // 18 remove(name); break; case 0xDFF: // 5 5b 6 6b if (equalIdAndMode(iId, iMode, mId, mMode)) keep(name, dce, f); // 5 6 else conflict(name, dce, h, m); // 5b 6b break; case 0xFDD: // 10 11 // TODO: make use of tree extension as soon as available in jgit // we would like to do something like // if (!equalIdAndMode(iId, iMode, mId, mMode) // conflict(name, i.getDirCacheEntry(), h, m); // But since we don't know the id of a tree in the index we do // nothing here and wait that conflicts between index and merge // are found later break; case 0xD0F: // 19 update(name, mId, mMode); break; case 0xDF0: // conflict without a rule case 0x0FD: // 15 conflict(name, dce, h, m); break; case 0xFDF: // 7 8 9 if (equalIdAndMode(hId, hMode, mId, mMode)) { if (isModifiedSubtree_IndexWorkingtree(name)) conflict(name, dce, h, m); // 8 else update(name, mId, mMode); // 7 } else conflict(name, dce, h, m); // 9 break; case 0xFD0: // keep without a rule keep(name, dce, f); break; case 0xFFD: // 12 13 14 if (equalIdAndMode(hId, hMode, iId, iMode)) if (f != null && f.isModified(dce, true, this.walk.getObjectReader())) conflict(name, dce, h, m); // 13 else remove(name); // 12 else conflict(name, dce, h, m); // 14 break; case 0x0DF: // 16 17 if (!isModifiedSubtree_IndexWorkingtree(name)) update(name, mId, mMode); else conflict(name, dce, h, m); break; default: keep(name, dce, f); } return; } if ((ffMask & 0x222) == 0) { // HEAD, MERGE and index don't contain a file (e.g. all contain a // folder) if (f == null || FileMode.TREE.equals(f.getEntryFileMode())) { // the workingtree entry doesn't exist or also contains a folder // -> no problem return; } // the workingtree entry exists and is not a folder if (!idEqual(h, m)) { // Because HEAD and MERGE differ we will try to update the // workingtree with a folder -> return a conflict conflict(name, null, null, null); } return; } if ((ffMask == 0x00F) && f != null && FileMode.TREE.equals(f.getEntryFileMode())) { // File/Directory conflict case #20 conflict(name, null, h, m); return; } if (i == null) { // Nothing in Index // At least one of Head, Index, Merge is not empty // make sure not to overwrite untracked files if (f != null && !f.isEntryIgnored()) { // A submodule is not a file. We should ignore it if (!FileMode.GITLINK.equals(mMode)) { // a dirty worktree: the index is empty but we have a // workingtree-file if (mId == null || !equalIdAndMode(mId, mMode, f.getEntryObjectId(), f.getEntryFileMode())) { conflict(name, null, h, m); return; } } } /** * <pre> * I (index) H M H==M Result * ------------------------------------------- * 0 nothing nothing nothing (does not happen) * 1 nothing nothing exists use M * 2 nothing exists nothing remove path from index * 3 nothing exists exists yes keep index if not in initial checkout * , otherwise use M * nothing exists exists no fail * </pre> */ if (h == null) // Nothing in Head // Nothing in Index // At least one of Head, Index, Merge is not empty // -> only Merge contains something for this path. Use it! // Potentially update the file update(name, mId, mMode); // 1 else if (m == null) // Nothing in Merge // Something in Head // Nothing in Index // -> only Head contains something for this path and it should // be deleted. Potentially removes the file! remove(name); // 2 else { // 3 // Something in Merge // Something in Head // Nothing in Index // -> Head and Merge contain something (maybe not the same) and // in the index there is nothing (e.g. 'git rm ...' was // called before). Ignore the cached deletion and use what we // find in Merge. Potentially updates the file. if (equalIdAndMode(hId, hMode, mId, mMode)) { if (initialCheckout) update(name, mId, mMode); else keep(name, dce, f); } else conflict(name, dce, h, m); } } else { // Something in Index if (h == null) { // Nothing in Head // Something in Index /** * <pre> * clean I==H I==M H M Result * ----------------------------------------------------- * 4 yes N/A N/A nothing nothing keep index * 5 no N/A N/A nothing nothing keep index * * 6 yes N/A yes nothing exists keep index * 7 no N/A yes nothing exists keep index * 8 yes N/A no nothing exists fail * 9 no N/A no nothing exists fail * </pre> */ if (m == null || !isModified_IndexTree(name, iId, iMode, mId, mMode, mergeCommitTree)) { // Merge contains nothing or the same as Index // Nothing in Head // Something in Index if (m==null && walk.isDirectoryFileConflict()) { // Nothing in Merge and current path is part of // File/Folder conflict // Nothing in Head // Something in Index if (dce != null && (f == null || f.isModified(dce, true, this.walk.getObjectReader()))) // No file or file is dirty // Nothing in Merge and current path is part of // File/Folder conflict // Nothing in Head // Something in Index // -> File folder conflict and Merge wants this // path to be removed. Since the file is dirty // report a conflict conflict(name, dce, h, m); else // A file is present and file is not dirty // Nothing in Merge and current path is part of // File/Folder conflict // Nothing in Head // Something in Index // -> File folder conflict and Merge wants this path // to be removed. Since the file is not dirty remove // file and index entry remove(name); } else // Something in Merge or current path is not part of // File/Folder conflict // Merge contains nothing or the same as Index // Nothing in Head // Something in Index // -> Merge contains nothing new. Keep the index. keep(name, dce, f); } else // Merge contains something and it is not the same as Index // Nothing in Head // Something in Index // -> Index contains something new (different from Head) // and Merge is different from Index. Report a conflict conflict(name, dce, h, m); } else if (m == null) { // Nothing in Merge // Something in Head // Something in Index /** * <pre> * clean I==H I==M H M Result * ----------------------------------------------------- * 10 yes yes N/A exists nothing remove path from index * 11 no yes N/A exists nothing keep file * 12 yes no N/A exists nothing fail * 13 no no N/A exists nothing fail * </pre> */ if (iMode == FileMode.GITLINK) { // A submodule in Index // Nothing in Merge // Something in Head // Submodules that disappear from the checkout must // be removed from the index, but not deleted from disk. remove(name); } else { // Something different from a submodule in Index // Nothing in Merge // Something in Head if (!isModified_IndexTree(name, iId, iMode, hId, hMode, headCommitTree)) { // Index contains the same as Head // Something different from a submodule in Index // Nothing in Merge // Something in Head if (f != null && f.isModified(dce, true, this.walk.getObjectReader())) { // file is dirty // Index contains the same as Head // Something different from a submodule in Index // Nothing in Merge // Something in Head if (!FileMode.TREE.equals(f.getEntryFileMode()) && FileMode.TREE.equals(iMode)) { // The workingtree contains a file and the index semantically contains a folder. // Git considers the workingtree file as untracked. Just keep the untracked file. return; } // -> file is dirty and tracked but is should be // removed. That's a conflict conflict(name, dce, h, m); } else { // file doesn't exist or is clean // Index contains the same as Head // Something different from a submodule in Index // Nothing in Merge // Something in Head // -> Remove from index and delete the file remove(name); } } else { // Index contains something different from Head // Something different from a submodule in Index // Nothing in Merge // Something in Head // -> Something new is in index (and maybe even on the // filesystem). But Merge wants the path to be removed. // Report a conflict conflict(name, dce, h, m); } } } else { // Something in Merge // Something in Head // Something in Index if (!equalIdAndMode(hId, hMode, mId, mMode) && isModified_IndexTree(name, iId, iMode, hId, hMode, headCommitTree) && isModified_IndexTree(name, iId, iMode, mId, mMode, mergeCommitTree)) // All three contents in Head, Merge, Index differ from each // other // -> All contents differ. Report a conflict. conflict(name, dce, h, m); else // At least two of the contents of Head, Index, Merge // are the same // Something in Merge // Something in Head // Something in Index if (!isModified_IndexTree(name, iId, iMode, hId, hMode, headCommitTree) && isModified_IndexTree(name, iId, iMode, mId, mMode, mergeCommitTree)) { // Head contains the same as Index. Merge differs // Something in Merge // For submodules just update the index with the new SHA-1 if (dce != null && FileMode.GITLINK.equals(dce.getFileMode())) { // Index and Head contain the same submodule. Merge // differs // Something in Merge // -> Nothing new in index. Move to merge. // Potentially updates the file // TODO check that we don't overwrite some unsaved // file content update(name, mId, mMode); } else if (dce != null && (f != null && f.isModified(dce, true, this.walk.getObjectReader()))) { // File exists and is dirty // Head and Index don't contain a submodule // Head contains the same as Index. Merge differs // Something in Merge // -> Merge wants the index and file to be updated // but the file is dirty. Report a conflict conflict(name, dce, h, m); } else { // File doesn't exist or is clean // Head and Index don't contain a submodule // Head contains the same as Index. Merge differs // Something in Merge // -> Standard case when switching between branches: // Nothing new in index but something different in // Merge. Update index and file update(name, mId, mMode); } } else { // Head differs from index or merge is same as index // At least two of the contents of Head, Index, Merge // are the same // Something in Merge // Something in Head // Something in Index // Can be formulated as: Either all three states are // equal or Merge is equal to Head or Index and differs // to the other one. // -> In all three cases we don't touch index and file. keep(name, dce, f); } } } } private static boolean idEqual(AbstractTreeIterator a, AbstractTreeIterator b) { if (a == b) { return true; } if (a == null || b == null) { return false; } return a.getEntryObjectId().equals(b.getEntryObjectId()); }
A conflict is detected - add the three different stages to the index
Params:
  • path – the path of the conflicting entry
  • e – the previous index entry
  • h – the first tree you want to merge (the HEAD)
  • m – the second tree you want to merge
/** * A conflict is detected - add the three different stages to the index * @param path the path of the conflicting entry * @param e the previous index entry * @param h the first tree you want to merge (the HEAD) * @param m the second tree you want to merge */
private void conflict(String path, DirCacheEntry e, AbstractTreeIterator h, AbstractTreeIterator m) { conflicts.add(path); DirCacheEntry entry; if (e != null) { entry = new DirCacheEntry(e.getPathString(), DirCacheEntry.STAGE_1); entry.copyMetaData(e, true); builder.add(entry); } if (h != null && !FileMode.TREE.equals(h.getEntryFileMode())) { entry = new DirCacheEntry(h.getEntryPathString(), DirCacheEntry.STAGE_2); entry.setFileMode(h.getEntryFileMode()); entry.setObjectId(h.getEntryObjectId()); builder.add(entry); } if (m != null && !FileMode.TREE.equals(m.getEntryFileMode())) { entry = new DirCacheEntry(m.getEntryPathString(), DirCacheEntry.STAGE_3); entry.setFileMode(m.getEntryFileMode()); entry.setObjectId(m.getEntryObjectId()); builder.add(entry); } } private void keep(String path, DirCacheEntry e, WorkingTreeIterator f) throws IOException { if (e != null && !FileMode.TREE.equals(e.getFileMode())) builder.add(e); if (force) { if (f == null || f.isModified(e, true, walk.getObjectReader())) { kept.add(path); checkoutEntry(repo, e, walk.getObjectReader(), false, new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP), walk.getFilterCommand( Constants.ATTR_FILTER_TYPE_SMUDGE))); } } } private void remove(String path) { removed.add(path); } private void update(String path, ObjectId mId, FileMode mode) throws IOException { if (!FileMode.TREE.equals(mode)) { updated.put(path, new CheckoutMetadata( walk.getEolStreamType(CHECKOUT_OP), walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE))); DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0); entry.setObjectId(mId); entry.setFileMode(mode); builder.add(entry); } }
If true, will scan first to see if it's possible to check out, otherwise throw CheckoutConflictException. If false, it will silently deal with the problem.
Params:
  • failOnConflict – a boolean.
/** * If <code>true</code>, will scan first to see if it's possible to check * out, otherwise throw * {@link org.eclipse.jgit.errors.CheckoutConflictException}. If * <code>false</code>, it will silently deal with the problem. * * @param failOnConflict * a boolean. */
public void setFailOnConflict(boolean failOnConflict) { this.failOnConflict = failOnConflict; }
If true, dirty worktree files may be overridden. If false dirty worktree files will not be overridden in order not to delete unsaved content. This corresponds to native git's 'git checkout -f' option. By default this option is set to false.
Params:
  • force – a boolean.
Since:5.3
/** * If <code>true</code>, dirty worktree files may be overridden. If * <code>false</code> dirty worktree files will not be overridden in order * not to delete unsaved content. This corresponds to native git's 'git * checkout -f' option. By default this option is set to false. * * @param force * a boolean. * @since 5.3 */
public void setForce(boolean force) { this.force = force; }
This method implements how to handle conflicts when failOnConflict is false
Throws:
/** * This method implements how to handle conflicts when * {@link #failOnConflict} is false * * @throws CheckoutConflictException */
private void cleanUpConflicts() throws CheckoutConflictException { // TODO: couldn't we delete unsaved worktree content here? for (String c : conflicts) { File conflict = new File(repo.getWorkTree(), c); if (!conflict.delete()) throw new CheckoutConflictException(MessageFormat.format( JGitText.get().cannotDeleteFile, c)); removeEmptyParents(conflict); } }
Checks whether the subtree starting at a given path differs between Index and workingtree.
Params:
  • path –
Throws:
Returns:true if the subtrees differ
/** * Checks whether the subtree starting at a given path differs between Index and * workingtree. * * @param path * @return true if the subtrees differ * @throws CorruptObjectException * @throws IOException */
private boolean isModifiedSubtree_IndexWorkingtree(String path) throws CorruptObjectException, IOException { try (NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { int dciPos = tw.addTree(new DirCacheIterator(dc)); FileTreeIterator fti = new FileTreeIterator(repo); tw.addTree(fti); fti.setDirCacheIterator(tw, dciPos); tw.setRecursive(true); tw.setFilter(PathFilter.create(path)); DirCacheIterator dcIt; WorkingTreeIterator wtIt; while (tw.next()) { dcIt = tw.getTree(0, DirCacheIterator.class); wtIt = tw.getTree(1, WorkingTreeIterator.class); if (dcIt == null || wtIt == null) return true; if (wtIt.isModified(dcIt.getDirCacheEntry(), true, this.walk.getObjectReader())) { return true; } } return false; } } private boolean isModified_IndexTree(String path, ObjectId iId, FileMode iMode, ObjectId tId, FileMode tMode, ObjectId rootTree) throws CorruptObjectException, IOException { if (iMode != tMode) { return true; } if (FileMode.TREE.equals(iMode) && (iId == null || ObjectId.zeroId().equals(iId))) { return isModifiedSubtree_IndexTree(path, rootTree); } return !equalIdAndMode(iId, iMode, tId, tMode); }
Checks whether the subtree starting at a given path differs between Index and some tree.
Params:
  • path –
  • tree – the tree to compare
Throws:
Returns:true if the subtrees differ
/** * Checks whether the subtree starting at a given path differs between Index and * some tree. * * @param path * @param tree * the tree to compare * @return true if the subtrees differ * @throws CorruptObjectException * @throws IOException */
private boolean isModifiedSubtree_IndexTree(String path, ObjectId tree) throws CorruptObjectException, IOException { try (NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { tw.addTree(new DirCacheIterator(dc)); tw.addTree(tree); tw.setRecursive(true); tw.setFilter(PathFilter.create(path)); while (tw.next()) { AbstractTreeIterator dcIt = tw.getTree(0, DirCacheIterator.class); AbstractTreeIterator treeIt = tw.getTree(1, AbstractTreeIterator.class); if (dcIt == null || treeIt == null) return true; if (dcIt.getEntryRawMode() != treeIt.getEntryRawMode()) return true; if (!dcIt.getEntryObjectId().equals(treeIt.getEntryObjectId())) return true; } return false; } }
Updates the file in the working tree with content and mode from an entry in the index. The new content is first written to a new temporary file in the same directory as the real file. Then that new file is renamed to the final filename.

Note: if the entry path on local file system exists as a non-empty directory, and the target entry type is a link or file, the checkout will fail with IOException since existing non-empty directory cannot be renamed to file or link without deleting it recursively.

TODO: this method works directly on File IO, we may need another abstraction (like WorkingTreeIterator). This way we could tell e.g. Eclipse that Files in the workspace got changed

Params:
  • repo – repository managing the destination work tree.
  • entry – the entry containing new mode and content
  • or – object reader to use for checkout
Throws:
Since:3.6
Deprecated:since 5.1, use checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata) instead
/** * Updates the file in the working tree with content and mode from an entry * in the index. The new content is first written to a new temporary file in * the same directory as the real file. Then that new file is renamed to the * final filename. * * <p> * <b>Note:</b> if the entry path on local file system exists as a non-empty * directory, and the target entry type is a link or file, the checkout will * fail with {@link java.io.IOException} since existing non-empty directory * cannot be renamed to file or link without deleting it recursively. * </p> * * <p> * TODO: this method works directly on File IO, we may need another * abstraction (like WorkingTreeIterator). This way we could tell e.g. * Eclipse that Files in the workspace got changed * </p> * * @param repo * repository managing the destination work tree. * @param entry * the entry containing new mode and content * @param or * object reader to use for checkout * @throws java.io.IOException * @since 3.6 * @deprecated since 5.1, use * {@link #checkoutEntry(Repository, DirCacheEntry, ObjectReader, boolean, CheckoutMetadata)} * instead */
@Deprecated public static void checkoutEntry(Repository repo, DirCacheEntry entry, ObjectReader or) throws IOException { checkoutEntry(repo, entry, or, false, null); }
Updates the file in the working tree with content and mode from an entry in the index. The new content is first written to a new temporary file in the same directory as the real file. Then that new file is renamed to the final filename.

Note: if the entry path on local file system exists as a file, it will be deleted and if it exists as a directory, it will be deleted recursively, independently if has any content.

TODO: this method works directly on File IO, we may need another abstraction (like WorkingTreeIterator). This way we could tell e.g. Eclipse that Files in the workspace got changed

Params:
  • repo – repository managing the destination work tree.
  • entry – the entry containing new mode and content
  • or – object reader to use for checkout
  • deleteRecursive – true to recursively delete final path if it exists on the file system
  • checkoutMetadata – containing
    • smudgeFilterCommand to be run for smudging the entry to be checked out
    • eolStreamType used for stream conversion
Throws:
Since:4.2
/** * Updates the file in the working tree with content and mode from an entry * in the index. The new content is first written to a new temporary file in * the same directory as the real file. Then that new file is renamed to the * final filename. * * <p> * <b>Note:</b> if the entry path on local file system exists as a file, it * will be deleted and if it exists as a directory, it will be deleted * recursively, independently if has any content. * </p> * * <p> * TODO: this method works directly on File IO, we may need another * abstraction (like WorkingTreeIterator). This way we could tell e.g. * Eclipse that Files in the workspace got changed * </p> * * @param repo * repository managing the destination work tree. * @param entry * the entry containing new mode and content * @param or * object reader to use for checkout * @param deleteRecursive * true to recursively delete final path if it exists on the file * system * @param checkoutMetadata * containing * <ul> * <li>smudgeFilterCommand to be run for smudging the entry to be * checked out</li> * <li>eolStreamType used for stream conversion</li> * </ul> * @throws java.io.IOException * @since 4.2 */
public static void checkoutEntry(Repository repo, DirCacheEntry entry, ObjectReader or, boolean deleteRecursive, CheckoutMetadata checkoutMetadata) throws IOException { if (checkoutMetadata == null) checkoutMetadata = CheckoutMetadata.EMPTY; ObjectLoader ol = or.open(entry.getObjectId()); File f = new File(repo.getWorkTree(), entry.getPathString()); File parentDir = f.getParentFile(); if (parentDir.isFile()) { FileUtils.delete(parentDir); } FileUtils.mkdirs(parentDir, true); FS fs = repo.getFS(); WorkingTreeOptions opt = repo.getConfig().get(WorkingTreeOptions.KEY); if (entry.getFileMode() == FileMode.SYMLINK && opt.getSymLinks() == SymLinks.TRUE) { byte[] bytes = ol.getBytes(); String target = RawParseUtils.decode(bytes); if (deleteRecursive && f.isDirectory()) { FileUtils.delete(f, FileUtils.RECURSIVE); } fs.createSymLink(f, target); entry.setLength(bytes.length); entry.setLastModified(fs.lastModifiedInstant(f)); return; } String name = f.getName(); if (name.length() > 200) { name = name.substring(0, 200); } File tmpFile = File.createTempFile( "._" + name, null, parentDir); //$NON-NLS-1$ getContent(repo, entry.getPathString(), checkoutMetadata, ol, opt, new FileOutputStream(tmpFile)); // The entry needs to correspond to the on-disk filesize. If the content // was filtered (either by autocrlf handling or smudge filters) ask the // filesystem again for the length. Otherwise the objectloader knows the // size if (checkoutMetadata.eolStreamType == EolStreamType.DIRECT && checkoutMetadata.smudgeFilterCommand == null) { entry.setLength(ol.getSize()); } else { entry.setLength(tmpFile.length()); } if (opt.isFileMode() && fs.supportsExecute()) { if (FileMode.EXECUTABLE_FILE.equals(entry.getRawMode())) { if (!fs.canExecute(tmpFile)) fs.setExecute(tmpFile, true); } else { if (fs.canExecute(tmpFile)) fs.setExecute(tmpFile, false); } } try { if (deleteRecursive && f.isDirectory()) { FileUtils.delete(f, FileUtils.RECURSIVE); } FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { throw new IOException( MessageFormat.format(JGitText.get().renameFileFailed, tmpFile.getPath(), f.getPath()), e); } finally { if (tmpFile.exists()) { FileUtils.delete(tmpFile); } } entry.setLastModified(fs.lastModifiedInstant(f)); }
Return filtered content for a specific object (blob). EOL handling and smudge-filter handling are applied in the same way as it would be done during a checkout.
Params:
  • repo – the repository
  • path – the path used to determine the correct filters for the object
  • checkoutMetadata – containing
    • smudgeFilterCommand to be run for smudging the object
    • eolStreamType used for stream conversion (can be null)
  • ol – the object loader to read raw content of the object
  • opt – the working tree options where only 'core.autocrlf' is used for EOL handling if 'checkoutMetadata.eolStreamType' is not valid
  • os – the output stream the filtered content is written to. The caller is responsible to close the stream.
Throws:
Since:5.7
/** * Return filtered content for a specific object (blob). EOL handling and * smudge-filter handling are applied in the same way as it would be done * during a checkout. * * @param repo * the repository * @param path * the path used to determine the correct filters for the object * @param checkoutMetadata * containing * <ul> * <li>smudgeFilterCommand to be run for smudging the object</li> * <li>eolStreamType used for stream conversion (can be * null)</li> * </ul> * @param ol * the object loader to read raw content of the object * @param opt * the working tree options where only 'core.autocrlf' is used * for EOL handling if 'checkoutMetadata.eolStreamType' is not * valid * @param os * the output stream the filtered content is written to. The * caller is responsible to close the stream. * @throws IOException * * @since 5.7 */
public static void getContent(Repository repo, String path, CheckoutMetadata checkoutMetadata, ObjectLoader ol, WorkingTreeOptions opt, OutputStream os) throws IOException { EolStreamType nonNullEolStreamType; if (checkoutMetadata.eolStreamType != null) { nonNullEolStreamType = checkoutMetadata.eolStreamType; } else if (opt.getAutoCRLF() == AutoCRLF.TRUE) { nonNullEolStreamType = EolStreamType.AUTO_CRLF; } else { nonNullEolStreamType = EolStreamType.DIRECT; } try (OutputStream channel = EolStreamTypeUtil.wrapOutputStream( os, nonNullEolStreamType)) { if (checkoutMetadata.smudgeFilterCommand != null) { if (FilterCommandRegistry .isRegistered(checkoutMetadata.smudgeFilterCommand)) { runBuiltinFilterCommand(repo, checkoutMetadata, ol, channel); } else { runExternalFilterCommand(repo, path, checkoutMetadata, ol, channel); } } else { ol.copyTo(channel); } } } // Run an external filter command private static void runExternalFilterCommand(Repository repo, String path, CheckoutMetadata checkoutMetadata, ObjectLoader ol, OutputStream channel) throws IOException { FS fs = repo.getFS(); ProcessBuilder filterProcessBuilder = fs.runInShell( checkoutMetadata.smudgeFilterCommand, new String[0]); filterProcessBuilder.directory(repo.getWorkTree()); filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY, repo.getDirectory().getAbsolutePath()); ExecutionResult result; int rc; try { // TODO: wire correctly with AUTOCRLF result = fs.execute(filterProcessBuilder, ol.openStream()); rc = result.getRc(); if (rc == 0) { result.getStdout().writeTo(channel, NullProgressMonitor.INSTANCE); } } catch (IOException | InterruptedException e) { throw new IOException(new FilterFailedException(e, checkoutMetadata.smudgeFilterCommand, path)); } if (rc != 0) { throw new IOException(new FilterFailedException(rc, checkoutMetadata.smudgeFilterCommand, path, result.getStdout().toByteArray(MAX_EXCEPTION_TEXT_SIZE), RawParseUtils.decode(result.getStderr() .toByteArray(MAX_EXCEPTION_TEXT_SIZE)))); } } // Run a builtin filter command private static void runBuiltinFilterCommand(Repository repo, CheckoutMetadata checkoutMetadata, ObjectLoader ol, OutputStream channel) throws MissingObjectException, IOException { boolean isMandatory = repo.getConfig().getBoolean( ConfigConstants.CONFIG_FILTER_SECTION, ConfigConstants.CONFIG_SECTION_LFS, ConfigConstants.CONFIG_KEY_REQUIRED, false); FilterCommand command = null; try { command = FilterCommandRegistry.createFilterCommand( checkoutMetadata.smudgeFilterCommand, repo, ol.openStream(), channel); } catch (IOException e) { LOG.error(JGitText.get().failedToDetermineFilterDefinition, e); if (!isMandatory) { // In case an IOException occurred during creating of the // command then proceed as if there would not have been a // builtin filter (only if the filter is not mandatory). ol.copyTo(channel); } else { throw e; } } if (command != null) { while (command.run() != -1) { // loop as long as command.run() tells there is work to do } } } @SuppressWarnings("deprecation") private static void checkValidPath(CanonicalTreeParser t) throws InvalidPathException { ObjectChecker chk = new ObjectChecker() .setSafeForWindows(SystemReader.getInstance().isWindows()) .setSafeForMacOS(SystemReader.getInstance().isMacOS()); for (CanonicalTreeParser i = t; i != null; i = i.getParent()) checkValidPathSegment(chk, i); } private static void checkValidPathSegment(ObjectChecker chk, CanonicalTreeParser t) throws InvalidPathException { try { int ptr = t.getNameOffset(); int end = ptr + t.getNameLength(); chk.checkPathSegment(t.getEntryPathBuffer(), ptr, end); } catch (CorruptObjectException err) { String path = t.getEntryPathString(); InvalidPathException i = new InvalidPathException(path); i.initCause(err); throw i; } } }