[checkout] Use .gitattributes from the commit to be checked out

JGit used only one set of attributes constructed from the global and
info attributes, plus the attributes from working tree, index, and
HEAD.

These attributes must be used to determine whether the working tree is
dirty.

But for actually checking out a file, one must use the attributes from
global, info, and *the commit to be checked out*. Otherwise one may not
pick up definitions that are only in the .gitattributes of the commit
to be checked out or that are changed in that commit with respect to
the attributes currently in HEAD, the index, or the working tree.

Maintain in TreeWalk different Attributes per tree, and add operations
to determine EOL handling and smudge filters per tree.

Use the new methods in DirCacheCheckout and ResolveMerger. Note that
merging in JGit actually used the attributes from the base, not those
from ours, which looks dubious at least. It now uses those from ours,
and for checking out the ones from theirs.

The canBeContentMerged() determination was also done from the base
attributes, and is newly done from the ours attributes. Possibly this
should take into account all three attributes, and only if all three
agree the item can be content merged, a content merge should be
attempted? (What if the binary/text setting changes between base, ours,
or theirs?)

Also note that JGit attempts to perform content merges on non-binary
LFS files; there it used the filter attribute from base, too, even for
the ours and theirs versions. Newly it takes the filter attribute from
the correct tree. I'm not convinced doing content merges on potentially
huge files like LFS files is really a good idea.

Add tests in FilterCommandsTest and LfsGitTest to verify the behavior.

Open question: using index and working tree as fallback for the
attributes of ours (assuming it is HEAD) is OK. But does it also make
sense for base and theirs in merging?

