Completed initial support for submodules

Fixes #29
This commit is contained in:
Abdelilah El Aissaoui 2023-04-30 22:41:50 +02:00
parent c24658952e
commit 07e1bbd4ed
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
14 changed files with 436 additions and 20 deletions

View File

@ -45,10 +45,12 @@ object AppIcons {
const val SOURCE = "source.svg" const val SOURCE = "source.svg"
const val START = "start.svg" const val START = "start.svg"
const val STASH = "stash.svg" const val STASH = "stash.svg"
const val SYNC = "sync.svg"
const val TAG = "tag.svg" const val TAG = "tag.svg"
const val TERMINAL = "terminal.svg" const val TERMINAL = "terminal.svg"
const val TOPIC = "topic.svg" const val TOPIC = "topic.svg"
const val UNDO = "undo.svg" const val UNDO = "undo.svg"
const val UPDATE = "update.svg"
const val UPLOAD = "upload.svg" const val UPLOAD = "upload.svg"
const val VISIBILITY = "visibility.svg" const val VISIBILITY = "visibility.svg"
const val VISIBILITY_OFF = "visibility_off.svg" const val VISIBILITY_OFF = "visibility_off.svg"

View File

@ -1,16 +1,31 @@
package com.jetpackduba.gitnuro.git.submodules package com.jetpackduba.gitnuro.git.submodules
import com.jetpackduba.gitnuro.models.ProgressMonitorInfo
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ProgressMonitor
import javax.inject.Inject import javax.inject.Inject
class AddSubmoduleUseCase @Inject constructor() { class AddSubmoduleUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, name: String, path: String, uri: String): Unit = withContext(Dispatchers.IO) { suspend operator fun invoke(git: Git, name: String, path: String, uri: String) = withContext(Dispatchers.IO) {
git.submoduleAdd() git.submoduleAdd()
.setName(name) .setName(name)
.setPath(path) .setPath(path)
.setURI(uri) .setURI(uri)
.setProgressMonitor(object : ProgressMonitor {
override fun start(totalTasks: Int) {}
override fun beginTask(title: String?, totalWork: Int) {}
override fun update(completed: Int) {}
override fun endTask() {}
override fun showDuration(enabled: Boolean) {}
override fun isCancelled() = !isActive
})
.call() .call()
} }
} }

View File

@ -0,0 +1,41 @@
package com.jetpackduba.gitnuro.git.submodules
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Config
import org.eclipse.jgit.storage.file.FileBasedConfig
import java.io.File
import javax.inject.Inject
private const val TAG = "DeleteSubmoduleUseCase"
class DeleteSubmoduleUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, path: String): Unit = withContext(Dispatchers.IO) {
git.rm()
.addFilepattern(path)
.call()
val repository = git.repository
val gitModules = File(repository.workTree, ".gitmodules")
if (gitModules.exists() && gitModules.isFile) {
val config = FileBasedConfig(gitModules, repository.fs)
config.load()
config.unsetSection("submodule", path)
config.save()
}
val moduleDir = File(repository.directory, "modules/$path")
val workspace = File(repository.workTree, path)
if (moduleDir.exists()) {
moduleDir.deleteRecursively()
}
if (workspace.exists()) {
workspace.deleteRecursively()
}
}
}

View File

@ -0,0 +1,25 @@
package com.jetpackduba.gitnuro.models
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.lib.ProgressMonitor
sealed interface ProgressMonitorInfo {
object Loading : ProgressMonitorInfo
data class Processing(
val totalTasks: Int,
val currentTaskCount: Int,
val currentTask: CurrentTask,
) : ProgressMonitorInfo
data class Failed(val ex: Exception) : ProgressMonitorInfo
object Completed : ProgressMonitorInfo
}
data class CurrentTask(
val title: String,
val totalWork: Int,
val completedWork: Int,
)

View File

