Added author info request when doing a new commit if the info is not set previously

This commit is contained in:
Abdelilah El Aissaoui 2023-03-13 11:36:56 +01:00
parent 96599b045c
commit 4388ccb690
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
6 changed files with 365 additions and 67 deletions

View File

@ -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"

View File

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

View File

@ -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<StatusEntry>
@ -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))
}
}
}

View File

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

View File

@ -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>(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>(CommitterDataRequestState.None)
val committerDataRequestState: StateFlow<CommitterDataRequestState> = _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;
}
}
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
}

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 0h24v24H0V0z" fill="none"/><path d="M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>

After

Width:  |  Height:  |  Size: 360 B