mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-05 20:00:55 +01:00
@@ -4,6 +4,7 @@ import com.google.common.base.Preconditions;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.spi.MergeCommand;
|
||||
import sonia.scm.repository.spi.MergeCommandRequest;
|
||||
import sonia.scm.repository.spi.MergeConflictResult;
|
||||
import sonia.scm.repository.util.AuthorUtil;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -168,7 +169,7 @@ public class MergeCommandBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to check whether the given branches can be merged autmatically. If this is possible,
|
||||
* Use this to check whether the given branches can be merged automatically. If this is possible,
|
||||
* {@link MergeDryRunCommandResult#isMergeable()} will return <code>true</code>.
|
||||
*
|
||||
* @return The result whether the given branches can be merged automatically.
|
||||
@@ -177,4 +178,14 @@ public class MergeCommandBuilder {
|
||||
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
|
||||
return mergeCommand.dryRun(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to compute concrete conflicts for a merge.
|
||||
*
|
||||
* @return A result containing all conflicts for the merge.
|
||||
*/
|
||||
public MergeConflictResult conflicts() {
|
||||
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
|
||||
return mergeCommand.computeConflicts(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
|
||||
*
|
||||
*
|
||||
* @param format format of the diff output
|
||||
*
|
||||
*
|
||||
* @since 1.34
|
||||
*/
|
||||
public void setFormat(DiffFormat format)
|
||||
@@ -119,7 +119,7 @@ public final class DiffCommandRequest extends FileBaseCommandRequest
|
||||
*
|
||||
*
|
||||
* @return output format
|
||||
*
|
||||
*
|
||||
* @since 1.34
|
||||
*/
|
||||
public DiffFormat getFormat()
|
||||
|
||||
@@ -17,6 +17,8 @@ public interface MergeCommand {
|
||||
|
||||
MergeDryRunCommandResult dryRun(MergeCommandRequest request);
|
||||
|
||||
MergeConflictResult computeConflicts(MergeCommandRequest request);
|
||||
|
||||
boolean isSupported(MergeStrategy strategy);
|
||||
|
||||
Set<MergeStrategy> getSupportedMergeStrategies();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.ADDED_BY_BOTH;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.BOTH_MODIFIED;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.DELETED_BY_THEM;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.DELETED_BY_US;
|
||||
|
||||
public class MergeConflictResult {
|
||||
|
||||
private final List<SingleMergeConflict> conflicts = new LinkedList<>();
|
||||
|
||||
public List<SingleMergeConflict> getConflicts() {
|
||||
return Collections.unmodifiableList(conflicts);
|
||||
}
|
||||
|
||||
public void addBothModified(String path, String diff) {
|
||||
conflicts.add(new SingleMergeConflict(BOTH_MODIFIED, path, diff));
|
||||
}
|
||||
|
||||
public void addDeletedByThem(String path) {
|
||||
conflicts.add(new SingleMergeConflict(DELETED_BY_THEM, path, null));
|
||||
}
|
||||
|
||||
public void addDeletedByUs(String path) {
|
||||
conflicts.add(new SingleMergeConflict(DELETED_BY_US, path, null));
|
||||
}
|
||||
|
||||
public void addAddedByBoth(String path) {
|
||||
conflicts.add(new SingleMergeConflict(ADDED_BY_BOTH, path, null));
|
||||
}
|
||||
|
||||
public static class SingleMergeConflict {
|
||||
private final ConflictTypes type;
|
||||
private final String path;
|
||||
private final String diff;
|
||||
|
||||
private SingleMergeConflict(ConflictTypes type, String path, String diff) {
|
||||
this.type = type;
|
||||
this.path = path;
|
||||
this.diff = diff;
|
||||
}
|
||||
|
||||
public ConflictTypes getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getDiff() {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConflictTypes {
|
||||
BOTH_MODIFIED, DELETED_BY_THEM, DELETED_BY_US, ADDED_BY_BOTH
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.MergeResult;
|
||||
import org.eclipse.jgit.api.Status;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.IndexDiff;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.merge.ResolveMerger;
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
|
||||
import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
@@ -10,10 +19,13 @@ import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.api.MergeStrategy;
|
||||
import sonia.scm.repository.api.MergeStrategyNotSupportedException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.eclipse.jgit.merge.MergeStrategy.RECURSIVE;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
|
||||
|
||||
@@ -35,6 +47,11 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
return mergeWithStrategy(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MergeConflictResult computeConflicts(MergeCommandRequest request) {
|
||||
return inClone(git -> new ConflictWorker(git, request), workdirFactory, request.getTargetBranch());
|
||||
}
|
||||
|
||||
private MergeCommandResult mergeWithStrategy(MergeCommandRequest request) {
|
||||
switch(request.getMergeStrategy()) {
|
||||
case SQUASH:
|
||||
@@ -75,4 +92,91 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
return STRATEGIES;
|
||||
}
|
||||
|
||||
private class ConflictWorker extends GitCloneWorker<MergeConflictResult> {
|
||||
private final String theirs;
|
||||
private final String ours;
|
||||
private final CanonicalTreeParser treeParser;
|
||||
private final ObjectId treeId;
|
||||
private final ByteArrayOutputStream diffBuffer;
|
||||
|
||||
private final MergeConflictResult result = new MergeConflictResult();
|
||||
|
||||
|
||||
private ConflictWorker(Git git, MergeCommandRequest request) {
|
||||
super(git, context, repository);
|
||||
theirs = request.getBranchToMerge();
|
||||
ours = request.getTargetBranch();
|
||||
|
||||
treeParser = new CanonicalTreeParser();
|
||||
diffBuffer = new ByteArrayOutputStream();
|
||||
try {
|
||||
treeId = git.getRepository().resolve(ours + "^{tree}");
|
||||
} catch (IOException e) {
|
||||
throw notFound(entity("branch", ours).in(repository));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
MergeConflictResult run() throws IOException {
|
||||
MergeResult mergeResult = doTemporaryMerge();
|
||||
if (mergeResult.getConflicts() != null) {
|
||||
getStatus().getConflictingStageState().forEach(this::computeConflict);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void computeConflict(String path, IndexDiff.StageState stageState) {
|
||||
switch (stageState) {
|
||||
case BOTH_MODIFIED:
|
||||
diffBuffer.reset();
|
||||
try (ObjectReader reader = getClone().getRepository().newObjectReader()) {
|
||||
treeParser.reset(reader, treeId);
|
||||
getClone()
|
||||
.diff()
|
||||
.setOldTree(treeParser)
|
||||
.setPathFilter(PathFilter.create(path))
|
||||
.setOutputStream(diffBuffer)
|
||||
.call();
|
||||
result.addBothModified(path, diffBuffer.toString());
|
||||
} catch (GitAPIException | IOException e) {
|
||||
throw new InternalRepositoryException(repository, "could not calculate diff for path " + path, e);
|
||||
}
|
||||
break;
|
||||
case BOTH_ADDED:
|
||||
result.addAddedByBoth(path);
|
||||
break;
|
||||
case DELETED_BY_THEM:
|
||||
result.addDeletedByUs(path);
|
||||
break;
|
||||
case DELETED_BY_US:
|
||||
result.addDeletedByThem(path);
|
||||
break;
|
||||
default:
|
||||
throw new InternalRepositoryException(context.getRepository(), "unexpected conflict type: " + stageState);
|
||||
}
|
||||
}
|
||||
|
||||
private MergeResult doTemporaryMerge() throws IOException {
|
||||
ObjectId sourceRevision = resolveRevision(theirs);
|
||||
try {
|
||||
return getClone().merge()
|
||||
.setFastForward(org.eclipse.jgit.api.MergeCommand.FastForwardMode.NO_FF)
|
||||
.setCommit(false)
|
||||
.include(theirs, sourceRevision)
|
||||
.call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not merge branch " + theirs + " into " + ours, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Status getStatus() {
|
||||
Status status;
|
||||
try {
|
||||
status = getClone().status().call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not get status", e);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.spi.MergeConflictResult.SingleMergeConflict;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.BOTH_MODIFIED;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.DELETED_BY_THEM;
|
||||
import static sonia.scm.repository.spi.MergeConflictResult.ConflictTypes.DELETED_BY_US;
|
||||
|
||||
public class GitMergeCommand_Conflict_Test extends AbstractGitCommandTestBase {
|
||||
|
||||
static final String DIFF_HEADER = "diff --git a/Main.java b/Main.java";
|
||||
static final String DIFF_FILE_CONFLICT = "--- a/Main.java\n" +
|
||||
"+++ b/Main.java\n" +
|
||||
"@@ -1,6 +1,13 @@\n" +
|
||||
"+import java.util.Arrays;\n" +
|
||||
"+\n" +
|
||||
" class Main {\n" +
|
||||
" public static void main(String[] args) {\n" +
|
||||
" System.out.println(\"Expect nothing more to happen.\");\n" +
|
||||
"+<<<<<<< HEAD\n" +
|
||||
" System.out.println(\"This is for demonstration, only.\");\n" +
|
||||
"+=======\n" +
|
||||
"+ System.out.println(\"Parameters:\");\n" +
|
||||
"+ Arrays.stream(args).map(arg -> \"- \" + arg).forEach(System.out::println);\n" +
|
||||
"+>>>>>>> feature/print_args\n" +
|
||||
" }\n" +
|
||||
" }";
|
||||
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesWithoutConflict() throws IOException {
|
||||
MergeConflictResult result = computeMergeConflictResult("feature/rename_variable", "integration");
|
||||
assertThat(result.getConflicts()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesWithSimpleConflict() throws IOException {
|
||||
MergeConflictResult result = computeMergeConflictResult("feature/print_args", "integration");
|
||||
SingleMergeConflict conflict = result.getConflicts().get(0);
|
||||
assertThat(conflict.getType()).isEqualTo(BOTH_MODIFIED);
|
||||
assertThat(conflict.getPath()).isEqualTo("Main.java");
|
||||
assertThat(conflict.getDiff()).contains(DIFF_HEADER, DIFF_FILE_CONFLICT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesWithDeletedByUs() throws IOException {
|
||||
MergeConflictResult result = computeMergeConflictResult("feature/remove_class", "integration");
|
||||
SingleMergeConflict conflict = result.getConflicts().get(0);
|
||||
assertThat(conflict.getType()).isEqualTo(DELETED_BY_US);
|
||||
assertThat(conflict.getPath()).isEqualTo("Main.java");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diffBetweenTwoBranchesWithDeletedByThem() throws IOException {
|
||||
MergeConflictResult result = computeMergeConflictResult("integration", "feature/remove_class");
|
||||
SingleMergeConflict conflict = result.getConflicts().get(0);
|
||||
assertThat(conflict.getType()).isEqualTo(DELETED_BY_THEM);
|
||||
assertThat(conflict.getPath()).isEqualTo("Main.java");
|
||||
}
|
||||
|
||||
private MergeConflictResult computeMergeConflictResult(String branchToMerge, String targetBranch) {
|
||||
GitMergeCommand gitMergeCommand = new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
|
||||
MergeCommandRequest mergeCommandRequest = new MergeCommandRequest();
|
||||
mergeCommandRequest.setBranchToMerge(branchToMerge);
|
||||
mergeCommandRequest.setTargetBranch(targetBranch);
|
||||
return gitMergeCommand.computeConflicts(mergeCommandRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZippedRepositoryResource() {
|
||||
return "sonia/scm/repository/spi/scm-git-spi-merge-diff-test.zip";
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -145,4 +145,27 @@ index 889cc49..d5a4811 100644
|
||||
}
|
||||
|
||||
@Test
|
||||
diff --git a/Main.java b/Main.java
|
||||
index e77e6da..f183b7c 100644
|
||||
--- a/Main.java
|
||||
+++ b/Main.java
|
||||
@@ -1,9 +1,18 @@
|
||||
+import java.io.PrintStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
class Main {
|
||||
+ private static final PrintStream OUT = System.out;
|
||||
+
|
||||
public static void main(String[] args) {
|
||||
+<<<<<<< HEAD
|
||||
System.out.println("Expect nothing more to happen.");
|
||||
System.out.println("The command line parameters are:");
|
||||
Arrays.stream(args).map(arg -> "- " + arg).forEach(System.out::println);
|
||||
+=======
|
||||
+ OUT.println("Expect nothing more to happen.");
|
||||
+ OUT.println("Parameters:");
|
||||
+ Arrays.stream(args).map(arg -> "- " + arg).forEach(OUT::println);
|
||||
+>>>>>>> feature/use_constant
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,8 @@ const ModifiedDiffComponent = styled(DiffComponent)`
|
||||
|
||||
class DiffFile extends React.Component<Props, State> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
defaultCollapse: false
|
||||
defaultCollapse: false,
|
||||
markConflicts: true
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -173,6 +174,9 @@ class DiffFile extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
renderHunk = (hunk: HunkType, i: number) => {
|
||||
if (this.props.markConflicts && hunk.changes) {
|
||||
this.markConflicts(hunk);
|
||||
}
|
||||
return [
|
||||
<Decoration key={"decoration-" + hunk.content}>{this.createHunkHeader(hunk, i)}</Decoration>,
|
||||
<Hunk
|
||||
@@ -184,6 +188,21 @@ class DiffFile extends React.Component<Props, State> {
|
||||
];
|
||||
};
|
||||
|
||||
markConflicts = (hunk: HunkType) => {
|
||||
let inConflict = false;
|
||||
for (let i = 0; i < hunk.changes.length; ++i) {
|
||||
if (hunk.changes[i].content === "<<<<<<< HEAD") {
|
||||
inConflict = true;
|
||||
}
|
||||
if (inConflict) {
|
||||
hunk.changes[i].type = "conflict";
|
||||
}
|
||||
if (hunk.changes[i].content.startsWith(">>>>>>>")) {
|
||||
inConflict = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderFileTitle = (file: File) => {
|
||||
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
|
||||
return (
|
||||
|
||||
@@ -27,7 +27,7 @@ export type Hunk = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ChangeType = "insert" | "delete" | "normal";
|
||||
export type ChangeType = "insert" | "delete" | "normal" | "conflict";
|
||||
|
||||
export type Change = {
|
||||
content: string;
|
||||
@@ -75,4 +75,5 @@ export type DiffObjectProps = {
|
||||
fileControlFactory?: FileControlFactory;
|
||||
fileAnnotationFactory?: FileAnnotationFactory;
|
||||
annotationFactory?: AnnotationFactory;
|
||||
markConflicts?: boolean;
|
||||
};
|
||||
|
||||
@@ -829,4 +829,12 @@ form .field:not(.is-grouped) {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.diff-gutter-conflict {
|
||||
background: $warning-50;
|
||||
}
|
||||
|
||||
.diff-code-conflict {
|
||||
background: $warning-25;
|
||||
}
|
||||
|
||||
@import "bulma-popover/css/bulma-popover";
|
||||
|
||||
Reference in New Issue
Block a user