diff --git a/app/server/appsmith-git/src/main/java/com/appsmith/git/handler/ce/FSGitHandlerCEImpl.java b/app/server/appsmith-git/src/main/java/com/appsmith/git/handler/ce/FSGitHandlerCEImpl.java index 624dabcebab8..a1be0d583302 100644 --- a/app/server/appsmith-git/src/main/java/com/appsmith/git/handler/ce/FSGitHandlerCEImpl.java +++ b/app/server/appsmith-git/src/main/java/com/appsmith/git/handler/ce/FSGitHandlerCEImpl.java @@ -10,6 +10,7 @@ import com.appsmith.external.dtos.MergeStatusDTO; import com.appsmith.external.git.constants.GitSpan; import com.appsmith.external.git.constants.ce.RefType; +import com.appsmith.external.git.dtos.FetchRemoteDTO; import com.appsmith.external.git.handler.FSGitHandler; import com.appsmith.external.helpers.Stopwatch; import com.appsmith.git.configurations.GitServiceConfig; @@ -33,7 +34,6 @@ import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.TransportConfigCallback; -import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.BranchTrackingStatus; import org.eclipse.jgit.lib.PersonIdent; @@ -42,6 +42,7 @@ import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.util.StringUtils; import org.springframework.stereotype.Component; import org.springframework.util.FileSystemUtils; @@ -92,6 +93,11 @@ public class FSGitHandlerCEImpl implements FSGitHandler { private final Scheduler scheduler = Schedulers.boundedElastic(); + private static final String BRANCH_REF_REMOTE_SRC = "refs/heads/"; + private static final String BRANCH_REF_LOCAL_DST = "refs/remotes/origin/"; + private static final String SRC_DST_DELIMITER = ":"; + private static final String TAG_REF = "refs/tags/"; + private static final String SUCCESS_MERGE_STATUS = "This branch has no conflicts with the base branch."; /** @@ -846,15 +852,14 @@ public Mono mergeBranch(Path repoSuffix, String sourceBranch, String des git -> Mono.fromCallable(() -> { Stopwatch processStopwatch = StopwatchHelpers.startStopwatch( repoSuffix, AnalyticsEvents.GIT_MERGE.getEventName()); - log.debug(Thread.currentThread().getName() + ": Merge branch " + sourceBranch - + " on " + destinationBranch); - try { - // checkout the branch on which the merge command is run - git.checkout() - .setName(destinationBranch) - .setCreateBranch(false) - .call(); + log.info( + "{}: Merge branch {} on {}", + Thread.currentThread().getName(), + sourceBranch, + destinationBranch); + + try { MergeResult mergeResult = git.merge() .include(git.getRepository().findRef(sourceBranch)) .setStrategy(MergeStrategy.RECURSIVE) @@ -934,9 +939,7 @@ public Mono fetchRemote( @Override public Mono fetchRemote( - Path repoSuffix, String publicKey, String privateKey, boolean isRepoPath, String... branchNames) { - Stopwatch processStopwatch = - StopwatchHelpers.startStopwatch(repoSuffix, AnalyticsEvents.GIT_FETCH.getEventName()); + Path repoSuffix, boolean isRepoPath, FetchRemoteDTO fetchRemoteDTO, String publicKey, String privateKey) { Path repoPath = TRUE.equals(isRepoPath) ? repoSuffix : createRepoPath(repoSuffix); return Mono.using( () -> Git.open(repoPath.toFile()), @@ -945,21 +948,35 @@ public Mono fetchRemote( new SshTransportConfigCallback(privateKey, publicKey); String fetchMessages; + List refNames = fetchRemoteDTO.getRefNames(); + RefType refType = fetchRemoteDTO.getRefType(); + List refSpecs = new ArrayList<>(); - for (String branchName : branchNames) { - RefSpec ref = new RefSpec( - "refs/heads/" + branchName + ":refs/remotes/origin/" + branchName); - refSpecs.add(ref); + if (RefType.tag.equals(refType)) { + for (String tagName : refNames) { + RefSpec refSpec = new RefSpec(TAG_REF + tagName + ":" + TAG_REF + tagName); + refSpecs.add(refSpec); + } + } else { + for (String refName : refNames) { + RefSpec ref = new RefSpec(BRANCH_REF_REMOTE_SRC + + refName + + SRC_DST_DELIMITER + + BRANCH_REF_LOCAL_DST + + refName); + refSpecs.add(ref); + } } fetchMessages = git.fetch() .setRefSpecs(refSpecs.toArray(new RefSpec[0])) .setRemoveDeletedRefs(true) + .setTagOpt(TagOpt.NO_TAGS) // no tags would mean that tags are fetched + // explicitly .setTransportConfigCallback(config) .call() .getMessages(); - processStopwatch.stopAndLogTimeInMillis(); return fetchMessages; }) .onErrorResume(error -> { @@ -980,30 +997,13 @@ public Mono isMergeBranch(Path repoSuffix, String sourceBranch, return Mono.using( () -> Git.open(createRepoPath(repoSuffix).toFile()), git -> Mono.fromCallable(() -> { - log.debug( - Thread.currentThread().getName() - + ": Check mergeability for repo {} with src: {}, dest: {}", + log.info( + "{}: Check merge-ability for repo {} with source: {}, destination: {}", + Thread.currentThread().getName(), repoSuffix, sourceBranch, destinationBranch); - // checkout the branch on which the merge command is run - try { - git.checkout() - .setName(destinationBranch) - .setCreateBranch(false) - .call(); - } catch (GitAPIException e) { - if (e instanceof CheckoutConflictException) { - MergeStatusDTO mergeStatus = new MergeStatusDTO(); - mergeStatus.setMergeAble(false); - mergeStatus.setConflictingFiles( - ((CheckoutConflictException) e).getConflictingPaths()); - processStopwatch.stopAndLogTimeInMillis(); - return mergeStatus; - } - } - MergeResult mergeResult = git.merge() .include(git.getRepository().findRef(sourceBranch)) .setFastForward(MergeCommand.FastForwardMode.NO_FF) @@ -1054,6 +1054,19 @@ public Mono isMergeBranch(Path repoSuffix, String sourceBranch, return Mono.error(e); } }) + .onErrorResume(error -> { + MergeStatusDTO mergeStatusDTO = new MergeStatusDTO(); + mergeStatusDTO.setMergeAble(false); + mergeStatusDTO.setMessage(error.getMessage()); + mergeStatusDTO.setReferenceDoc(ErrorReferenceDocUrl.GIT_MERGE_CONFLICT.getDocUrl()); + try { + return resetToLastCommit(repoSuffix, destinationBranch) + .thenReturn(mergeStatusDTO); + } catch (GitAPIException | IOException e) { + log.error("Error while hard resetting to latest commit {0}", e); + return Mono.error(e); + } + }) .timeout(Duration.ofMillis(Constraint.TIMEOUT_MILLIS)), Git::close) .subscribeOn(scheduler); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/dtos/FetchRemoteDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/dtos/FetchRemoteDTO.java new file mode 100644 index 000000000000..eba9e1fe7c7b --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/dtos/FetchRemoteDTO.java @@ -0,0 +1,29 @@ +package com.appsmith.external.git.dtos; + +import com.appsmith.external.git.constants.ce.RefType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FetchRemoteDTO { + + /** + * List of references which is to be fetched from remote. + */ + List refNames; + + /** + * Assumption is that we fetch only one type of refs at once. + */ + RefType refType; + + /** + * fetch all the remotes + */ + Boolean isFetchAll; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/handler/FSGitHandler.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/handler/FSGitHandler.java index b1eb6d68a303..8ff45d21cbad 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/handler/FSGitHandler.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/git/handler/FSGitHandler.java @@ -5,6 +5,7 @@ import com.appsmith.external.dtos.GitRefDTO; import com.appsmith.external.dtos.GitStatusDTO; import com.appsmith.external.dtos.MergeStatusDTO; +import com.appsmith.external.git.dtos.FetchRemoteDTO; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.BranchTrackingStatus; import reactor.core.publisher.Mono; @@ -135,6 +136,8 @@ Mono pullApplication( Mono getStatus(Path repoPath, String branchName); /** + * This method merges source branch into destination branch for a git repository which is present on the partial + * path provided. This assumes that the branch on which the merge will happen is already checked out * @param repoSuffix suffixedPath used to generate the base repo path this includes orgId, defaultAppId, repoName * @param sourceBranch name of the branch whose commits will be referred amd merged to destinationBranch * @param destinationBranch Merge operation is performed on this branch @@ -158,7 +161,7 @@ Mono fetchRemote( boolean isFetchAll); Mono fetchRemote( - Path repoSuffix, String publicKey, String privateKey, boolean isRepoPath, String... branchNames); + Path repoSuffix, boolean isRepoPath, FetchRemoteDTO fetchRemoteDTO, String publicKey, String privateKey); /** * diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java index ccadec5b3c63..752dced6f13b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java @@ -3,6 +3,7 @@ import com.appsmith.external.dtos.GitBranchDTO; import com.appsmith.external.dtos.GitRefDTO; import com.appsmith.external.dtos.GitStatusDTO; +import com.appsmith.external.dtos.MergeStatusDTO; import com.appsmith.external.git.constants.ce.RefType; import com.appsmith.git.dto.CommitDTO; import com.appsmith.server.constants.ArtifactType; @@ -13,6 +14,7 @@ import com.appsmith.server.dtos.AutoCommitResponseDTO; import com.appsmith.server.dtos.GitConnectDTO; import com.appsmith.server.dtos.GitDocsDTO; +import com.appsmith.server.dtos.GitMergeDTO; import com.appsmith.server.dtos.GitPullDTO; import reactor.core.publisher.Mono; @@ -45,6 +47,12 @@ Mono fetchRemoteChanges( GitType gitType, RefType refType); + Mono mergeBranch( + String branchedArtifactId, ArtifactType artifactType, GitMergeDTO gitMergeDTO, GitType gitType); + + Mono isBranchMergable( + String branchedArtifactId, ArtifactType artifactType, GitMergeDTO gitMergeDTO, GitType gitType); + Mono discardChanges(String branchedArtifactId, ArtifactType artifactType, GitType gitType); Mono getStatus( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java index db895a42bb83..a8471e978b27 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java @@ -9,6 +9,7 @@ import com.appsmith.external.git.constants.GitConstants; import com.appsmith.external.git.constants.GitSpan; import com.appsmith.external.git.constants.ce.RefType; +import com.appsmith.external.git.dtos.FetchRemoteDTO; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceStorage; import com.appsmith.git.dto.CommitDTO; @@ -34,6 +35,7 @@ import com.appsmith.server.dtos.AutoCommitResponseDTO; import com.appsmith.server.dtos.GitConnectDTO; import com.appsmith.server.dtos.GitDocsDTO; +import com.appsmith.server.dtos.GitMergeDTO; import com.appsmith.server.dtos.GitPullDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -59,6 +61,7 @@ import io.micrometer.observation.ObservationRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.TransportException; @@ -519,8 +522,8 @@ private Mono checkoutRemoteReference( if (baseRefName.equals(finalRemoteRefName)) { /* in this case, user deleted the initial default branch and now wants to check out to that branch. - as we didn't delete the application object but only the branch from git repo, - we can just use this existing application without creating a new one. + as we didn't delete the artifact object but only the branch from git repo, + we can just use this existing artifact without creating a new one. */ artifactMono = Mono.just(baseArtifact); } else { @@ -529,7 +532,7 @@ private Mono checkoutRemoteReference( } Mono checkedOutRemoteArtifactMono = gitHandlingService - .fetchRemoteChanges(jsonTransformationDTO, baseGitMetadata.getGitAuth(), false) + .fetchRemoteReferences(jsonTransformationDTO, baseGitMetadata.getGitAuth(), false) .onErrorResume(error -> Mono.error( new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "checkout branch", error.getMessage()))) .flatMap(ignoreRemoteChanges -> { @@ -624,7 +627,8 @@ protected Mono createReference( baseGitMetadata.getDefaultArtifactId(), GitConstants.GitCommandConstants.CREATE_BRANCH, FALSE); - Mono fetchRemoteMono = gitHandlingService.fetchRemoteChanges(jsonTransformationDTO, baseGitAuth, TRUE); + Mono fetchRemoteMono = + gitHandlingService.fetchRemoteReferences(jsonTransformationDTO, baseGitAuth, TRUE); Mono createBranchMono = acquireGitLockMono .flatMap(ignoreLockAcquisition -> fetchRemoteMono.onErrorResume( @@ -1139,10 +1143,10 @@ public Mono commitArtifact( GitType gitType, Boolean isFileLock) { /* - 1. Check if application exists and user have sufficient permissions + 1. Check if artifact exists and user have sufficient permissions 2. Check if branch name exists in git metadata - 3. Save application to the existing local repo - 4. Commit application : git add, git commit (Also check if git init required) + 3. Save artifact to the existing local repo + 4. Commit artifact : git add, git commit (Also check if git init required) */ String commitMessage = commitDTO.getMessage(); @@ -1390,7 +1394,7 @@ private Mono commitArtifact( /** * Method to remove all the git metadata for the artifact and connected resources. This will remove: * - local repo - * - all the branched applications present in DB except for default application + * - all the branched artifacts present in DB except for default artifact * * @param branchedArtifactId : id of any branched artifact for the given repo * @param artifactType : type of artifact @@ -1443,7 +1447,7 @@ public Mono detachRemote( jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType()); jsonTransformationDTO.setRefName(gitArtifactMetadata.getRefName()); - // Remove the parent application branch name from the list + // Remove the parent artifact branch name from the list Mono removeRepoMono = gitHandlingService.removeRepository(jsonTransformationDTO); Mono updatedArtifactMono = gitArtifactHelper.saveArtifact(baseArtifact); @@ -1583,7 +1587,7 @@ protected Mono getStatus( hence we don't need to do that manually */ log.error( - "Error to get status for application: {}, branch: {}", + "Error to get status for artifact: {}, branch: {}", baseArtifactId, finalBranchName, throwable); @@ -1693,11 +1697,11 @@ protected Mono pullArtifact(Artifact baseArtifact, Artifact branched } /** - * Method to pull the files from remote repo and rehydrate the application + * Method to pull the files from remote repo and rehydrate the artifact * * @param baseArtifact : base artifact * @param branchedArtifact : a branch created from branches of base artifact - * @return pull DTO with updated application + * @return pull DTO with updated artifact */ private Mono pullAndRehydrateArtifact( Artifact baseArtifact, Artifact branchedArtifact, GitType gitType) { @@ -1706,7 +1710,7 @@ private Mono pullAndRehydrateArtifact( 2. Do git pull after On Merge conflict - throw exception and ask user to resolve these conflicts on remote TODO create new branch and push the changes to remote and ask the user to resolve it on github/gitlab UI - 3. Rehydrate the application from filesystem so that the latest changes from remote are rendered to the application + 3. Rehydrate the artifact from filesystem so that the latest changes from remote are rendered to the artifact */ ArtifactType artifactType = baseArtifact.getArtifactType(); @@ -1821,7 +1825,7 @@ public Mono fetchRemoteChanges( // current user mono has been zipped just to run in parallel. Mono fetchRemoteMono = acquireGitLockMono - .then(Mono.defer(() -> gitHandlingService.fetchRemoteChanges( + .then(Mono.defer(() -> gitHandlingService.fetchRemoteReferences( jsonTransformationDTO, baseArtifactGitData.getGitAuth(), FALSE))) .flatMap(fetchedRemoteStatusString -> { return gitRedisUtils @@ -1834,7 +1838,7 @@ public Mono fetchRemoteChanges( hence we don't need to do that manually */ log.error( - "Error to fetch from remote for application: {}, branch: {}, git type {}", + "Error to fetch from remote for artifact: {}, ref: {}, git type {}", baseArtifactId, refArtifactGitData.getRefName(), gitType, @@ -1964,9 +1968,9 @@ private Mono updateArtifactWithGitMetadataGivenPermission( } artifact.setGitArtifactMetadata(gitMetadata); - // For default application we expect a GitAuth to be a part of gitMetadata. We are using save method to leverage + // For base artifact we expect a GitAuth to be a part of gitMetadata. We are using save method to leverage // @Encrypted annotation used for private SSH keys - // applicationService.save sets the transient fields so no need to set it again from this method + // saveArtifact method sets the transient fields so no need to set it again from this method return gitArtifactHelperResolver .getArtifactHelper(artifact.getArtifactType()) .saveArtifact(artifact); @@ -2245,8 +2249,8 @@ private Flux updateDefaultBranchName( private Mono> handleRepoNotFoundException( ArtifactJsonTransformationDTO jsonTransformationDTO, GitType gitType) { - // clone application to the local filesystem again and update the defaultBranch for the application - // list branch and compare with branch applications and checkout if not exists + // clone artifact to the local git system again and update the defaultBranch for the artifact + // list branch and compare with branch artifacts and checkout if not exists GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType); GitArtifactHelper gitArtifactHelper = @@ -2356,7 +2360,7 @@ private Mono> getBranchListWithDefaultBranchName( if (TRUE.equals(pruneBranches)) { return gitHandlingService - .fetchRemoteChanges(jsonTransformationDTO, baseGitData.getGitAuth(), TRUE) + .fetchRemoteReferences(jsonTransformationDTO, baseGitData.getGitAuth(), TRUE) .then(listBranchesMono); } return listBranchesMono; @@ -2593,4 +2597,420 @@ public Mono> getGitDocUrls() { } return Mono.just(gitDocsDTOList); } + + @Override + public Mono mergeBranch( + String branchedArtifactId, ArtifactType artifactType, GitMergeDTO gitMergeDTO, GitType gitType) { + /* + * 1.Dehydrate the artifact from Mongodb so that the file system has the latest artifact data for both the source and destination branch artifact + * 2.Do git checkout destinationBranch ---> git merge sourceBranch after the rehydration + * On Merge conflict - create new branch and push the changes to remote and ask the user to resolve it on Github/Gitlab UI + * 3.Then rehydrate from the file system to mongodb so that the latest changes from remote are rendered to the artifact + * 4.Get the latest artifact mono from the mongodb and send it back to client + * */ + + final String sourceBranch = gitMergeDTO.getSourceBranch(); + final String destinationBranch = gitMergeDTO.getDestinationBranch(); + + if (!hasText(sourceBranch) || !hasText(destinationBranch)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.BRANCH_NAME)); + } else if (sourceBranch.startsWith(ORIGIN)) { + return Mono.error( + new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION_FOR_REMOTE_BRANCH, sourceBranch)); + } else if (destinationBranch.startsWith(ORIGIN)) { + return Mono.error( + new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION_FOR_REMOTE_BRANCH, destinationBranch)); + } + + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + AclPermission artifactEditPermission = gitArtifactHelper.getArtifactEditPermission(); + + Mono> baseAndBranchedArtifactsMono = + getBaseAndBranchedArtifacts(branchedArtifactId, artifactType).cache(); + + Mono destinationArtifactMono = baseAndBranchedArtifactsMono.flatMap(artifactTuples -> { + Artifact baseArtifact = artifactTuples.getT1(); + if (destinationBranch.equals(baseArtifact.getGitArtifactMetadata().getRefName())) { + return Mono.just(baseArtifact); + } + + return gitArtifactHelper.getArtifactByBaseIdAndBranchName( + baseArtifact.getId(), destinationBranch, artifactEditPermission); + }); + + Mono mergeMono = baseAndBranchedArtifactsMono + .zipWith(destinationArtifactMono) + .flatMap(artifactTuples -> { + Artifact baseArtifact = artifactTuples.getT1().getT1(); + Artifact sourceArtifact = artifactTuples.getT1().getT2(); + Artifact destinationArtifact = artifactTuples.getT2(); + + GitArtifactMetadata baseGitMetadata = baseArtifact.getGitArtifactMetadata(); + if (isBaseGitMetadataInvalid(baseArtifact.getGitArtifactMetadata(), gitType)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION)); + } + + final String workspaceId = baseArtifact.getWorkspaceId(); + final String baseArtifactId = baseGitMetadata.getDefaultArtifactId(); + final String repoName = baseGitMetadata.getRepoName(); + + // 1. Hydrate from db to git system for both ref Artifacts + // Update function call + return Mono.usingWhen( + gitRedisUtils.acquireGitLock( + artifactType, baseArtifactId, GitConstants.GitCommandConstants.MERGE_BRANCH, TRUE), + ignoreLock -> { + ArtifactJsonTransformationDTO jsonTransformationDTO = + new ArtifactJsonTransformationDTO(workspaceId, baseArtifactId, repoName); + jsonTransformationDTO.setArtifactType(artifactType); + + FetchRemoteDTO fetchRemoteDTO = new FetchRemoteDTO(); + fetchRemoteDTO.setRefNames(List.of(sourceBranch, destinationBranch)); + + GitHandlingService gitHandlingService = + gitHandlingServiceResolver.getGitHandlingService(gitType); + + Mono fetchingRemoteMono = gitHandlingService.fetchRemoteReferences( + jsonTransformationDTO, fetchRemoteDTO, baseGitMetadata.getGitAuth()); + + Mono> statusTupleMono = fetchingRemoteMono + .flatMap(remoteSpecs -> { + Mono sourceBranchStatusMono = Mono.defer(() -> getStatus( + baseArtifact, sourceArtifact, false, false, gitType) + .flatMap(srcBranchStatus -> { + if (srcBranchStatus.getIsClean()) { + return Mono.just(srcBranchStatus); + } + + AppsmithException statusFailureException; + + if (!Integer.valueOf(0) + .equals(srcBranchStatus.getBehindCount())) { + statusFailureException = new AppsmithException( + AppsmithError.GIT_MERGE_FAILED_REMOTE_CHANGES, + srcBranchStatus.getBehindCount(), + sourceBranch); + } else { + statusFailureException = new AppsmithException( + AppsmithError.GIT_MERGE_FAILED_LOCAL_CHANGES, + sourceBranch); + } + + return Mono.error(statusFailureException); + })); + + Mono destinationBranchStatusMono = Mono.defer(() -> getStatus( + baseArtifact, destinationArtifact, false, false, gitType) + .flatMap(destinationBranchStatus -> { + if (destinationBranchStatus.getIsClean()) { + Mono.just(destinationBranchStatus); + } + + AppsmithException statusFailureException; + + if (!Integer.valueOf(0) + .equals(destinationBranchStatus.getBehindCount())) { + statusFailureException = new AppsmithException( + AppsmithError.GIT_MERGE_FAILED_REMOTE_CHANGES, + destinationBranchStatus.getBehindCount(), + destinationBranch); + } else { + statusFailureException = new AppsmithException( + AppsmithError.GIT_MERGE_FAILED_LOCAL_CHANGES, + destinationBranch); + } + + return Mono.error(statusFailureException); + })); + + return sourceBranchStatusMono.zipWith(destinationBranchStatusMono); + }) + .onErrorResume(error -> { + log.error( + "Error in repo status check for artifact: {}, Details: {}", + branchedArtifactId, + error.getMessage()); + + if (error instanceof AppsmithException) { + Mono.error(error); + } + + return Mono.error(new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, "status", error)); + }); + + return statusTupleMono + .flatMap(statusTuples -> { + GitMergeDTO mergeDTO = new GitMergeDTO(); + mergeDTO.setSourceBranch(sourceBranch); + mergeDTO.setDestinationBranch(destinationBranch); + + Mono mergeBranchesMono = + gitHandlingService.mergeBranches(jsonTransformationDTO, mergeDTO); + + return mergeBranchesMono.onErrorResume(error -> gitAnalyticsUtils + .addAnalyticsForGitOperation( + AnalyticsEvents.GIT_MERGE, + baseArtifact, + error.getClass().getName(), + error.getMessage(), + baseGitMetadata.getIsRepoPrivate()) + .flatMap(artifact -> { + if (error instanceof GitAPIException) { + return Mono.error(new AppsmithException( + AppsmithError.GIT_MERGE_CONFLICTS, + error.getMessage())); + } + + return Mono.error(new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, + "merge", + error.getMessage())); + })); + }) + .zipWhen(mergeStatus -> { + ArtifactJsonTransformationDTO constructJsonDTO = + new ArtifactJsonTransformationDTO( + workspaceId, baseArtifactId, repoName); + constructJsonDTO.setArtifactType(artifactType); + constructJsonDTO.setRefName(destinationBranch); + return gitHandlingService.reconstructArtifactJsonFromGitRepository( + constructJsonDTO); + }) + .flatMap(tuple2 -> { + ArtifactExchangeJson artifactExchangeJson = tuple2.getT2(); + MergeStatusDTO mergeStatusDTO = new MergeStatusDTO(); + mergeStatusDTO.setStatus(tuple2.getT1()); + mergeStatusDTO.setMergeAble(TRUE); + + // 4. Get the latest artifact mono with all the changes + return importService + .importArtifactInWorkspaceFromGit( + workspaceId, + destinationArtifact.getId(), + artifactExchangeJson, + destinationBranch.replaceFirst( + ORIGIN, REMOTE_NAME_REPLACEMENT)) + .flatMap(importedDestinationArtifact -> { + CommitDTO commitDTO = new CommitDTO(); + commitDTO.setMessage(DEFAULT_COMMIT_MESSAGE + + GitDefaultCommitMessage.SYNC_REMOTE_AFTER_MERGE + .getReason() + + sourceBranch); + + return commitArtifact( + commitDTO, + importedDestinationArtifact.getId(), + artifactType, + gitType) + .then(gitAnalyticsUtils.addAnalyticsForGitOperation( + AnalyticsEvents.GIT_MERGE, + importedDestinationArtifact, + importedDestinationArtifact + .getGitArtifactMetadata() + .getIsRepoPrivate())) + .thenReturn(mergeStatusDTO); + }); + }); + }, + ignoreLock -> gitRedisUtils.releaseFileLock(artifactType, baseArtifactId, TRUE)); + }) + .name(GitSpan.OPS_MERGE_BRANCH) + .tap(Micrometer.observation(observationRegistry)); + + return Mono.create(sink -> mergeMono.subscribe(sink::success, sink::error, null, sink.currentContext())); + } + + @Override + public Mono isBranchMergable( + String branchedArtifactId, ArtifactType artifactType, GitMergeDTO gitMergeDTO, GitType gitType) { + + final String sourceBranch = gitMergeDTO.getSourceBranch(); + final String destinationBranch = gitMergeDTO.getDestinationBranch(); + + if (!hasText(sourceBranch) || !hasText(destinationBranch)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.BRANCH_NAME)); + } else if (sourceBranch.startsWith(ORIGIN)) { + return Mono.error( + new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION_FOR_REMOTE_BRANCH, sourceBranch)); + } else if (destinationBranch.startsWith(ORIGIN)) { + return Mono.error( + new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION_FOR_REMOTE_BRANCH, destinationBranch)); + } + + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + AclPermission artifactEditPermission = gitArtifactHelper.getArtifactEditPermission(); + + Mono> baseAndBranchedArtifactsMono = + getBaseAndBranchedArtifacts(branchedArtifactId, artifactType).cache(); + + Mono destinationArtifactMono = baseAndBranchedArtifactsMono.flatMap(artifactTuples -> { + Artifact baseArtifact = artifactTuples.getT1(); + if (destinationBranch.equals(baseArtifact.getGitArtifactMetadata().getRefName())) { + return Mono.just(baseArtifact); + } + + return gitArtifactHelper.getArtifactByBaseIdAndBranchName( + baseArtifact.getId(), destinationBranch, artifactEditPermission); + }); + + Mono mergeableStatusMono = baseAndBranchedArtifactsMono + .zipWith(destinationArtifactMono) + .flatMap(artifactTuples -> { + Artifact baseArtifact = artifactTuples.getT1().getT1(); + Artifact sourceArtifact = artifactTuples.getT1().getT2(); + Artifact destinationArtifact = artifactTuples.getT2(); + + GitArtifactMetadata baseGitMetadata = baseArtifact.getGitArtifactMetadata(); + if (isBaseGitMetadataInvalid(baseArtifact.getGitArtifactMetadata(), gitType)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION)); + } + + final String workspaceId = baseArtifact.getWorkspaceId(); + final String baseArtifactId = baseGitMetadata.getDefaultArtifactId(); + final String repoName = baseGitMetadata.getRepoName(); + + // 1. Hydrate from db to git system for both ref Artifacts + // Update function call + return Mono.usingWhen( + gitRedisUtils.acquireGitLock( + artifactType, baseArtifactId, GitConstants.GitCommandConstants.MERGE_BRANCH, TRUE), + ignoreLock -> { + ArtifactJsonTransformationDTO jsonTransformationDTO = + new ArtifactJsonTransformationDTO(workspaceId, baseArtifactId, repoName); + jsonTransformationDTO.setArtifactType(artifactType); + + FetchRemoteDTO fetchRemoteDTO = new FetchRemoteDTO(); + fetchRemoteDTO.setRefNames(List.of(sourceBranch, destinationBranch)); + + GitHandlingService gitHandlingService = + gitHandlingServiceResolver.getGitHandlingService(gitType); + + Mono fetchRemoteReferencesMono = gitHandlingService.fetchRemoteReferences( + jsonTransformationDTO, fetchRemoteDTO, baseGitMetadata.getGitAuth()); + + Mono> statusTupleMono = fetchRemoteReferencesMono + .flatMap(remoteSpecs -> { + Mono sourceBranchStatusMono = Mono.defer(() -> getStatus( + baseArtifact, sourceArtifact, false, false, gitType) + .flatMap(srcBranchStatus -> { + if (srcBranchStatus.getIsClean()) { + return Mono.just(srcBranchStatus); + } + + AppsmithError uncleanStatusError; + AppsmithException uncleanStatusException; + + if (!Integer.valueOf(0) + .equals(srcBranchStatus.getBehindCount())) { + uncleanStatusError = + AppsmithError.GIT_MERGE_FAILED_REMOTE_CHANGES; + uncleanStatusException = new AppsmithException( + uncleanStatusError, + srcBranchStatus.getBehindCount(), + sourceBranch); + } else { + uncleanStatusError = + AppsmithError.GIT_MERGE_FAILED_LOCAL_CHANGES; + uncleanStatusException = new AppsmithException( + uncleanStatusError, sourceBranch); + } + + return gitAnalyticsUtils + .addAnalyticsForGitOperation( + AnalyticsEvents.GIT_MERGE_CHECK, + baseArtifact, + uncleanStatusError.name(), + uncleanStatusException.getMessage(), + baseGitMetadata.getIsRepoPrivate(), + false, + false) + .then(Mono.error(uncleanStatusException)); + })); + + Mono destinationBranchStatusMono = Mono.defer(() -> getStatus( + baseArtifact, destinationArtifact, false, false, gitType) + .flatMap(destinationBranchStatus -> { + if (destinationBranchStatus.getIsClean()) { + return Mono.just(destinationBranchStatus); + } + + AppsmithError uncleanStatusError; + AppsmithException uncleanStatusException; + + if (!Integer.valueOf(0) + .equals(destinationBranchStatus.getBehindCount())) { + uncleanStatusError = + AppsmithError.GIT_MERGE_FAILED_REMOTE_CHANGES; + uncleanStatusException = new AppsmithException( + uncleanStatusError, + destinationBranchStatus.getBehindCount(), + destinationBranch); + } else { + uncleanStatusError = + AppsmithError.GIT_MERGE_FAILED_LOCAL_CHANGES; + uncleanStatusException = new AppsmithException( + uncleanStatusError, destinationBranch); + } + + return gitAnalyticsUtils + .addAnalyticsForGitOperation( + AnalyticsEvents.GIT_MERGE_CHECK, + baseArtifact, + uncleanStatusError.name(), + uncleanStatusException.getMessage(), + baseGitMetadata.getIsRepoPrivate(), + false, + false) + .then(Mono.error(uncleanStatusException)); + })); + + return sourceBranchStatusMono.zipWith(destinationBranchStatusMono); + }) + .onErrorResume(error -> { + log.error( + "Error in merge status check for baseArtifact {} ", + baseArtifactId, + error); + if (error instanceof AppsmithException) { + Mono.error(error); + } + + return Mono.error(new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, "status", error)); + }); + + return statusTupleMono.flatMap(statusTuple -> { + GitMergeDTO mergeDTO = new GitMergeDTO(); + mergeDTO.setSourceBranch(sourceBranch); + mergeDTO.setDestinationBranch(destinationBranch); + + Mono isBranchMergable = + gitHandlingService.isBranchMergable(jsonTransformationDTO, mergeDTO); + + return isBranchMergable.onErrorResume(error -> { + MergeStatusDTO mergeStatus = new MergeStatusDTO(); + mergeStatus.setMergeAble(false); + mergeStatus.setStatus("Merge check failed!"); + mergeStatus.setMessage(error.getMessage()); + + return gitAnalyticsUtils + .addAnalyticsForGitOperation( + AnalyticsEvents.GIT_MERGE_CHECK, + baseArtifact, + error.getClass().getName(), + error.getMessage(), + baseGitMetadata.getIsRepoPrivate(), + false, + false) + .thenReturn(mergeStatus); + }); + }); + }, + ignoreLock -> gitRedisUtils.releaseFileLock(artifactType, baseArtifactId, TRUE)); + }); + + return Mono.create( + sink -> mergeableStatusMono.subscribe(sink::success, sink::error, null, sink.currentContext())); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java index 43459bd9682a..473d5f81ec36 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java @@ -3,12 +3,14 @@ import com.appsmith.external.dtos.GitRefDTO; import com.appsmith.external.dtos.GitStatusDTO; import com.appsmith.external.dtos.MergeStatusDTO; +import com.appsmith.external.git.dtos.FetchRemoteDTO; import com.appsmith.git.dto.CommitDTO; import com.appsmith.server.domains.Artifact; import com.appsmith.server.domains.GitArtifactMetadata; import com.appsmith.server.domains.GitAuth; import com.appsmith.server.dtos.ArtifactExchangeJson; import com.appsmith.server.dtos.GitConnectDTO; +import com.appsmith.server.dtos.GitMergeDTO; import com.appsmith.server.git.dtos.ArtifactJsonTransformationDTO; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -70,9 +72,16 @@ Mono prepareChangesToBeCommitted( Mono> commitArtifact( Artifact branchedArtifact, CommitDTO commitDTO, ArtifactJsonTransformationDTO jsonTransformationDTO); - Mono fetchRemoteChanges( + Mono fetchRemoteReferences( ArtifactJsonTransformationDTO jsonTransformationDTO, GitAuth gitAuth, Boolean isFetchAll); + Mono fetchRemoteReferences( + ArtifactJsonTransformationDTO jsonTransformationDTO, FetchRemoteDTO fetchRemoteDTO, GitAuth gitAuth); + + Mono mergeBranches(ArtifactJsonTransformationDTO jsonTransformationDTO, GitMergeDTO gitMergeDTO); + + Mono isBranchMergable(ArtifactJsonTransformationDTO JsonTransformationDTO, GitMergeDTO gitMergeDTO); + Mono recreateArtifactJsonFromLastCommit( ArtifactJsonTransformationDTO jsonTransformationDTO); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitApplicationControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitApplicationControllerCE.java index aefef9970fe6..9adf691c24d8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitApplicationControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitApplicationControllerCE.java @@ -3,6 +3,7 @@ import com.appsmith.external.dtos.GitBranchDTO; import com.appsmith.external.dtos.GitRefDTO; import com.appsmith.external.dtos.GitStatusDTO; +import com.appsmith.external.dtos.MergeStatusDTO; import com.appsmith.external.git.constants.ce.RefType; import com.appsmith.external.views.Views; import com.appsmith.git.dto.CommitDTO; @@ -14,6 +15,7 @@ import com.appsmith.server.dtos.AutoCommitResponseDTO; import com.appsmith.server.dtos.BranchProtectionRequestDTO; import com.appsmith.server.dtos.GitConnectDTO; +import com.appsmith.server.dtos.GitMergeDTO; import com.appsmith.server.dtos.GitPullDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.git.autocommit.AutoCommitService; @@ -148,6 +150,34 @@ public Mono> fetchRemoteChanges( .map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null)); } + @JsonView(Views.Public.class) + @PostMapping("/{branchedApplicationId}/merge") + public Mono> merge( + @PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) { + log.debug( + "Going to merge branch {} with branch {} for application {}", + gitMergeDTO.getSourceBranch(), + gitMergeDTO.getDestinationBranch(), + branchedApplicationId); + return centralGitService + .mergeBranch(branchedApplicationId, ARTIFACT_TYPE, gitMergeDTO, GIT_TYPE) + .map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null)); + } + + @JsonView(Views.Public.class) + @PostMapping("/{branchedApplicationId}/merge/status") + public Mono> mergeStatus( + @PathVariable String branchedApplicationId, @RequestBody GitMergeDTO gitMergeDTO) { + log.info( + "Check if branch {} can be merged with branch {} for application {}", + gitMergeDTO.getSourceBranch(), + gitMergeDTO.getDestinationBranch(), + branchedApplicationId); + return centralGitService + .isBranchMergable(branchedApplicationId, ARTIFACT_TYPE, gitMergeDTO, GIT_TYPE) + .map(result -> new ResponseDTO<>(HttpStatus.OK.value(), result, null)); + } + @JsonView(Views.Public.class) @DeleteMapping("/{baseArtifactId}/ref") public Mono> deleteBranch( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java index 71215783c16e..d2fd03b8ad04 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java @@ -4,7 +4,7 @@ import com.appsmith.server.constants.ArtifactType; import com.appsmith.server.constants.Url; import com.appsmith.server.domains.GitAuth; -import com.appsmith.server.dtos.ApplicationImportDTO; +import com.appsmith.server.dtos.ArtifactImportDTO; import com.appsmith.server.dtos.GitConnectDTO; import com.appsmith.server.dtos.GitDeployKeyDTO; import com.appsmith.server.dtos.GitDocsDTO; @@ -38,13 +38,12 @@ public class GitArtifactControllerCE { @JsonView(Views.Public.class) @PostMapping("/import") - public Mono> importApplicationFromGit( + public Mono> importArtifactFromGit( @RequestParam String workspaceId, @RequestBody GitConnectDTO gitConnectDTO) { // TODO: remove artifact type from methods. return centralGitService .importArtifactFromGit(workspaceId, gitConnectDTO, ArtifactType.APPLICATION, GIT_TYPE) - .map(artifactImportDTO -> (ApplicationImportDTO) artifactImportDTO) .map(result -> new ResponseDTO<>(HttpStatus.CREATED.value(), result, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/dtos/ArtifactJsonTransformationDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/dtos/ArtifactJsonTransformationDTO.java index f6d3d1ef5ace..a4a1470ffeae 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/dtos/ArtifactJsonTransformationDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/dtos/ArtifactJsonTransformationDTO.java @@ -2,7 +2,9 @@ import com.appsmith.external.git.constants.ce.RefType; import com.appsmith.server.constants.ArtifactType; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; // TODO: Find a better name for this DTO @@ -11,6 +13,8 @@ * this is also responsible for traversing paths for fs ops */ @Data +@NoArgsConstructor +@AllArgsConstructor public class ArtifactJsonTransformationDTO { String workspaceId; @@ -24,4 +28,10 @@ public class ArtifactJsonTransformationDTO { ArtifactType artifactType; RefType refType; + + public ArtifactJsonTransformationDTO(String workspaceId, String baseArtifactId, String repoName) { + this.workspaceId = workspaceId; + this.baseArtifactId = baseArtifactId; + this.repoName = repoName; + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java index 598147b8f81b..541c57058ceb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java @@ -8,6 +8,7 @@ import com.appsmith.external.git.constants.GitConstants; import com.appsmith.external.git.constants.GitSpan; import com.appsmith.external.git.constants.ce.RefType; +import com.appsmith.external.git.dtos.FetchRemoteDTO; import com.appsmith.external.git.handler.FSGitHandler; import com.appsmith.git.dto.CommitDTO; import com.appsmith.server.configurations.EmailConfig; @@ -19,6 +20,7 @@ import com.appsmith.server.domains.GitDeployKeys; import com.appsmith.server.dtos.ArtifactExchangeJson; import com.appsmith.server.dtos.GitConnectDTO; +import com.appsmith.server.dtos.GitMergeDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.exports.internal.ExportService; @@ -29,6 +31,7 @@ import com.appsmith.server.git.resolver.GitArtifactHelperResolver; import com.appsmith.server.git.utils.GitAnalyticsUtils; import com.appsmith.server.git.utils.GitProfileUtils; +import com.appsmith.server.helpers.CollectionUtils; import com.appsmith.server.helpers.CommonGitFileUtils; import com.appsmith.server.helpers.GitPrivateRepoHelper; import com.appsmith.server.helpers.GitUtils; @@ -579,7 +582,7 @@ private Mono pushArtifactErrorRecovery(String pushResult, Artifact artif * @return : returns string for remote fetch */ @Override - public Mono fetchRemoteChanges( + public Mono fetchRemoteReferences( ArtifactJsonTransformationDTO jsonTransformationDTO, GitAuth gitAuth, Boolean isFetchAll) { String workspaceId = jsonTransformationDTO.getWorkspaceId(); @@ -600,6 +603,57 @@ public Mono fetchRemoteChanges( return fetchRemoteMono.flatMap(remoteFetched -> checkoutBranchMono.thenReturn(remoteFetched)); } + @Override + public Mono fetchRemoteReferences( + ArtifactJsonTransformationDTO jsonTransformationDTO, FetchRemoteDTO fetchRemoteDTO, GitAuth gitAuth) { + String workspaceId = jsonTransformationDTO.getWorkspaceId(); + String baseArtifactId = jsonTransformationDTO.getBaseArtifactId(); + String repoName = jsonTransformationDTO.getRepoName(); + + ArtifactType artifactType = jsonTransformationDTO.getArtifactType(); + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName); + + String publicKey = gitAuth.getPublicKey(); + String privateKey = gitAuth.getPrivateKey(); + + if (CollectionUtils.isNullOrEmpty(fetchRemoteDTO.getRefNames())) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_GIT_CONFIGURATION)); + } + + return fsGitHandler.fetchRemote(repoSuffix, false, fetchRemoteDTO, publicKey, privateKey); + } + + @Override + public Mono mergeBranches(ArtifactJsonTransformationDTO jsonTransformationDTO, GitMergeDTO gitMergeDTO) { + String workspaceId = jsonTransformationDTO.getWorkspaceId(); + String baseArtifactId = jsonTransformationDTO.getBaseArtifactId(); + String repoName = jsonTransformationDTO.getRepoName(); + + ArtifactType artifactType = jsonTransformationDTO.getArtifactType(); + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName); + + // At this point the assumption is that the repository has already checked out the destination branch + return fsGitHandler.mergeBranch(repoSuffix, gitMergeDTO.getSourceBranch(), gitMergeDTO.getDestinationBranch()); + } + + @Override + public Mono isBranchMergable( + ArtifactJsonTransformationDTO jsonTransformationDTO, GitMergeDTO gitMergeDTO) { + String workspaceId = jsonTransformationDTO.getWorkspaceId(); + String baseArtifactId = jsonTransformationDTO.getBaseArtifactId(); + String repoName = jsonTransformationDTO.getRepoName(); + + ArtifactType artifactType = jsonTransformationDTO.getArtifactType(); + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName); + + // At this point the assumption is that the repository has already checked out the destination branch + return fsGitHandler.isMergeBranch( + repoSuffix, gitMergeDTO.getSourceBranch(), gitMergeDTO.getDestinationBranch()); + } + @Override public Mono recreateArtifactJsonFromLastCommit( ArtifactJsonTransformationDTO jsonTransformationDTO) {