Added basic remotes management

This commit is contained in:
Abdelilah El Aissaoui 2022-02-17 19:22:48 +01:00
parent df3f47f073
commit afce2a2aa7
20 changed files with 595 additions and 38 deletions

View File

@ -0,0 +1,3 @@
package app.exceptions
abstract class GitnuroException(msg: String) : RuntimeException(msg)

View File

@ -0,0 +1,3 @@
package app.exceptions
class InvalidRemoteUrlException(msg: String) : GitnuroException(msg)

View File

@ -55,6 +55,14 @@ class BranchesManager @Inject constructor() {
.call() .call()
} }
suspend fun deleteLocallyRemoteBranches(git: Git, branches: List<String>) = withContext(Dispatchers.IO) {
git
.branchDelete()
.setBranchNames(*branches.toTypedArray())
.setForce(true)
.call()
}
suspend fun remoteBranches(git: Git) = withContext(Dispatchers.IO) { suspend fun remoteBranches(git: Git) = withContext(Dispatchers.IO) {
git git
.branchList() .branchList()

View File

@ -3,13 +3,13 @@ package app.git
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RemoteSetUrlCommand
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.RemoteConfig import org.eclipse.jgit.transport.RemoteConfig
import org.eclipse.jgit.transport.URIish
import javax.inject.Inject import javax.inject.Inject
class RemotesManager @Inject constructor() { class RemotesManager @Inject constructor() {
suspend fun loadRemotes(git: Git, allRemoteBranches: List<Ref>) = withContext(Dispatchers.IO) { suspend fun loadRemotes(git: Git, allRemoteBranches: List<Ref>) = withContext(Dispatchers.IO) {
val remotes = git.remoteList() val remotes = git.remoteList()
.call() .call()
@ -21,6 +21,28 @@ class RemotesManager @Inject constructor() {
RemoteInfo(remoteConfig, remoteBranches) RemoteInfo(remoteConfig, remoteBranches)
} }
} }
suspend fun deleteRemote(git: Git, remoteName: String) = withContext(Dispatchers.IO) {
git.remoteRemove()
.setRemoteName(remoteName)
.call()
}
suspend fun addRemote(git: Git, remoteName: String, fetchUri: String) = withContext(Dispatchers.IO) {
git.remoteAdd()
.setName(remoteName)
.setUri(URIish(fetchUri))
.call()
}
suspend fun updateRemote(git: Git, remoteName: String, uri: String, uriType: RemoteSetUrlCommand.UriType) =
withContext(Dispatchers.IO) {
git.remoteSetUrl()
.setRemoteName(remoteName)
.setRemoteUri(URIish(uri))
.setUriType(uriType)
.call()
}
} }
data class RemoteInfo(val remoteConfig: RemoteConfig, val branchesList: List<Ref>) data class RemoteInfo(val remoteConfig: RemoteConfig, val branchesList: List<Ref>)

View File

@ -159,10 +159,6 @@ class TabState @Inject constructor(
fun newSelectedItem(selectedItem: SelectedItem) { fun newSelectedItem(selectedItem: SelectedItem) {
_selectedItem.value = selectedItem _selectedItem.value = selectedItem
println(selectedItem)
// if (selectedItem is SelectedItem.CommitBasedItem) {
// commitChangesViewModel.loadChanges(selectedItem.revCommit)
// }
} }
} }
@ -173,4 +169,5 @@ enum class RefreshType {
STASHES, STASHES,
UNCOMMITED_CHANGES, UNCOMMITED_CHANGES,
UNCOMMITED_CHANGES_AND_LOG, UNCOMMITED_CHANGES_AND_LOG,
REMOTES,
} }

View File