Bug: 578707
Change-Id: I0bf433e9e3eb28479b6272e17c0666e175e67d08
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2022-02-13 23:30:36 +01:00 committed by Matthias Sohn
parent 72bba7bd53
commit f26ab4ebee
7 changed files with 489 additions and 145 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
* Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@ -67,6 +67,27 @@ public void setUp() throws Exception {
config.save();
}
@Test
public void testBranchSwitch() throws Exception {
git.branchCreate().setName("abranch").call();
git.checkout().setName("abranch").call();
File aFile = writeTrashFile("a.bin", "aaa");
writeTrashFile(".gitattributes", "a.bin filter=lfs");
git.add().addFilepattern(".").call();
git.commit().setMessage("acommit").call();
git.checkout().setName("master").call();
git.branchCreate().setName("bbranch").call();
git.checkout().setName("bbranch").call();
File bFile = writeTrashFile("b.bin", "bbb");
writeTrashFile(".gitattributes", "b.bin filter=lfs");
git.add().addFilepattern(".").call();
git.commit().setMessage("bcommit").call();
git.checkout().setName("abranch").call();
checkFile(aFile, "aaa");
git.checkout().setName("bbranch").call();
checkFile(bFile, "bbb");
}
@Test
public void checkoutNonLfsPointer() throws Exception {
String content = "size_t\nsome_function(void* ptr);\n";

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016, Christian Halstrick <christian.halstrick@sap.com> and others
* Copyright (C) 2016, 2022 Christian Halstrick <christian.halstrick@sap.com> 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
@ -10,12 +10,17 @@
package org.eclipse.jgit.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.attributes.FilterCommand;
import org.eclipse.jgit.attributes.FilterCommandFactory;
@ -86,6 +91,14 @@ public void setUp() throws Exception {
secondCommit = git.commit().setMessage("Second commit").call();
}
@Override
public void tearDown() throws Exception {
Set<String> existingFilters = new HashSet<>(
FilterCommandRegistry.getRegisteredFilterCommands());
existingFilters.forEach(FilterCommandRegistry::unregister);
super.tearDown();
}
@Test
public void testBuiltinCleanFilter()
throws IOException, GitAPIException {
@ -217,4 +230,133 @@ public void testBuiltinCleanAndSmudgeFilter() throws IOException, GitAPIExceptio
config.save();
}
@Test
public void testBranchSwitch() throws Exception {
String builtinCommandPrefix = "jgit://builtin/test/";
FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
new TestCommandFactory('s'));
FilterCommandRegistry.register(builtinCommandPrefix + "clean",
new TestCommandFactory('c'));
StoredConfig config = git.getRepository().getConfig();
config.setString("filter", "test", "smudge",
builtinCommandPrefix + "smudge");
config.setString("filter", "test", "clean",
builtinCommandPrefix + "clean");
config.save();
// We're on the test branch
File aFile = writeTrashFile("a.txt", "a");
writeTrashFile(".gitattributes", "a.txt filter=test");
File cFile = writeTrashFile("cc/c.txt", "C");
writeTrashFile("cc/.gitattributes", "c.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On test").call();
git.checkout().setName("master").call();
git.branchCreate().setName("other").call();
git.checkout().setName("other").call();
writeTrashFile("b.txt", "b");
writeTrashFile(".gitattributes", "b.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On other").call();
git.checkout().setName("test").call();
checkFile(aFile, "scsa");
checkFile(cFile, "scsC");
}
@Test
public void testCheckoutSingleFile() throws Exception {
String builtinCommandPrefix = "jgit://builtin/test/";
FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
new TestCommandFactory('s'));
FilterCommandRegistry.register(builtinCommandPrefix + "clean",
new TestCommandFactory('c'));
StoredConfig config = git.getRepository().getConfig();
config.setString("filter", "test", "smudge",
builtinCommandPrefix + "smudge");
config.setString("filter", "test", "clean",
builtinCommandPrefix + "clean");
config.save();
// We're on the test branch
File aFile = writeTrashFile("a.txt", "a");
File attributes = writeTrashFile(".gitattributes", "a.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On test").call();
git.checkout().setName("master").call();
git.branchCreate().setName("other").call();
git.checkout().setName("other").call();
writeTrashFile("b.txt", "b");
writeTrashFile(".gitattributes", "b.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On other").call();
git.checkout().setName("master").call();
assertFalse(aFile.exists());
assertFalse(attributes.exists());
git.checkout().setStartPoint("test").addPath("a.txt").call();
checkFile(aFile, "scsa");
}
@Test
public void testCheckoutSingleFile2() throws Exception {
String builtinCommandPrefix = "jgit://builtin/test/";
FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
new TestCommandFactory('s'));
FilterCommandRegistry.register(builtinCommandPrefix + "clean",
new TestCommandFactory('c'));
StoredConfig config = git.getRepository().getConfig();
config.setString("filter", "test", "smudge",
builtinCommandPrefix + "smudge");
config.setString("filter", "test", "clean",
builtinCommandPrefix + "clean");
config.save();
// We're on the test branch
File aFile = writeTrashFile("a.txt", "a");
File attributes = writeTrashFile(".gitattributes", "a.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On test").call();
git.checkout().setName("master").call();
git.branchCreate().setName("other").call();
git.checkout().setName("other").call();
writeTrashFile("b.txt", "b");
writeTrashFile(".gitattributes", "b.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On other").call();
git.checkout().setName("master").call();
assertFalse(aFile.exists());
assertFalse(attributes.exists());
writeTrashFile(".gitattributes", "");
git.checkout().setStartPoint("test").addPath("a.txt").call();
checkFile(aFile, "scsa");
}
@Test
public void testMerge() throws Exception {
String builtinCommandPrefix = "jgit://builtin/test/";
FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
new TestCommandFactory('s'));
FilterCommandRegistry.register(builtinCommandPrefix + "clean",
new TestCommandFactory('c'));
StoredConfig config = git.getRepository().getConfig();
config.setString("filter", "test", "smudge",
builtinCommandPrefix + "smudge");
config.setString("filter", "test", "clean",
builtinCommandPrefix + "clean");
config.save();
// We're on the test branch. Set up two branches that are expected to
// merge cleanly.
File aFile = writeTrashFile("a.txt", "a");
writeTrashFile(".gitattributes", "a.txt filter=test");
git.add().addFilepattern(".").call();
RevCommit aCommit = git.commit().setMessage("On test").call();
git.checkout().setName("master").call();
assertFalse(aFile.exists());
git.branchCreate().setName("other").call();
git.checkout().setName("other").call();
writeTrashFile("b/b.txt", "b");
writeTrashFile("b/.gitattributes", "b.txt filter=test");
git.add().addFilepattern(".").call();
git.commit().setMessage("On other").call();
MergeResult result = git.merge().include(aCommit).call();
assertEquals(MergeResult.MergeStatus.MERGED, result.getMergeStatus());
checkFile(aFile, "scsa");
}
}

View File

