[git4idea] Add a flow for resolving conflicts from MR/PR

GitOrigin-RevId: e517f674266631bd04417c9aceb9c6044f8ca5a6
This commit is contained in:
Chris Lemaire
2024-06-21 17:27:57 +02:00
committed by intellij-monorepo-bot
parent 3ccb7e84b6
commit dd900f8db2
7 changed files with 214 additions and 2 deletions

View File

@@ -108,6 +108,9 @@ review.details.status.reviewer.wait.for.updates={0} is waiting for updates
review.details.view.timeline.action=View Timeline
review.details.resolve-conflicts.head=Source
review.details.resolve-conflicts.base=Target
# Review submit
review.comment.placeholder=Review comment
review.submit.action=Submit

View File

@@ -1496,6 +1496,8 @@ notification.title.error.merging=Merge Error
progress.text.merging.repository=Merging{0}\u2026
dialog.title.rebasing.merge.commits=Rebasing Merge Commits
dialog.message.rebasing.merge.commits=You are about to rebase a merge commit with conflicts.\n\nChoose 'Merge' if you don't want to resolve conflicts again, or you still can rebase if you want to linearize the history.
dialog.title.update.branch=Update {0}
dialog.message.update.branch=You are about to update {0} from {1}.\n\nChoose ''Merge'' if you want to create a merge commit, ''Rebase'' if you want to rebase onto {1}.
rebasing.merge.commits.button.merge=Merge
rebasing.merge.commits.button.rebase=Rebase
rebasing.merge.commits.button.cancel=Cancel

View File

@@ -206,7 +206,23 @@ public interface GitBrancher {
@NotNull List<? extends @NotNull GitRepository> repositories);
/**
* @deprecated use {@link #merge(GitReference, DeleteOnMergeOption, List)}
* <p>Merges the given branch to the HEAD.</p>
* <p>{@code git merge <name>}</p>
* <p>If local changes prevent merging, proposes the "Smart merge" procedure (stash-merge-unstash).</p>
* <p>If untracked files prevent merging, shows them in an error dialog.</p>
*
* @param reference local/remote branch or tag to be merged into HEAD.
* @param deleteOnMerge specify whether the branch should be automatically deleted or proposed to be deleted after merge.
* @param repositories repositories to operate on.
* @param allowRollback whether to prompt the user to rollback on conflicts. Useful to set to `false` when prompting is not necessary.
*/
void merge(@NotNull GitReference reference,
@NotNull DeleteOnMergeOption deleteOnMerge,
@NotNull List<? extends @NotNull GitRepository> repositories,
@NotNull Boolean allowRollback);
/**
* @deprecated use {@link #merge(GitReference, DeleteOnMergeOption, List, Boolean)}
* @param branchName
* @param deleteOnMerge
* @param repositories

View File

@@ -50,6 +50,14 @@ class GitBrancherImpl implements GitBrancher {
return new GitBranchWorker(myProject, Git.getInstance(), new GitBranchUiHandlerImpl(myProject, indicator));
}
private @NotNull GitBranchWorker newWorkerWithoutRollback(@NotNull ProgressIndicator indicator) {
return new GitBranchWorker(myProject, Git.getInstance(), new GitBranchUiHandlerImpl(myProject, indicator) {
@Override
public boolean showUnmergedFilesMessageWithRollback(@NotNull String operationName, @NotNull String rollbackProposal) {
return false;
}
});
}
@Override
public void createBranch(@NotNull String name, @NotNull Map<GitRepository, String> startPoints, @Nullable Runnable callInAwtLater) {
createBranch(name, startPoints, false, callInAwtLater);
@@ -190,10 +198,19 @@ class GitBrancherImpl implements GitBrancher {
public void merge(@NotNull GitReference reference,
@NotNull DeleteOnMergeOption deleteOnMerge,
@NotNull List<? extends @NotNull GitRepository> repositories) {
merge(reference, deleteOnMerge, repositories, true);
}
@Override
public void merge(@NotNull GitReference reference,
@NotNull DeleteOnMergeOption deleteOnMerge,
@NotNull List<? extends @NotNull GitRepository> repositories,
@NotNull Boolean allowRollback) {
new CommonBackgroundTask(myProject, GitBundle.message("branch.merging.process", reference.getName()), null) {
@Override
public void execute(@NotNull ProgressIndicator indicator) {
newWorker(indicator).merge(reference, deleteOnMerge, repositories);
GitBranchWorker worker = allowRollback ? newWorker(indicator) : newWorkerWithoutRollback(indicator);
worker.merge(reference, deleteOnMerge, repositories);
}
}.runInBackground();
}

View File

@@ -0,0 +1,24 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.remote.hosting.ui
import com.intellij.collaboration.messages.CollaborationToolsBundle
import git4idea.remote.hosting.HostedGitRepositoryRemote
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
/**
* Represents a request to resolve conflicts in a code review locally.
*/
@ApiStatus.Internal
class ResolveConflictsLocallyCoordinates(
val headRemoteDescriptor: HostedGitRepositoryRemote,
val headRefName: String,
val baseRemoteDescriptor: HostedGitRepositoryRemote,
val baseRefName: String
)
@ApiStatus.Internal
enum class BaseOrHead(val text: @Nls String) {
Base(CollaborationToolsBundle.message("review.details.resolve-conflicts.base")),
Head(CollaborationToolsBundle.message("review.details.resolve-conflicts.head"));
}

View File

