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()
|
.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()
|
||||||
|
@ -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>)
|
@ -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,
|
||||||
}
|
}
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
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
|
@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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
@ -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) {
|
||||||
|
@ -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 ->
|
||||||
|
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