From 250ca295b989b81dbc261797ea244b8df8075d03 Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Thu, 24 Feb 2022 14:25:15 +0100 Subject: [PATCH] Added option to pull/push from specific branch --- README.md | 2 +- .../kotlin/app/extensions/RefExtensions.kt | 14 ++++ .../kotlin/app/git/RemoteOperationsManager.kt | 64 ++++++++++++++++++- src/main/kotlin/app/ui/Branches.kt | 10 +++ .../app/ui/context_menu/BranchContextMenu.kt | 20 ++++++ src/main/kotlin/app/ui/log/Log.kt | 14 ++++ .../app/viewmodels/BranchesViewModel.kt | 22 +++++++ .../kotlin/app/viewmodels/LogViewModel.kt | 23 +++++++ 8 files changed, 167 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2adf32e..996ebc2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The main goal of Gitnuro is to provide a multiplatform open source Git client wi can use it nor relying on web technologies. The project it is still in alpha and many features are lacking or missing, but can be good for basic usage. - +q Right now you CAN: - View diffs for text based files. diff --git a/src/main/kotlin/app/extensions/RefExtensions.kt b/src/main/kotlin/app/extensions/RefExtensions.kt index 578c192..d8e6b68 100644 --- a/src/main/kotlin/app/extensions/RefExtensions.kt +++ b/src/main/kotlin/app/extensions/RefExtensions.kt @@ -41,6 +41,20 @@ val Ref.simpleLogName: String } } +val Ref.remoteName: String + get() { + if(this.isLocal) { + throw Exception("Trying to get remote name from a local branch") + } + val remoteWithoutPrefix = name.replace("refs/remotes/", "") + val remoteName = remoteWithoutPrefix.split("/").firstOrNull() + + if(remoteName == null) + throw Exception("Invalid remote name") + else + return remoteName + } + val Ref.isBranch: Boolean get() { return this is ObjectIdRef.PeeledNonTag diff --git a/src/main/kotlin/app/git/RemoteOperationsManager.kt b/src/main/kotlin/app/git/RemoteOperationsManager.kt index 9ac83c7..96c972f 100644 --- a/src/main/kotlin/app/git/RemoteOperationsManager.kt +++ b/src/main/kotlin/app/git/RemoteOperationsManager.kt @@ -2,6 +2,8 @@ package app.git import app.credentials.GSessionManager import app.credentials.HttpCredentialsProvider +import app.extensions.remoteName +import app.extensions.simpleName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -48,6 +50,33 @@ class RemoteOperationsManager @Inject constructor( } } + suspend fun pullFromBranch(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) { + val pullResult = git + .pull() + .setTransportConfigCallback { + handleTransportCredentials(it) + } + .setRemote(remoteBranch.remoteName) + .setRemoteBranchName(remoteBranch.simpleName) + .setRebase(rebase) + .setCredentialsProvider(CredentialsProvider.getDefault()) + .call() + + if (!pullResult.isSuccessful) { + var message = "Pull failed" + + if (rebase) { + message = when (pullResult.rebaseResult.status) { + RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes" + RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue" + else -> message + } + } + + throw Exception(message) + } + } + suspend fun fetchAll(git: Git) = withContext(Dispatchers.IO) { val remotes = git.remoteList().call() @@ -71,7 +100,7 @@ class RemoteOperationsManager @Inject constructor( .setRefSpecs(RefSpec(currentBranchRefSpec)) .setForce(force) .apply { - if(pushTags) + if (pushTags) setPushTags() } .setTransportConfigCallback { @@ -94,6 +123,39 @@ class RemoteOperationsManager @Inject constructor( } } + suspend fun pushToBranch(git: Git, force: Boolean, pushTags: Boolean, remoteBranch: Ref) = + withContext(Dispatchers.IO) { + val currentBranchRefSpec = git.repository.fullBranch + + val pushResult = git + .push() + .setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}")) + .setRemote(remoteBranch.remoteName) + .setForce(force) + .apply { + if (pushTags) + setPushTags() + } + .setTransportConfigCallback { + handleTransportCredentials(it) + } + .call() + + val results = + pushResult.map { it.remoteUpdates.filter { remoteRefUpdate -> remoteRefUpdate.status.isRejected } } + .flatten() + if (results.isNotEmpty()) { + val error = StringBuilder() + + results.forEach { result -> + error.append(result.statusMessage) + error.append("\n") + } + + throw Exception(error.toString()) + } + } + private fun handleTransportCredentials(transport: Transport?) { if (transport is SshTransport) { transport.sshSessionFactory = sessionManager.generateSshSessionFactory() diff --git a/src/main/kotlin/app/ui/Branches.kt b/src/main/kotlin/app/ui/Branches.kt index be1e636..74a2e55 100644 --- a/src/main/kotlin/app/ui/Branches.kt +++ b/src/main/kotlin/app/ui/Branches.kt @@ -36,12 +36,15 @@ fun Branches( itemContent = { branch -> BranchLineEntry( branch = branch, + currentBranchName = currentBranch, isCurrentBranch = currentBranch == branch.name, onBranchClicked = { branchesViewModel.selectBranch(branch) }, onCheckoutBranch = { branchesViewModel.checkoutRef(branch) }, onMergeBranch = { setMergeBranch(branch) }, onDeleteBranch = { branchesViewModel.deleteBranch(branch) }, onRebaseBranch = { setRebaseBranch(branch) }, + onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) }, + onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) }, ) } ) @@ -69,22 +72,29 @@ fun Branches( @Composable private fun BranchLineEntry( branch: Ref, + currentBranchName: String, isCurrentBranch: Boolean, onBranchClicked: () -> Unit, onCheckoutBranch: () -> Unit, onMergeBranch: () -> Unit, onRebaseBranch: () -> Unit, onDeleteBranch: () -> Unit, + onPushToRemoteBranch: () -> Unit, + onPullFromRemoteBranch: () -> Unit, ) { ContextMenuArea( items = { branchContextMenuItems( + branch = branch, + currentBranchName = currentBranchName, isCurrentBranch = isCurrentBranch, isLocal = branch.isLocal, onCheckoutBranch = onCheckoutBranch, onMergeBranch = onMergeBranch, onDeleteBranch = onDeleteBranch, onRebaseBranch = onRebaseBranch, + onPushToRemoteBranch = onPushToRemoteBranch, + onPullFromRemoteBranch = onPullFromRemoteBranch, ) } ) { diff --git a/src/main/kotlin/app/ui/context_menu/BranchContextMenu.kt b/src/main/kotlin/app/ui/context_menu/BranchContextMenu.kt index 293d27e..1ff8851 100644 --- a/src/main/kotlin/app/ui/context_menu/BranchContextMenu.kt +++ b/src/main/kotlin/app/ui/context_menu/BranchContextMenu.kt @@ -2,15 +2,21 @@ package app.ui.context_menu import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ExperimentalFoundationApi +import app.extensions.simpleLogName +import org.eclipse.jgit.lib.Ref @OptIn(ExperimentalFoundationApi::class) fun branchContextMenuItems( + branch: Ref, isCurrentBranch: Boolean, + currentBranchName: String, isLocal: Boolean, onCheckoutBranch: () -> Unit, onMergeBranch: () -> Unit, onRebaseBranch: () -> Unit, onDeleteBranch: () -> Unit, + onPushToRemoteBranch: () -> Unit, + onPullFromRemoteBranch: () -> Unit, ): List { return mutableListOf().apply { if (!isCurrentBranch) { @@ -41,5 +47,19 @@ fun branchContextMenuItems( ) ) } + if (!isLocal) { + add( + ContextMenuItem( + label = "Push $currentBranchName to ${branch.simpleLogName}", + onClick = onPushToRemoteBranch + ) + ) + add( + ContextMenuItem( + label = "Pull ${branch.simpleLogName} to $currentBranchName", + onClick = onPullFromRemoteBranch + ) + ) + } } } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/log/Log.kt b/src/main/kotlin/app/ui/log/Log.kt index 698b31c..90a5a5a 100644 --- a/src/main/kotlin/app/ui/log/Log.kt +++ b/src/main/kotlin/app/ui/log/Log.kt @@ -573,6 +573,8 @@ fun CommitLine( onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) }, onDeleteTag = { ref -> logViewModel.deleteTag(ref) }, onRebaseBranch = { ref -> onRebaseBranch(ref) }, + onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) }, + onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) }, ) } } @@ -592,6 +594,8 @@ fun CommitMessage( onDeleteBranch: (ref: Ref) -> Unit, onRebaseBranch: (ref: Ref) -> Unit, onDeleteTag: (ref: Ref) -> Unit, + onPushRemoteBranch: (ref: Ref) -> Unit, + onPullRemoteBranch: (ref: Ref) -> Unit, ) { val textColor = if (selected) { MaterialTheme.colors.primary @@ -632,11 +636,14 @@ fun CommitMessage( BranchChip( ref = ref, color = nodeColor, + currentBranch = currentBranch?.name.orEmpty(), isCurrentBranch = ref.isSameBranch(currentBranch), onCheckoutBranch = { onCheckoutRef(ref) }, onMergeBranch = { onMergeBranch(ref) }, onDeleteBranch = { onDeleteBranch(ref) }, onRebaseBranch = { onRebaseBranch(ref) }, + onPullRemoteBranch = { onPullRemoteBranch(ref) }, + onPushRemoteBranch = { onPushRemoteBranch(ref) }, ) } } @@ -818,20 +825,27 @@ fun BranchChip( modifier: Modifier = Modifier, isCurrentBranch: Boolean = false, ref: Ref, + currentBranch: String, onCheckoutBranch: () -> Unit, onMergeBranch: () -> Unit, onDeleteBranch: () -> Unit, onRebaseBranch: () -> Unit, + onPushRemoteBranch: () -> Unit, + onPullRemoteBranch: () -> Unit, color: Color, ) { val contextMenuItemsList = { branchContextMenuItems( + branch = ref, + currentBranchName = currentBranch, isCurrentBranch = isCurrentBranch, isLocal = ref.isLocal, onCheckoutBranch = onCheckoutBranch, onMergeBranch = onMergeBranch, onDeleteBranch = onDeleteBranch, onRebaseBranch = onRebaseBranch, + onPushToRemoteBranch = onPushRemoteBranch, + onPullFromRemoteBranch = onPullRemoteBranch, ) } diff --git a/src/main/kotlin/app/viewmodels/BranchesViewModel.kt b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt index 94daad3..33708dd 100644 --- a/src/main/kotlin/app/viewmodels/BranchesViewModel.kt +++ b/src/main/kotlin/app/viewmodels/BranchesViewModel.kt @@ -11,6 +11,7 @@ class BranchesViewModel @Inject constructor( private val branchesManager: BranchesManager, private val rebaseManager: RebaseManager, private val mergeManager: MergeManager, + private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, ) { private val _branches = MutableStateFlow>(listOf()) @@ -75,4 +76,25 @@ class BranchesViewModel @Inject constructor( fun selectBranch(ref: Ref) { tabState.newSelectedRef(ref.objectId) } + + fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.ALL_DATA, + ) { git -> + remoteOperationsManager.pushToBranch( + git = git, + force = false, + pushTags = false, + remoteBranch = branch, + ) + } + + fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.ALL_DATA, + ) { git -> + remoteOperationsManager.pullFromBranch( + git = git, + rebase = false, + remoteBranch = branch, + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/LogViewModel.kt b/src/main/kotlin/app/viewmodels/LogViewModel.kt index e635d4c..12d912a 100644 --- a/src/main/kotlin/app/viewmodels/LogViewModel.kt +++ b/src/main/kotlin/app/viewmodels/LogViewModel.kt @@ -1,5 +1,6 @@ package app.viewmodels +import app.extensions.simpleName import app.git.* import app.git.graph.GraphCommitList import app.ui.SelectedItem @@ -18,6 +19,7 @@ class LogViewModel @Inject constructor( private val tagsManager: TagsManager, private val mergeManager: MergeManager, private val repositoryManager: RepositoryManager, + private val remoteOperationsManager: RemoteOperationsManager, private val tabState: TabState, ) { private val _logStatus = MutableStateFlow(LogStatus.Loading) @@ -42,6 +44,27 @@ class LogViewModel @Inject constructor( _logStatus.value = LogStatus.Loaded(hasUncommitedChanges, log, currentBranch, statusSummary) } + fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.ALL_DATA, + ) { git -> + remoteOperationsManager.pushToBranch( + git = git, + force = false, + pushTags = false, + remoteBranch = branch, + ) + } + + fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing( + refreshType = RefreshType.ALL_DATA, + ) { git -> + remoteOperationsManager.pullFromBranch( + git = git, + rebase = false, + remoteBranch = branch, + ) + } + fun checkoutCommit(revCommit: RevCommit) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git ->