diff --git a/src/main/kotlin/app/git/RemoteOperationsManager.kt b/src/main/kotlin/app/git/RemoteOperationsManager.kt index a5c96e3..5f81b03 100644 --- a/src/main/kotlin/app/git/RemoteOperationsManager.kt +++ b/src/main/kotlin/app/git/RemoteOperationsManager.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.ProgressMonitor +import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.transport.* import java.io.File import javax.inject.Inject @@ -25,11 +26,7 @@ class RemoteOperationsManager @Inject constructor( git .pull() .setTransportConfigCallback { - if (it is SshTransport) { - it.sshSessionFactory = sessionManager.generateSshSessionFactory() - } else if (it is HttpTransport) { - it.credentialsProvider = HttpCredentialsProvider() - } + handleTransportCredentials(it) } .setRebase(rebase) .setCredentialsProvider(CredentialsProvider.getDefault()) @@ -44,11 +41,7 @@ class RemoteOperationsManager @Inject constructor( .setRemote(remote.name) .setRefSpecs(remote.fetchRefSpecs) .setTransportConfigCallback { - if (it is SshTransport) { - it.sshSessionFactory = sessionManager.generateSshSessionFactory() - } else if (it is HttpTransport) { - it.credentialsProvider = HttpCredentialsProvider() - } + handleTransportCredentials(it) } .setCredentialsProvider(CredentialsProvider.getDefault()) .call() @@ -64,11 +57,7 @@ class RemoteOperationsManager @Inject constructor( .setForce(force) .setPushTags() .setTransportConfigCallback { - if (it is SshTransport) { - it.sshSessionFactory = sessionManager.generateSshSessionFactory() - } else if (it is HttpTransport) { - it.credentialsProvider = HttpCredentialsProvider() - } + handleTransportCredentials(it) } .call() @@ -87,6 +76,40 @@ class RemoteOperationsManager @Inject constructor( } } + private fun handleTransportCredentials(transport: Transport?) { + if (transport is SshTransport) { + transport.sshSessionFactory = sessionManager.generateSshSessionFactory() + } else if (transport is HttpTransport) { + transport.credentialsProvider = HttpCredentialsProvider() + } + } + + suspend fun deleteBranch(git: Git, ref: Ref) = withContext(Dispatchers.IO) { + git + .branchDelete() + .setBranchNames(ref.name) + .call() + + val branchSplit = ref.name.split("/").toMutableList() + val remoteName = branchSplit[2] // Remote name + repeat(3) { + branchSplit.removeAt(0) + } + + val branchName = "refs/heads/${branchSplit.joinToString("/")}" + + val refSpec = RefSpec() + .setSource(null) + .setDestination(branchName) + git.push() + .setTransportConfigCallback { + handleTransportCredentials(it) + } + .setRefSpecs(refSpec) + .setRemote(remoteName) + .call() + } + private val RemoteRefUpdate.Status.isRejected: Boolean get() { return this == RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD || @@ -140,11 +163,7 @@ class RemoteOperationsManager @Inject constructor( }) .setTransportConfigCallback { - if (it is SshTransport) { - it.sshSessionFactory = sessionManager.generateSshSessionFactory() - } else if (it is HttpTransport) { - it.credentialsProvider = HttpCredentialsProvider() - } + handleTransportCredentials(it) } .call() @@ -158,6 +177,7 @@ class RemoteOperationsManager @Inject constructor( _cloneStatus.value = CloneStatus.None } + } sealed class CloneStatus { diff --git a/src/main/kotlin/app/ui/Remotes.kt b/src/main/kotlin/app/ui/Remotes.kt index eb99b0c..9ed38fa 100644 --- a/src/main/kotlin/app/ui/Remotes.kt +++ b/src/main/kotlin/app/ui/Remotes.kt @@ -1,5 +1,7 @@ package app.ui +import androidx.compose.foundation.ContextMenuArea +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -11,10 +13,16 @@ import app.extensions.simpleVisibleName import app.git.RemoteInfo import app.ui.components.SideMenuPanel import app.ui.components.SideMenuSubentry +import app.ui.components.VerticalExpandable +import app.ui.context_menu.remoteBranchesContextMenu import app.viewmodels.RemotesViewModel +import org.eclipse.jgit.lib.Ref @Composable -fun Remotes(remotesViewModel: RemotesViewModel) { +fun Remotes( + remotesViewModel: RemotesViewModel, + onBranchClicked: (Ref) -> Unit, +) { val remotes by remotesViewModel.remotes.collectAsState() val itemsCount = remember(remotes) { @@ -28,28 +36,48 @@ fun Remotes(remotesViewModel: RemotesViewModel) { items = remotes, itemsCountForMaxHeight = itemsCount, itemContent = { remoteInfo -> - RemoteRow(remoteInfo) + RemoteRow( + remote = remoteInfo, + onBranchClicked = { branch -> onBranchClicked(branch) }, + onDeleteBranch = { branch -> remotesViewModel.deleteBranch(branch) } + ) } ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun RemoteRow( remote: RemoteInfo, + onBranchClicked: (Ref) -> Unit, + onDeleteBranch: (Ref) -> Unit, ) { - SideMenuSubentry( - text = remote.remoteConfig.name, - iconResourcePath = "cloud.svg", - ) - - val branches = remote.branchesList - Column { - branches.forEach { branch -> + VerticalExpandable( + header = { SideMenuSubentry( - text = branch.simpleVisibleName, - extraPadding = 8.dp, - iconResourcePath = "branch.svg", + text = remote.remoteConfig.name, + iconResourcePath = "cloud.svg", ) } + ) { + val branches = remote.branchesList + Column { + branches.forEach { branch -> + ContextMenuArea( + items = { + remoteBranchesContextMenu( + onDeleteBranch = { onDeleteBranch(branch) } + ) + } + ) { + SideMenuSubentry( + text = branch.simpleVisibleName, + extraPadding = 8.dp, + iconResourcePath = "branch.svg", + onClick = { onBranchClicked(branch) } + ) + } + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index a90d5d1..7649dea 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -65,7 +65,12 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) { tabViewModel.newSelectedRef(it.objectId) } ) - Remotes(remotesViewModel = tabViewModel.remotesViewModel) + Remotes( + remotesViewModel = tabViewModel.remotesViewModel, + onBranchClicked = { + tabViewModel.newSelectedRef(it.objectId) + } + ) Tags( tagsViewModel = tabViewModel.tagsViewModel, onTagClicked = { diff --git a/src/main/kotlin/app/ui/components/SideMenuSubentry.kt b/src/main/kotlin/app/ui/components/SideMenuSubentry.kt index a2102d1..d82edb1 100644 --- a/src/main/kotlin/app/ui/components/SideMenuSubentry.kt +++ b/src/main/kotlin/app/ui/components/SideMenuSubentry.kt @@ -25,14 +25,19 @@ fun SideMenuSubentry( iconResourcePath: String, bold: Boolean = false, extraPadding: Dp = 0.dp, - onClick: () -> Unit = {}, + onClick: (() -> Unit)? = null, additionalInfo: @Composable () -> Unit = {} ) { Row( modifier = Modifier .height(ENTRY_HEIGHT.dp) .fillMaxWidth() - .clickable(onClick = onClick) + .run { + if(onClick != null) + clickable(onClick = onClick) + else + this + } .padding(start = extraPadding), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/src/main/kotlin/app/ui/context_menu/RemoteBranchesContextMenu.kt b/src/main/kotlin/app/ui/context_menu/RemoteBranchesContextMenu.kt new file mode 100644 index 0000000..ab95dc7 --- /dev/null +++ b/src/main/kotlin/app/ui/context_menu/RemoteBranchesContextMenu.kt @@ -0,0 +1,17 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package app.ui.context_menu + +import androidx.compose.foundation.ContextMenuItem +import androidx.compose.foundation.ExperimentalFoundationApi + +fun remoteBranchesContextMenu( + onDeleteBranch: () -> Unit +): List { + return mutableListOf( + ContextMenuItem( + label = "Delete remote branch", + onClick = onDeleteBranch + ), + ) +} \ No newline at end of file diff --git a/src/main/kotlin/app/viewmodels/RemotesViewModel.kt b/src/main/kotlin/app/viewmodels/RemotesViewModel.kt index 5c86ca8..eb228d5 100644 --- a/src/main/kotlin/app/viewmodels/RemotesViewModel.kt +++ b/src/main/kotlin/app/viewmodels/RemotesViewModel.kt @@ -1,18 +1,19 @@ package app.viewmodels -import app.git.BranchesManager -import app.git.RemoteInfo -import app.git.RemotesManager +import app.git.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Ref import javax.inject.Inject class RemotesViewModel @Inject constructor( private val remotesManager: RemotesManager, + private val remoteOperationsManager: RemoteOperationsManager, private val branchesManager: BranchesManager, + private val tabState: TabState, ) { private val _remotes = MutableStateFlow>(listOf()) val remotes: StateFlow> @@ -34,6 +35,12 @@ class RemotesViewModel @Inject constructor( _remotes.value = remoteInfoList } + fun deleteBranch(ref: Ref) = tabState.safeProcessing { git -> + remoteOperationsManager.deleteBranch(git, ref) + + return@safeProcessing RefreshType.ALL_DATA + } + suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { loadRemotes(git) }