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()
}
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) {
git
.branchList()

View File

@ -3,13 +3,13 @@ package app.git
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RemoteSetUrlCommand
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.RemoteConfig
import org.eclipse.jgit.transport.URIish
import javax.inject.Inject
class RemotesManager @Inject constructor() {
suspend fun loadRemotes(git: Git, allRemoteBranches: List<Ref>) = withContext(Dispatchers.IO) {
val remotes = git.remoteList()
.call()
@ -21,6 +21,28 @@ class RemotesManager @Inject constructor() {
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>)

View File

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

View File

@ -53,6 +53,13 @@ val Colors.inversePrimaryTextColor: Color
val Colors.secondaryTextColor: Color
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
val Colors.headerBackground: Color
get() {

View File

@ -98,7 +98,7 @@ fun AppTab(
.padding(end = 32.dp, bottom = 32.dp)
) {
val interactionSource = remember { MutableInteractionSource() }
// TODO: Rework popup to appear on top of every other UI component, even dialogs
Card(
modifier = Modifier
.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.SideMenuSubentry
import app.ui.context_menu.branchContextMenuItems
import app.ui.context_menu.remoteContextMenu
import app.ui.dialogs.MergeDialog
import app.ui.dialogs.RebaseDialog
import app.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Branches(
branchesViewModel: BranchesViewModel,
@ -31,18 +33,19 @@ fun Branches(
SideMenuPanel(
title = "Local branches",
icon = painterResource("branch.svg"),
items = branches
) { branch ->
BranchLineEntry(
branch = branch,
isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { branchesViewModel.selectBranch(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
onRebaseBranch = { setRebaseBranch(branch) },
)
}
items = branches,
itemContent = { branch ->
BranchLineEntry(
branch = branch,
isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { branchesViewModel.selectBranch(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
onRebaseBranch = { setRebaseBranch(branch) },
)
}
)
if (mergeBranch != null) {
MergeDialog(

View File

@ -1,19 +1,30 @@
@file:OptIn(ExperimentalFoundationApi::class)
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
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.unit.dp
import app.extensions.simpleName
import app.theme.primaryTextColor
import app.ui.components.SideMenuPanel
import app.ui.components.SideMenuSubentry
import app.ui.components.VerticalExpandable
import app.ui.context_menu.remoteBranchesContextMenu
import app.ui.context_menu.remoteContextMenu
import app.ui.dialogs.EditRemotesDialog
import app.viewmodels.RemoteView
import app.viewmodels.RemotesViewModel
import org.eclipse.jgit.lib.Ref
@ -23,6 +34,7 @@ fun Remotes(
remotesViewModel: RemotesViewModel,
) {
val remotes by remotesViewModel.remotes.collectAsState()
var showEditRemotesDialog by remember { mutableStateOf(false) }
val itemsCount = remember(remotes) {
val allBranches = remotes.filter { remoteView ->
@ -34,11 +46,39 @@ fun Remotes(
allBranches.count() + remotes.count()
}
if (showEditRemotesDialog) {
EditRemotesDialog(
remotesViewModel = remotesViewModel,
onDismiss = {
showEditRemotesDialog = false
},
)
}
SideMenuPanel(
title = "Remotes",
icon = painterResource("cloud.svg"),
items = remotes,
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 ->
RemoteRow(
remote = remoteInfo,
@ -50,6 +90,7 @@ fun Remotes(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun RemoteRow(

View File

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

View File

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

View File

@ -1,5 +1,7 @@
package app.ui.components
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
@ -21,16 +23,23 @@ fun <T> SideMenuPanel(
items: List<T>,
itemsCountForMaxHeight: Int = items.count(),
itemContent: @Composable (T) -> Unit,
headerHoverIcon: @Composable (() -> Unit)? = null,
contextItems: () -> List<ContextMenuItem> = { emptyList() },
) {
val maxHeight = remember(items) { maxSidePanelHeight(itemsCountForMaxHeight) }
VerticalExpandable(
header = {
SideMenuEntry(
text = title,
icon = icon,
itemsCount = items.count()
)
ContextMenuArea(
items = contextItems
) {
SideMenuEntry(
text = title,
icon = icon,
itemsCount = items.count(),
hoverIcon = headerHoverIcon
)
}
},
) {
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
fun MaterialDialog(
alignment: Alignment = Alignment.Center,
paddingHorizontal: Dp = 16.dp,
paddingVertical: Dp = 16.dp,
content: @Composable () -> Unit
) {
Popup(
@ -41,7 +43,10 @@ fun MaterialDialog(
modifier = Modifier
.clip(RoundedCornerShape(15.dp))
.background(MaterialTheme.colors.background)
.padding(all = 16.dp)
.padding(
horizontal = paddingHorizontal,
vertical = paddingVertical,
)
) {
content()
}

View File

@ -1,12 +1,16 @@
package app.viewmodels
import app.exceptions.InvalidRemoteUrlException
import app.git.*
import app.ui.dialogs.RemoteWrapper
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.api.RemoteSetUrlCommand
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.URIish
import javax.inject.Inject
class RemotesViewModel @Inject constructor(
@ -62,6 +66,68 @@ class RemotesViewModel @Inject constructor(
fun selectBranch(ref: Ref) {
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)

View File

@ -21,7 +21,7 @@ class StashesViewModel @Inject constructor(
suspend fun loadStashes(git: Git) {
_stashStatus.value = StashStatus.Loading
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) {

View File

@ -77,11 +77,18 @@ class TabViewModel @Inject constructor(
RefreshType.STASHES -> refreshStashes()
RefreshType.UNCOMMITED_CHANGES -> checkUncommitedChanges()
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(
refreshType = RefreshType.NONE
) { 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