Implemented merge & rebase

This commit is contained in:
Abdelilah El Aissaoui 2022-01-31 01:30:25 +01:00
parent ca1eeb2d11
commit 42fec7c591
13 changed files with 388 additions and 75 deletions

View File

@ -3,13 +3,8 @@ package app.git
import app.extensions.isBranch
import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.CreateBranchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.api.MergeCommand
import org.eclipse.jgit.api.*
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
@ -50,19 +45,6 @@ class BranchesManager @Inject constructor() {
.call()
}
suspend fun mergeBranch(git: Git, branch: Ref, fastForward: Boolean) = withContext(Dispatchers.IO) {
val fastForwardMode = if (fastForward)
MergeCommand.FastForwardMode.FF
else
MergeCommand.FastForwardMode.NO_FF
git
.merge()
.include(branch)
.setFastForward(fastForwardMode)
.call()
}
suspend fun deleteBranch(git: Git, branch: Ref) = withContext(Dispatchers.IO) {
git
.branchDelete()

View File

@ -25,7 +25,7 @@ class LogManager @Inject constructor(
suspend fun loadLog(git: Git, currentBranch: Ref?) = withContext(Dispatchers.IO) {
val commitList = GraphCommitList()
val repositoryState = git.repository.repositoryState
println("Repository state ${repositoryState.description}")
if(currentBranch != null || repositoryState.isRebasing) { // Current branch is null when there is no log (new repo) or rebasing
val logList = git.log().setMaxCount(2).call().toList()

View File

@ -0,0 +1,31 @@
package app.git
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.MergeCommand
import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class MergeManager @Inject constructor() {
suspend fun mergeBranch(git: Git, branch: Ref, fastForward: Boolean) = withContext(Dispatchers.IO) {
val fastForwardMode = if (fastForward)
MergeCommand.FastForwardMode.FF
else
MergeCommand.FastForwardMode.NO_FF
git
.merge()
.include(branch)
.setFastForward(fastForwardMode)
.call()
}
suspend fun abortBranch(git: Git) = withContext(Dispatchers.IO) {
git.repository.writeMergeCommitMsg(null);
git.repository.writeMergeHeads(null);
git.reset().setMode(ResetCommand.ResetType.HARD).call();
}
}

View File

@ -0,0 +1,36 @@
package app.git
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class RebaseManager @Inject constructor() {
suspend fun rebaseBranch(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
git.rebase()
.setOperation(RebaseCommand.Operation.BEGIN)
.setUpstream(ref.objectId)
.call()
}
suspend fun continueRebase(git: Git) = withContext(Dispatchers.IO) {
git.rebase()
.setOperation(RebaseCommand.Operation.CONTINUE)
.call()
}
suspend fun abortRebase(git: Git) = withContext(Dispatchers.IO) {
git.rebase()
.setOperation(RebaseCommand.Operation.ABORT)
.call()
}
suspend fun skipRebase(git: Git) = withContext(Dispatchers.IO) {
git.rebase()
.setOperation(RebaseCommand.Operation.SKIP)
.call()
}
}

View File

@ -19,6 +19,7 @@ import app.ui.components.SideMenuSubentry
import app.ui.components.entryHeight
import app.ui.context_menu.branchContextMenuItems
import app.ui.dialogs.MergeDialog
import app.ui.dialogs.RebaseDialog
import app.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref
@ -30,6 +31,7 @@ fun Branches(
val branches by branchesViewModel.branches.collectAsState()
val currentBranch by branchesViewModel.currentBranch.collectAsState()
val (mergeBranch, setMergeBranch) = remember { mutableStateOf<Ref?>(null) }
val (rebaseBranch, setRebaseBranch) = remember { mutableStateOf<Ref?>(null) }
Column {
SideMenuEntry("Local branches")
@ -43,13 +45,14 @@ fun Branches(
Box(modifier = Modifier.heightIn(max = maxHeight.dp)) {
ScrollableLazyColumn(modifier = Modifier.fillMaxWidth()) {
itemsIndexed(branches) { _, branch ->
BranchRow(
BranchLineEntry(
branch = branch,
isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { onBranchClicked(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(branch) },
onDeleteBranch = { branchesViewModel.deleteBranch(branch) },
onRebaseBranch = { branchesViewModel.deleteBranch(branch) },
onDeleteBranch = { setRebaseBranch(branch) },
)
}
}
@ -64,16 +67,26 @@ fun Branches(
onAccept = { ff -> branchesViewModel.mergeBranch(mergeBranch, ff) }
)
}
if (rebaseBranch != null) {
RebaseDialog(
currentBranch,
rebaseBranchName = rebaseBranch.name,
onReject = { setRebaseBranch(null) },
onAccept = { branchesViewModel.rebaseBranch(rebaseBranch) }
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun BranchRow(
private fun BranchLineEntry(
branch: Ref,
isCurrentBranch: Boolean,
onBranchClicked: () -> Unit,
onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit,
) {
ContextMenuArea(
@ -84,6 +97,7 @@ private fun BranchRow(
onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch,
onRebaseBranch = onRebaseBranch,
)
}
) {

View File

@ -22,7 +22,6 @@ import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import app.extensions.filePath
@ -58,7 +57,7 @@ fun UncommitedChanges(
staged = stageStatus.staged
unstaged = stageStatus.unstaged
LaunchedEffect(staged) {
if(selectedEntryType != null) {
if (selectedEntryType != null) {
checkIfSelectedEntryShouldBeUpdated(
selectedEntryType = selectedEntryType,
staged = staged,
@ -69,8 +68,8 @@ fun UncommitedChanges(
}
}
} else {
staged = listOf<StatusEntry>()
unstaged = listOf<StatusEntry>() // return empty lists if still loading
staged = listOf()
unstaged = listOf() // return empty lists if still loading
}
val doCommit = {
@ -134,16 +133,20 @@ fun UncommitedChanges(
allActionTitle = "Stage all"
)
Card(
modifier = Modifier
.padding(8.dp)
.height(192.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
.run {
// When rebasing, we don't need a fixed size as we don't show the message TextField
if(!repositoryState.isRebasing) {
height(192.dp)
} else
this
}
.fillMaxWidth()
) {
// Don't show the message TextField when rebasing as it can't be edited
if (!repositoryState.isRebasing)
TextField(
modifier = Modifier
.fillMaxWidth()
@ -159,32 +162,131 @@ fun UncommitedChanges(
onValueChange = { statusViewModel.newCommitMessage = it },
label = { Text("Write your commit message here", fontSize = 14.sp) },
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background),
textStyle = TextStyle.Default.copy(fontSize = 14.sp),
textStyle = TextStyle.Default.copy(fontSize = 14.sp, color = MaterialTheme.colors.primaryTextColor),
)
when {
repositoryState.isMerging -> MergeButtons(
haveConflictsBeenSolved = unstaged.isEmpty(),
onAbort = { statusViewModel.abortMerge() },
onMerge = { doCommit() }
)
repositoryState.isRebasing -> RebasingButtons(
canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(),
haveConflictsBeenSolved = unstaged.isEmpty(),
onAbort = { statusViewModel.abortRebase() },
onContinue = { statusViewModel.continueRebase() },
onSkip = { statusViewModel.skipRebase() },
)
else -> {
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = {
doCommit()
},
onClick = doCommit,
enabled = canCommit,
shape = RectangleShape,
) {
val buttonText = if(repositoryState.isMerging)
"Merge"
else if (repositoryState.isRebasing)
"Continue rebasing"
else
"Commit"
Text(
text = buttonText,
text = "Commit",
fontSize = 14.sp,
)
}
}
}
}
}
}
@Composable
fun MergeButtons(
haveConflictsBeenSolved: Boolean,
onAbort: () -> Unit,
onMerge: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = onAbort,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 4.dp),
) {
Text(
text = "Abort",
fontSize = 14.sp,
)
}
Button(
onClick = onMerge,
enabled = haveConflictsBeenSolved,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 4.dp),
) {
Text(
text = "Merge",
fontSize = 14.sp,
)
}
}
}
@Composable
fun RebasingButtons(
canContinue: Boolean,
haveConflictsBeenSolved: Boolean,
onAbort: () -> Unit,
onContinue: () -> Unit,
onSkip: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = onAbort,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 4.dp),
) {
Text(
text = "Abort",
fontSize = 14.sp,
)
}
if (canContinue) {
Button(
onClick = onContinue,
enabled = haveConflictsBeenSolved,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 4.dp),
) {
Text(
text = "Continue",
fontSize = 14.sp,
)
}
} else {
Button(
onClick = onSkip,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 4.dp),
) {
Text(
text = "Skip",
fontSize = 14.sp,
)
}
}
}
}
// TODO: This logic should be part of the diffViewModel where it gets the latest version of the diffEntry
@ -199,9 +301,10 @@ fun checkIfSelectedEntryShouldBeUpdated(
val selectedEntryTypeNewId = selectedDiffEntry.newId.name()
if (selectedEntryType is DiffEntryType.StagedDiff) {
val entryType = staged.firstOrNull { stagedEntry -> stagedEntry.diffEntry.newPath == selectedDiffEntry.newPath }?.diffEntry
val entryType =
staged.firstOrNull { stagedEntry -> stagedEntry.diffEntry.newPath == selectedDiffEntry.newPath }?.diffEntry
if(
if (
entryType != null &&
selectedEntryTypeNewId != entryType.newId.name()
) {
@ -210,15 +313,15 @@ fun checkIfSelectedEntryShouldBeUpdated(
} else if (entryType == null) {
onStagedDiffEntrySelected(null)
}
} else if(selectedEntryType is DiffEntryType.UnstagedDiff) {
} else if (selectedEntryType is DiffEntryType.UnstagedDiff) {
val entryType = unstaged.firstOrNull { unstagedEntry ->
if(selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE)
if (selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE)
unstagedEntry.diffEntry.oldPath == selectedDiffEntry.oldPath
else
unstagedEntry.diffEntry.newPath == selectedDiffEntry.newPath
}
if(entryType != null) {
if (entryType != null) {
onUnstagedDiffEntrySelected(entryType.diffEntry)
} else
onStagedDiffEntrySelected(null)

View File

@ -9,22 +9,29 @@ fun branchContextMenuItems(
isLocal: Boolean,
onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit,
): List<ContextMenuItem> {
return mutableListOf(
return mutableListOf<ContextMenuItem>().apply {
if (!isCurrentBranch) {
add(
ContextMenuItem(
label = "Checkout branch",
onClick = onCheckoutBranch
),
).apply {
if (!isCurrentBranch) {
)
)
add(
ContextMenuItem(
label = "Merge branch",
onClick = onMergeBranch
)
)
add(
ContextMenuItem(
label = "Rebase branch",
onClick = onRebaseBranch
)
)
}
if (isLocal && !isCurrentBranch) {
add(

View File

@ -0,0 +1,83 @@
package app.ui.dialogs
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import app.theme.primaryTextColor
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RebaseDialog(
currentBranchName: String,
rebaseBranchName: String,
onReject: () -> Unit,
onAccept: () -> Unit
) {
MaterialDialog {
Column(
modifier = Modifier
.background(MaterialTheme.colors.background),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = currentBranchName,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primaryTextColor,
)
Text(
text = "will rebase ",
modifier = Modifier.padding(horizontal = 8.dp),
color = MaterialTheme.colors.primaryTextColor,
)
Text(
text = rebaseBranchName,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.primaryTextColor,
)
}
Text(
text = "After completing the operation, $currentBranchName will contain $rebaseBranchName changes",
color = MaterialTheme.colors.primaryTextColor,
)
Row(
modifier = Modifier
.padding(top = 16.dp)
.align(Alignment.End)
) {
TextButton(
modifier = Modifier.padding(end = 8.dp),
onClick = {
onReject()
}
) {
Text("Cancel")
}
Button(
onClick = {
onAccept()
}
) {
Text("Rebase")
}
}
}
}
}

View File

@ -39,10 +39,7 @@ import app.ui.components.AvatarImage
import app.ui.components.ScrollableLazyColumn
import app.ui.context_menu.branchContextMenuItems
import app.ui.context_menu.tagContextMenuItems
import app.ui.dialogs.MergeDialog
import app.ui.dialogs.NewBranchDialog
import app.ui.dialogs.NewTagDialog
import app.ui.dialogs.ResetBranchDialog
import app.ui.dialogs.*
import app.viewmodels.LogStatus
import app.viewmodels.LogViewModel
import org.eclipse.jgit.lib.Ref
@ -156,6 +153,7 @@ fun Log(
showCreateNewTag = { showLogDialog.value = LogDialog.NewTag(graphNode) },
resetBranch = { showLogDialog.value = LogDialog.ResetBranch(graphNode) },
onMergeBranch = { ref -> showLogDialog.value = LogDialog.MergeBranch(ref) },
onRebaseBranch = { ref -> showLogDialog.value = LogDialog.RebaseBranch(ref) },
onRevCommitSelected = {
onItemSelected(SelectedItem.Commit(graphNode))
}
@ -212,6 +210,19 @@ fun LogDialogs(
onResetShowLogDialog()
}
)
is LogDialog.RebaseBranch -> {
if(currentBranch != null) {
RebaseDialog(
currentBranchName = currentBranch.simpleName,
rebaseBranchName = showLogDialog.ref.simpleName,
onReject = onResetShowLogDialog,
onAccept = {
logViewModel.rebaseBranch(showLogDialog.ref)
onResetShowLogDialog()
}
)
}
}
LogDialog.None -> {
}
}
@ -334,6 +345,7 @@ fun CommitLine(
showCreateNewTag: () -> Unit,
resetBranch: (GraphNode) -> Unit,
onMergeBranch: (Ref) -> Unit,
onRebaseBranch: (Ref) -> Unit,
onRevCommitSelected: (GraphNode) -> Unit,
) {
val commitRefs = graphNode.refs
@ -405,6 +417,7 @@ fun CommitLine(
onMergeBranch = { ref -> onMergeBranch(ref) },
onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) },
onDeleteTag = { ref -> logViewModel.deleteTag(ref) },
onRebaseBranch = { ref -> onRebaseBranch(ref) },
)
}
}
@ -422,6 +435,7 @@ fun CommitMessage(
onCheckoutRef: (ref: Ref) -> Unit,
onMergeBranch: (ref: Ref) -> Unit,
onDeleteBranch: (ref: Ref) -> Unit,
onRebaseBranch: (ref: Ref) -> Unit,
onDeleteTag: (ref: Ref) -> Unit,
) {
val textColor = if (selected) {
@ -467,6 +481,7 @@ fun CommitMessage(
onCheckoutBranch = { onCheckoutRef(ref) },
onMergeBranch = { onMergeBranch(ref) },
onDeleteBranch = { onDeleteBranch(ref) },
onRebaseBranch = { onRebaseBranch(ref) },
)
}
}
@ -644,6 +659,7 @@ fun BranchChip(
onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit,
onDeleteBranch: () -> Unit,
onRebaseBranch: () -> Unit,
color: Color,
) {
val contextMenuItemsList = {
@ -653,6 +669,7 @@ fun BranchChip(
onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch,
onRebaseBranch = onRebaseBranch,
)
}

View File

@ -9,4 +9,5 @@ sealed class LogDialog {
data class NewTag(val graphNode: GraphNode) : LogDialog()
data class ResetBranch(val graphNode: GraphNode) : LogDialog()
data class MergeBranch(val ref: Ref) : LogDialog()
data class RebaseBranch(val ref: Ref) : LogDialog()
}

View File

@ -1,8 +1,6 @@
package app.viewmodels
import app.git.BranchesManager
import app.git.RefreshType
import app.git.TabState
import app.git.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.eclipse.jgit.api.Git
@ -12,6 +10,8 @@ import javax.inject.Inject
class BranchesViewModel @Inject constructor(
private val branchesManager: BranchesManager,
private val rebaseManager: RebaseManager,
private val mergeManager: MergeManager,
private val tabState: TabState,
) {
private val _branches = MutableStateFlow<List<Ref>>(listOf())
@ -37,7 +37,7 @@ class BranchesViewModel @Inject constructor(
}
fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git ->
branchesManager.mergeBranch(git, ref, fastForward)
mergeManager.mergeBranch(git, ref, fastForward)
return@safeProcessing RefreshType.ALL_DATA
}
@ -57,4 +57,10 @@ class BranchesViewModel @Inject constructor(
suspend fun refresh(git: Git) {
loadBranches(git)
}
fun rebaseBranch(ref: Ref) = tabState.safeProcessing { git ->
rebaseManager.rebaseBranch(git, ref)
return@safeProcessing RefreshType.ALL_DATA
}
}

View File

@ -4,7 +4,6 @@ import app.git.*
import app.git.graph.GraphCommitList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
@ -14,7 +13,9 @@ class LogViewModel @Inject constructor(
private val logManager: LogManager,
private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
private val rebaseManager: RebaseManager,
private val tagsManager: TagsManager,
private val mergeManager: MergeManager,
private val tabState: TabState,
) {
private val _logStatus = MutableStateFlow<LogStatus>(LogStatus.Loading)
@ -69,7 +70,7 @@ class LogViewModel @Inject constructor(
}
fun mergeBranch(ref: Ref, fastForward: Boolean) = tabState.safeProcessing { git ->
branchesManager.mergeBranch(git, ref, fastForward)
mergeManager.mergeBranch(git, ref, fastForward)
return@safeProcessing RefreshType.ALL_DATA
}
@ -89,6 +90,12 @@ class LogViewModel @Inject constructor(
suspend fun refresh(git: Git) {
loadLog(git)
}
fun rebaseBranch(ref: Ref) = tabState.safeProcessing { git ->
rebaseManager.rebaseBranch(git, ref)
return@safeProcessing RefreshType.ALL_DATA
}
}
sealed class LogStatus {

View File

@ -14,6 +14,8 @@ class StatusViewModel @Inject constructor(
private val statusManager: StatusManager,
private val branchesManager: BranchesManager,
private val repositoryManager: RepositoryManager,
private val rebaseManager: RebaseManager,
private val mergeManager: MergeManager,
) {
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus
@ -112,6 +114,30 @@ class StatusViewModel @Inject constructor(
// Return true to update the log only if the uncommitedChanges status has changed
return (hasNowUncommitedChanges != hadUncommitedChanges)
}
fun continueRebase() = tabState.safeProcessing { git ->
rebaseManager.continueRebase(git)
return@safeProcessing RefreshType.ALL_DATA
}
fun abortRebase() = tabState.safeProcessing { git ->
rebaseManager.abortRebase(git)
return@safeProcessing RefreshType.ALL_DATA
}
fun skipRebase() = tabState.safeProcessing { git ->
rebaseManager.skipRebase(git)
return@safeProcessing RefreshType.ALL_DATA
}
fun abortMerge() = tabState.safeProcessing { git ->
mergeManager.abortBranch(git)
return@safeProcessing RefreshType.ALL_DATA
}
}
sealed class StageStatus {