Added "File history" feature

This commit is contained in:
Abdelilah El Aissaoui 2022-05-30 01:43:44 +02:00
parent 9c1133a292
commit 969233ec99
12 changed files with 433 additions and 29 deletions

View File

@ -32,6 +32,7 @@ fun CommitChanges(
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
LaunchedEffect(selectedItem) {
commitChangesViewModel.loadChanges(selectedItem.revCommit)
@ -49,7 +50,8 @@ fun CommitChanges(
commit = commitChangesStatus.commit,
changes = commitChangesStatus.changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame
onBlame = onBlame,
onHistory = onHistory,
)
}
}
@ -62,6 +64,7 @@ fun CommitChangesView(
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
Column(
modifier = Modifier
@ -119,6 +122,7 @@ fun CommitChangesView(
diffEntries = changes,
onDiffSelected = onDiffSelected,
onBlame = onBlame,
onHistory = onHistory,
)
}
}
@ -170,7 +174,6 @@ fun Author(commit: RevCommit) {
fontSize = 13.sp,
tooltipTitle = authorIdent.`when`.toSystemDateTimeString()
)
}
Text(
@ -190,6 +193,7 @@ fun CommitLogChanges(
onDiffSelected: (DiffEntry) -> Unit,
diffSelected: DiffEntryType?,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
) {
ScrollableLazyColumn(
modifier = Modifier
@ -200,7 +204,8 @@ fun CommitLogChanges(
items = {
commitedChangesEntriesContextMenuItems(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) }
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
)
}
) {
@ -219,7 +224,6 @@ fun CommitLogChanges(
) {
Spacer(modifier = Modifier.weight(2f))
Row {
Icon(
modifier = Modifier

View File

@ -5,6 +5,7 @@ package app.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
@ -79,12 +80,23 @@ fun Diff(
)
if (diffResult is DiffResult.Text) {
TextDiff(diffEntryType, diffViewModel, diffResult)
val scrollState by diffViewModel.lazyListState.collectAsState()
TextDiff(
diffEntryType = diffEntryType,
scrollState = scrollState,
diffResult = diffResult,
onUnstageHunk = { entry, hunk ->
diffViewModel.unstageHunk(entry, hunk)
},
) { entry, hunk ->
diffViewModel.stageHunk(entry, hunk)
}
} else if (diffResult is DiffResult.NonText) {
NonTextDiff(diffResult)
}
}
ViewDiffResult.Loading -> {
ViewDiffResult.Loading, ViewDiffResult.None -> {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
@ -186,25 +198,29 @@ fun BinaryDiff() {
}
@Composable
fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffResult: DiffResult.Text) {
fun TextDiff(
diffEntryType: DiffEntryType,
scrollState: LazyListState,
diffResult: DiffResult.Text,
onUnstageHunk: (DiffEntry, Hunk) -> Unit,
onStageHunk: (DiffEntry, Hunk) -> Unit,
) {
val hunks = diffResult.hunks
val scrollState by diffViewModel.lazyListState.collectAsState()
SelectionContainer {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxSize(),
state = scrollState
) {
for (hunk in hunks) {
item {
DisableSelection {
HunkHeader(
hunk = hunk,
diffViewModel = diffViewModel,
diffEntryType = diffEntryType,
diffEntry =diffResult.diffEntry,
onUnstageHunk = { onUnstageHunk(diffResult.diffEntry, hunk) },
onStageHunk = { onStageHunk(diffResult.diffEntry, hunk) },
)
}
}
@ -227,8 +243,8 @@ fun TextDiff(diffEntryType: DiffEntryType, diffViewModel: DiffViewModel, diffRes
fun HunkHeader(
hunk: Hunk,
diffEntryType: DiffEntryType,
diffViewModel: DiffViewModel,
diffEntry: DiffEntry,
onUnstageHunk: () -> Unit,
onStageHunk: () -> Unit,
) {
Row(
modifier = Modifier
@ -266,9 +282,9 @@ fun HunkHeader(
backgroundButton = color,
onClick = {
if (diffEntryType is DiffEntryType.StagedDiff) {
diffViewModel.unstageHunk(diffEntry, hunk)
onUnstageHunk()
} else {
diffViewModel.stageHunk(diffEntry, hunk)
onStageHunk()
}
}
)
@ -346,7 +362,10 @@ fun DiffHeader(
}
@Composable
fun DiffLine(highestLineNumberLength: Int, line: Line) {
fun DiffLine(
highestLineNumberLength: Int,
line: Line,
) {
val backgroundColor = when (line.lineType) {
LineType.ADDED -> {
Color(0x77a9d49b)

View File

@ -0,0 +1,222 @@
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalSplitPaneApi::class)
package app.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.handMouseClickable
import app.extensions.toSmartSystemString
import app.extensions.toSystemDateTimeString
import app.git.diff.DiffResult
import app.theme.primaryTextColor
import app.theme.secondaryTextColor
import app.ui.components.AvatarImage
import app.ui.components.ScrollableLazyColumn
import app.ui.components.TooltipText
import app.viewmodels.HistoryState
import app.viewmodels.HistoryViewModel
import app.viewmodels.ViewDiffResult
import org.eclipse.jgit.revwalk.RevCommit
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
@Composable
fun FileHistory(
historyViewModel: HistoryViewModel,
onClose: () -> Unit
) {
val historyState by historyViewModel.historyState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
) {
Header(filePath = historyState.filePath, onClose = onClose)
HistoryContent(
historyViewModel,
historyState,
onCommitSelected = { historyViewModel.selectCommit(it) }
)
}
}
@Composable
private fun Header(
filePath: String,
onClose: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.padding(start = 8.dp, end = 8.dp, top = 8.dp)
.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,
modifier = Modifier
.pointerHoverIcon(PointerIconDefaults.Hand)
) {
Image(
painter = painterResource("close.svg"),
contentDescription = "Close history",
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryTextColor),
)
}
}
}
@Composable
private fun HistoryContent(
historyViewModel: HistoryViewModel,
historyState: HistoryState,
onCommitSelected: (RevCommit) -> Unit,
) {
val textScrollState by historyViewModel.lazyListState.collectAsState()
val viewDiffResult by historyViewModel.viewDiffResult.collectAsState()
when (historyState) {
is HistoryState.Loaded -> HistoryContentLoaded(
historyState = historyState,
viewDiffResult = viewDiffResult,
scrollState = textScrollState,
onCommitSelected = onCommitSelected,
)
is HistoryState.Loading -> Box { }
}
}
@Composable
fun HistoryContentLoaded(
historyState: HistoryState.Loaded,
viewDiffResult: ViewDiffResult?,
scrollState: LazyListState,
onCommitSelected: (RevCommit) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxSize()
) {
ScrollableLazyColumn(
modifier = Modifier
.fillMaxHeight()
.width(300.dp)
.background(MaterialTheme.colors.surface)
) {
items(historyState.commits) { commit ->
HistoryCommit(commit, onCommitSelected = { onCommitSelected(commit) })
}
}
Column(
modifier = Modifier
.fillMaxSize()
) {
if (
viewDiffResult != null &&
viewDiffResult is ViewDiffResult.Loaded
) {
val diffResult = viewDiffResult.diffResult
if (diffResult is DiffResult.Text) {
TextDiff(
diffEntryType = viewDiffResult.diffEntryType,
scrollState = scrollState,
diffResult = diffResult,
onUnstageHunk = { _, _ -> },
onStageHunk = { _, _ -> }
)
} else {
Box(
modifier = Modifier.fillMaxSize()
.background(MaterialTheme.colors.background)
)
}
} else {
Box(
modifier = Modifier.fillMaxSize()
.background(MaterialTheme.colors.background)
)
}
}
}
}
@Composable
fun HistoryCommit(commit: RevCommit, onCommitSelected: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.handMouseClickable { onCommitSelected() }
.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AvatarImage(
modifier = Modifier
.padding(horizontal = 16.dp)
.size(40.dp),
personIdent = commit.authorIdent,
)
Column {
Text(
text = commit.shortMessage,
maxLines = 1,
fontSize = 14.sp,
color = MaterialTheme.colors.primaryTextColor,
)
Row {
Text(
text = commit.name.take(7),
maxLines = 1,
fontSize = 12.sp,
color = MaterialTheme.colors.secondaryTextColor,
)
Spacer(modifier = Modifier.weight(1f))
val date = remember(commit.authorIdent) {
commit.authorIdent.`when`.toSmartSystemString()
}
TooltipText(
text = date,
color = MaterialTheme.colors.secondaryTextColor,
maxLines = 1,
modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 12.sp,
tooltipTitle = date
)
}
}
}
}

View File

@ -1,3 +1,5 @@
@file:OptIn(ExperimentalSplitPaneApi::class)
package app.ui
import androidx.compose.foundation.border
@ -31,6 +33,7 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
val diffSelected by tabViewModel.diffSelected.collectAsState()
val selectedItem by tabViewModel.selectedItem.collectAsState()
val blameState by tabViewModel.blameState.collectAsState()
val showHistory by tabViewModel.showHistory.collectAsState()
var showNewBranchDialog by remember { mutableStateOf(false) }
@ -65,13 +68,12 @@ fun RepositoryOpenPage(tabViewModel: TabViewModel) {
onCreateBranch = { showNewBranchDialog = true }
)
RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState)
RepoContent(tabViewModel, diffSelected, selectedItem, repositoryState, blameState, showHistory)
}
}
}
@OptIn(ExperimentalSplitPaneApi::class)
@Composable
fun RepoContent(
tabViewModel: TabViewModel,
@ -79,6 +81,39 @@ fun RepoContent(
selectedItem: SelectedItem,
repositoryState: RepositoryState,
blameState: BlameState,
showHistory: Boolean,
) {
if(showHistory) {
val historyViewModel = tabViewModel.historyViewModel
if(historyViewModel != null) {
FileHistory(
historyViewModel = historyViewModel,
onClose = {
tabViewModel.closeHistory()
}
)
}
} else {
MainContentView(
tabViewModel,
diffSelected,
selectedItem,
repositoryState,
blameState,
)
}
}
@Composable
fun MainContentView(
tabViewModel: TabViewModel,
diffSelected: DiffEntryType?,
selectedItem: SelectedItem,
repositoryState: RepositoryState,
blameState: BlameState
) {
Row {
HorizontalSplitPane {
@ -187,7 +222,8 @@ fun RepoContent(
else
tabViewModel.newDiffSelected = DiffEntryType.UnsafeUnstagedDiff(diffEntry)
},
onBlameFile = { tabViewModel.blameFile(it) }
onBlameFile = { tabViewModel.blameFile(it) },
onHistoryFile = { tabViewModel.fileHistory(it) }
)
} else if (safeSelectedItem is SelectedItem.CommitBasedItem) {
CommitChanges(
@ -198,7 +234,8 @@ fun RepoContent(
tabViewModel.minimizeBlame()
tabViewModel.newDiffSelected = DiffEntryType.CommitDiff(diffEntry)
},
onBlame = { tabViewModel.blameFile(it) }
onBlame = { tabViewModel.blameFile(it) },
onHistory = { tabViewModel.fileHistory(it) },
)
}
}

View File

@ -50,6 +50,7 @@ fun UncommitedChanges(
onStagedDiffEntrySelected: (StatusEntry?) -> Unit,
onUnstagedDiffEntrySelected: (StatusEntry) -> Unit,
onBlameFile: (String) -> Unit,
onHistoryFile: (String) -> Unit,
) {
val stageStatusState = statusViewModel.stageStatus.collectAsState()
var commitMessage by remember { mutableStateOf(statusViewModel.savedCommitMessage) }
@ -106,6 +107,7 @@ fun UncommitedChanges(
entryType = EntryType.STAGED,
onBlame = { onBlameFile(statusEntry.filePath) },
onReset = { statusViewModel.resetStaged(statusEntry) },
onHistory = { onHistoryFile(statusEntry.filePath) },
)
},
onAllAction = {
@ -132,10 +134,11 @@ fun UncommitedChanges(
statusEntry = statusEntry,
entryType = EntryType.UNSTAGED,
onBlame = { onBlameFile(statusEntry.filePath) },
onHistory = { onHistoryFile(statusEntry.filePath) },
onReset = { statusViewModel.resetUnstaged(statusEntry) },
onDelete = {
statusViewModel.deleteFile(statusEntry)
}
},
)
},
onAllAction = {

View File

@ -9,6 +9,7 @@ import org.eclipse.jgit.diff.DiffEntry
fun commitedChangesEntriesContextMenuItems(
diffEntry: DiffEntry,
onBlame: () -> Unit,
onHistory: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf<ContextMenuItem>().apply {
if (diffEntry.changeType != DiffEntry.ChangeType.ADD ||
@ -19,6 +20,12 @@ fun commitedChangesEntriesContextMenuItems(
onClick = onBlame,
)
)
add(
ContextMenuItem(
label = "File history",
onClick = onHistory,
)
)
}
}
}

View File

@ -12,6 +12,7 @@ fun statusEntriesContextMenuItems(
onReset: () -> Unit,
onDelete: () -> Unit = {},
onBlame: () -> Unit,
onHistory: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf<ContextMenuItem>().apply {
if (statusEntry.statusType != StatusType.ADDED) {
@ -29,6 +30,13 @@ fun statusEntriesContextMenuItems(
onClick = onBlame,
)
)
add(
ContextMenuItem(
label = "File history",
onClick = onHistory,
)
)
}
}

View File

@ -779,7 +779,7 @@ fun CommitMessage(
)
Text(
text = commit.committerIdent.`when`.toSmartSystemString(),
text = commit.authorIdent.`when`.toSmartSystemString(),
modifier = Modifier.padding(horizontal = 16.dp),
fontSize = 12.sp,
color = MaterialTheme.colors.secondaryTextColor,

View File

@ -3,7 +3,6 @@ package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState
import app.exceptions.MissingDiffEntryException
import app.git.*
import app.git.diff.DiffResult
import app.git.diff.Hunk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -87,8 +86,3 @@ class DiffViewModel @Inject constructor(
}
sealed interface ViewDiffResult {
object Loading: ViewDiffResult
object DiffNotFound: ViewDiffResult
data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult): ViewDiffResult
}

View File

@ -0,0 +1,82 @@
package app.viewmodels
import androidx.compose.foundation.lazy.LazyListState
import app.exceptions.MissingDiffEntryException
import app.extensions.filePath
import app.git.DiffEntryType
import app.git.DiffManager
import app.git.RefreshType
import app.git.TabState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class HistoryViewModel @Inject constructor(
private val tabState: TabState,
private val diffManager: DiffManager,
) {
private val _historyState = MutableStateFlow<HistoryState>(HistoryState.Loading(""))
val historyState: StateFlow<HistoryState> = _historyState
private val _viewDiffResult = MutableStateFlow<ViewDiffResult>(ViewDiffResult.None)
val viewDiffResult: StateFlow<ViewDiffResult> = _viewDiffResult
var filePath: String = ""
val lazyListState = MutableStateFlow(
LazyListState(
0,
0
)
)
fun fileHistory(filePath: String) = tabState.safeProcessing(
refreshType = RefreshType.NONE,
) { git ->
this.filePath = filePath
_historyState.value = HistoryState.Loading(filePath)
val log = git.log()
.addPath(filePath)
.call()
.toList()
_historyState.value = HistoryState.Loaded(filePath, log)
}
fun selectCommit(commit: RevCommit) = tabState.runOperation(
refreshType = RefreshType.NONE,
showError = true,
) { git ->
try {
val diffEntries = diffManager.commitDiffEntries(git, commit)
val diffEntry = diffEntries.firstOrNull {entry ->
entry.filePath == this.filePath
}
if(diffEntry == null) {
_viewDiffResult.value = ViewDiffResult.DiffNotFound
return@runOperation
}
val diffEntryType = DiffEntryType.CommitDiff(diffEntry)
val diffFormat = diffManager.diffFormat(git, diffEntryType)
_viewDiffResult.value = ViewDiffResult.Loaded(diffEntryType, diffFormat)
} catch (ex: Exception) {
if(ex is MissingDiffEntryException) {
tabState.refreshData(refreshType = RefreshType.UNCOMMITED_CHANGES)
_viewDiffResult.value = ViewDiffResult.DiffNotFound
} else
ex.printStackTrace()
}
}
}
sealed class HistoryState(val filePath: String) {
class Loading(filePath: String) : HistoryState(filePath)
class Loaded(filePath: String, val commits: List<RevCommit>) : HistoryState(filePath)
}

View File

@ -41,6 +41,7 @@ class TabViewModel @Inject constructor(
val commitChangesViewModel: CommitChangesViewModel,
val cloneViewModel: CloneViewModel,
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
private val historyViewModelProvider: Provider<HistoryViewModel>,
private val repositoryManager: RepositoryManager,
private val tabState: TabState,
val appStateManager: AppStateManager,
@ -79,6 +80,12 @@ class TabViewModel @Inject constructor(
private val _blameState = MutableStateFlow<BlameState>(BlameState.None)
val blameState: StateFlow<BlameState> = _blameState
private val _showHistory = MutableStateFlow(false)
val showHistory: StateFlow<Boolean> = _showHistory
var historyViewModel: HistoryViewModel? = null
private set
val showError = MutableStateFlow(false)
init {
@ -361,6 +368,17 @@ class TabViewModel @Inject constructor(
fun selectCommit(commit: RevCommit) {
tabState.newSelectedItem(SelectedItem.Commit(commit))
}
fun fileHistory(filePath: String) {
historyViewModel = historyViewModelProvider.get()
historyViewModel?.fileHistory(filePath)
_showHistory.value = true
}
fun closeHistory() {
_showHistory.value = false
historyViewModel = null
}
}
@ -370,7 +388,6 @@ sealed class RepositorySelectionStatus {
data class Open(val repository: Repository) : RepositorySelectionStatus()
}
sealed interface BlameState {
data class Loading(val filePath: String) : BlameState

View File

@ -0,0 +1,11 @@
package app.viewmodels
import app.git.DiffEntryType
import app.git.diff.DiffResult
sealed interface ViewDiffResult {
object None: ViewDiffResult
object Loading: ViewDiffResult
object DiffNotFound: ViewDiffResult
data class Loaded(val diffEntryType: DiffEntryType, val diffResult: DiffResult): ViewDiffResult
}