Added basic version of blame

This commit is contained in:
Abdelilah El Aissaoui 2022-05-26 23:35:32 +02:00
parent 543545d93d
commit 8e366741ac
8 changed files with 297 additions and 85 deletions

View File

@ -33,7 +33,7 @@ class TabState @Inject constructor(
return git
}
val mutex = Mutex()
private val mutex = Mutex()
private val _refreshData = MutableSharedFlow<RefreshType>()
val refreshData: Flow<RefreshType> = _refreshData

View File

@ -0,0 +1,106 @@
@file:Suppress("UNUSED_PARAMETER")
package app.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.lineAt
import app.theme.primaryTextColor
import app.ui.components.ScrollableLazyColumn
import org.eclipse.jgit.blame.BlameResult
@Composable
fun Blame(
filePath: String,
blameResult: BlameResult,
onClose: () -> Unit,
) {
Column {
Header(filePath, onClose = onClose)
ScrollableLazyColumn(
modifier = Modifier.fillMaxSize()
) {
val contents = blameResult.resultContents
items(contents.size()) { index ->
val line = contents.lineAt(index)
val author = blameResult.getSourceAuthor(index)
val commit = blameResult.getSourceCommit(index)
Row(
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.background)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.width(200.dp).fillMaxHeight().background(MaterialTheme.colors.surface),
verticalArrangement = Arrangement.Center,
) {
Text(
text = author?.name.orEmpty(),
color = MaterialTheme.colors.primaryTextColor,
maxLines = 1,
modifier = Modifier.padding(start = 16.dp),
fontSize = 12.sp,
)
Text(
text = commit.shortMessage,
color = MaterialTheme.colors.primaryTextColor,
maxLines = 1,
modifier = Modifier.padding(start = 16.dp),
fontSize = 10.sp,
)
}
Text(
text = line,
color = MaterialTheme.colors.primaryTextColor,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
fontFamily = FontFamily.Monospace,
)
}
}
}
}
}
@Composable
private fun Header(
filePath: String,
onClose: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth().background(MaterialTheme.colors.surface),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = filePath,
color = MaterialTheme.colors.primaryTextColor,
fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = onClose
) {
Image(
painter = painterResource("close.svg"),
contentDescription = "Close diff",
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryTextColor),
)
}
}
}

View File

