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
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.extensions.filePath
import app.extensions.hasUntrackedChanges
import app.extensions.isMerging
import app.extensions.withoutLineEnding
import app.extensions.*
import app.git.diff.Hunk
import app.git.diff.LineType
import app.theme.conflictFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
@ -66,38 +70,53 @@ class StatusManager @Inject constructor(
loadHasUncommitedChanges(git)
val currentBranch = branchesManager.currentBranchRef(git)
val repositoryState = git.repository.repositoryState
val staged = git.diff().apply {
if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing)
setOldTree(EmptyTreeIterator()) // Required if the repository is empty
setCached(true)
}
val staged = git
.diff()
.setShowNameAndStatusOnly(true).apply {
if (currentBranch == null && !repositoryState.isMerging && !repositoryState.isRebasing)
setOldTree(EmptyTreeIterator()) // Required if the repository is empty
setCached(true)
}
.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)
.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 {
val entries = it.value
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
entries.filter { entry -> entry.oldPath != "/dev/null" }
else
entries
}.flatten()
val hasConflicts =
(entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
StatusEntry(entries.first(), isConflict = hasConflicts)
}
ensureActive()
val unstaged = git
.diff()
.setShowNameAndStatusOnly(true)
.call()
.groupBy { it.oldPath }
.groupBy {
if(it.oldPath != "/dev/null")
it.oldPath
else
it.newPath
}
.map {
val entries = it.value
if (entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
entries.filter { entry -> entry.newPath != "/dev/null" }
else
entries
}.flatten()
val hasConflicts =
(entries.count() > 1 && (repositoryState.isMerging || repositoryState.isRebasing))
StatusEntry(entries.first(), isConflict = hasConflicts)
}
ensureActive()
_stageStatus.value = StageStatus.Loaded(staged, unstaged)
@ -209,7 +228,7 @@ class StatusManager @Inject constructor(
loadStatus(git)
} finally {
if(completedWithErrors)
if (completedWithErrors)
dirCache.unlock()
}
}
@ -301,5 +320,23 @@ class StatusManager @Inject constructor(
sealed class 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 deleteFileLight = errorColor
val modifyFileLight = primary
val conflictFileLight = Color(0xFFFFB638)
val tabColorActiveDark = Color(0xFF606061)
val tabColorInactiveDark = Color(0xFF262626)

View File

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