Added conflict indicator during merge in uncommited changes
This commit is contained in:
parent
2313ad4591
commit
c0c07ef5b1
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user