@ -1,13 +1,10 @@
package app.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -24,6 +21,7 @@ import app.theme.*
import app.ui.components.AvatarImage
import app.ui.components.ScrollableLazyColumn
import app.ui.components.TooltipText
import app.ui.context_menu.commitedChangesEntriesContextMenuItems
import app.viewmodels.CommitChangesStatus
import app.viewmodels.CommitChangesViewModel
import org.eclipse.jgit.diff.DiffEntry
@ -34,7 +32,8 @@ fun CommitChanges(
commitChangesViewModel: CommitChangesViewModel,
selectedItem: SelectedItem.CommitBasedItem,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
) {
LaunchedEffect(selectedItem) {
commitChangesViewModel.loadChanges(selectedItem.revCommit)
@ -52,6 +51,7 @@ fun CommitChanges(
commit = commitChangesStatus.commit,
changes = commitChangesStatus.changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame
)
}
}
@ -62,7 +62,8 @@ fun CommitChangesView(
commit: RevCommit,
changes: List<DiffEntry>,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
) {
Column(
modifier = Modifier
@ -118,7 +119,8 @@ fun CommitChangesView(
CommitLogChanges(
diffSelected = diffSelected,
diffEntries = changes,
onDiffSelected = onDiffSelected
onDiffSelected = onDiffSelected,
onBlame = onBlame,
)
}
}
@ -183,17 +185,27 @@ fun Author(commit: RevCommit) {
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CommitLogChanges(
diffEntries: List<DiffEntry>,
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
) {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize()
) {
items(items = diffEntries) { diffEntry ->
ContextMenuArea(
items = {
commitedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) }
)
}
) {
Column(
modifier = Modifier
.height(40.dp)
@ -220,7 +232,7 @@ fun CommitLogChanges(
tint = diffEntry.iconColor,
)
if(diffEntry.parentDirectoryPath.isNotEmpty()) {
if (diffEntry.parentDirectoryPath.isNotEmpty()) {
Text(
text = diffEntry.parentDirectoryPath,
modifier = Modifier.weight(1f, fill = false),
@ -247,4 +259,5 @@ fun CommitLogChanges(
}
}
}
}
}

View File

@ -14,6 +14,7 @@ import app.theme.primaryTextColor
import app.ui.dialogs.NewBranchDialog
import app.ui.dialogs.RebaseInteractive
import app.ui.log.Log
import app.viewmodels.BlameState
import app.viewmodels.TabViewModel
import openRepositoryDialog
import org.eclipse.jgit.lib.RepositoryState
@ -29,6 +30,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
val repositoryState by tabViewModel.repositoryState.collectAsState()
val diffSelected by tabViewModel.diffSelected.collectAsState()
val selectedItem by tabViewModel.selectedItem.collectAsState()
val blameState by tabViewModel.blameState.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) }
@ -63,7 +65,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
onCreateBranch = { showNewBranchDialog = true }
)
RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState)
RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState)
}
}
@ -75,7 +77,8 @@ fun RepoContent(
tabViewModel: TabViewModel,
diffSelected: DiffEntryType?,
selectedItem: SelectedItem,
repositoryState: RepositoryState
repositoryState: RepositoryState,
blameState: BlameState,
) {
Row {
HorizontalSplitPane {
@ -115,6 +118,13 @@ fun RepoContent(
shape = RoundedCornerShape(4.dp)
)
) {
if (blameState is BlameState.Loaded) {
Blame(
filePath = blameState.filePath,
blameResult = blameState.blameResult,
onClose = { tabViewModel.resetBlameState() }
)
} else {
when (diffSelected) {
null -> {
Log(
@ -131,6 +141,7 @@ fun RepoContent(
}
}
}
}
second(minSize = 300.dp) {
Box(
@ -144,6 +155,11 @@ fun RepoContent(
selectedEntryType = diffSelected,
repositoryState = repositoryState,
onStagedDiffEntrySelected = { diffEntry ->
// TODO: Instead of resetting the state, create a new one where the blame
// is "on hold". In this state we can show a bar at the bottom so the user
// can click on it and return to the blame
tabViewModel.resetBlameState()
tabViewModel.newDiffSelected = if (diffEntry != null) {
if (repositoryState == RepositoryState.SAFE)
DiffEntryType.SafeStagedDiff(diffEntry)
@ -154,11 +170,14 @@ fun RepoContent(
}
},
onUnstagedDiffEntrySelected = { diffEntry ->
tabViewModel.resetBlameState()
if (repositoryState == RepositoryState.SAFE)
tabViewModel.newDiffSelected = DiffEntryType.SafeUnstagedDiff(diffEntry)
else
tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry)
}
},
onBlameFile = { tabViewModel.blameFile(it) }
)
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
CommitChanges(
@ -166,8 +185,10 @@ fun RepoContent(
selectedItem = safeSelectedItem,
diffSelected = diffSelected,
onDiffSelected = { diffEntry ->
tabViewModel.resetBlameState()
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
}
},
onBlame = { tabViewModel.blameFile(it) }
)
}
}

View File

@ -40,14 +40,10 @@ 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.ui.context_menu.*
import app.viewmodels.StageStatus
import app.viewmodels.StatusViewModel
import org.eclipse.jgit.lib.RepositoryState
import kotlin.reflect.KClass
@Composable
fun UncommitedChanges(
@ -56,6 +52,7 @@ fun UncommitedChanges(
repositoryState: RepositoryState,
onStagedDiffEntrySelected: (StatusEntry?) -> Unit,
onUnstagedDiffEntrySelected: (StatusEntry) -> Unit,
onBlameFile: (String) -> Unit,
) {
val stageStatusState = statusViewModel.stageStatus.collectAsState()
var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) }
@ -106,12 +103,12 @@ fun UncommitedChanges(
onDiffEntryOptionSelected = {
statusViewModel.unstage(it)
},
onGenerateContextMenu = { diffEntry ->
stagedEntriesContextMenuItems(
diffEntry = diffEntry,
onReset = {
statusViewModel.resetStaged(diffEntry)
},
onGenerateContextMenu = { statusEntry ->
statusEntriesContextMenuItems(
statusEntry = statusEntry,
entryType = EntryType.STAGED,
onBlame = { onBlameFile(statusEntry.filePath) },
onReset = { statusViewModel.resetStaged(statusEntry) },
)
},
onAllAction = {
@ -134,11 +131,11 @@ fun UncommitedChanges(
statusViewModel.stage(it)
},
onGenerateContextMenu = { statusEntry ->
unstagedEntriesContextMenuItems(
statusEntriesContextMenuItems(
statusEntry = statusEntry,
onReset = {
statusViewModel.resetUnstaged(statusEntry)
},
entryType = EntryType.UNSTAGED,
onBlame = { onBlameFile(statusEntry.filePath) },
onReset = { statusViewModel.resetUnstaged(statusEntry) },
onDelete = {
statusViewModel.deleteFile(statusEntry)
}

View File

@ -0,0 +1,24 @@
package app.ui.context_menu
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import app.git.StatusType
import org.eclipse.jgit.diff.DiffEntry
@OptIn(ExperimentalFoundationApi::class)
fun commitedChangesEntriesContextMenuItems(
diffEntry: DiffEntry,
onBlame: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf<ContextMenuItem>().apply {
if (diffEntry.changeType != DiffEntry.ChangeType.ADD ||
diffEntry.changeType != DiffEntry.ChangeType.DELETE) {
add(
ContextMenuItem(
label = "Blame file",
onClick = onBlame,
)
)
}
}
}

View File

@ -4,13 +4,14 @@ import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import app.git.StatusEntry
import app.git.StatusType
import org.eclipse.jgit.diff.DiffEntry
@OptIn(ExperimentalFoundationApi::class)
fun unstagedEntriesContextMenuItems(
fun statusEntriesContextMenuItems(
statusEntry: StatusEntry,
entryType: EntryType,
onReset: () -> Unit,
onDelete: () -> Unit,
onDelete: () -> Unit = {},
onBlame: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf<ContextMenuItem>().apply {
if (statusEntry.statusType != StatusType.ADDED) {
@ -20,9 +21,21 @@ fun unstagedEntriesContextMenuItems(
onClick = onReset,
)
)
}
if (statusEntry.statusType != StatusType.REMOVED) {
add(
ContextMenuItem(
label = "Blame file",
onClick = onBlame,
)
)
}
}
if (
entryType == EntryType.UNSTAGED &&
statusEntry.statusType != StatusType.REMOVED
) {
add(
ContextMenuItem(
label = "Delete file",
@ -32,3 +45,9 @@ fun unstagedEntriesContextMenuItems(
}
}
}
enum class EntryType {
STAGED,
UNSTAGED,
}

View File

@ -1,6 +1,5 @@
package app.viewmodels
import app.AppPreferences
import app.AppStateManager
import app.ErrorsManager
import app.credentials.CredentialsState
@ -15,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.blame.BlameResult
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.RepositoryState
import java.io.File
@ -75,6 +75,9 @@ class TabViewModel @Inject constructor(
private val _repositoryState = MutableStateFlow(RepositoryState.SAFE)
val repositoryState: StateFlow<RepositoryState> = _repositoryState
private val _blameState = MutableStateFlow<BlameState>(BlameState.None)
val blameState: StateFlow<BlameState> = _blameState
val showError = MutableStateFlow(false)
init {
@ -315,6 +318,28 @@ class TabViewModel @Inject constructor(
null
}
}
fun blameFile(filePath: String) = tabState.safeProcessing(
refreshType = RefreshType.NONE,
) { git ->
_blameState.value = BlameState.Loading(filePath)
try {
val result = git.blame()
.setFilePath(filePath)
.setFollowFileRenames(true)
.call()
_blameState.value = BlameState.Loaded(filePath, result)
} catch (ex: Exception) {
resetBlameState()
throw ex
}
}
fun resetBlameState() {
_blameState.value = BlameState.None
}
}
@ -323,3 +348,10 @@ sealed class RepositorySelectionStatus {
data class Opening(val path: String) : RepositorySelectionStatus()
data class Open(val repository: Repository) : RepositorySelectionStatus()
}
sealed interface BlameState {
data class Loading(val filePath: String) : BlameState
data class Loaded(val filePath: String, val blameResult: BlameResult) : BlameState
object None : BlameState
}