@ -20,6 +20,7 @@ import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.* import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog
import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog
import com.jetpackduba.gitnuro.ui.dialogs.AddSubmodulesDialog
import com.jetpackduba.gitnuro.viewmodels.sidepanel.* import com.jetpackduba.gitnuro.viewmodels.sidepanel.*
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
@ -44,6 +45,7 @@ fun SidePanel(
var showEditRemotesDialog by remember { mutableStateOf(false) } var showEditRemotesDialog by remember { mutableStateOf(false) }
val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf<Ref?>(null) } val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf<Ref?>(null) }
var showEditSubmodulesDialog by remember { mutableStateOf(false) }
Column { Column {
FilterTextField( FilterTextField(
@ -85,7 +87,8 @@ fun SidePanel(
submodules( submodules(
submodulesState = submodulesState, submodulesState = submodulesState,
submodulesViewModel = submodulesViewModel submodulesViewModel = submodulesViewModel,
onShowEditSubmodulesDialog = { showEditSubmodulesDialog = true },
) )
} }
} }
@ -106,6 +109,18 @@ fun SidePanel(
onClose = { setBranchToChangeUpstream(null) } onClose = { setBranchToChangeUpstream(null) }
) )
} }
if (showEditSubmodulesDialog) {
AddSubmodulesDialog(
onCancel = {
showEditSubmodulesDialog = false
},
onAccept = { repository, directory ->
submodulesViewModel.onCreateSubmodule(repository, directory)
showEditSubmodulesDialog = false
}
)
}
} }
@Composable @Composable
@ -262,14 +277,12 @@ fun LazyListScope.tags(
if (isExpanded) { if (isExpanded) {
items(tags, key = { it.name }) { tag -> items(tags, key = { it.name }) { tag ->
// if () {
Tag( Tag(
tag, tag,
onTagClicked = { tagsViewModel.selectTag(tag) }, onTagClicked = { tagsViewModel.selectTag(tag) },
onCheckoutTag = { tagsViewModel.checkoutRef(tag) }, onCheckoutTag = { tagsViewModel.checkoutRef(tag) },
onDeleteTag = { tagsViewModel.deleteTag(tag) } onDeleteTag = { tagsViewModel.deleteTag(tag) }
) )
// }
} }
} }
} }
@ -312,6 +325,7 @@ fun LazyListScope.stashes(
fun LazyListScope.submodules( fun LazyListScope.submodules(
submodulesState: SubmodulesState, submodulesState: SubmodulesState,
submodulesViewModel: SubmodulesViewModel, submodulesViewModel: SubmodulesViewModel,
onShowEditSubmodulesDialog: () -> Unit,
) { ) {
val isExpanded = submodulesState.isExpanded val isExpanded = submodulesState.isExpanded
val submodules = submodulesState.submodules val submodules = submodulesState.submodules
@ -324,7 +338,23 @@ fun LazyListScope.submodules(
text = "Submodules", text = "Submodules",
icon = painterResource(AppIcons.TOPIC), icon = painterResource(AppIcons.TOPIC),
itemsCount = submodules.count(), itemsCount = submodules.count(),
hoverIcon = null, hoverIcon = {
IconButton(
onClick = onShowEditSubmodulesDialog,
modifier = Modifier
.padding(end = 16.dp)
.size(16.dp)
.handOnHover(),
) {
Icon(
painter = painterResource(AppIcons.ADD),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
tint = MaterialTheme.colors.onBackground,
)
}
},
isExpanded = isExpanded, isExpanded = isExpanded,
onExpand = { submodulesViewModel.onExpand() } onExpand = { submodulesViewModel.onExpand() }
) )
@ -336,10 +366,11 @@ fun LazyListScope.submodules(
Submodule( Submodule(
submodule = submodule, submodule = submodule,
onInitializeSubmodule = { submodulesViewModel.initializeSubmodule(submodule.first) }, onInitializeSubmodule = { submodulesViewModel.initializeSubmodule(submodule.first) },
onDeinitializeSubmodule = { submodulesViewModel.onDeinitializeSubmodule(submodule.first) }, // onDeinitializeSubmodule = { submodulesViewModel.onDeinitializeSubmodule(submodule.first) },
onSyncSubmodule = { submodulesViewModel.onSyncSubmodule(submodule.first) }, onSyncSubmodule = { submodulesViewModel.onSyncSubmodule(submodule.first) },
onUpdateSubmodule = { submodulesViewModel.onUpdateSubmodule(submodule.first) }, onUpdateSubmodule = { submodulesViewModel.onUpdateSubmodule(submodule.first) },
onOpenSubmoduleInTab = { submodulesViewModel.onOpenSubmoduleInTab(submodule.first) }, onOpenSubmoduleInTab = { submodulesViewModel.onOpenSubmoduleInTab(submodule.first) },
onDeleteSubmodule = { submodulesViewModel.onDeleteSubmodule(submodule.first) },
) )
} }
} }
@ -486,26 +517,29 @@ private fun Stash(
private fun Submodule( private fun Submodule(
submodule: Pair<String, SubmoduleStatus>, submodule: Pair<String, SubmoduleStatus>,
onInitializeSubmodule: () -> Unit, onInitializeSubmodule: () -> Unit,
onDeinitializeSubmodule: () -> Unit, // onDeinitializeSubmodule: () -> Unit,
onSyncSubmodule: () -> Unit, onSyncSubmodule: () -> Unit,
onUpdateSubmodule: () -> Unit, onUpdateSubmodule: () -> Unit,
onOpenSubmoduleInTab: () -> Unit, onOpenSubmoduleInTab: () -> Unit,
onDeleteSubmodule: () -> Unit,
) { ) {
ContextMenu( ContextMenu(
items = { items = {
submoduleContextMenuItems( submoduleContextMenuItems(
submodule.second, submodule.second,
onInitializeSubmodule = onInitializeSubmodule, onInitializeSubmodule = onInitializeSubmodule,
onDeinitializeSubmodule = onDeinitializeSubmodule, // onDeinitializeSubmodule = onDeinitializeSubmodule,
onSyncSubmodule = onSyncSubmodule, onSyncSubmodule = onSyncSubmodule,
onUpdateSubmodule = onUpdateSubmodule, onUpdateSubmodule = onUpdateSubmodule,
onOpenSubmoduleInTab = onOpenSubmoduleInTab, onOpenSubmoduleInTab = onOpenSubmoduleInTab,
onDeleteSubmodule = onDeleteSubmodule,
) )
} }
) { ) {
SideMenuSubentry( SideMenuSubentry(
text = submodule.first, text = submodule.first,
iconResourcePath = AppIcons.TOPIC, iconResourcePath = AppIcons.TOPIC,
onClick = onOpenSubmoduleInTab,
) { ) {
val stateName = submodule.second.type.toString() val stateName = submodule.second.type.toString()
Tooltip(stateName) { Tooltip(stateName) {

View File

@ -1,15 +1,18 @@
package com.jetpackduba.gitnuro.ui.context_menu package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.ui.res.painterResource
import com.jetpackduba.gitnuro.AppIcons
import org.eclipse.jgit.submodule.SubmoduleStatus import org.eclipse.jgit.submodule.SubmoduleStatus
import org.eclipse.jgit.submodule.SubmoduleStatusType import org.eclipse.jgit.submodule.SubmoduleStatusType
fun submoduleContextMenuItems( fun submoduleContextMenuItems(
submoduleStatus: SubmoduleStatus, submoduleStatus: SubmoduleStatus,
onInitializeSubmodule: () -> Unit, onInitializeSubmodule: () -> Unit,
onDeinitializeSubmodule: () -> Unit, // onDeinitializeSubmodule: () -> Unit,
onSyncSubmodule: () -> Unit, onSyncSubmodule: () -> Unit,
onUpdateSubmodule: () -> Unit, onUpdateSubmodule: () -> Unit,
onOpenSubmoduleInTab: () -> Unit, onOpenSubmoduleInTab: () -> Unit,
onDeleteSubmodule: () -> Unit,
): List<ContextMenuElement> { ): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().apply { return mutableListOf<ContextMenuElement>().apply {
if (submoduleStatus.type == SubmoduleStatusType.UNINITIALIZED) { if (submoduleStatus.type == SubmoduleStatusType.UNINITIALIZED) {
@ -24,12 +27,14 @@ fun submoduleContextMenuItems(
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(
label = "Open submodule in new tab", label = "Open submodule in new tab",
icon = { painterResource(AppIcons.OPEN) },
onClick = onOpenSubmoduleInTab, onClick = onOpenSubmoduleInTab,
) )
) )
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(
label = "Sync submodule", label = "Sync submodule",
icon = { painterResource(AppIcons.SYNC) },
onClick = onSyncSubmodule, onClick = onSyncSubmodule,
) )
) )
@ -37,16 +42,29 @@ fun submoduleContextMenuItems(
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(
label = "Update submodule", label = "Update submodule",
icon = { painterResource(AppIcons.UPDATE) },
onClick = onUpdateSubmodule, onClick = onUpdateSubmodule,
) )
) )
// TODO This crashes with a NPE in jgit even when providing correct arguments, feature removed for now
// add(
// ContextMenuElement.ContextTextEntry(
// label = "DeInitialize submodule",
// onClick = onDeinitializeSubmodule,
// )
// )
}
add(
ContextMenuElement.ContextSeparator,
)
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(
label = "DeInitialize submodule", label = "Delete submodule",
onClick = onDeinitializeSubmodule, icon = { painterResource(AppIcons.DELETE) },
onClick = onDeleteSubmodule,
),
) )
)
}
} }
} }