@ -39,6 +39,26 @@
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger">
<filter id="338792546">
<message_arguments>
<message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
<message_argument value="addCheckoutMetadata(String, Attributes)"/>
</message_arguments>
</filter>
<filter id="338792546">
<message_arguments>
<message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
<message_argument value="addToCheckout(String, DirCacheEntry, Attributes)"/>
</message_arguments>
</filter>
<filter id="338792546">
<message_arguments>
<message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
<message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean, Attributes)"/>
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection">
<filter id="338792546">
<message_arguments>

View File

@ -1,43 +1,11 @@
/*
* Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
* Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> and others
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
* 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.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.attributes;
@ -46,6 +14,7 @@
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.function.Supplier;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.attributes.Attribute.State;
@ -84,6 +53,8 @@ public class AttributesHandler {
private final TreeWalk treeWalk;
private final Supplier<CanonicalTreeParser> attributesTree;
private final AttributesNode globalNode;
private final AttributesNode infoNode;
@ -98,22 +69,41 @@ public class AttributesHandler {
* @param treeWalk
* a {@link org.eclipse.jgit.treewalk.TreeWalk}
* @throws java.io.IOException
* @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)}
* instead
*/
@Deprecated
public AttributesHandler(TreeWalk treeWalk) throws IOException {
this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class));
}
/**
* Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with
* default rules as well as merged rules from global, info and worktree root
* attributes
*
* @param treeWalk
* a {@link org.eclipse.jgit.treewalk.TreeWalk}
* @param attributesTree
* the tree to read .gitattributes from
* @throws java.io.IOException
* @since 6.1
*/
public AttributesHandler(TreeWalk treeWalk,
Supplier<CanonicalTreeParser> attributesTree) throws IOException {
this.treeWalk = treeWalk;
AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider();
this.attributesTree = attributesTree;
AttributesNodeProvider attributesNodeProvider = treeWalk
.getAttributesNodeProvider();
this.globalNode = attributesNodeProvider != null
? attributesNodeProvider.getGlobalAttributesNode() : null;
this.infoNode = attributesNodeProvider != null
? attributesNodeProvider.getInfoAttributesNode() : null;
AttributesNode rootNode = attributesNode(treeWalk,
rootOf(
treeWalk.getTree(WorkingTreeIterator.class)),
rootOf(
treeWalk.getTree(DirCacheIterator.class)),
rootOf(treeWalk
.getTree(CanonicalTreeParser.class)));
rootOf(treeWalk.getTree(WorkingTreeIterator.class)),
rootOf(treeWalk.getTree(DirCacheIterator.class)),
rootOf(attributesTree.get()));
expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES);
for (AttributesNode node : new AttributesNode[] { globalNode, rootNode,
@ -152,7 +142,7 @@ public Attributes getAttributes() throws IOException {
isDirectory,
treeWalk.getTree(WorkingTreeIterator.class),
treeWalk.getTree(DirCacheIterator.class),
treeWalk.getTree(CanonicalTreeParser.class),
attributesTree.get(),
attributes);
// Gets the attributes located in the global attribute file

View File

@ -4,7 +4,8 @@
* 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>
* Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com>
* Copyright (C) 2017, 2022, Thomas Wolf <thomas.wolf@paranor.ch> 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
@ -299,7 +300,7 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException {
walk = new NameConflictTreeWalk(repo);
builder = dc.builder();
addTree(walk, headCommitTree);
walk.setHead(addTree(walk, headCommitTree));
addTree(walk, mergeCommitTree);
int dciPos = walk.addTree(new DirCacheBuildIterator(builder));
walk.addTree(workingTree);
@ -315,13 +316,6 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException {
}
}
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.
@ -341,7 +335,7 @@ public void prescanOneTree()
builder = dc.builder();
walk = new NameConflictTreeWalk(repo);
addTree(walk, mergeCommitTree);
walk.setHead(addTree(walk, mergeCommitTree));
int dciPos = walk.addTree(new DirCacheBuildIterator(builder));
walk.addTree(workingTree);
workingTree.setDirCacheIterator(walk, dciPos);
@ -356,6 +350,14 @@ public void prescanOneTree()
conflicts.removeAll(removed);
}
private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException,
IncorrectObjectTypeException, IOException {
if (id == null) {
return tw.addTree(new EmptyTreeIterator());
}
return tw.addTree(id);
}
/**
* Processing an entry in the context of {@link #prescanOneTree()} when only
* one tree is given
@ -382,17 +384,14 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
// 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());
update(m);
}
} else
update(m.getEntryPathString(), m.getEntryObjectId(),
m.getEntryFileMode());
update(m);
} 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());
update(m);
} else if (i.getDirCacheEntry() != null) {
// The index contains a file (and not a folder)
if (f.isModified(i.getDirCacheEntry(), true,
@ -400,8 +399,7 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
|| i.getDirCacheEntry().getStage() != 0)
// The working tree file is dirty or the index contains a
// conflict
update(m.getEntryPathString(), m.getEntryObjectId(),
m.getEntryFileMode());
update(m);
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.
@ -802,7 +800,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
if (f != null && isModifiedSubtree_IndexWorkingtree(name)) {
conflict(name, dce, h, m); // 1
} else {
update(name, mId, mMode); // 2
update(1, name, mId, mMode); // 2
}
break;
@ -828,7 +826,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
// are found later
break;
case 0xD0F: // 19
update(name, mId, mMode);
update(1, name, mId, mMode);
break;
case 0xDF0: // conflict without a rule
case 0x0FD: // 15
@ -839,7 +837,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
if (isModifiedSubtree_IndexWorkingtree(name))
conflict(name, dce, h, m); // 8
else
update(name, mId, mMode); // 7
update(1, name, mId, mMode); // 7
} else
conflict(name, dce, h, m); // 9
break;
@ -859,7 +857,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
break;
case 0x0DF: // 16 17
if (!isModifiedSubtree_IndexWorkingtree(name))
update(name, mId, mMode);
update(1, name, mId, mMode);
else
conflict(name, dce, h, m);
break;
@ -929,7 +927,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
// 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
update(1, name, mId, mMode); // 1
else if (m == null)
// Nothing in Merge
// Something in Head
@ -947,7 +945,7 @@ else if (m == null)
// find in Merge. Potentially updates the file.
if (equalIdAndMode(hId, hMode, mId, mMode)) {
if (initialCheckout || force) {
update(name, mId, mMode);
update(1, name, mId, mMode);
} else {
keep(name, dce, f);
}
@ -1131,7 +1129,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode,
// TODO check that we don't overwrite some unsaved
// file content
update(name, mId, mMode);
update(1, name, mId, mMode);
} else if (dce != null
&& (f != null && f.isModified(dce, true,
this.walk.getObjectReader()))) {
@ -1150,7 +1148,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode,
// -> Standard case when switching between branches:
// Nothing new in index but something different in
// Merge. Update index and file
update(name, mId, mMode);
update(1, name, mId, mMode);
}
} else {
// Head differs from index or merge is same as index
@ -1237,12 +1235,17 @@ private void remove(String path) {
removed.add(path);
}
private void update(String path, ObjectId mId, FileMode mode)
throws IOException {
private void update(CanonicalTreeParser tree) throws IOException {
update(0, tree.getEntryPathString(), tree.getEntryObjectId(),
tree.getEntryFileMode());
}
private void update(int index, 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)));
walk.getCheckoutEolStreamType(index),
walk.getSmudgeCommand(index)));
DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0);
entry.setObjectId(mId);

View File

@ -3,7 +3,7 @@
* Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2012, Research In Motion Limited
* Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
* Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@ -276,11 +276,15 @@ public enum MergeFailureReason {
private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
/**
* Keeps {@link CheckoutMetadata} for {@link #checkout()} and
* {@link #cleanUp()}.
* Keeps {@link CheckoutMetadata} for {@link #checkout()}.
*/
private Map<String, CheckoutMetadata> checkoutMetadata;
/**
* Keeps {@link CheckoutMetadata} for {@link #cleanUp()}.
*/
private Map<String, CheckoutMetadata> cleanupMetadata;
private static MergeAlgorithm getMergeAlgorithm(Config config) {
SupportedAlgorithm diffAlg = config.getEnum(
CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM,
@ -383,12 +387,14 @@ protected boolean mergeImpl() throws IOException {
}
if (!inCore) {
checkoutMetadata = new HashMap<>();
cleanupMetadata = new HashMap<>();
}
try {
return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1],
false);
} finally {
checkoutMetadata = null;
cleanupMetadata = null;
if (implicitDirCache) {
dircache.unlock();
}
@ -447,7 +453,7 @@ protected void cleanUp() throws NoWorkTreeException,
DirCacheEntry entry = dc.getEntry(mpath);
if (entry != null) {
DirCacheCheckout.checkoutEntry(db, entry, reader, false,
checkoutMetadata.get(mpath));
cleanupMetadata.get(mpath));
}
mpathsIt.remove();
}
@ -501,22 +507,26 @@ private DirCacheEntry keep(DirCacheEntry e) {
* Remembers the {@link CheckoutMetadata} for the given path; it may be
* needed in {@link #checkout()} or in {@link #cleanUp()}.
*
* @param map
* to add the metadata to
* @param path
* of the current node
* @param attributes
* for the current node
* to use for determining the metadata
* @throws IOException
* if the smudge filter cannot be determined
* @since 5.1
* @since 6.1
*/
protected void addCheckoutMetadata(String path, Attributes attributes)
protected void addCheckoutMetadata(Map<String, CheckoutMetadata> map,
String path, Attributes attributes)
throws IOException {
if (checkoutMetadata != null) {
if (map != null) {
EolStreamType eol = EolStreamTypeUtil.detectStreamType(
OperationType.CHECKOUT_OP, workingTreeOptions, attributes);
OperationType.CHECKOUT_OP, workingTreeOptions,
attributes);
CheckoutMetadata data = new CheckoutMetadata(eol,
tw.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE));
checkoutMetadata.put(path, data);
tw.getSmudgeCommand(attributes));
map.put(path, data);
}
}
@ -529,15 +539,17 @@ protected void addCheckoutMetadata(String path, Attributes attributes)
* @param entry
* to add
* @param attributes
* for the current entry
* the {@link Attributes} of the trees
* @throws IOException
* if the {@link CheckoutMetadata} cannot be determined
* @since 5.1
* @since 6.1
*/
protected void addToCheckout(String path, DirCacheEntry entry,
Attributes attributes) throws IOException {
Attributes[] attributes)
throws IOException {
toBeCheckedOut.put(path, entry);
addCheckoutMetadata(path, attributes);
addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]);
addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]);
}
/**
@ -549,7 +561,7 @@ protected void addToCheckout(String path, DirCacheEntry entry,
* @param isFile
* whether it is a file
* @param attributes
* for the entry
* to use for determining the {@link CheckoutMetadata}
* @throws IOException
* if the {@link CheckoutMetadata} cannot be determined
* @since 5.1
@ -558,7 +570,7 @@ protected void addDeletion(String path, boolean isFile,
Attributes attributes) throws IOException {
toBeDeleted.add(path);
if (isFile) {
addCheckoutMetadata(path, attributes);
addCheckoutMetadata(cleanupMetadata, path, attributes);
}
}
@ -599,7 +611,7 @@ protected void addDeletion(String path, boolean isFile,
* see
* {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)}
* @param attributes
* the attributes defined for this entry
* the {@link Attributes} for the three trees
* @return <code>false</code> if the merge will fail because the index entry
* didn't match ours or the working-dir file was dirty and a
* conflict occurred
@ -607,12 +619,12 @@ protected void addDeletion(String path, boolean isFile,
* @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
* @throws org.eclipse.jgit.errors.CorruptObjectException
* @throws java.io.IOException
* @since 4.9
* @since 6.1
*/
protected boolean processEntry(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs,
DirCacheBuildIterator index, WorkingTreeIterator work,
boolean ignoreConflicts, Attributes attributes)
boolean ignoreConflicts, Attributes[] attributes)
throws MissingObjectException, IncorrectObjectTypeException,
CorruptObjectException, IOException {
enterSubtree = true;
@ -729,7 +741,7 @@ protected boolean processEntry(CanonicalTreeParser base,
// Base, ours, and theirs all contain a folder: don't delete
return true;
}
addDeletion(tw.getPathString(), nonTree(modeO), attributes);
addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]);
return true;
}
@ -772,7 +784,7 @@ protected boolean processEntry(CanonicalTreeParser base,
if (nonTree(modeO) && nonTree(modeT)) {
// Check worktree before modifying files
boolean worktreeDirty = isWorktreeDirty(work, ourDce);
if (!attributes.canBeContentMerged() && worktreeDirty) {
if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) {
return false;
}
@ -791,7 +803,7 @@ protected boolean processEntry(CanonicalTreeParser base,
mergeResults.put(tw.getPathString(), result);
unmergedPaths.add(tw.getPathString());
return true;
} else if (!attributes.canBeContentMerged()) {
} else if (!attributes[T_OURS].canBeContentMerged()) {
// File marked as binary
switch (getContentMergeStrategy()) {
case OURS:
@ -842,13 +854,16 @@ protected boolean processEntry(CanonicalTreeParser base,
if (ignoreConflicts) {
result.setContainsConflicts(false);
}
updateIndex(base, ours, theirs, result, attributes);
updateIndex(base, ours, theirs, result, attributes[T_OURS]);
String currentPath = tw.getPathString();
if (result.containsConflicts() && !ignoreConflicts) {
unmergedPaths.add(currentPath);
}
modifiedFiles.add(currentPath);
addCheckoutMetadata(currentPath, attributes);
addCheckoutMetadata(cleanupMetadata, currentPath,
attributes[T_OURS]);
addCheckoutMetadata(checkoutMetadata, currentPath,
attributes[T_THEIRS]);
} else if (modeO != modeT) {
// OURS or THEIRS has been deleted
if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw
@ -881,7 +896,8 @@ protected boolean processEntry(CanonicalTreeParser base,
// markers). But also stage 0 of the index is filled
// with that content.
result.setContainsConflicts(false);
updateIndex(base, ours, theirs, result, attributes);
updateIndex(base, ours, theirs, result,
attributes[T_OURS]);
} else {
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH,
0);
@ -896,11 +912,9 @@ protected boolean processEntry(CanonicalTreeParser base,
if (isWorktreeDirty(work, ourDce)) {
return false;
}
if (nonTree(modeT)) {
if (e != null) {
addToCheckout(tw.getPathString(), e,
attributes);
}
if (nonTree(modeT) && e != null) {
addToCheckout(tw.getPathString(), e,
attributes);
}
}
@ -945,14 +959,16 @@ private static MergeResult<SubmoduleConflict> createGitLinksMergeResult(
*/
private MergeResult<RawText> contentMerge(CanonicalTreeParser base,
CanonicalTreeParser ours, CanonicalTreeParser theirs,
Attributes attributes, ContentMergeStrategy strategy)
Attributes[] attributes, ContentMergeStrategy strategy)
throws BinaryBlobException, IOException {
// TW: The attributes here are used to determine the LFS smudge filter.
// Is doing a content merge on LFS items really a good idea??
RawText baseText = base == null ? RawText.EMPTY_TEXT
: getRawText(base.getEntryObjectId(), attributes);
: getRawText(base.getEntryObjectId(), attributes[T_BASE]);
RawText ourText = ours == null ? RawText.EMPTY_TEXT
: getRawText(ours.getEntryObjectId(), attributes);
: getRawText(ours.getEntryObjectId(), attributes[T_OURS]);
RawText theirsText = theirs == null ? RawText.EMPTY_TEXT
: getRawText(theirs.getEntryObjectId(), attributes);
: getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]);
mergeAlgorithm.setContentMergeStrategy(strategy);
return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText,
ourText, theirsText);
@ -1342,7 +1358,7 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree,
tw = new NameConflictTreeWalk(db, reader);
tw.addTree(baseTree);
tw.addTree(headTree);
tw.setHead(tw.addTree(headTree));
tw.addTree(mergeTree);
int dciPos = tw.addTree(buildIt);
if (workingTreeIterator != null) {
@ -1403,6 +1419,13 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts)
boolean hasAttributeNodeProvider = treeWalk
.getAttributesNodeProvider() != null;
while (treeWalk.next()) {
Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES,
NO_ATTRIBUTES };
if (hasAttributeNodeProvider) {
attributes[T_BASE] = treeWalk.getAttributes(T_BASE);
attributes[T_OURS] = treeWalk.getAttributes(T_OURS);
attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS);
}
if (!processEntry(
treeWalk.getTree(T_BASE, CanonicalTreeParser.class),
treeWalk.getTree(T_OURS, CanonicalTreeParser.class),
@ -1410,9 +1433,7 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts)
treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class),
hasWorkingTreeIterator ? treeWalk.getTree(T_FILE,
WorkingTreeIterator.class) : null,
ignoreConflicts, hasAttributeNodeProvider
? treeWalk.getAttributes()
: NO_ATTRIBUTES)) {
ignoreConflicts, attributes)) {
cleanUp();
return false;
}

