Add Fast Forward Only Strategy

This commit is contained in:
Thomas Zerr
2025-08-19 11:48:54 +02:00
committed by Till-André Diegeler
parent 6792328e32
commit 8a8d18c33e
10 changed files with 138 additions and 6 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Merge strategy fast forward only, which either fast forwards the commits if possible or throws an error

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.api;
import sonia.scm.BadRequestException;
import sonia.scm.repository.Repository;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class FastForwardNotPossible extends BadRequestException {
private static final String CODE = "Re6hF5g14U";
public FastForwardNotPossible(Repository repository, String source, String target) {
super(entity(repository).build(), createMessage(source, target));
}
@Override
public String getCode() {
return CODE;
}
private static String createMessage(String source, String target) {
return String.format("Fast forward not possible with source revision %s and target revision %s", source, target);
}
}

View File

@@ -19,6 +19,7 @@ package sonia.scm.repository.api;
public enum MergeStrategy {
MERGE_COMMIT(true),
FAST_FORWARD_IF_POSSIBLE(true),
FAST_FORWARD_ONLY(false),
SQUASH(true),
REBASE(false);

View File

@@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.spi;
public enum FastForwardFallbackStrategy {
MERGE_COMMIT,
THROW_EXCEPTION
}

View File

@@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.lib.ObjectId;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.FastForwardNotPossible;
import sonia.scm.repository.api.MergeCommandResult;
@Slf4j
@@ -30,9 +31,11 @@ class GitFastForwardIfPossible {
private final GitMergeCommit fallbackMerge;
private final CommitHelper commitHelper;
private final Repository repository;
private final FastForwardFallbackStrategy fastForwardStrategy;
GitFastForwardIfPossible(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
GitFastForwardIfPossible(MergeCommandRequest request, GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory, FastForwardFallbackStrategy fastForwardStrategy) {
this.request = request;
this.fastForwardStrategy = fastForwardStrategy;
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
this.fallbackMerge = new GitMergeCommit(request, context, repositoryManager, eventFactory);
this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
@@ -48,9 +51,17 @@ class GitFastForwardIfPossible {
log.trace("fast forward branch {} onto {}", request.getBranchToMerge(), request.getTargetBranch());
commitHelper.updateBranch(request.getTargetBranch(), sourceRevision, targetRevision);
return MergeCommandResult.success(targetRevision.name(), mergeHelper.getRevisionToMerge().name(), sourceRevision.name());
} else {
log.trace("fast forward is not possible, fallback to merge");
return fallbackMerge.run();
}
return switch (fastForwardStrategy) {
case MERGE_COMMIT -> {
log.trace("fast forward is not possible, fallback to merge");
yield fallbackMerge.run();
}
case THROW_EXCEPTION -> {
log.trace("fast forward is not possible, fallback to exception");
throw new FastForwardNotPossible(repository, request.getBranchToMerge(), request.getTargetBranch());
}
};
}
}

View File

@@ -63,6 +63,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
private static final Set<MergeStrategy> STRATEGIES = Set.of(
MergeStrategy.MERGE_COMMIT,
MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
MergeStrategy.FAST_FORWARD_ONLY,
MergeStrategy.SQUASH,
MergeStrategy.REBASE
);
@@ -102,7 +103,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
return switch (request.getMergeStrategy()) {
case SQUASH -> new GitMergeWithSquash(request, context, repositoryManager, eventFactory).run();
case FAST_FORWARD_IF_POSSIBLE ->
new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory).run();
new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory, FastForwardFallbackStrategy.MERGE_COMMIT).run();
case FAST_FORWARD_ONLY ->
new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory, FastForwardFallbackStrategy.THROW_EXCEPTION).run();
case MERGE_COMMIT -> new GitMergeCommit(request, context, repositoryManager, eventFactory).run();
case REBASE -> new GitMergeRebase(request, context, repositoryManager, eventFactory).run();
default -> throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());

View File

@@ -53,7 +53,7 @@ class GitMergeRebase {
this.context = context;
this.mergeHelper = new MergeHelper(context, request, repositoryManager, eventFactory);
this.commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
this.fastForwardMerge = new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory);
this.fastForwardMerge = new GitFastForwardIfPossible(request, context, repositoryManager, eventFactory, FastForwardFallbackStrategy.THROW_EXCEPTION);
}
MergeCommandResult run() {

View File

@@ -45,6 +45,7 @@ import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.Person;
import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.FastForwardNotPossible;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.MergePreventReason;
@@ -445,6 +446,38 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
}
}
@Test
public void shouldMergeWithFastForwardOnly() throws IOException, GitAPIException {
try (GitContext context = createContext(); Repository repository = context.open()) {
ObjectId featureBranchHead = new Git(repository).log().add(repository.resolve("squash")).setMaxCount(1).call().iterator().next().getId();
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setBranchToMerge("squash");
request.setTargetBranch("master");
request.setMergeStrategy(MergeStrategy.FAST_FORWARD_ONLY);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.getNewHeadRevision()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27");
assertThat(mergeCommandResult.getRevisionToMerge()).isEqualTo("35597e9e98fe53167266583848bfef985c2adb27");
assertThat(mergeCommandResult.getTargetRevision()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
assertThat(mergeCommandResult.isSuccess()).isTrue();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getParentCount()).isEqualTo(1);
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
PersonIdent mergeCommitter = mergeCommit.getCommitterIdent();
assertThat(mergeAuthor.getName()).isEqualTo("Philip J Fry");
assertThat(mergeCommitter.getName()).isEqualTo("Eduard Heimbuch");
assertThat(mergeCommit.getId()).isEqualTo(featureBranchHead);
}
}
@Test
public void shouldDoMergeCommitIfFastForwardIsNotPossible() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
@@ -470,6 +503,18 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
}
}
@Test(expected = FastForwardNotPossible.class)
public void shouldThrowErrorBecauseFastForwardNotPossible() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.FAST_FORWARD_ONLY);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
}
@Test(expected = NotFoundException.class)
public void shouldHandleNotExistingSourceBranchInMerge() {
GitMergeCommand command = createCommand();

View File

@@ -289,6 +289,10 @@
"displayName": "Nicht unterstützte Merge-Strategie",
"description": "Die gewählte Merge-Strategie wird von dem Repository nicht unterstützt."
},
"Re6hF5g14U": {
"displayName": "Fast-Forward fehlgeschlagen",
"description": "Die beiden Branches können nicht per Fast-Forward miteinander gemerged werden"
},
"78RhWxTIw1": {
"displayName": "Der Default-Branch kann nicht gelöscht werden",
"description": "Der Default-Branch kann nicht gelöscht werden. Bitte wählen Sie zuerst einen neuen Default-Branch."

View File

@@ -289,6 +289,10 @@
"displayName": "Merge strategy not supported",
"description": "The selected merge strategy is not supported by the repository."
},
"Re6hF5g14U": {
"displayName": "Fast forward failed",
"description": "The branches cannot be merged via fast forward"
},
"78RhWxTIw1": {
"displayName": "Default branch cannot be deleted",
"description": "The default branch of a repository cannot be deleted. Please select another default branch first."