diff --git a/README.md b/README.md index 4366d77..765c319 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Right now you CAN: - Commit. - Reset commits. - Revert commits. +- Amend previous commit. - Merge. - Rebase. - Create and delete branches locally. diff --git a/src/main/kotlin/app/git/LogManager.kt b/src/main/kotlin/app/git/LogManager.kt index 46948ac..c96cd98 100644 --- a/src/main/kotlin/app/git/LogManager.kt +++ b/src/main/kotlin/app/git/LogManager.kt @@ -70,9 +70,38 @@ class LogManager @Inject constructor( .setRef(revCommit.name) .call() } + + suspend fun latestMessage(git: Git): String = withContext(Dispatchers.IO) { + try { + val log = git.log().setMaxCount(1).call() + val latestCommitNode = log.firstOrNull() + + return@withContext if(latestCommitNode == null) + "" + else + latestCommitNode.fullMessage + + } catch (ex: Exception) { + ex.printStackTrace() + return@withContext "" + } + + } + + suspend fun hasPreviousCommits(git: Git): Boolean = withContext(Dispatchers.IO) { + try { + val log = git.log().setMaxCount(1).call() + val latestCommitNode = log.firstOrNull() + + return@withContext latestCommitNode != null + + } catch (ex: Exception) { + ex.printStackTrace() + return@withContext false + } + } } -// TODO Move this to enum class ResetType { SOFT, MIXED, diff --git a/src/main/kotlin/app/git/StatusManager.kt b/src/main/kotlin/app/git/StatusManager.kt index c4d8f46..825eb70 100644 --- a/src/main/kotlin/app/git/StatusManager.kt +++ b/src/main/kotlin/app/git/StatusManager.kt @@ -188,10 +188,11 @@ class StatusManager @Inject constructor( .call() } - suspend fun commit(git: Git, message: String) = withContext(Dispatchers.IO) { + suspend fun commit(git: Git, message: String, amend: Boolean) = withContext(Dispatchers.IO) { git.commit() .setMessage(message) .setAllowEmpty(false) + .setAmend(amend) .call() } diff --git a/src/main/kotlin/app/git/TabState.kt b/src/main/kotlin/app/git/TabState.kt index a6e9769..efbabd0 100644 --- a/src/main/kotlin/app/git/TabState.kt +++ b/src/main/kotlin/app/git/TabState.kt @@ -122,6 +122,8 @@ class TabState @Inject constructor( if (showError) errorsManager.addError(newErrorNow(ex, ex.localizedMessage)) } finally { + operationRunning = false + if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes)) _refreshData.emit(refreshType) } diff --git a/src/main/kotlin/app/ui/UncommitedChanges.kt b/src/main/kotlin/app/ui/UncommitedChanges.kt index 9dd3fdb..06daa41 100644 --- a/src/main/kotlin/app/ui/UncommitedChanges.kt +++ b/src/main/kotlin/app/ui/UncommitedChanges.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -33,6 +35,8 @@ import app.git.StatusEntry import app.theme.* import app.ui.components.ScrollableLazyColumn import app.ui.components.SecondaryButton +import app.ui.context_menu.DropDownContent +import app.ui.context_menu.DropDownContentData import app.ui.context_menu.stagedEntriesContextMenuItems import app.ui.context_menu.unstagedEntriesContextMenuItems import app.viewmodels.StageStatus @@ -75,12 +79,14 @@ fun UncommitedChanges( unstaged = listOf() // return empty lists if still loading } - val doCommit = { - statusViewModel.commit(commitMessage) + val doCommit = { amend: Boolean -> + statusViewModel.commit(commitMessage, amend) onStagedDiffEntrySelected(null) statusViewModel.newCommitMessage = "" } + val canCommit = commitMessage.isNotEmpty() && staged.isNotEmpty() + val canAmend = (commitMessage.isNotEmpty() || staged.isNotEmpty()) && statusViewModel.hasPreviousCommits Column { AnimatedVisibility( @@ -91,7 +97,6 @@ fun UncommitedChanges( LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } - EntriesList( modifier = Modifier .padding(start = 8.dp, end = 8.dp, bottom = 4.dp) @@ -169,7 +174,7 @@ fun UncommitedChanges( .weight(weight = 1f, fill = true) .onPreviewKeyEvent { if (it.isCtrlPressed && it.key == Key.Enter && canCommit) { - doCommit() + doCommit(false) true } else false @@ -185,7 +190,7 @@ fun UncommitedChanges( repositoryState.isMerging -> MergeButtons( haveConflictsBeenSolved = unstaged.isEmpty(), onAbort = { statusViewModel.abortMerge() }, - onMerge = { doCommit() } + onMerge = { doCommit(false) } ) repositoryState.isRebasing -> RebasingButtons( canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(), @@ -194,27 +199,86 @@ fun UncommitedChanges( onContinue = { statusViewModel.continueRebase() }, onSkip = { statusViewModel.skipRebase() }, ) - else -> { - Button( - modifier = Modifier - .fillMaxWidth(), - onClick = doCommit, - enabled = canCommit, - shape = RectangleShape, - ) { - - Text( - text = "Commit", - fontSize = 14.sp, - ) - } - } + else -> UncommitedChangesButtons( + canCommit = canCommit, + canAmend = canAmend, + onCommit = { amend -> doCommit(amend) }, + ) } } } } +@Composable +fun UncommitedChangesButtons( + canCommit: Boolean, + canAmend: Boolean, + onCommit: (Boolean) -> Unit +) { + var showDropDownMenu by remember { mutableStateOf(false) } + + + Row( + modifier = Modifier + .padding(top = 2.dp) + ) { + Button( + modifier = Modifier + .weight(1f) + .height(40.dp), + onClick = { onCommit(false) }, + enabled = canCommit, + shape = RectangleShape, + ) { + Text( + text = "Commit", + fontSize = 14.sp, + ) + } + Spacer( + modifier = Modifier + .width(1.dp) + .height(40.dp), + ) + + Box( + modifier = Modifier + .height(40.dp) + .background(MaterialTheme.colors.primary) + .clickable { showDropDownMenu = true }, + ) { + Icon( + Icons.Default.ArrowDropDown, + contentDescription = null, + tint = MaterialTheme.colors.inversePrimaryTextColor, + modifier = Modifier + .padding(horizontal = 8.dp) + .align(Alignment.Center), + ) + DropdownMenu( + onDismissRequest = { + showDropDownMenu = false + }, + content = { + DropDownContent( + enabled = canAmend, + dropDownContentData = DropDownContentData( + label = "Amend previous commit", + icon = null, + onClick = { onCommit(true) } + ), + onDismiss = { showDropDownMenu = false } + ) + }, + expanded = showDropDownMenu, + ) + + } +// } + } +} + @Composable fun MergeButtons( haveConflictsBeenSolved: Boolean, diff --git a/src/main/kotlin/app/ui/context_menu/DropDownContent.kt b/src/main/kotlin/app/ui/context_menu/DropDownContent.kt index 211cb7d..02a6d30 100644 --- a/src/main/kotlin/app/ui/context_menu/DropDownContent.kt +++ b/src/main/kotlin/app/ui/context_menu/DropDownContent.kt @@ -8,8 +8,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.sp @Composable -fun DropDownContent(dropDownContentData: DropDownContentData, onDismiss: () -> Unit) { +fun DropDownContent( + dropDownContentData: DropDownContentData, + enabled: Boolean = true, + onDismiss: () -> Unit, +) { DropdownMenuItem( + enabled = enabled, onClick = { dropDownContentData.onClick() onDismiss() diff --git a/src/main/kotlin/app/viewmodels/StatusViewModel.kt b/src/main/kotlin/app/viewmodels/StatusViewModel.kt index d1cc9df..7b0ac5d 100644 --- a/src/main/kotlin/app/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/app/viewmodels/StatusViewModel.kt @@ -17,6 +17,7 @@ class StatusViewModel @Inject constructor( private val repositoryManager: RepositoryManager, private val rebaseManager: RebaseManager, private val mergeManager: MergeManager, + private val logManager: LogManager, ) { private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf())) val stageStatus: StateFlow = _stageStatus @@ -29,6 +30,8 @@ class StatusViewModel @Inject constructor( _commitMessage.value = value } + var hasPreviousCommits = true // When false, disable "amend previous commit" + private var lastUncommitedChangesState = false fun stage(diffEntry: DiffEntry) = tabState.runOperation( @@ -89,10 +92,15 @@ class StatusViewModel @Inject constructor( lastUncommitedChangesState = statusManager.hasUncommitedChanges(git) } - fun commit(message: String) = tabState.safeProcessing( + fun commit(message: String, amend: Boolean) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, ) { git -> - statusManager.commit(git, message) + val commitMessage = if(amend && message.isBlank()) { + logManager.latestMessage(git) + } else + message + + statusManager.commit(git, commitMessage, amend) } suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { @@ -110,6 +118,7 @@ class StatusViewModel @Inject constructor( loadHasUncommitedChanges(git) val hasNowUncommitedChanges = this.lastUncommitedChangesState + hasPreviousCommits = logManager.hasPreviousCommits(git) // Return true to update the log only if the uncommitedChanges status has changed return (hasNowUncommitedChanges != hadUncommitedChanges)