View File

@ -0,0 +1,201 @@
package com.jetpackduba.gitnuro.ui.dialogs
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.jetpackduba.gitnuro.AppIcons
import com.jetpackduba.gitnuro.keybindings.KeybindingOption
import com.jetpackduba.gitnuro.keybindings.matchesBinding
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
import com.jetpackduba.gitnuro.ui.components.PrimaryButton
import com.jetpackduba.gitnuro.ui.components.gitnuroDynamicViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.SubmoduleDialogViewModel
@Composable
fun AddSubmodulesDialog(
viewModel: SubmoduleDialogViewModel = gitnuroDynamicViewModel(),
onCancel: () -> Unit,
onAccept: (repository: String, directory: String) -> Unit,
) {
var repositoryField by remember { mutableStateOf("") }
var directoryField by remember { mutableStateOf(TextFieldValue("")) }
val repositoryFocusRequester = remember { FocusRequester() }
val directoryFocusRequester = remember { FocusRequester() }
val buttonFieldFocusRequester = remember { FocusRequester() }
val error by viewModel.error.collectAsState()
LaunchedEffect(viewModel) {
viewModel.onDataIsValid.collect {
onAccept(repositoryField, directoryField.text)
}
}
MaterialDialog(
background = MaterialTheme.colors.surface,
onCloseRequested = onCancel
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.animateContentSize().width(IntrinsicSize.Min)
) {
Icon(
painterResource(AppIcons.TOPIC),
contentDescription = null,
modifier = Modifier
.padding(bottom = 16.dp)
.size(64.dp),
tint = MaterialTheme.colors.onBackground,
)
Text(
text = "New submodule",
modifier = Modifier
.padding(bottom = 8.dp),
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.SemiBold,
)
Text(
text = "Create a new submodule from an existing repository",
modifier = Modifier
.padding(bottom = 16.dp),
color = MaterialTheme.colors.onBackground,
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Center,
)
Text(
"Repository URL",
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 8.dp)
.align(Alignment.Start),
)
AdjustableOutlinedTextField(
modifier = Modifier
.focusRequester(repositoryFocusRequester)
.focusProperties {
this.next = directoryFocusRequester
}
.width(480.dp)
.onFocusChanged { focusState ->
if (!focusState.isFocused && directoryField.text.isBlank() && repositoryField.isNotBlank()) {
val repo = repositoryField.split("/", "\\").last().removeSuffix(".git")
directoryField = TextFieldValue(repo, selection = TextRange(repo.count()))
}
},
value = repositoryField,
maxLines = 1,
onValueChange = {
viewModel.resetError()
repositoryField = it
},
)
Text(
"Directory",
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
.align(Alignment.Start),
)
AdjustableOutlinedTextField(
modifier = Modifier
.focusRequester(directoryFocusRequester)
.focusProperties {
this.next = buttonFieldFocusRequester
}
.width(480.dp)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.matchesBinding(KeybindingOption.SIMPLE_ACCEPT) && repositoryField.isNotBlank() && directoryField.text.isNotBlank()) {
viewModel.verifyData(repositoryField, directoryField.text)
true
} else {
false
}
},
value = directoryField,
maxLines = 1,
onValueChange = {
viewModel.resetError()
directoryField = it
},
)
AnimatedVisibility (error.isNotBlank()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.error)
) {
Text(
error,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 8.dp),
color = MaterialTheme.colors.onError,
)
}
}
Row(
modifier = Modifier
.padding(top = 24.dp)
.align(Alignment.End)
) {
PrimaryButton(
text = "Cancel",
modifier = Modifier.padding(end = 8.dp),
onClick = onCancel,
backgroundColor = Color.Transparent,
textColor = MaterialTheme.colors.onBackground,
)
PrimaryButton(
modifier = Modifier
.focusRequester(buttonFieldFocusRequester)
.focusProperties {
this.previous = repositoryFocusRequester
this.next = repositoryFocusRequester
},
enabled = repositoryField.isNotBlank() && directoryField.text.isNotBlank(),
onClick = {
viewModel.verifyData(repositoryField, directoryField.text)
},
text = "Create submodule"
)
}
}
LaunchedEffect(Unit) {
repositoryFocusRequester.requestFocus()
}
}
}

