Added basic remotes management
This commit is contained in:
parent
df3f47f073
commit
afce2a2aa7
3
src/main/kotlin/app/exceptions/GitnuroException.kt
Normal file
3
src/main/kotlin/app/exceptions/GitnuroException.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package app.exceptions
|
||||
|
||||
abstract class GitnuroException(msg: String) : RuntimeException(msg)
|
@ -0,0 +1,3 @@
|
||||
package app.exceptions
|
||||
|
||||
class InvalidRemoteUrlException(msg: String) : GitnuroException(msg)
|
@ -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()
|
||||
|
@ -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>)
|
@ -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,
|
||||
}
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
14
src/main/kotlin/app/ui/context_menu/RemoteContextMenu.kt
Normal file
14
src/main/kotlin/app/ui/context_menu/RemoteContextMenu.kt
Normal 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
|
||||
),
|
||||
)
|
358
src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt
Normal file
358
src/main/kotlin/app/ui/dialogs/EditRemotesDialog.kt
Normal 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(),
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
@ -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) {
|
||||
|
@ -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 ->
|
||||
|
1
src/main/resources/add.svg
Normal file
1
src/main/resources/add.svg
Normal 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 |
1
src/main/resources/remove.svg
Normal file
1
src/main/resources/remove.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user