diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/merge')
16 files changed, 350 insertions, 174 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java index 6d568643d5..a835a1dfc5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ContentMergeStrategy.java @@ -23,5 +23,12 @@ public enum ContentMergeStrategy { OURS, /** Resolve the conflict hunk using the theirs version. */ - THEIRS -}
\ No newline at end of file + THEIRS, + + /** + * Resolve the conflict hunk using a union of both ours and theirs versions. + * + * @since 6.10.1 + */ + UNION +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java index e44970abff..8857ef8571 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java @@ -74,19 +74,21 @@ class EolAwareOutputStream extends OutputStream { write('\n'); } - /** @return true if a new line has just begun. */ + /** + * Whether a new line has just begun + * + * @return true if a new line has just begun. + */ boolean isBeginln() { return bol; } - /** {@inheritDoc} */ @Override public void write(int val) throws IOException { out.write(val); bol = (val == '\n'); } - /** {@inheritDoc} */ @Override public void write(byte[] buf, int pos, int cnt) throws IOException { if (cnt > 0) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java index 80607351ae..d0d4d367b9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java @@ -90,10 +90,16 @@ public final class MergeAlgorithm { /** * Does the three way merge between a common base and two sequences. * - * @param cmp comparison method for this execution. - * @param base the common base sequence - * @param ours the first sequence to be merged - * @param theirs the second sequence to be merged + * @param <S> + * type of the sequences + * @param cmp + * comparison method for this execution. + * @param base + * the common base sequence + * @param ours + * the first sequence to be merged + * @param theirs + * the second sequence to be merged * @return the resulting content */ public <S extends Sequence> MergeResult<S> merge( @@ -114,6 +120,7 @@ public final class MergeAlgorithm { result.add(1, 0, 0, ConflictState.NO_CONFLICT); break; case THEIRS: + case UNION: result.add(2, 0, theirs.size(), ConflictState.NO_CONFLICT); break; @@ -121,6 +128,8 @@ public final class MergeAlgorithm { // Let their complete content conflict with empty text result.add(1, 0, 0, ConflictState.FIRST_CONFLICTING_RANGE); + result.add(0, 0, base.size(), + ConflictState.BASE_CONFLICTING_RANGE); result.add(2, 0, theirs.size(), ConflictState.NEXT_CONFLICTING_RANGE); break; @@ -140,6 +149,7 @@ public final class MergeAlgorithm { // we modified, they deleted switch (strategy) { case OURS: + case UNION: result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT); break; case THEIRS: @@ -149,6 +159,8 @@ public final class MergeAlgorithm { // Let our complete content conflict with empty text result.add(1, 0, ours.size(), ConflictState.FIRST_CONFLICTING_RANGE); + result.add(0, 0, base.size(), + ConflictState.BASE_CONFLICTING_RANGE); result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE); break; } @@ -208,13 +220,18 @@ public final class MergeAlgorithm { // set some initial values for the ranges in A and B which we // want to handle + int oursBeginA = oursEdit.getBeginA(); + int theirsBeginA = theirsEdit.getBeginA(); int oursBeginB = oursEdit.getBeginB(); int theirsBeginB = theirsEdit.getBeginB(); // harmonize the start of the ranges in A and B if (oursEdit.getBeginA() < theirsEdit.getBeginA()) { + theirsBeginA -= theirsEdit.getBeginA() + - oursEdit.getBeginA(); theirsBeginB -= theirsEdit.getBeginA() - oursEdit.getBeginA(); } else { + oursBeginA -= oursEdit.getBeginA() - theirsEdit.getBeginA(); oursBeginB -= oursEdit.getBeginA() - theirsEdit.getBeginA(); } @@ -260,11 +277,15 @@ public final class MergeAlgorithm { } // harmonize the end of the ranges in A and B + int oursEndA = oursEdit.getEndA(); + int theirsEndA = theirsEdit.getEndA(); int oursEndB = oursEdit.getEndB(); int theirsEndB = theirsEdit.getEndB(); if (oursEdit.getEndA() < theirsEdit.getEndA()) { + oursEndA += theirsEdit.getEndA() - oursEdit.getEndA(); oursEndB += theirsEdit.getEndA() - oursEdit.getEndA(); } else { + theirsEndA += oursEdit.getEndA() - theirsEdit.getEndA(); theirsEndB += oursEdit.getEndA() - theirsEdit.getEndA(); } @@ -314,10 +335,27 @@ public final class MergeAlgorithm { theirsEndB - commonSuffix, ConflictState.NO_CONFLICT); break; + case UNION: + result.add(1, oursBeginB + commonPrefix, + oursEndB - commonSuffix, + ConflictState.NO_CONFLICT); + + result.add(2, theirsBeginB + commonPrefix, + theirsEndB - commonSuffix, + ConflictState.NO_CONFLICT); + break; default: result.add(1, oursBeginB + commonPrefix, oursEndB - commonSuffix, ConflictState.FIRST_CONFLICTING_RANGE); + + int baseBegin = Math.min(oursBeginA, theirsBeginA) + + commonPrefix; + int baseEnd = Math.min(base.size(), + Math.max(oursEndA, theirsEndA)) - commonSuffix; + result.add(0, baseBegin, baseEnd, + ConflictState.BASE_CONFLICTING_RANGE); + result.add(2, theirsBeginB + commonPrefix, theirsEndB - commonSuffix, ConflictState.NEXT_CONFLICTING_RANGE); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java index ca998e30bc..7102aa6998 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeChunk.java @@ -29,14 +29,22 @@ public class MergeChunk { NO_CONFLICT, /** - * This chunk does belong to a conflict and is the first one of the + * This chunk does belong to a conflict and is the ours section of the * conflicting chunks */ FIRST_CONFLICTING_RANGE, /** - * This chunk does belong to a conflict but is not the first one of the - * conflicting chunks. It's a subsequent one. + * This chunk does belong to a conflict and is the base section of the + * conflicting chunks + * + * @since 6.7 + */ + BASE_CONFLICTING_RANGE, + + /** + * This chunk does belong to a conflict and is the theirs section of + * the conflicting chunks. It's a subsequent one. */ NEXT_CONFLICTING_RANGE } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java index 18b0ad92c8..079db4a07f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java @@ -22,6 +22,7 @@ import org.eclipse.jgit.diff.RawText; * A class to convert merge results into a Git conformant textual presentation */ public class MergeFormatter { + /** * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText} * objects in a Git conformant way. This method also assumes that the @@ -39,27 +40,24 @@ public class MergeFormatter { * name. This name is following the "<<<<<<< * " or ">>>>>>> " conflict markers. The * names for the sequences are given in this list - * @param charsetName - * the name of the character set used when writing conflict - * metadata + * @param charset + * the character set used when writing conflict metadata * @throws java.io.IOException - * @deprecated Use - * {@link #formatMerge(OutputStream, MergeResult, List, Charset)} - * instead. + * if an IO error occurred + * @since 5.2 */ - @Deprecated public void formatMerge(OutputStream out, MergeResult<RawText> res, - List<String> seqName, String charsetName) throws IOException { - formatMerge(out, res, seqName, Charset.forName(charsetName)); + List<String> seqName, Charset charset) throws IOException { + new MergeFormatterPass(out, res, seqName, charset).formatMerge(); } /** * Formats the results of a merge of {@link org.eclipse.jgit.diff.RawText} - * objects in a Git conformant way. This method also assumes that the - * {@link org.eclipse.jgit.diff.RawText} objects being merged are line - * oriented files which use LF as delimiter. This method will also use LF to - * separate chunks and conflict metadata, therefore it fits only to texts - * that are LF-separated lines. + * objects in a Git conformant way using diff3 style. This method also + * assumes that the {@link org.eclipse.jgit.diff.RawText} objects being + * merged are line oriented files which use LF as delimiter. This method + * will also use LF to separate chunks and conflict metadata, therefore it + * fits only to texts that are LF-separated lines. * * @param out * the output stream where to write the textual presentation @@ -68,16 +66,18 @@ public class MergeFormatter { * @param seqName * When a conflict is reported each conflicting range will get a * name. This name is following the "<<<<<<< - * " or ">>>>>>> " conflict markers. The - * names for the sequences are given in this list + * ", "|||||||" or ">>>>>>> " conflict + * markers. The names for the sequences are given in this list * @param charset * the character set used when writing conflict metadata * @throws java.io.IOException - * @since 5.2 + * if an IO error occurred + * @since 6.7 */ - public void formatMerge(OutputStream out, MergeResult<RawText> res, - List<String> seqName, Charset charset) throws IOException { - new MergeFormatterPass(out, res, seqName, charset).formatMerge(); + public void formatMergeDiff3(OutputStream out, + MergeResult<RawText> res, List<String> seqName, Charset charset) + throws IOException { + new MergeFormatterPass(out, res, seqName, charset, true).formatMerge(); } /** @@ -98,27 +98,29 @@ public class MergeFormatter { * the name ranges from ours should get * @param theirsName * the name ranges from theirs should get - * @param charsetName - * the name of the character set used when writing conflict - * metadata + * @param charset + * the character set used when writing conflict metadata * @throws java.io.IOException - * @deprecated use - * {@link #formatMerge(OutputStream, MergeResult, String, String, String, Charset)} - * instead. + * if an IO error occurred + * @since 5.2 */ - @Deprecated + @SuppressWarnings("unchecked") public void formatMerge(OutputStream out, MergeResult res, String baseName, - String oursName, String theirsName, String charsetName) throws IOException { - formatMerge(out, res, baseName, oursName, theirsName, - Charset.forName(charsetName)); + String oursName, String theirsName, Charset charset) + throws IOException { + List<String> names = new ArrayList<>(3); + names.add(baseName); + names.add(oursName); + names.add(theirsName); + formatMerge(out, res, names, charset); } /** - * Formats the results of a merge of exactly two - * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way. - * This convenience method accepts the names for the three sequences (base - * and the two merged sequences) as explicit parameters and doesn't require - * the caller to specify a List + * Formats the results of a merge of three + * {@link org.eclipse.jgit.diff.RawText} objects in a Git conformant way, + * using diff-3 style. This convenience method accepts the names for the + * three sequences (base and the two merged sequences) as explicit + * parameters and doesn't require the caller to specify a List * * @param out * the {@link java.io.OutputStream} where to write the textual @@ -134,16 +136,17 @@ public class MergeFormatter { * @param charset * the character set used when writing conflict metadata * @throws java.io.IOException - * @since 5.2 + * if an IO error occurred + * @since 6.7 */ @SuppressWarnings("unchecked") - public void formatMerge(OutputStream out, MergeResult res, String baseName, - String oursName, String theirsName, Charset charset) - throws IOException { + public void formatMergeDiff3(OutputStream out, + MergeResult res, String baseName, String oursName, + String theirsName, Charset charset) throws IOException { List<String> names = new ArrayList<>(3); names.add(baseName); names.add(oursName); names.add(theirsName); - formatMerge(out, res, names, charset); + formatMergeDiff3(out, res, names, charset); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java index f09b343007..5e80b5fb23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java @@ -31,6 +31,8 @@ class MergeFormatterPass { private final boolean threeWayMerge; + private final boolean writeBase; // diff3-style requested + private String lastConflictingName; // is set to non-null whenever we are in // a conflict @@ -50,22 +52,47 @@ class MergeFormatterPass { */ MergeFormatterPass(OutputStream out, MergeResult<RawText> res, List<String> seqName, Charset charset) { + this(out, res, seqName, charset, false); + } + + /** + * @param out + * the {@link java.io.OutputStream} where to write the textual + * presentation + * @param res + * the merge result which should be presented + * @param seqName + * When a conflict is reported each conflicting range will get a + * name. This name is following the "<<<<<<< + * ", "|||||||" or ">>>>>>> " conflict + * markers. The names for the sequences are given in this list + * @param charset + * the character set used when writing conflict metadata + * @param writeBase + * base's contribution should be written in conflicts + */ + MergeFormatterPass(OutputStream out, MergeResult<RawText> res, + List<String> seqName, Charset charset, boolean writeBase) { this.out = new EolAwareOutputStream(out); this.res = res; this.seqName = seqName; this.charset = charset; this.threeWayMerge = (res.getSequences().size() == 3); + this.writeBase = writeBase; } void formatMerge() throws IOException { boolean missingNewlineAtEnd = false; for (MergeChunk chunk : res) { - RawText seq = res.getSequences().get(chunk.getSequenceIndex()); - writeConflictMetadata(chunk); - // the lines with conflict-metadata are written. Now write the chunk - for (int i = chunk.getBegin(); i < chunk.getEnd(); i++) - writeLine(seq, i); - missingNewlineAtEnd = seq.isMissingNewlineAtEnd(); + if (!isBase(chunk) || writeBase) { + RawText seq = res.getSequences().get(chunk.getSequenceIndex()); + writeConflictMetadata(chunk); + // the lines with conflict-metadata are written. Now write the + // chunk + for (int i = chunk.getBegin(); i < chunk.getEnd(); i++) + writeLine(seq, i); + missingNewlineAtEnd = seq.isMissingNewlineAtEnd(); + } } // one possible leftover: if the merge result ended with a conflict we // have to close the last conflict here @@ -77,16 +104,19 @@ class MergeFormatterPass { private void writeConflictMetadata(MergeChunk chunk) throws IOException { if (lastConflictingName != null - && chunk.getConflictState() != ConflictState.NEXT_CONFLICTING_RANGE) { - // found the end of an conflict + && !isTheirs(chunk) && !isBase(chunk)) { + // found the end of a conflict writeConflictEnd(); } - if (chunk.getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE) { - // found the start of an conflict + if (isOurs(chunk)) { + // found the start of a conflict writeConflictStart(chunk); - } else if (chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE) { - // found another conflicting chunk + } else if (isTheirs(chunk)) { + // found the theirs conflicting chunk writeConflictChange(chunk); + } else if (isBase(chunk)) { + // found the base conflicting chunk + writeConflictBase(chunk); } } @@ -113,6 +143,11 @@ class MergeFormatterPass { + lastConflictingName); } + private void writeConflictBase(MergeChunk chunk) throws IOException { + lastConflictingName = seqName.get(chunk.getSequenceIndex()); + writeln("||||||| " + lastConflictingName); //$NON-NLS-1$ + } + private void writeln(String s) throws IOException { out.beginln(); out.write((s + "\n").getBytes(charset)); //$NON-NLS-1$ @@ -125,4 +160,17 @@ class MergeFormatterPass { if (out.isBeginln()) out.write('\n'); } + + private boolean isBase(MergeChunk chunk) { + return chunk.getConflictState() == ConflictState.BASE_CONFLICTING_RANGE; + } + + private boolean isOurs(MergeChunk chunk) { + return chunk + .getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE; + } + + private boolean isTheirs(MergeChunk chunk) { + return chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index e0c083f55c..039d7d844c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -92,24 +92,6 @@ public class MergeMessageFormatter { } /** - * Add section with conflicting paths to merge message. Lines are prefixed - * with a hash. - * - * @param message - * the original merge message - * @param conflictingPaths - * the paths with conflicts - * @return merge message with conflicting paths added - * @deprecated since 6.1; use - * {@link #formatWithConflicts(String, Iterable, char)} instead - */ - @Deprecated - public String formatWithConflicts(String message, - List<String> conflictingPaths) { - return formatWithConflicts(message, conflictingPaths, '#'); - } - - /** * Add section with conflicting paths to merge message. * * @param message diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java index 20fc3c339c..acf955303d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java @@ -97,7 +97,6 @@ public class MergeResult<S extends Sequence> implements Iterable<MergeChunk> { static final ConflictState[] states = ConflictState.values(); - /** {@inheritDoc} */ @Override public Iterator<MergeChunk> iterator() { return new Iterator<>() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java index df6068925b..f58ef4faba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -18,10 +18,11 @@ package org.eclipse.jgit.merge; import java.io.IOException; import java.text.MessageFormat; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.TimeZone; +import java.util.stream.Collectors; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -123,6 +124,7 @@ public class RecursiveMerger extends ResolveMerger { * synthetic merge base this commit is visible only the merger's * RevWalk and will not be in the repository. * @throws java.io.IOException + * if an IO error occurred * @throws IncorrectObjectTypeException * one of the input objects is not a commit. * @throws NoMergeBaseException @@ -184,12 +186,15 @@ public class RecursiveMerger extends ResolveMerger { if (mergeTrees(bcTree, currentBase.getTree(), nextBase.getTree(), true)) currentBase = createCommitForTree(resultTree, parents); - else + else { + String failedPaths = failingPathsMessage(); throw new NoMergeBaseException( NoMergeBaseException.MergeBaseFailureReason.CONFLICTS_DURING_MERGE_BASE_CALCULATION, MessageFormat.format( JGitText.get().mergeRecursiveConflictsWhenMergingCommonAncestors, - currentBase.getName(), nextBase.getName())); + currentBase.getName(), nextBase.getName(), + failedPaths)); + } } } finally { inCore = oldIncore; @@ -213,6 +218,7 @@ public class RecursiveMerger extends ResolveMerger { * the list of parent commits * @return a new commit visible only within this merger's RevWalk. * @throws IOException + * if an IO error occurred */ private RevCommit createCommitForTree(ObjectId tree, List<RevCommit> parents) throws IOException { @@ -227,11 +233,23 @@ public class RecursiveMerger extends ResolveMerger { private static PersonIdent mockAuthor(List<RevCommit> parents) { String name = RecursiveMerger.class.getSimpleName(); int time = 0; - for (RevCommit p : parents) + for (RevCommit p : parents) { time = Math.max(time, p.getCommitTime()); - return new PersonIdent( - name, name + "@JGit", //$NON-NLS-1$ - new Date((time + 1) * 1000L), - TimeZone.getTimeZone("GMT+0000")); //$NON-NLS-1$ + } + return new PersonIdent(name, name + "@JGit", //$NON-NLS-1$ + Instant.ofEpochSecond(time+1), ZoneOffset.UTC); + } + + private String failingPathsMessage() { + int max = 25; + String failedPaths = failingPaths.entrySet().stream().limit(max) + .map(entry -> entry.getKey() + ":" + entry.getValue()) //$NON-NLS-1$ + .collect(Collectors.joining("\n")); //$NON-NLS-1$ + + if (failingPaths.size() > max) { + failedPaths = String.format("%s\n... (%s failing paths omitted)", //$NON-NLS-1$ + failedPaths, Integer.valueOf(failingPaths.size() - max)); + } + return failedPaths; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index e56513d4e9..dc96f65b87 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,8 +3,8 @@ * 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, 2022 Thomas Wolf <twolf@apache.org> - * Copyright (C) 2022, Google Inc. and others + * Copyright (C) 2018, 2023 Thomas Wolf <twolf@apache.org> + * Copyright (C) 2023, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -42,11 +41,13 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.attributes.AttributesNodeProvider; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.Sequence; +import org.eclipse.jgit.dircache.Checkout; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -79,7 +80,6 @@ import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.TreeFilter; -import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.LfsFactory; import org.eclipse.jgit.util.LfsFactory.LfsInputStream; import org.eclipse.jgit.util.TemporaryBuffer; @@ -106,13 +106,15 @@ public class ResolveMerger extends ThreeWayMerger { */ public static class Result { - private final List<String> modifiedFiles = new LinkedList<>(); + private final List<String> modifiedFiles = new ArrayList<>(); - private final List<String> failedToDelete = new LinkedList<>(); + private final List<String> failedToDelete = new ArrayList<>(); private ObjectId treeId = null; /** + * Get modified tree id if any + * * @return Modified tree ID if any, or null otherwise. */ public ObjectId getTreeId() { @@ -120,6 +122,8 @@ public class ResolveMerger extends ThreeWayMerger { } /** + * Get path of files that couldn't be deleted + * * @return Files that couldn't be deleted. */ public List<String> getFailedToDelete() { @@ -127,6 +131,8 @@ public class ResolveMerger extends ThreeWayMerger { } /** + * Get path of modified files + * * @return Files modified during this operation. */ public List<String> getModifiedFiles() { @@ -205,6 +211,12 @@ public class ResolveMerger extends ThreeWayMerger { private boolean indexChangesWritten; /** + * {@link Checkout} to use for actually checking out files if + * {@link #inCore} is {@code false}. + */ + private Checkout checkout; + + /** * @param repo * the {@link Repository}. * @param dirCache @@ -223,6 +235,7 @@ public class ResolveMerger extends ThreeWayMerger { this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config); this.checkoutMetadataByPath = new HashMap<>(); this.cleanupMetadataByPath = new HashMap<>(); + this.checkout = new Checkout(nonNullRepo(), workingTreeOptions); } /** @@ -350,9 +363,8 @@ public class ResolveMerger extends ThreeWayMerger { } // All content operations are successfully done. If we can now write - // the - // new index we are on quite safe ground. Even if the checkout of - // files coming from "theirs" fails the user can work around such + // the new index we are on quite safe ground. Even if the checkout + // of files coming from "theirs" fails the user can work around such // failures by checking out the index again. if (!builder.commit()) { revertModifiedFiles(); @@ -466,7 +478,6 @@ public class ResolveMerger extends ThreeWayMerger { /** * Detects if CRLF conversion has been configured. * <p> - * </p> * See {@link EolStreamTypeUtil#detectStreamType} for more info. * * @param attributes @@ -518,14 +529,14 @@ public class ResolveMerger extends ThreeWayMerger { for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut .entrySet()) { DirCacheEntry dirCacheEntry = entry.getValue(); + String gitPath = entry.getKey(); if (dirCacheEntry.getFileMode() == FileMode.GITLINK) { - new File(nonNullRepo().getWorkTree(), entry.getKey()) - .mkdirs(); + checkout.checkoutGitlink(dirCacheEntry, gitPath); } else { - DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader, - false, checkoutMetadataByPath.get(entry.getKey()), - workingTreeOptions); - result.modifiedFiles.add(entry.getKey()); + checkout.checkout(dirCacheEntry, + checkoutMetadataByPath.get(gitPath), reader, + gitPath); + result.modifiedFiles.add(gitPath); } } } @@ -550,9 +561,8 @@ public class ResolveMerger extends ThreeWayMerger { for (String path : result.modifiedFiles) { DirCacheEntry entry = dirCache.getEntry(path); if (entry != null) { - DirCacheCheckout.checkoutEntry(repo, entry, reader, false, - cleanupMetadataByPath.get(path), - workingTreeOptions); + checkout.checkout(entry, cleanupMetadataByPath.get(path), + reader, path); } } } @@ -586,6 +596,8 @@ public class ResolveMerger extends ThreeWayMerger { if (inCore) { return; } + checkout.safeCreateParentDirectory(path, file.getParentFile(), + false); CheckoutMetadata metadata = new CheckoutMetadata(streamType, smudgeCommand); @@ -826,6 +838,13 @@ public class ResolveMerger extends ThreeWayMerger { @NonNull private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; + /** + * The {@link AttributesNodeProvider} to use while merging trees. + * + * @since 6.10.1 + */ + protected AttributesNodeProvider attributesNodeProvider; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -904,7 +923,6 @@ public class ResolveMerger extends ThreeWayMerger { : strategy; } - /** {@inheritDoc} */ @Override protected boolean mergeImpl() throws IOException { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], @@ -915,18 +933,23 @@ public class ResolveMerger extends ThreeWayMerger { * adds a new path with the specified stage to the index builder * * @param path + * the new path * @param p + * canonical tree parser * @param stage - * @param lastMod + * the stage + * @param lastModified + * lastModified attribute of the file * @param len + * file length * @return the entry which was added to the index */ private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, - Instant lastMod, long len) { + Instant lastModified, long len) { if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { return workTreeUpdater.addExistingToIndex(p.getEntryObjectId(), path, p.getEntryFileMode(), stage, - lastMod, (int) len); + lastModified, (int) len); } return null; } @@ -1056,6 +1079,7 @@ public class ResolveMerger extends ThreeWayMerger { * didn't match ours or the working-dir file was dirty and a * conflict occurred * @throws java.io.IOException + * if an IO error occurred * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, @@ -1257,10 +1281,22 @@ public class ResolveMerger extends ThreeWayMerger { default: break; } + if (ignoreConflicts) { + // If the path is selected to be treated as binary via attributes, we do not perform + // content merge. When ignoreConflicts = true, we simply keep OURS to allow virtual commit + // to be built. + keep(ourDce); + return true; + } + // add the conflicting path to merge result + String currentPath = tw.getPathString(); + MergeResult<RawText> result = new MergeResult<>( + Collections.emptyList()); + result.setContainsConflicts(true); + mergeResults.put(currentPath, result); addConflict(base, ours, theirs); - // attribute merge issues are conflicts but not failures - unmergedPaths.add(tw.getPathString()); + unmergedPaths.add(currentPath); return true; } @@ -1272,38 +1308,52 @@ public class ResolveMerger extends ThreeWayMerger { MergeResult<RawText> result = null; boolean hasSymlink = FileMode.SYMLINK.equals(modeO) || FileMode.SYMLINK.equals(modeT); + + String currentPath = tw.getPathString(); + // if the path is not a symlink in ours and theirs if (!hasSymlink) { try { result = contentMerge(base, ours, theirs, attributes, getContentMergeStrategy()); - } catch (BinaryBlobException e) { - // result == null - } - } - if (result == null) { - switch (getContentMergeStrategy()) { - case OURS: - keep(ourDce); - return true; - case THEIRS: - DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - if (e != null) { - addToCheckout(tw.getPathString(), e, attributes); + if (result.containsConflicts() && !ignoreConflicts) { + result.setContainsConflicts(true); + unmergedPaths.add(currentPath); + } else if (ignoreConflicts) { + result.setContainsConflicts(false); } + updateIndex(base, ours, theirs, result, attributes[T_OURS]); + workTreeUpdater.markAsModified(currentPath); + // Entry is null - only add the metadata + addToCheckout(currentPath, null, attributes); return true; - default: - result = new MergeResult<>(Collections.emptyList()); - result.setContainsConflicts(true); - break; + } catch (BinaryBlobException e) { + // The file is binary in either OURS, THEIRS or BASE + if (ignoreConflicts) { + // When ignoreConflicts = true, we simply keep OURS to allow virtual commit to be built. + keep(ourDce); + return true; + } } } - if (ignoreConflicts) { - result.setContainsConflicts(false); + switch (getContentMergeStrategy()) { + case OURS: + keep(ourDce); + return true; + case THEIRS: + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + if (e != null) { + addToCheckout(currentPath, e, attributes); + } + return true; + default: + result = new MergeResult<>(Collections.emptyList()); + result.setContainsConflicts(true); + break; } - String currentPath = tw.getPathString(); if (hasSymlink) { if (ignoreConflicts) { + result.setContainsConflicts(false); if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE)) { DirCacheEntry e = add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_0, EPOCH, 0); @@ -1312,9 +1362,9 @@ public class ResolveMerger extends ThreeWayMerger { keep(ourDce); } } else { - // Record the conflict DirCacheEntry e = addConflict(base, ours, theirs); mergeResults.put(currentPath, result); + unmergedPaths.add(currentPath); // If theirs is a file, check it out. In link/file // conflicts, C git prefers the file. if (((modeT & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) @@ -1323,14 +1373,14 @@ public class ResolveMerger extends ThreeWayMerger { } } } else { - updateIndex(base, ours, theirs, result, attributes[T_OURS]); - } - if (result.containsConflicts() && !ignoreConflicts) { + // This is reachable if contentMerge() call above threw BinaryBlobException, so we don't + // need to check ignoreConflicts here, since it's already handled above. + result.setContainsConflicts(true); + addConflict(base, ours, theirs); unmergedPaths.add(currentPath); + mergeResults.put(currentPath, result); } - workTreeUpdater.markAsModified(currentPath); - // Entry is null - only adds the metadata. - addToCheckout(currentPath, null, attributes); + return true; } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -1432,15 +1482,21 @@ public class ResolveMerger extends ThreeWayMerger { * specified as <code>null</code> then an empty text will be used instead. * * @param base + * used to parse base tree * @param ours + * used to parse ours tree * @param theirs + * used to parse theirs tree * @param attributes + * attributes for the different stages * @param strategy + * merge strategy * * @return the result of the content merge * @throws BinaryBlobException * if any of the blobs looks like a binary blob * @throws IOException + * if an IO error occurred */ private MergeResult<RawText> contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, @@ -1454,11 +1510,26 @@ public class ResolveMerger extends ThreeWayMerger { : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); - mergeAlgorithm.setContentMergeStrategy(strategy); + mergeAlgorithm.setContentMergeStrategy( + getAttributesContentMergeStrategy(attributes[T_OURS], + strategy)); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); } + private ContentMergeStrategy getAttributesContentMergeStrategy( + Attributes attributes, ContentMergeStrategy strategy) { + Attribute attr = attributes.get(Constants.ATTR_MERGE); + if (attr != null) { + String attrValue = attr.getValue(); + if (attrValue != null && attrValue + .equals(Constants.ATTR_BUILTIN_UNION_MERGE_DRIVER)) { + return ContentMergeStrategy.UNION; + } + } + return strategy; + } + private boolean isIndexDirty() { if (inCore) { return false; @@ -1516,11 +1587,17 @@ public class ResolveMerger extends ThreeWayMerger { * correct stages to the index. * * @param base + * used to parse base tree * @param ours + * used to parse ours tree * @param theirs + * used to parse theirs tree * @param result + * merge result * @param attributes + * the file's attributes * @throws IOException + * if an IO error occurred */ private void updateIndex(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, @@ -1571,20 +1648,17 @@ public class ResolveMerger extends ThreeWayMerger { * the files .gitattributes entries * @return the working tree file to which the merged content was written. * @throws IOException + * if an IO error occurred */ private File writeMergedFile(TemporaryBuffer rawMerged, Attributes attributes) throws IOException { File workTree = nonNullRepo().getWorkTree(); - FS fs = nonNullRepo().getFS(); - File of = new File(workTree, tw.getPathString()); - File parentFolder = of.getParentFile(); + String gitPath = tw.getPathString(); + File of = new File(workTree, gitPath); EolStreamType eol = workTreeUpdater.detectCheckoutStreamType(attributes); - if (!fs.exists(parentFolder)) { - parentFolder.mkdirs(); - } workTreeUpdater.updateFileWithContent(rawMerged::openInputStream, - eol, tw.getSmudgeCommand(attributes), of.getPath(), of); + eol, tw.getSmudgeCommand(attributes), gitPath, of); return of; } @@ -1659,7 +1733,6 @@ public class ResolveMerger extends ThreeWayMerger { return FileMode.GITLINK.equals(mode); } - /** {@inheritDoc} */ @Override public ObjectId getResultTreeId() { return (resultTree == null) ? null : resultTree.toObjectId(); @@ -1787,6 +1860,18 @@ public class ResolveMerger extends ThreeWayMerger { this.workingTreeIterator = workingTreeIterator; } + /** + * Sets the {@link AttributesNodeProvider} to be used by this merger. + * + * @param attributesNodeProvider + * the attributeNodeProvider to set + * @since 6.10.1 + */ + public void setAttributesNodeProvider( + AttributesNodeProvider attributesNodeProvider) { + this.attributesNodeProvider = attributesNodeProvider; + } + /** * The resolve conflict way of three way merging @@ -1819,6 +1904,7 @@ public class ResolveMerger extends ThreeWayMerger { * content-merge conflicts. * @return whether the trees merged cleanly * @throws java.io.IOException + * if an IO error occurred * @since 3.5 */ protected boolean mergeTrees(AbstractTreeIterator baseTree, @@ -1830,6 +1916,9 @@ public class ResolveMerger extends ThreeWayMerger { WorkTreeUpdater.createWorkTreeUpdater(db, dircache); dircache = workTreeUpdater.getLockedDirCache(); tw = new NameConflictTreeWalk(db, reader); + if (attributesNodeProvider != null) { + tw.setAttributesNodeProvider(attributesNodeProvider); + } tw.addTree(baseTree); tw.setHead(tw.addTree(headTree)); @@ -1878,6 +1967,7 @@ public class ResolveMerger extends ThreeWayMerger { * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @return Whether the trees merged cleanly. * @throws java.io.IOException + * if an IO error occurred * @since 3.5 */ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java index 7055ccd127..6064ebe326 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java @@ -43,25 +43,21 @@ public class StrategyOneSided extends MergeStrategy { treeIndex = index; } - /** {@inheritDoc} */ @Override public String getName() { return strategyName; } - /** {@inheritDoc} */ @Override public Merger newMerger(Repository db) { return new OneSide(db, treeIndex); } - /** {@inheritDoc} */ @Override public Merger newMerger(Repository db, boolean inCore) { return new OneSide(db, treeIndex); } - /** {@inheritDoc} */ @Override public Merger newMerger(ObjectInserter inserter, Config config) { return new OneSide(inserter, treeIndex); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java index c9512edf63..a74bfc0379 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyRecursive.java @@ -21,25 +21,21 @@ import org.eclipse.jgit.lib.Repository; */ public class StrategyRecursive extends StrategyResolve { - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new RecursiveMerger(db, false); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { return new RecursiveMerger(db, inCore); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new RecursiveMerger(inserter, config); } - /** {@inheritDoc} */ @Override public String getName() { return "recursive"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java index 5991ef68a3..a686fd0964 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyResolve.java @@ -19,25 +19,21 @@ import org.eclipse.jgit.lib.Repository; */ public class StrategyResolve extends ThreeWayMergeStrategy { - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new ResolveMerger(db, false); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { return new ResolveMerger(db, inCore); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new ResolveMerger(inserter, config); } - /** {@inheritDoc} */ @Override public String getName() { return "resolve"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java index ff40fc1d68..c3180abd35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java @@ -43,26 +43,22 @@ public class StrategySimpleTwoWayInCore extends ThreeWayMergeStrategy { // } - /** {@inheritDoc} */ @Override public String getName() { return "simple-two-way-in-core"; //$NON-NLS-1$ } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db) { return new InCoreMerger(db); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(Repository db, boolean inCore) { // This class is always inCore, so ignore the parameter return newMerger(db); } - /** {@inheritDoc} */ @Override public ThreeWayMerger newMerger(ObjectInserter inserter, Config config) { return new InCoreMerger(inserter); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java index 411789fd95..8cefa6566e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java @@ -16,11 +16,9 @@ import org.eclipse.jgit.lib.Repository; * A merge strategy to merge 2 trees, using a common base ancestor tree. */ public abstract class ThreeWayMergeStrategy extends MergeStrategy { - /** {@inheritDoc} */ @Override public abstract ThreeWayMerger newMerger(Repository db); - /** {@inheritDoc} */ @Override public abstract ThreeWayMerger newMerger(Repository db, boolean inCore); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java index f283edee01..68a1b5e509 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java @@ -88,7 +88,6 @@ public abstract class ThreeWayMerger extends Merger { } } - /** {@inheritDoc} */ @Override public boolean merge(AnyObjectId... tips) throws IOException { if (tips.length != 2) @@ -96,7 +95,6 @@ public abstract class ThreeWayMerger extends Merger { return super.merge(tips); } - /** {@inheritDoc} */ @Override public ObjectId getBaseCommitId() { return baseCommitId; @@ -108,6 +106,7 @@ public abstract class ThreeWayMerger extends Merger { * @return an iterator over the caller-specified merge base, or the natural * merge base of the two input commits. * @throws java.io.IOException + * if an IO error occurred */ protected AbstractTreeIterator mergeBase() throws IOException { if (baseTree != null) { |