@@ -0,0 +1,31 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.remote.hosting.ui
import com.intellij.openapi.ui.Messages
import git4idea.DialogManager
import git4idea.i18n.GitBundle
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
@ApiStatus.Internal
object ResolveConflictsLocallyDialogComponentFactory {
private fun ResolveConflictsMethod.toMessage(): @Nls String = when (this) {
ResolveConflictsMethod.REBASE -> GitBundle.message("rebasing.merge.commits.button.rebase")
ResolveConflictsMethod.MERGE -> GitBundle.message("rebasing.merge.commits.button.merge")
}
/**
* Shows a dialog to choose between rebasing or merging a branch with conflicts known on the remote.
*/
fun showBranchUpdateDialog(headRefName: String, baseRefName: String): ResolveConflictsMethod? {
val exitCode = DialogManager.showMessage(
GitBundle.message("dialog.message.update.branch", headRefName, baseRefName),
GitBundle.message("dialog.title.update.branch", headRefName),
ResolveConflictsMethod.entries.map { it.toMessage() }.toTypedArray() + GitBundle.message("rebasing.merge.commits.button.cancel"),
ResolveConflictsMethod.entries.indexOf(ResolveConflictsMethod.REBASE),
ResolveConflictsMethod.entries.indexOf(ResolveConflictsMethod.REBASE),
Messages.getQuestionIcon(), null
)
return ResolveConflictsMethod.entries.getOrNull(exitCode)
}
}

View File

@@ -0,0 +1,119 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package git4idea.remote.hosting.ui
import com.intellij.collaboration.async.mapState
import com.intellij.collaboration.ui.Either
import com.intellij.collaboration.util.SingleCoroutineLauncher
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.withBackgroundProgress
import com.intellij.platform.util.coroutines.childScope
import git4idea.GitRemoteBranch
import git4idea.GitStandardRemoteBranch
import git4idea.branch.GitBrancher
import git4idea.fetch.GitFetchSupport
import git4idea.i18n.GitBundle
import git4idea.remote.hosting.GitRemoteBranchesUtil
import git4idea.remote.hosting.HostedGitRepositoryRemote
import git4idea.repo.GitRemote
import git4idea.repo.GitRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
enum class ResolveConflictsMethod {
REBASE,
MERGE;
}
@ApiStatus.Internal
interface ResolveConflictsLocallyViewModel<Error : Any> {
/**
* Whether there are conflicts that need to be resolved before merging.
*
* If the value is `null`, there is a check currently in progress or there is something
* else preventing us from knowing whether there are conflicts.
*/
val hasConflicts: StateFlow<Boolean?>
val requestOrError: StateFlow<Either<Error, ResolveConflictsLocallyCoordinates>>
val isBusy: StateFlow<Boolean>
fun performResolveConflicts(chooseMethod: suspend () -> ResolveConflictsMethod?)
}
@ApiStatus.Internal
abstract class BaseResolveConflictsLocallyViewModel<Error : Any>(
parentCs: CoroutineScope,
private val project: Project,
private val gitRepository: GitRepository,
) : ResolveConflictsLocallyViewModel<Error> {
protected val cs = parentCs.childScope("Resolve Conflicts Locally Scope")
private val taskLauncher = SingleCoroutineLauncher(cs.childScope("Resolve Conflicts Locally Task"))
private val requestFlow: StateFlow<ResolveConflictsLocallyCoordinates?>
get() = requestOrError.mapState { it.asRightOrNull() }
private val repositories = listOf(gitRepository)
private val _isBusyFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isBusy: StateFlow<Boolean> = taskLauncher.busy
override fun performResolveConflicts(chooseMethod: suspend () -> ResolveConflictsMethod?) {
taskLauncher.launch {
val method = chooseMethod()
when (method) {
ResolveConflictsMethod.REBASE -> rebase()
ResolveConflictsMethod.MERGE -> merge()
else -> {}
}
}
}
private suspend fun rebase() {
prepareAndPerformUpdate { brancher, baseBranch ->
brancher.rebase(repositories, baseBranch.nameForLocalOperations)
}
}
private suspend fun merge() {
prepareAndPerformUpdate { brancher, baseBranch ->
brancher.merge(baseBranch, GitBrancher.DeleteOnMergeOption.NOTHING, repositories, false)
}
}
private suspend fun prepareAndPerformUpdate(updater: suspend (brancher: GitBrancher, baseBranch: GitRemoteBranch) -> Unit) {
val request = requestFlow.value ?: return
val headRemote = getOrCreateRemote(request.headRemoteDescriptor) ?: return
val headBranch = GitStandardRemoteBranch(headRemote, request.headRefName)
val baseRemote = getOrCreateRemote(request.baseRemoteDescriptor) ?: return
val baseBranch = GitStandardRemoteBranch(baseRemote, request.baseRefName)
// Fetch base ref
val fetcher = GitFetchSupport.fetchSupport(project)
withContext(Dispatchers.IO) {
withBackgroundProgress(project, GitBundle.message("git.fetch.progress")) {
fetcher.fetch(gitRepository, baseRemote, baseBranch.nameForRemoteOperations)
}
}
// Checkout the head branch
withContext(Dispatchers.Main) {
GitRemoteBranchesUtil.checkoutRemoteBranch(gitRepository, headBranch)
}
// Rebase or merge on the base ref
val brancher = GitBrancher.getInstance(project)
return updater(brancher, baseBranch)
}
private suspend fun getOrCreateRemote(remoteDescriptor: HostedGitRepositoryRemote): GitRemote? =
GitRemoteBranchesUtil.findOrCreateRemote(gitRepository, remoteDescriptor)
}