View File

@ -46,7 +46,7 @@ fun NewBranchDialog(
) )
Text( Text(
text = "Set branch name", text = "New branch",
modifier = Modifier modifier = Modifier
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
color = MaterialTheme.colors.onBackground, color = MaterialTheme.colors.onBackground,

View File

@ -46,7 +46,6 @@ class CloneViewModel @Inject constructor(
return@safeProcessingWithoutGit return@safeProcessingWithoutGit
} }
val urlSplit = url.split("/", "\\").toMutableList() val urlSplit = url.split("/", "\\").toMutableList()
// Removes the last element for URLs that end with "/" or "\" instead of the repo name like https://github.com/JetpackDuba/Gitnuro/ // Removes the last element for URLs that end with "/" or "\" instead of the repo name like https://github.com/JetpackDuba/Gitnuro/

View File

@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels
import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.viewmodels.sidepanel.SidePanelViewModel import com.jetpackduba.gitnuro.viewmodels.sidepanel.SidePanelViewModel
import com.jetpackduba.gitnuro.viewmodels.sidepanel.SubmoduleDialogViewModel
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -21,6 +22,7 @@ class TabViewModelsHolder @Inject constructor(
private val historyViewModelProvider: Provider<HistoryViewModel>, private val historyViewModelProvider: Provider<HistoryViewModel>,
private val authorViewModelProvider: Provider<AuthorViewModel>, private val authorViewModelProvider: Provider<AuthorViewModel>,
private val changeDefaultUpstreamBranchViewModelProvider: Provider<ChangeDefaultUpstreamBranchViewModel>, private val changeDefaultUpstreamBranchViewModelProvider: Provider<ChangeDefaultUpstreamBranchViewModel>,
private val submoduleDialogViewModelProvider: Provider<SubmoduleDialogViewModel>,
) { ) {
val viewModels = mapOf( val viewModels = mapOf(
logViewModel::class to logViewModel, logViewModel::class to logViewModel,
@ -40,6 +42,7 @@ class TabViewModelsHolder @Inject constructor(
HistoryViewModel::class -> historyViewModelProvider.get() HistoryViewModel::class -> historyViewModelProvider.get()
AuthorViewModel::class -> authorViewModelProvider.get() AuthorViewModel::class -> authorViewModelProvider.get()
ChangeDefaultUpstreamBranchViewModel::class -> changeDefaultUpstreamBranchViewModelProvider.get() ChangeDefaultUpstreamBranchViewModel::class -> changeDefaultUpstreamBranchViewModelProvider.get()
SubmoduleDialogViewModel::class -> submoduleDialogViewModelProvider.get()
else -> throw NotImplementedError("View model provider not implemented") else -> throw NotImplementedError("View model provider not implemented")
} }
} }

View File

@ -0,0 +1,58 @@
package com.jetpackduba.gitnuro.viewmodels.sidepanel
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.*
import org.eclipse.jgit.transport.URIish
import java.io.File
import javax.inject.Inject
class SubmoduleDialogViewModel @Inject constructor(
private val tabState: TabState,
) {
private val _error = MutableStateFlow("")
val error = _error.asStateFlow()
private val _onDataIsValid = MutableSharedFlow<Unit>()
val onDataIsValid = _onDataIsValid.asSharedFlow()
fun verifyData(
repositoryUrl: String,
directoryPath: String,
) = tabState.runOperation(
refreshType = RefreshType.NONE,
) { git ->
val message = when {
directoryPath.isBlank() -> "Invalid empty directory"
repositoryUrl.isBlank() -> "Invalid empty URL"
else -> null
}
if (message != null) {
_error.value = message
return@runOperation
}
try {
URIish(repositoryUrl)
} catch (ex: Exception) {
_error.value = "${ex.message.orEmpty()}. Check your repository URL and try again."
return@runOperation
}
val directory = File(git.repository.workTree, directoryPath)
if (directory.exists() && (directory.listFiles()?.count() ?: 0) > 0) {
_error.value = "Directory $directoryPath contains files. Try again with a different name."
return@runOperation
}
_onDataIsValid.emit(Unit)
}
fun resetError() {
_error.value = ""
}
}

View File

@ -20,6 +20,8 @@ class SubmodulesViewModel @AssistedInject constructor(
private val updateSubmoduleUseCase: UpdateSubmoduleUseCase, private val updateSubmoduleUseCase: UpdateSubmoduleUseCase,
private val syncSubmoduleUseCase: SyncSubmoduleUseCase, private val syncSubmoduleUseCase: SyncSubmoduleUseCase,
private val deInitializeSubmoduleUseCase: DeInitializeSubmoduleUseCase, private val deInitializeSubmoduleUseCase: DeInitializeSubmoduleUseCase,
private val addSubmoduleUseCase: AddSubmoduleUseCase,
private val deleteSubmoduleUseCase: DeleteSubmoduleUseCase,
private val tabScope: CoroutineScope, private val tabScope: CoroutineScope,
private val tabsManager: TabsManager, private val tabsManager: TabsManager,
@Assisted @Assisted
@ -41,8 +43,7 @@ class SubmodulesViewModel @AssistedInject constructor(
init { init {
tabScope.launch { tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.SUBMODULES) tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.UNCOMMITED_CHANGES, RefreshType.SUBMODULES) {
{
refresh(tabState.git) refresh(tabState.git)
} }
} }
@ -90,6 +91,23 @@ class SubmodulesViewModel @AssistedInject constructor(
) { git -> ) { git ->
updateSubmoduleUseCase(git, path) updateSubmoduleUseCase(git, path)
} }
fun onCreateSubmodule(repository: String, directory: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
) { git ->
addSubmoduleUseCase(
git = git,
name = directory,
path = directory,
uri = repository,
)
}
fun onDeleteSubmodule(path: String) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA,
) { git ->
deleteSubmoduleUseCase(git, path)
}
} }
data class SubmodulesState(val submodules: List<Pair<String, SubmoduleStatus>>, val isExpanded: Boolean) data class SubmodulesState(val submodules: List<Pair<String, SubmoduleStatus>>, val isExpanded: Boolean)

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="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24" x="0"/></g><g><g><g><path d="M21,10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-0.1c-2.73,2.71-2.73,7.08,0,9.79s7.15,2.71,9.88,0 C18.32,15.65,19,14.08,19,12.1h2c0,1.98-0.88,4.55-2.64,6.29c-3.51,3.48-9.21,3.48-12.72,0c-3.5-3.47-3.53-9.11-0.02-12.58 s9.14-3.47,12.65,0L21,3V10.12z M12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 525 B