Added conflict indicator during merge in uncommited changes

This commit is contained in:
Abdelilah El Aissaoui 2022-01-02 01:46:23 +01:00
parent 2313ad4591
commit c0c07ef5b1
4 changed files with 98 additions and 43 deletions

View File

@ -1,12 +1,16 @@
package app.git package app.git
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import app.di.RawFileManagerFactory import app.di.RawFileManagerFactory
import app.extensions.filePath import app.extensions.*
import app.extensions.hasUntrackedChanges
import app.extensions.isMerging
import app.extensions.withoutLineEnding
import app.git.diff.Hunk import app.git.diff.Hunk
import app.git.diff.LineType import app.git.diff.LineType
import app.theme.conflictFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -66,7 +70,10 @@ class StatusManager @Inject constructor(
loadHasUncommitedChanges(git) loadHasUncommitedChanges(git)
val currentBranch = branchesManager.currentBranchRef(git) val currentBranch = branchesManager.currentBranchRef(git)
val repositoryState = git.repository.repositoryState val repositoryState = git.repository.repositoryState
val staged = git.diff().apply {
val staged = git
.diff()
.setShowNameAndStatusOnly(true).apply {
if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing) if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing)
setOldTree(EmptyTreeIterator()) // Required if the repository is empty setOldTree(EmptyTreeIterator()) // Required if the repository is empty
@ -74,30 +81,42 @@ class StatusManager @Inject constructor(
} }
.call() .call()
// TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing) // TODO: Grouping and fitlering allows us to remove duplicates when conflicts appear, requires more testing (what happens in windows? /dev/null is a unix thing)
.groupBy { it.oldPath } // TODO: Test if we should group by old path or new path
.groupBy {
if(it.newPath != "/dev/null")
it.newPath
else
it.oldPath
}
.map { .map {
val entries = it.value val entries = it.value
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) val hasConflicts =
entries.filter { entry -> entry.oldPath != "/dev/null" } (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
else
entries StatusEntry(entries.first(), isConflict = hasConflicts)
}.flatten() }
ensureActive() ensureActive()
val unstaged = git val unstaged = git
.diff() .diff()
.setShowNameAndStatusOnly(true)
.call() .call()
.groupBy { it.oldPath } .groupBy {
if(it.oldPath != "/dev/null")
it.oldPath
else
it.newPath
}
.map { .map {
val entries = it.value val entries = it.value
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing)) val hasConflicts =
entries.filter { entry -> entry.newPath != "/dev/null" } (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
else
entries StatusEntry(entries.first(), isConflict = hasConflicts)
}.flatten() }
ensureActive() ensureActive()
_stageStatus.value = StageStatus.Loaded(staged, unstaged) _stageStatus.value = StageStatus.Loaded(staged, unstaged)
@ -301,5 +320,23 @@ class StatusManager @Inject constructor(
sealed class StageStatus { sealed class StageStatus {
object Loading : StageStatus() object Loading : StageStatus()
data class Loaded(val staged: List<DiffEntry>, val unstaged: List<DiffEntry>) : StageStatus() data class Loaded(val staged: List<StatusEntry>, val unstaged: List<StatusEntry>) : StageStatus()
}
data class StatusEntry(val diffEntry: DiffEntry, val isConflict: Boolean) {
val icon: ImageVector
get() {
return if (isConflict)
Icons.Default.Warning
else
diffEntry.icon
}
val iconColor: Color
@Composable
get() {
return if (isConflict)
MaterialTheme.colors.conflictFile
else
diffEntry.iconColor
}
} }

View File

@ -24,6 +24,7 @@ val headerBackgroundDark = Color(0xFF303132)
val addFileLight = Color(0xFF32A852) val addFileLight = Color(0xFF32A852)
val deleteFileLight = errorColor val deleteFileLight = errorColor
val modifyFileLight = primary val modifyFileLight = primary
val conflictFileLight = Color(0xFFFFB638)
val tabColorActiveDark = Color(0xFF606061) val tabColorActiveDark = Color(0xFF606061)
val tabColorInactiveDark = Color(0xFF262626) val tabColorInactiveDark = Color(0xFF262626)

View File

@ -85,6 +85,10 @@ val Colors.deleteFile: Color
val Colors.modifyFile: Color val Colors.modifyFile: Color
get() = modifyFileLight get() = modifyFileLight
@get:Composable
val Colors.conflictFile: Color
get() = conflictFileLight
@get:Composable @get:Composable
val Colors.headerText: Color val Colors.headerText: Color
get() = if (isLight) primary else mainTextDark get() = if (isLight) primary else mainTextDark

View File

@ -30,9 +30,11 @@ import androidx.compose.ui.unit.sp
import app.extensions.filePath import app.extensions.filePath
import app.extensions.icon import app.extensions.icon
import app.extensions.iconColor import app.extensions.iconColor
import app.extensions.isMerging
import app.git.DiffEntryType import app.git.DiffEntryType
import app.git.GitManager import app.git.GitManager
import app.git.StageStatus import app.git.StageStatus
import app.git.StatusEntry
import app.theme.headerBackground import app.theme.headerBackground
import app.theme.headerText import app.theme.headerText
import app.theme.primaryTextColor import app.theme.primaryTextColor
@ -57,8 +59,8 @@ fun UncommitedChanges(
gitManager.loadStatus() gitManager.loadStatus()
} }
val staged: List<DiffEntry> val staged: List<StatusEntry>
val unstaged: List<DiffEntry> val unstaged: List<StatusEntry>
if (stageStatus is StageStatus.Loaded) { if (stageStatus is StageStatus.Loaded) {
staged = stageStatus.staged staged = stageStatus.staged
unstaged = stageStatus.unstaged unstaged = stageStatus.unstaged
@ -74,8 +76,8 @@ fun UncommitedChanges(
} }
} }
} else { } else {
staged = listOf<DiffEntry>() staged = listOf<StatusEntry>()
unstaged = listOf<DiffEntry>() // return empty lists if still loading unstaged = listOf<StatusEntry>() // return empty lists if still loading
} }
@ -178,8 +180,14 @@ fun UncommitedChanges(
enabled = canCommit, enabled = canCommit,
shape = RectangleShape, shape = RectangleShape,
) { ) {
val buttonText = if(repositoryState.isMerging)
"Merge"
else if (repositoryState.isRebasing)
"Continue rebasing"
else
"Commit"
Text( Text(
text = "Commit", text = buttonText,
fontSize = 14.sp, fontSize = 14.sp,
) )
} }
@ -190,8 +198,8 @@ fun UncommitedChanges(
fun checkIfSelectedEntryShouldBeUpdated( fun checkIfSelectedEntryShouldBeUpdated(
selectedEntryType: DiffEntryType, selectedEntryType: DiffEntryType,
staged: List<DiffEntry>, staged: List<StatusEntry>,
unstaged: List<DiffEntry>, unstaged: List<StatusEntry>,
onStagedDiffEntrySelected: (DiffEntry?) -> Unit, onStagedDiffEntrySelected: (DiffEntry?) -> Unit,
onUnstagedDiffEntrySelected: (DiffEntry) -> Unit, onUnstagedDiffEntrySelected: (DiffEntry) -> Unit,
) { ) {
@ -199,26 +207,29 @@ fun checkIfSelectedEntryShouldBeUpdated(
val selectedEntryTypeNewId = selectedDiffEntry.newId.name() val selectedEntryTypeNewId = selectedDiffEntry.newId.name()
if (selectedEntryType is DiffEntryType.StagedDiff) { if (selectedEntryType is DiffEntryType.StagedDiff) {
val entryType = staged.firstOrNull { it.newPath == selectedDiffEntry.newPath } val entryType = staged.firstOrNull { stagedEntry -> stagedEntry.diffEntry.newPath == selectedDiffEntry.newPath }?.diffEntry
if( if(
entryType != null && entryType != null &&
selectedEntryTypeNewId != entryType.newId.name() selectedEntryTypeNewId != entryType.newId.name()
) { ) {
onStagedDiffEntrySelected(entryType) onStagedDiffEntrySelected(entryType)
} else if (entryType == null)
} else if (entryType == null) {
onStagedDiffEntrySelected(null) onStagedDiffEntrySelected(null)
}
} else if(selectedEntryType is DiffEntryType.UnstagedDiff) { } else if(selectedEntryType is DiffEntryType.UnstagedDiff) {
val entryType = unstaged.firstOrNull { val entryType = unstaged.firstOrNull { unstagedEntry ->
if(selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE) if(selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE)
it.oldPath == selectedDiffEntry.oldPath unstagedEntry.diffEntry.oldPath == selectedDiffEntry.oldPath
else else
it.newPath == selectedDiffEntry.newPath unstagedEntry.diffEntry.newPath == selectedDiffEntry.newPath
} }
if(entryType != null) { if(entryType != null) {
onUnstagedDiffEntrySelected(entryType) onUnstagedDiffEntrySelected(entryType.diffEntry)
} else onStagedDiffEntrySelected(null) } else
onStagedDiffEntrySelected(null)
} }
} }
@ -229,7 +240,7 @@ private fun EntriesList(
title: String, title: String,
actionTitle: String, actionTitle: String,
actionColor: Color, actionColor: Color,
diffEntries: List<DiffEntry>, diffEntries: List<StatusEntry>,
onDiffEntrySelected: (DiffEntry) -> Unit, onDiffEntrySelected: (DiffEntry) -> Unit,
onDiffEntryOptionSelected: (DiffEntry) -> Unit, onDiffEntryOptionSelected: (DiffEntry) -> Unit,
onReset: (DiffEntry) -> Unit, onReset: (DiffEntry) -> Unit,
@ -266,9 +277,10 @@ private fun EntriesList(
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
) { ) {
itemsIndexed(diffEntries) { index, diffEntry -> itemsIndexed(diffEntries) { index, statusEntry ->
val diffEntry = statusEntry.diffEntry
FileEntry( FileEntry(
diffEntry = diffEntry, statusEntry = statusEntry,
actionTitle = actionTitle, actionTitle = actionTitle,
actionColor = actionColor, actionColor = actionColor,
onClick = { onClick = {
@ -296,7 +308,7 @@ private fun EntriesList(
) )
@Composable @Composable
private fun FileEntry( private fun FileEntry(
diffEntry: DiffEntry, statusEntry: StatusEntry,
actionTitle: String, actionTitle: String,
actionColor: Color, actionColor: Color,
onClick: () -> Unit, onClick: () -> Unit,
@ -304,6 +316,7 @@ private fun FileEntry(
onReset: () -> Unit, onReset: () -> Unit,
) { ) {
var active by remember { mutableStateOf(false) } var active by remember { mutableStateOf(false) }
val diffEntry = statusEntry.diffEntry
Box( Box(
modifier = Modifier modifier = Modifier
@ -338,12 +351,12 @@ private fun FileEntry(
) { ) {
Icon( Icon(
imageVector = diffEntry.icon, imageVector = statusEntry.icon,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.size(16.dp), .size(16.dp),
tint = diffEntry.iconColor, tint = statusEntry.iconColor,
) )
Text( Text(