View File

@ -1,6 +1,6 @@
/*
* Copyright (C) 2008-2009, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
* Copyright (C) 2008, 2009 Google Inc.
* Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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
@ -14,6 +14,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ -73,6 +74,7 @@
* threads.
*/
public class TreeWalk implements AutoCloseable, AttributesProvider {
private static final AbstractTreeIterator[] NO_TREES = {};
/**
@ -92,7 +94,7 @@ public enum OperationType {
}
/**
* Type of operation you want to retrieve the git attributes for.
* Type of operation you want to retrieve the git attributes for.
*/
private OperationType operationType = OperationType.CHECKOUT_OP;
@ -284,11 +286,20 @@ public static TreeWalk forPath(final Repository db, final String path,
AbstractTreeIterator currentHead;
/** Cached attribute for the current entry */
private Attributes attrs = null;
/**
* Cached attributes for the current entry; per tree. Index i+1 is for tree
* i; index 0 is for the deprecated legacy behavior.
*/
private Attributes[] attrs;
/** Cached attributes handler */
private AttributesHandler attributesHandler;
/**
* Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is
* for the deprecated legacy behavior.
*/
private AttributesHandler[] attributesHandlers;
/** Can be set to identify the tree to use for {@link #getAttributes()}. */
private int headIndex = -1;
private Config config;
@ -514,6 +525,24 @@ public AttributesNodeProvider getAttributesNodeProvider() {
return attributesNodeProvider;
}
/**
* Identifies the tree at the given index as the head tree. This is the tree
* use by default to determine attributes and EOL modes.
*
* @param index
* of the tree to use as head
* @throws IllegalArgumentException
* if the index is out of range
* @since 6.1
*/
public void setHead(int index) {
if (index < 0 || index >= trees.length) {
throw new IllegalArgumentException("Head index " + index //$NON-NLS-1$
+ " out of range [0," + trees.length + ')'); //$NON-NLS-1$
}
headIndex = index;
}
/**
* {@inheritDoc}
* <p>
@ -556,25 +585,51 @@ public AttributesNodeProvider getAttributesNodeProvider() {
*/
@Override
public Attributes getAttributes() {
if (attrs != null)
return attrs;
return getAttributes(headIndex);
}
/**
* Retrieves the git attributes based on the given tree.
*
* @param index
* of the tree to use as base for the attributes
* @return the attributes
* @since 6.1
*/
public Attributes getAttributes(int index) {
int attrIndex = index + 1;
Attributes result = attrs[attrIndex];
if (result != null) {
return result;
}
if (attributesNodeProvider == null) {
// The work tree should have a AttributesNodeProvider to be able to
// retrieve the info and global attributes node
throw new IllegalStateException(
"The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$
}
try {
// Lazy create the attributesHandler on the first access of
// attributes. This requires the info, global and root
// attributes nodes
if (attributesHandler == null) {
attributesHandler = new AttributesHandler(this);
AttributesHandler handler = attributesHandlers[attrIndex];
if (handler == null) {
if (index < 0) {
// Legacy behavior (headIndex not set, getAttributes() above
// called)
handler = new AttributesHandler(this, () -> {
return getTree(CanonicalTreeParser.class);
});
} else {
handler = new AttributesHandler(this, () -> {
AbstractTreeIterator tree = trees[index];
if (tree instanceof CanonicalTreeParser) {
return (CanonicalTreeParser) tree;
}
return null;
});
}
attributesHandlers[attrIndex] = handler;
}
attrs = attributesHandler.getAttributes();
return attrs;
result = handler.getAttributes();
attrs[attrIndex] = result;
return result;
} catch (IOException e) {
throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$
e);
@ -595,11 +650,34 @@ public Attributes getAttributes() {
*/
@Nullable
public EolStreamType getEolStreamType(OperationType opType) {
if (attributesNodeProvider == null || config == null)
if (attributesNodeProvider == null || config == null) {
return null;
return EolStreamTypeUtil.detectStreamType(
opType != null ? opType : operationType,
config.get(WorkingTreeOptions.KEY), getAttributes());
}
OperationType op = opType != null ? opType : operationType;
return EolStreamTypeUtil.detectStreamType(op,
config.get(WorkingTreeOptions.KEY), getAttributes());
}
/**
* Get the EOL stream type of the current entry for checking out using the
* config and {@link #getAttributes()}.
*
* @param tree
* index of the tree the check-out is to be from
* @return the EOL stream type of the current entry using the config and
* {@link #getAttributes()}. Note that this method may return null
* if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on
* a working tree
* @since 6.1
*/
@Nullable
public EolStreamType getCheckoutEolStreamType(int tree) {
if (attributesNodeProvider == null || config == null) {
return null;
}
Attributes attr = getAttributes(tree);
return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP,
config.get(WorkingTreeOptions.KEY), attr);
}
/**
@ -607,7 +685,8 @@ public EolStreamType getEolStreamType(OperationType opType) {
*/
public void reset() {
attrs = null;
attributesHandler = null;
attributesHandlers = null;
headIndex = -1;
trees = NO_TREES;
advance = false;
depth = 0;
@ -651,7 +730,9 @@ public void reset(AnyObjectId id) throws MissingObjectException,
advance = false;
depth = 0;
attrs = null;
attrs = new Attributes[2];
attributesHandlers = new AttributesHandler[2];
headIndex = -1;
}
/**
@ -701,7 +782,14 @@ public void reset(AnyObjectId... ids) throws MissingObjectException,
trees = r;
advance = false;
depth = 0;
attrs = null;
if (oldLen == newLen) {
Arrays.fill(attrs, null);
Arrays.fill(attributesHandlers, null);
} else {
attrs = new Attributes[newLen + 1];
attributesHandlers = new AttributesHandler[newLen + 1];
}
headIndex = -1;
}
/**
@ -758,6 +846,16 @@ public int addTree(AbstractTreeIterator p) {
p.matchShift = 0;
trees = newTrees;
if (attrs == null) {
attrs = new Attributes[n + 2];
} else {
attrs = Arrays.copyOf(attrs, n + 2);
}
if (attributesHandlers == null) {
attributesHandlers = new AttributesHandler[n + 2];
} else {
attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2);
}
return n;
}
@ -800,7 +898,7 @@ public boolean next() throws MissingObjectException,
}
for (;;) {
attrs = null;
Arrays.fill(attrs, null);
final AbstractTreeIterator t = min();
if (t.eof()) {
if (depth > 0) {
@ -1255,7 +1353,7 @@ public boolean isPostChildren() {
*/
public void enterSubtree() throws MissingObjectException,
IncorrectObjectTypeException, CorruptObjectException, IOException {
attrs = null;
Arrays.fill(attrs, null);
final AbstractTreeIterator ch = currentHead;
final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length];
for (int i = 0; i < trees.length; i++) {
@ -1374,11 +1472,12 @@ public <T extends AbstractTreeIterator> T getTree(Class<T> type) {
/**
* Inspect config and attributes to return a filtercommand applicable for
* the current path, but without expanding %f occurences
* the current path.
*
* @param filterCommandType
* which type of filterCommand should be executed. E.g. "clean",
* "smudge"
* "smudge". For "smudge" consider using
* {{@link #getSmudgeCommand(int)} instead.
* @return a filter command
* @throws java.io.IOException
* @since 4.2
@ -1406,6 +1505,54 @@ public String getFilterCommand(String filterCommandType)
QuotedString.BOURNE.quote((getPathString()))));
}
/**
* Inspect config and attributes to return a filtercommand applicable for
* the current path.
*
* @param index
* of the tree the item to be smudged is in
* @return a filter command
* @throws java.io.IOException
* @since 6.1
*/
public String getSmudgeCommand(int index)
throws IOException {
return getSmudgeCommand(getAttributes(index));
}
/**
* Inspect config and attributes to return a filtercommand applicable for
* the current path.
*
* @param attributes
* to use
* @return a filter command
* @throws java.io.IOException
* @since 6.1
*/
public String getSmudgeCommand(Attributes attributes) throws IOException {
if (attributes == null) {
return null;
}
Attribute f = attributes.get(Constants.ATTR_FILTER);
if (f == null) {
return null;
}
String filterValue = f.getValue();
if (filterValue == null) {
return null;
}
String filterCommand = getFilterCommandDefinition(filterValue,
Constants.ATTR_FILTER_TYPE_SMUDGE);
if (filterCommand == null) {
return null;
}
return filterCommand.replaceAll("%f", //$NON-NLS-1$
Matcher.quoteReplacement(
QuotedString.BOURNE.quote((getPathString()))));
}
/**
* Get the filter command how it is defined in gitconfig. The returned
* string may contain "%f" which needs to be replaced by the current path