From 4388ccb6905f518f4568f877386a7426154a72aa Mon Sep 17 00:00:00 2001 From: Abdelilah El Aissaoui Date: Mon, 13 Mar 2023 11:36:56 +0100 Subject: [PATCH] Added author info request when doing a new commit if the info is not set previously --- .../kotlin/com/jetpackduba/gitnuro/Icons.kt | 1 + .../gitnuro/git/workspace/DoCommitUseCase.kt | 9 +- .../gitnuro/ui/UncommitedChanges.kt | 183 ++++++++++++------ .../gitnuro/ui/dialogs/CommitAuthorDialog.kt | 182 +++++++++++++++++ .../gitnuro/viewmodels/StatusViewModel.kt | 56 +++++- src/main/resources/person.svg | 1 + 6 files changed, 365 insertions(+), 67 deletions(-) create mode 100644 src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CommitAuthorDialog.kt create mode 100644 src/main/resources/person.svg diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt index 37d3c59..af0d1d2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/Icons.kt @@ -30,6 +30,7 @@ object AppIcons { const val MERGE = "merge.svg" const val MORE_VERT = "more_vert.svg" const val OPEN = "open.svg" + const val PERSON = "person.svg" const val REFRESH = "refresh.svg" const val REMOVE = "remove.svg" const val REVERT = "revert.svg" diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt index 4b8fa77..58497eb 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/workspace/DoCommitUseCase.kt @@ -3,15 +3,22 @@ package com.jetpackduba.gitnuro.git.workspace import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject class DoCommitUseCase @Inject constructor() { - suspend operator fun invoke(git: Git, message: String, amend: Boolean): RevCommit = withContext(Dispatchers.IO) { + suspend operator fun invoke( + git: Git, + message: String, + amend: Boolean, + author: PersonIdent?, + ): RevCommit = withContext(Dispatchers.IO) { git.commit() .setMessage(message) .setAllowEmpty(amend) // Only allow empty commits when amending .setAmend(amend) + .setAuthor(author) .call() } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt index eea9a95..45da53b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import com.jetpackduba.gitnuro.extensions.* import com.jetpackduba.gitnuro.git.DiffEntryType @@ -44,6 +45,8 @@ import com.jetpackduba.gitnuro.ui.context_menu.ContextMenu import com.jetpackduba.gitnuro.ui.context_menu.ContextMenuElement import com.jetpackduba.gitnuro.ui.context_menu.EntryType import com.jetpackduba.gitnuro.ui.context_menu.statusEntriesContextMenuItems +import com.jetpackduba.gitnuro.ui.dialogs.CommitAuthorDialog +import com.jetpackduba.gitnuro.viewmodels.CommitterDataRequestState import com.jetpackduba.gitnuro.viewmodels.StageStatus import com.jetpackduba.gitnuro.viewmodels.StatusViewModel import org.eclipse.jgit.lib.RepositoryState @@ -63,6 +66,8 @@ fun UncommitedChanges( val stagedListState by statusViewModel.stagedLazyListState.collectAsState() val unstagedListState by statusViewModel.unstagedLazyListState.collectAsState() val isAmend by statusViewModel.isAmend.collectAsState() + val committerDataRequestState = statusViewModel.committerDataRequestState.collectAsState() + val committerDataRequestStateValue = committerDataRequestState.value val stageStatus = stageStatusState.value val staged: List @@ -94,10 +99,20 @@ fun UncommitedChanges( } } + if (committerDataRequestStateValue is CommitterDataRequestState.WaitingInput) { + CommitAuthorDialog( + committerDataRequestStateValue.authorInfo, + onClose = { statusViewModel.onRejectCommitterData() }, + onAccept = { newAuthorInfo, persist -> + statusViewModel.onAcceptCommitterData(newAuthorInfo, persist) + }, + ) + } + Column( modifier = Modifier .padding(end = 8.dp, bottom = 8.dp) - .fillMaxWidth() + .fillMaxWidth(), ) { AnimatedVisibility( visible = isLoading, @@ -107,70 +122,74 @@ fun UncommitedChanges( LinearProgressIndicator(modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.primaryVariant) } - EntriesList( - modifier = Modifier - .weight(5f) - .padding(bottom = 4.dp) - .fillMaxWidth(), - title = "Staged", - allActionTitle = "Unstage all", - actionTitle = "Unstage", - selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, - actionColor = MaterialTheme.colors.error, - actionTextColor = MaterialTheme.colors.onError, - statusEntries = staged, - lazyListState = stagedListState, - onDiffEntrySelected = onStagedDiffEntrySelected, - onDiffEntryOptionSelected = { - statusViewModel.unstage(it) - }, - onGenerateContextMenu = { statusEntry -> - statusEntriesContextMenuItems( - statusEntry = statusEntry, - entryType = EntryType.STAGED, - onBlame = { onBlameFile(statusEntry.filePath) }, - onReset = { statusViewModel.resetStaged(statusEntry) }, - onHistory = { onHistoryFile(statusEntry.filePath) }, - ) - }, - onAllAction = { - statusViewModel.unstageAll() - }, - ) + Column( + modifier = Modifier.weight(1f) + ) { + EntriesList( + modifier = Modifier + .weight(5f) + .padding(bottom = 4.dp) + .fillMaxWidth(), + title = "Staged", + allActionTitle = "Unstage all", + actionTitle = "Unstage", + selectedEntryType = if (selectedEntryType is DiffEntryType.StagedDiff) selectedEntryType else null, + actionColor = MaterialTheme.colors.error, + actionTextColor = MaterialTheme.colors.onError, + statusEntries = staged, + lazyListState = stagedListState, + onDiffEntrySelected = onStagedDiffEntrySelected, + onDiffEntryOptionSelected = { + statusViewModel.unstage(it) + }, + onGenerateContextMenu = { statusEntry -> + statusEntriesContextMenuItems( + statusEntry = statusEntry, + entryType = EntryType.STAGED, + onBlame = { onBlameFile(statusEntry.filePath) }, + onReset = { statusViewModel.resetStaged(statusEntry) }, + onHistory = { onHistoryFile(statusEntry.filePath) }, + ) + }, + onAllAction = { + statusViewModel.unstageAll() + }, + ) - EntriesList( - modifier = Modifier - .weight(5f) - .padding(bottom = 4.dp) - .fillMaxWidth(), - title = "Unstaged", - actionTitle = "Stage", - selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, - actionColor = MaterialTheme.colors.primary, - actionTextColor = MaterialTheme.colors.onPrimary, - statusEntries = unstaged, - lazyListState = unstagedListState, - onDiffEntrySelected = onUnstagedDiffEntrySelected, - onDiffEntryOptionSelected = { - statusViewModel.stage(it) - }, - onGenerateContextMenu = { statusEntry -> - statusEntriesContextMenuItems( - statusEntry = statusEntry, - entryType = EntryType.UNSTAGED, - onBlame = { onBlameFile(statusEntry.filePath) }, - onHistory = { onHistoryFile(statusEntry.filePath) }, - onReset = { statusViewModel.resetUnstaged(statusEntry) }, - onDelete = { - statusViewModel.deleteFile(statusEntry) - }, - ) - }, - onAllAction = { - statusViewModel.stageAll() - }, - allActionTitle = "Stage all", - ) + EntriesList( + modifier = Modifier + .weight(5f) + .padding(bottom = 4.dp) + .fillMaxWidth(), + title = "Unstaged", + actionTitle = "Stage", + selectedEntryType = if (selectedEntryType is DiffEntryType.UnstagedDiff) selectedEntryType else null, + actionColor = MaterialTheme.colors.primary, + actionTextColor = MaterialTheme.colors.onPrimary, + statusEntries = unstaged, + lazyListState = unstagedListState, + onDiffEntrySelected = onUnstagedDiffEntrySelected, + onDiffEntryOptionSelected = { + statusViewModel.stage(it) + }, + onGenerateContextMenu = { statusEntry -> + statusEntriesContextMenuItems( + statusEntry = statusEntry, + entryType = EntryType.UNSTAGED, + onBlame = { onBlameFile(statusEntry.filePath) }, + onHistory = { onHistoryFile(statusEntry.filePath) }, + onReset = { statusViewModel.resetUnstaged(statusEntry) }, + onDelete = { + statusViewModel.deleteFile(statusEntry) + }, + ) + }, + onAllAction = { + statusViewModel.stageAll() + }, + allActionTitle = "Stage all", + ) + } Column( modifier = Modifier @@ -693,4 +712,40 @@ private fun FileEntry( ) } } +} + + +@Stable +val BottomReversed = object : Arrangement.Vertical { + override fun Density.arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true) + + override fun toString() = "Arrangement#BottomReversed" +} + +internal fun placeRightOrBottom( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean +) { + val consumedSize = size.fold(0) { a, b -> a + b } + var current = totalSize - consumedSize + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current + current += it + } +} + +private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) { + if (!reversed) { + forEachIndexed(action) + } else { + for (i in (size - 1) downTo 0) { + action(i, get(i)) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CommitAuthorDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CommitAuthorDialog.kt new file mode 100644 index 0000000..263cb44 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/CommitAuthorDialog.kt @@ -0,0 +1,182 @@ +package com.jetpackduba.gitnuro.ui.dialogs + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Checkbox +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.AppIcons +import com.jetpackduba.gitnuro.extensions.handMouseClickable +import com.jetpackduba.gitnuro.models.AuthorInfo +import com.jetpackduba.gitnuro.theme.onBackgroundSecondary +import com.jetpackduba.gitnuro.theme.outlinedTextFieldColors +import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField +import com.jetpackduba.gitnuro.ui.components.PrimaryButton + +@Composable +fun CommitAuthorDialog( + authorInfo: AuthorInfo, + onClose: () -> Unit, + onAccept: (newAuthorInfo: AuthorInfo, persist: Boolean) -> Unit, +) { + var globalName by remember(authorInfo) { mutableStateOf(authorInfo.globalName.orEmpty()) } + var globalEmail by remember(authorInfo) { mutableStateOf(authorInfo.globalEmail.orEmpty()) } + var persist by remember { mutableStateOf(false) } + + MaterialDialog( + onCloseRequested = onClose, + background = MaterialTheme.colors.surface, + ) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .width(IntrinsicSize.Min), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + + Icon( + painterResource(AppIcons.PERSON), + contentDescription = null, + modifier = Modifier + .padding(bottom = 16.dp) + .size(64.dp), + tint = MaterialTheme.colors.onBackground, + ) + + + Text( + text = "Author identity", + modifier = Modifier + .padding(bottom = 8.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.SemiBold, + ) + + Text( + text = "Your Git configuration does not have a user identity set.", + modifier = Modifier, + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + Text( + text = "What identity would you like to use for this commit?", + modifier = Modifier + .padding(bottom = 8.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold + ) + + + TextInput( + title = "Name", + value = globalName, + onValueChange = { globalName = it }, + ) + + TextInput( + title = "Email", + value = globalEmail, + onValueChange = { globalEmail = it }, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.handMouseClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { + persist = !persist + }.fillMaxWidth() + ) { + Checkbox( + checked = persist, + onCheckedChange = { + persist = it + }, + modifier = Modifier + .padding(all = 8.dp) + .size(12.dp) + ) + + Text( + "Save identity in the .gitconfig file", + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackground, + ) + } + + Row( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.End) + ) { + PrimaryButton( + text = "Cancel", + modifier = Modifier.padding(end = 8.dp), + onClick = onClose, + backgroundColor = Color.Transparent, + textColor = MaterialTheme.colors.onBackground, + ) + PrimaryButton( + onClick = { + onAccept( + AuthorInfo( + globalName, + globalEmail, + authorInfo.name, + authorInfo.email, + ), + persist, + ) + }, + text = "Continue" + ) + } + } + } +} + +@Composable +private fun TextInput( + title: String, + value: String, + enabled: Boolean = true, + onValueChange: (String) -> Unit, +) { + Column( + modifier = Modifier + .width(400.dp) + .padding(vertical = 8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(bottom = 8.dp), + ) + + AdjustableOutlinedTextField( + value = value, + modifier = Modifier + .fillMaxWidth(), + enabled = enabled, + onValueChange = onValueChange, + colors = outlinedTextFieldColors(), + singleLine = true, + ) + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt index 8bc12af..b655005 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/StatusViewModel.kt @@ -6,6 +6,8 @@ import com.jetpackduba.gitnuro.extensions.isMerging import com.jetpackduba.gitnuro.extensions.isReverting import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState +import com.jetpackduba.gitnuro.git.author.LoadAuthorUseCase +import com.jetpackduba.gitnuro.git.author.SaveAuthorUseCase import com.jetpackduba.gitnuro.git.log.CheckHasPreviousCommitsUseCase import com.jetpackduba.gitnuro.git.log.GetLastCommitMessageUseCase import com.jetpackduba.gitnuro.git.rebase.AbortRebaseUseCase @@ -13,6 +15,7 @@ import com.jetpackduba.gitnuro.git.rebase.ContinueRebaseUseCase import com.jetpackduba.gitnuro.git.rebase.SkipRebaseUseCase import com.jetpackduba.gitnuro.git.repository.ResetRepositoryStateUseCase import com.jetpackduba.gitnuro.git.workspace.* +import com.jetpackduba.gitnuro.models.AuthorInfo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -22,6 +25,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.RepositoryState import java.io.File import javax.inject.Inject @@ -46,6 +50,8 @@ class StatusViewModel @Inject constructor( private val getUnstagedUseCase: GetUnstagedUseCase, private val checkHasUncommitedChangedUseCase: CheckHasUncommitedChangedUseCase, private val doCommitUseCase: DoCommitUseCase, + private val loadAuthorUseCase: LoadAuthorUseCase, + private val saveAuthorUseCase: SaveAuthorUseCase, private val tabScope: CoroutineScope, ) { private val _stageStatus = MutableStateFlow(StageStatus.Loaded(listOf(), listOf(), false)) @@ -60,6 +66,9 @@ class StatusViewModel @Inject constructor( val stagedLazyListState = MutableStateFlow(LazyListState(0, 0)) val unstagedLazyListState = MutableStateFlow(LazyListState(0, 0)) + private val _committerDataRequestState = MutableStateFlow(CommitterDataRequestState.None) + val committerDataRequestState: StateFlow = _committerDataRequestState + /** * Notify the UI that the commit message has been changed by the view model */ @@ -213,7 +222,35 @@ class StatusViewModel @Inject constructor( } else message - doCommitUseCase(git, commitMessage, amend) + val author = loadAuthorUseCase(git) + + val personIdent = if ( + author.name.isNullOrEmpty() && author.globalName.isNullOrEmpty() || + author.email.isNullOrEmpty() && author.globalEmail.isNullOrEmpty() + ) { + _committerDataRequestState.value = CommitterDataRequestState.WaitingInput(author) + + var committerData = _committerDataRequestState.value + + while (committerData is CommitterDataRequestState.WaitingInput) { + committerData = _committerDataRequestState.value + } + + if (committerData is CommitterDataRequestState.Accepted) { + val authorInfo = committerData.authorInfo + + if (committerData.persist) { + saveAuthorUseCase(git, authorInfo) + } + + PersonIdent(authorInfo.globalName, authorInfo.globalEmail) + } else { + null + } + } else + null + + doCommitUseCase(git, commitMessage, amend, personIdent) updateCommitMessage("") _isAmend.value = false } @@ -285,6 +322,14 @@ class StatusViewModel @Inject constructor( persistMessage() _commitMessageChangesFlow.emit(savedCommitMessage.message) } + + fun onRejectCommitterData() { + this._committerDataRequestState.value = CommitterDataRequestState.Reject + } + + fun onAcceptCommitterData(newAuthorInfo: AuthorInfo, persist: Boolean) { + this._committerDataRequestState.value = CommitterDataRequestState.Accepted(newAuthorInfo, persist) + } } sealed class StageStatus { @@ -301,4 +346,11 @@ data class CommitMessage(val message: String, val messageType: MessageType) enum class MessageType { NORMAL, MERGE; -} \ No newline at end of file +} + +sealed interface CommitterDataRequestState { + object None : CommitterDataRequestState + data class WaitingInput(val authorInfo: AuthorInfo) : CommitterDataRequestState + data class Accepted(val authorInfo: AuthorInfo, val persist: Boolean) : CommitterDataRequestState + object Reject : CommitterDataRequestState +} diff --git a/src/main/resources/person.svg b/src/main/resources/person.svg new file mode 100644 index 0000000..3476043 --- /dev/null +++ b/src/main/resources/person.svg @@ -0,0 +1 @@ + \ No newline at end of file