@ -53,6 +53,13 @@ val Colors.inversePrimaryTextColor: Color
val Colors.secondaryTextColor: Color val Colors.secondaryTextColor: Color
get() = if (isLight) secondaryText else secondaryTextDark get() = if (isLight) secondaryText else secondaryTextDark
@get:Composable
val Colors.semiSecondaryTextColor: Color
get() = if (isLight)
secondaryText.copy(alpha = 0.3f)
else
secondaryTextDark.copy(alpha = 0.3f)
@get:Composable @get:Composable
val Colors.headerBackground: Color val Colors.headerBackground: Color
get() { get() {

View File

@ -98,7 +98,7 @@ fun AppTab(
.padding(end = 32.dp, bottom = 32.dp) .padding(end = 32.dp, bottom = 32.dp)
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
// TODO: Rework popup to appear on top of every other UI component, even dialogs
Card( Card(
modifier = Modifier modifier = Modifier
.defaultMinSize(minWidth = 200.dp, minHeight = 100.dp) .defaultMinSize(minWidth = 200.dp, minHeight = 100.dp)

View File

@ -14,11 +14,13 @@ import app.extensions.simpleName
import app.ui.components.SideMenuPanel import app.ui.components.SideMenuPanel
import app.ui.components.SideMenuSubentry import app.ui.components.SideMenuSubentry
import app.ui.context_menu.branchContextMenuItems import app.ui.context_menu.branchContextMenuItems
import app.ui.context_menu.remoteContextMenu
import app.ui.dialogs.MergeDialog import app.ui.dialogs.MergeDialog
import app.ui.dialogs.RebaseDialog import app.ui.dialogs.RebaseDialog
import app.viewmodels.BranchesViewModel import app.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Branches( fun Branches(
branchesViewModel: BranchesViewModel, branchesViewModel: BranchesViewModel,
@ -31,8 +33,8 @@ fun Branches(
SideMenuPanel( SideMenuPanel(
title = "Local branches", title = "Local branches",
icon = painterResource("branch.svg"), icon = painterResource("branch.svg"),
items = branches items = branches,
) { branch -> itemContent = { branch ->
BranchLineEntry( BranchLineEntry(
branch = branch, branch = branch,
isCurrentBranch = currentBranch == branch.name, isCurrentBranch = currentBranch == branch.name,
@ -43,6 +45,7 @@ fun Branches(
onRebaseBranch = { setRebaseBranch(branch) }, onRebaseBranch = { setRebaseBranch(branch) },
) )
} }
)
if (mergeBranch != null) { if (mergeBranch != null) {
MergeDialog( MergeDialog(

View File

@ -1,19 +1,30 @@
@file:OptIn(ExperimentalFoundationApi::class)
package app.ui package app.ui
import androidx.compose.foundation.ContextMenuArea import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.collectAsState import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.getValue import androidx.compose.foundation.layout.size
import androidx.compose.runtime.remember import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.extensions.simpleName import app.extensions.simpleName
import app.theme.primaryTextColor
import app.ui.components.SideMenuPanel import app.ui.components.SideMenuPanel
import app.ui.components.SideMenuSubentry import app.ui.components.SideMenuSubentry
import app.ui.components.VerticalExpandable import app.ui.components.VerticalExpandable
import app.ui.context_menu.remoteBranchesContextMenu import app.ui.context_menu.remoteBranchesContextMenu
import app.ui.context_menu.remoteContextMenu
import app.ui.dialogs.EditRemotesDialog
import app.viewmodels.RemoteView import app.viewmodels.RemoteView
import app.viewmodels.RemotesViewModel import app.viewmodels.RemotesViewModel
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@ -23,6 +34,7 @@ fun Remotes(
remotesViewModel: RemotesViewModel, remotesViewModel: RemotesViewModel,
) { ) {
val remotes by remotesViewModel.remotes.collectAsState() val remotes by remotesViewModel.remotes.collectAsState()
var showEditRemotesDialog by remember { mutableStateOf(false) }
val itemsCount = remember(remotes) { val itemsCount = remember(remotes) {
val allBranches = remotes.filter { remoteView -> val allBranches = remotes.filter { remoteView ->
@ -34,11 +46,39 @@ fun Remotes(
allBranches.count() + remotes.count() allBranches.count() + remotes.count()
} }
if (showEditRemotesDialog) {
EditRemotesDialog(
remotesViewModel = remotesViewModel,
onDismiss = {
showEditRemotesDialog = false
},
)
}
SideMenuPanel( SideMenuPanel(
title = "Remotes", title = "Remotes",
icon = painterResource("cloud.svg"), icon = painterResource("cloud.svg"),
items = remotes, items = remotes,
itemsCountForMaxHeight = itemsCount, itemsCountForMaxHeight = itemsCount,
contextItems = {
remoteContextMenu { showEditRemotesDialog = true }
},
headerHoverIcon = {
IconButton(
onClick = { showEditRemotesDialog = true },
modifier = Modifier
.padding(end = 8.dp)
.size(16.dp),
) {
Icon(
painter = painterResource("settings.svg"),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
tint = MaterialTheme.colors.primaryTextColor,
)
}
},
itemContent = { remoteInfo -> itemContent = { remoteInfo ->
RemoteRow( RemoteRow(
remote = remoteInfo, remote = remoteInfo,
@ -50,6 +90,7 @@ fun Remotes(
) )
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun RemoteRow( private fun RemoteRow(

View File

@ -12,6 +12,7 @@ import app.ui.context_menu.tagContextMenuItems
import app.viewmodels.TagsViewModel import app.viewmodels.TagsViewModel
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Tags( fun Tags(
tagsViewModel: TagsViewModel, tagsViewModel: TagsViewModel,

View File

@ -1,11 +1,15 @@
package app.ui.components package app.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
@ -21,11 +25,16 @@ fun SideMenuEntry(
text: String, text: String,
icon: Painter? = null, icon: Painter? = null,
itemsCount: Int, itemsCount: Int,
hoverIcon: @Composable (() -> Unit)? = null,
) { ) {
val hoverInteraction = remember { MutableInteractionSource() }
val isHovered by hoverInteraction.collectIsHoveredAsState()
Row( Row(
modifier = Modifier modifier = Modifier
.height(32.dp) .height(32.dp)
.fillMaxWidth() .fillMaxWidth()
.hoverable(hoverInteraction)
.background(color = MaterialTheme.colors.headerBackground), .background(color = MaterialTheme.colors.headerBackground),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -51,7 +60,9 @@ fun SideMenuEntry(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if(hoverIcon != null && isHovered) {
hoverIcon()
} else
Text( Text(
text = itemsCount.toString(), text = itemsCount.toString(),
fontSize = 14.sp, fontSize = 14.sp,

View File

@ -1,5 +1,7 @@
package app.ui.components package app.ui.components
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -21,16 +23,23 @@ fun <T> SideMenuPanel(
items: List<T>, items: List<T>,
itemsCountForMaxHeight: Int = items.count(), itemsCountForMaxHeight: Int = items.count(),
itemContent: @Composable (T) -> Unit, itemContent: @Composable (T) -> Unit,
headerHoverIcon: @Composable (() -> Unit)? = null,
contextItems: () -> List<ContextMenuItem> = { emptyList() },
) { ) {
val maxHeight = remember(items) { maxSidePanelHeight(itemsCountForMaxHeight) } val maxHeight = remember(items) { maxSidePanelHeight(itemsCountForMaxHeight) }
VerticalExpandable( VerticalExpandable(
header = { header = {
ContextMenuArea(
items = contextItems
) {
SideMenuEntry( SideMenuEntry(
text = title, text = title,
icon = icon, icon = icon,
itemsCount = items.count() itemsCount = items.count(),
hoverIcon = headerHoverIcon
) )
}
}, },
) { ) {
ScrollableLazyColumn( ScrollableLazyColumn(

View File

@ -0,0 +1,14 @@
package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
@OptIn(ExperimentalFoundationApi::class)
fun remoteContextMenu(
onEditRemotes: () -> Unit,
) = listOf(
ContextMenuItem(
label = "Edit remotes",
onClick = onEditRemotes
),
)

View File

@ -0,0 +1,358 @@
package app.ui.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import app.theme.primaryTextColor
import app.theme.secondaryTextColor
import app.theme.semiSecondaryTextColor
import app.viewmodels.RemotesViewModel
import org.eclipse.jgit.transport.RemoteConfig
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun EditRemotesDialog(
remotesViewModel: RemotesViewModel,
onDismiss: () -> Unit,
) {
var remotesEditorData by remember {
mutableStateOf(
RemotesEditorData(
emptyList(),
null,
)
)
}
val remotes by remotesViewModel.remotes.collectAsState()
var remoteChanged by remember { mutableStateOf(false) }
val selectedRemote = remotesEditorData.selectedRemote
LaunchedEffect(remotes) {
val newRemotesWrappers = remotes.map {
val remoteConfig = it.remoteInfo.remoteConfig
remoteConfig.toRemoteWrapper()
}
val safeSelectedRemote = remotesEditorData.selectedRemote
var newSelectedRemote: RemoteWrapper? = null
if (safeSelectedRemote != null) {
newSelectedRemote = newRemotesWrappers.firstOrNull { it.remoteName == safeSelectedRemote.remoteName }
}
remoteChanged = newSelectedRemote?.haveUrisChanged ?: false
remotesEditorData = remotesEditorData.copy(
listRemotes = newRemotesWrappers,
selectedRemote = newSelectedRemote,
)
}
MaterialDialog(
paddingVertical = 8.dp,
paddingHorizontal = 16.dp,
) {
Column(
modifier = Modifier
.size(width = 900.dp, height = 600.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Row(
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Remotes",
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier.padding(vertical = 8.dp),
)
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = onDismiss,
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null,
tint = MaterialTheme.colors.primaryTextColor,
)
}
}
Row(
modifier = Modifier
.padding(bottom = 8.dp)
.border(
width = 1.dp,
shape = RoundedCornerShape(5.dp),
color = MaterialTheme.colors.semiSecondaryTextColor,
)
.background(MaterialTheme.colors.surface)
) {
Column(
modifier = Modifier
.width(200.dp)
.fillMaxHeight(),
) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(remotesEditorData.listRemotes) { remote ->
val background = if(remote == selectedRemote) {
MaterialTheme.colors.background
} else
MaterialTheme.colors.surface
Text(
text = remote.remoteName,
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier
.fillMaxWidth()
.clickable {
remotesEditorData = remotesEditorData.copy(selectedRemote = remote)
}
.background(background)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
) {
IconButton(
modifier = Modifier.size(36.dp),
onClick = {
val remotesWithNew = remotesEditorData.listRemotes.toMutableList()
val newRemote = RemoteWrapper(
remoteName = "new_remote",
fetchUri = "",
originalFetchUri = "",
pushUri = "",
originalPushUri = "",
isNew = true
)
remotesWithNew.add(newRemote)
remotesEditorData = remotesEditorData.copy(
listRemotes = remotesWithNew,
selectedRemote = newRemote
)
}
) {
Icon(
painter = painterResource("add.svg"),
contentDescription = null,
tint = MaterialTheme.colors.primaryTextColor,
)
}
IconButton(
modifier = Modifier.size(36.dp),
enabled = selectedRemote != null,
onClick = {
if (selectedRemote != null)
remotesViewModel.deleteRemote(selectedRemote.remoteName, selectedRemote.isNew)
}
) {
Icon(
painter = painterResource("remove.svg"),
contentDescription = null,
tint = if (selectedRemote != null)
MaterialTheme.colors.primaryTextColor
else
MaterialTheme.colors.secondaryTextColor,
)
}
}
}
Box(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(MaterialTheme.colors.semiSecondaryTextColor)
)
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.surface)
.padding(horizontal = 8.dp)
) {
if (selectedRemote != null) {
Column {
if (selectedRemote.isNew) {
Text(
text = "New remote name",
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier.padding(vertical = 8.dp),
)
OutlinedTextField(
value = selectedRemote.remoteName,
onValueChange = { newValue ->
val newSelectedRemoteConfig = selectedRemote.copy(remoteName = newValue)
val listRemotes = remotesEditorData.listRemotes.toMutableList()
val newRemoteToBeReplacedIndex = listRemotes.indexOfFirst { it.isNew }
listRemotes[newRemoteToBeReplacedIndex] = newSelectedRemoteConfig
remotesEditorData = remotesEditorData.copy(
listRemotes = listRemotes,
selectedRemote = newSelectedRemoteConfig
)
},
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
}
Text(
text = "Fetch URL",
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier.padding(vertical = 8.dp),
)
OutlinedTextField(
value = selectedRemote.fetchUri,
onValueChange = { newValue ->
val newSelectedRemoteConfig = selectedRemote.copy(fetchUri = newValue)
remotesEditorData = remotesEditorData.copy(selectedRemote = newSelectedRemoteConfig)
remoteChanged = newSelectedRemoteConfig.haveUrisChanged
},
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
Text(
text = "Push URL",
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier.padding(vertical = 8.dp),
)
OutlinedTextField(
value = selectedRemote.pushUri,
onValueChange = { newValue ->
val newSelectedRemoteConfig = selectedRemote.copy(pushUri = newValue)
remotesEditorData = remotesEditorData.copy(selectedRemote = newSelectedRemoteConfig)
remoteChanged = newSelectedRemoteConfig.haveUrisChanged
},
textStyle = TextStyle.Default.copy(color = MaterialTheme.colors.primaryTextColor),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier
.padding(top = 16.dp)
.align(Alignment.End)
) {
Spacer(modifier = Modifier.weight(1f))
if (!selectedRemote.isNew) {
TextButton(
modifier = Modifier.padding(end = 8.dp),
enabled = remoteChanged,
onClick = {
remotesEditorData = remotesEditorData.copy(
selectedRemote = selectedRemote.copy(
fetchUri = selectedRemote.originalFetchUri,
pushUri = selectedRemote.originalPushUri,
)
)
remoteChanged = false
}
) {
Text("Discard changes")
}
}
Button(
modifier = Modifier,
enabled = remoteChanged,
onClick = {
if (selectedRemote.isNew)
remotesViewModel.addRemote(selectedRemote)
else
remotesViewModel.updateRemote(selectedRemote)
},
) {
val text = if (selectedRemote.isNew)
"Add new remote"
else
"Save ${selectedRemote.remoteName} changes"
Text(text)
}
}
}
}
}
}
}
}
}
data class RemoteWrapper(
val remoteName: String,
val fetchUri: String,
val originalFetchUri: String,
val pushUri: String,
val originalPushUri: String,
val isNew: Boolean = false,
) {
val haveUrisChanged: Boolean = isNew ||
fetchUri != originalFetchUri ||
pushUri.toString() != originalPushUri
}
data class RemotesEditorData(
val listRemotes: List<RemoteWrapper>,
val selectedRemote: RemoteWrapper?,
)
fun RemoteConfig.toRemoteWrapper(): RemoteWrapper {
val fetchUri = this.urIs.firstOrNull()
val pushUri = this.pushURIs.firstOrNull()
?: this.urIs.firstOrNull() // If push URI == null, take fetch URI
return RemoteWrapper(
remoteName = this.name,
fetchUri = fetchUri?.toString().orEmpty(),
originalFetchUri = fetchUri?.toString().orEmpty(),
pushUri = pushUri?.toString().orEmpty(),
originalPushUri = pushUri?.toString().orEmpty(),
)
}

View File

@ -18,6 +18,8 @@ import app.theme.dialogBackgroundColor
@Composable @Composable
fun MaterialDialog( fun MaterialDialog(
alignment: Alignment = Alignment.Center, alignment: Alignment = Alignment.Center,
paddingHorizontal: Dp = 16.dp,
paddingVertical: Dp = 16.dp,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
Popup( Popup(
@ -41,7 +43,10 @@ fun MaterialDialog(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(15.dp)) .clip(RoundedCornerShape(15.dp))
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.padding(all = 16.dp) .padding(
horizontal = paddingHorizontal,
vertical = paddingVertical,
)
) { ) {
content() content()
} }

View File

@ -1,12 +1,16 @@
package app.viewmodels package app.viewmodels
import app.exceptions.InvalidRemoteUrlException
import app.git.* import app.git.*
import app.ui.dialogs.RemoteWrapper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RemoteSetUrlCommand
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.URIish
import javax.inject.Inject import javax.inject.Inject
class RemotesViewModel @Inject constructor( class RemotesViewModel @Inject constructor(
@ -62,6 +66,68 @@ class RemotesViewModel @Inject constructor(
fun selectBranch(ref: Ref) { fun selectBranch(ref: Ref) {
tabState.newSelectedRef(ref.objectId) tabState.newSelectedRef(ref.objectId)
} }
fun deleteRemote(remoteName: String, isNew: Boolean) = tabState.safeProcessing(
refreshType = if(isNew) RefreshType.REMOTES else RefreshType.ALL_DATA,
showError = true,
) { git ->
remotesManager.deleteRemote(git, remoteName)
val remoteBranches = branchesManager.remoteBranches(git)
val remoteToDeleteBranchesNames = remoteBranches.filter {
it.name.startsWith("refs/remotes/$remoteName/")
}.map {
it.name
}
branchesManager.deleteLocallyRemoteBranches(git, remoteToDeleteBranchesNames)
}
fun addRemote(selectedRemoteConfig: RemoteWrapper) = tabState.runOperation(
refreshType = RefreshType.REMOTES,
showError = true,
) { git ->
if(selectedRemoteConfig.fetchUri.isBlank()) {
throw InvalidRemoteUrlException("Invalid empty fetch URI")
}
if(selectedRemoteConfig.pushUri.isBlank()) {
throw InvalidRemoteUrlException("Invalid empty push URI")
}
remotesManager.addRemote(git, selectedRemoteConfig.remoteName, selectedRemoteConfig.fetchUri)
updateRemote(selectedRemoteConfig) // Sets both, fetch and push uri
}
fun updateRemote(selectedRemoteConfig: RemoteWrapper) = tabState.runOperation(
refreshType = RefreshType.REMOTES,
showError = true,
) { git ->
if(selectedRemoteConfig.fetchUri.isBlank()) {
throw InvalidRemoteUrlException("Invalid empty fetch URI")
}
if(selectedRemoteConfig.pushUri.isBlank()) {
throw InvalidRemoteUrlException("Invalid empty push URI")
}
remotesManager.updateRemote(
git = git,
remoteName = selectedRemoteConfig.remoteName,
uri = selectedRemoteConfig.fetchUri,
uriType = RemoteSetUrlCommand.UriType.FETCH
)
remotesManager.updateRemote(
git = git,
remoteName = selectedRemoteConfig.remoteName,
uri = selectedRemoteConfig.pushUri,
uriType = RemoteSetUrlCommand.UriType.PUSH
)
}
} }
data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean) data class RemoteView(val remoteInfo: RemoteInfo, val isExpanded: Boolean)

View File

@ -21,7 +21,7 @@ class StashesViewModel @Inject constructor(
suspend fun loadStashes(git: Git) { suspend fun loadStashes(git: Git) {
_stashStatus.value = StashStatus.Loading _stashStatus.value = StashStatus.Loading
val stashList = stashManager.getStashList(git) val stashList = stashManager.getStashList(git)
_stashStatus.value = StashStatus.Loaded(stashList.toList()) // TODO: Is the list cast necessary? _stashStatus.value = StashStatus.Loaded(stashList.toList())
} }
suspend fun refresh(git: Git) { suspend fun refresh(git: Git) {

View File

@ -77,11 +77,18 @@ class TabViewModel @Inject constructor(
RefreshType.STASHES -> refreshStashes() RefreshType.STASHES -> refreshStashes()
RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges() RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges()
RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true) RefreshType.UNCOMMITED_CHANGES_AND_LOG -> checkUncommitedChanges(true)
RefreshType.REMOTES -> refreshRemotes()
} }
} }
} }
} }
private fun refreshRemotes() = tabState.runOperation(
refreshType = RefreshType.NONE
) { git ->
remotesViewModel.refresh(git)
}
private fun refreshStashes() = tabState.runOperation( private fun refreshStashes() = tabState.runOperation(
refreshType = RefreshType.NONE refreshType = RefreshType.NONE
) { git -> ) { git ->

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>

After

Width:  |  Height:  |  Size: 174 B