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.isBranch
import app.extensions.simpleName import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.CreateBranchCommand import org.eclipse.jgit.api.*
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.api.MergeCommand
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject import javax.inject.Inject
@ -50,19 +45,6 @@ class BranchesManager @Inject constructor() {
.call() .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) { suspend fun deleteBranch(git: Git, branch: Ref) = withContext(Dispatchers.IO) {
git git
.branchDelete() .branchDelete()

View File

@ -25,7 +25,7 @@ class LogManager @Inject constructor(
suspend fun loadLog(git: Git, currentBranch: Ref?) = withContext(Dispatchers.IO) { suspend fun loadLog(git: Git, currentBranch: Ref?) = withContext(Dispatchers.IO) {
val commitList = GraphCommitList() val commitList = GraphCommitList()
val repositoryState = git.repository.repositoryState 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 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() 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.components.entryHeight
import app.ui.context_menu.branchContextMenuItems import app.ui.context_menu.branchContextMenuItems
import app.ui.dialogs.MergeDialog import app.ui.dialogs.MergeDialog
import app.ui.dialogs.RebaseDialog
import app.viewmodels.BranchesViewModel import app.viewmodels.BranchesViewModel
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@ -30,6 +31,7 @@ fun Branches(
val branches by branchesViewModel.branches.collectAsState() val branches by branchesViewModel.branches.collectAsState()
val currentBranch by branchesViewModel.currentBranch.collectAsState() val currentBranch by branchesViewModel.currentBranch.collectAsState()
val (mergeBranch, setMergeBranch) = remember { mutableStateOf<Ref?>(null) } val (mergeBranch, setMergeBranch) = remember { mutableStateOf<Ref?>(null) }
val (rebaseBranch, setRebaseBranch) = remember { mutableStateOf<Ref?>(null) }
Column { Column {
SideMenuEntry("Local branches") SideMenuEntry("Local branches")
@ -43,13 +45,14 @@ fun Branches(
Box(modifier = Modifier.heightIn(max = maxHeight.dp)) { Box(modifier = Modifier.heightIn(max = maxHeight.dp)) {
ScrollableLazyColumn(modifier = Modifier.fillMaxWidth()) { ScrollableLazyColumn(modifier = Modifier.fillMaxWidth()) {
itemsIndexed(branches) { _, branch -> itemsIndexed(branches) { _, branch ->
BranchRow( BranchLineEntry(
branch = branch, branch = branch,
isCurrentBranch = currentBranch == branch.name, isCurrentBranch = currentBranch == branch.name,
onBranchClicked = { onBranchClicked(branch) }, onBranchClicked = { onBranchClicked(branch) },
onCheckoutBranch = { branchesViewModel.checkoutRef(branch) }, onCheckoutBranch = { branchesViewModel.checkoutRef(branch) },
onMergeBranch = { setMergeBranch(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) } onAccept = { ff -> branchesViewModel.mergeBranch(mergeBranch, ff) }
) )
} }
if (rebaseBranch != null) {
RebaseDialog(
currentBranch,
rebaseBranchName = rebaseBranch.name,
onReject = { setRebaseBranch(null) },
onAccept = { branchesViewModel.rebaseBranch(rebaseBranch) }
)
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun BranchRow( private fun BranchLineEntry(
branch: Ref, branch: Ref,
isCurrentBranch: Boolean, isCurrentBranch: Boolean,
onBranchClicked: () -> Unit, onBranchClicked: () -> Unit,
onCheckoutBranch: () -> Unit, onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit, onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit, onDeleteBranch: () -> Unit,
) { ) {
ContextMenuArea( ContextMenuArea(
@ -84,6 +97,7 @@ private fun BranchRow(
onCheckoutBranch = onCheckoutBranch, onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch, onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch, 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.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.extensions.filePath import app.extensions.filePath
@ -58,7 +57,7 @@ fun UncommitedChanges(
staged = stageStatus.staged staged = stageStatus.staged
unstaged = stageStatus.unstaged unstaged = stageStatus.unstaged
LaunchedEffect(staged) { LaunchedEffect(staged) {
if(selectedEntryType != null) { if (selectedEntryType != null) {
checkIfSelectedEntryShouldBeUpdated( checkIfSelectedEntryShouldBeUpdated(
selectedEntryType = selectedEntryType, selectedEntryType = selectedEntryType,
staged = staged, staged = staged,
@ -69,8 +68,8 @@ fun UncommitedChanges(
} }
} }
} else { } else {
staged = listOf<StatusEntry>() staged = listOf()
unstaged = listOf<StatusEntry>() // return empty lists if still loading unstaged = listOf() // return empty lists if still loading
} }
val doCommit = { val doCommit = {
@ -134,16 +133,20 @@ fun UncommitedChanges(
allActionTitle = "Stage all" allActionTitle = "Stage all"
) )
Card( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.height(192.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() .fillMaxWidth()
) { ) {
Column( // Don't show the message TextField when rebasing as it can't be edited
modifier = Modifier if (!repositoryState.isRebasing)
.fillMaxSize()
) {
TextField( TextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -159,32 +162,131 @@ fun UncommitedChanges(
onValueChange = { statusViewModel.newCommitMessage = it }, onValueChange = { statusViewModel.newCommitMessage = it },
label = { Text("Write your commit message here", fontSize = 14.sp) }, label = { Text("Write your commit message here", fontSize = 14.sp) },
colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.background), 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),
) )
Button( when {
modifier = Modifier repositoryState.isMerging -> MergeButtons(
.fillMaxWidth(), haveConflictsBeenSolved = unstaged.isEmpty(),
onClick = { onAbort = { statusViewModel.abortMerge() },
doCommit() onMerge = { doCommit() }
}, )
enabled = canCommit, repositoryState.isRebasing -> RebasingButtons(
shape = RectangleShape, canContinue = staged.isNotEmpty() || unstaged.isNotEmpty(),
) { haveConflictsBeenSolved = unstaged.isEmpty(),
val buttonText = if(repositoryState.isMerging) onAbort = { statusViewModel.abortRebase() },
"Merge" onContinue = { statusViewModel.continueRebase() },
else if (repositoryState.isRebasing) onSkip = { statusViewModel.skipRebase() },
"Continue rebasing" )
else else -> {
"Commit" Button(
Text( modifier = Modifier
text = buttonText, .fillMaxWidth(),
fontSize = 14.sp, onClick = doCommit,
) enabled = canCommit,
shape = RectangleShape,
) {
Text(
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 // 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() val selectedEntryTypeNewId = selectedDiffEntry.newId.name()
if (selectedEntryType is DiffEntryType.StagedDiff) { 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 && entryType != null &&
selectedEntryTypeNewId != entryType.newId.name() selectedEntryTypeNewId != entryType.newId.name()
) { ) {
@ -210,15 +313,15 @@ fun checkIfSelectedEntryShouldBeUpdated(
} 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 { unstagedEntry -> val entryType = unstaged.firstOrNull { unstagedEntry ->
if(selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE) if (selectedDiffEntry.changeType == DiffEntry.ChangeType.DELETE)
unstagedEntry.diffEntry.oldPath == selectedDiffEntry.oldPath unstagedEntry.diffEntry.oldPath == selectedDiffEntry.oldPath
else else
unstagedEntry.diffEntry.newPath == selectedDiffEntry.newPath unstagedEntry.diffEntry.newPath == selectedDiffEntry.newPath
} }
if(entryType != null) { if (entryType != null) {
onUnstagedDiffEntrySelected(entryType.diffEntry) onUnstagedDiffEntrySelected(entryType.diffEntry)
} else } else
onStagedDiffEntrySelected(null) onStagedDiffEntrySelected(null)

View File

@ -9,22 +9,29 @@ fun branchContextMenuItems(
isLocal: Boolean, isLocal: Boolean,
onCheckoutBranch: () -> Unit, onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit, onMergeBranch: () -> Unit,
onRebaseBranch: () -> Unit,
onDeleteBranch: () -> Unit, onDeleteBranch: () -> Unit,
): List<ContextMenuItem> { ): List<ContextMenuItem> {
return mutableListOf( return mutableListOf<ContextMenuItem>().apply {
ContextMenuItem(
label = "Checkout branch",
onClick = onCheckoutBranch
),
).apply {
if (!isCurrentBranch) { if (!isCurrentBranch) {
add(
ContextMenuItem(
label = "Checkout branch",
onClick = onCheckoutBranch
)
)
add( add(
ContextMenuItem( ContextMenuItem(
label = "Merge branch", label = "Merge branch",
onClick = onMergeBranch onClick = onMergeBranch
) )
) )
add(
ContextMenuItem(
label = "Rebase branch",
onClick = onRebaseBranch
)
)
} }
if (isLocal && !isCurrentBranch) { if (isLocal && !isCurrentBranch) {
add( 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.components.ScrollableLazyColumn
import app.ui.context_menu.branchContextMenuItems import app.ui.context_menu.branchContextMenuItems
import app.ui.context_menu.tagContextMenuItems import app.ui.context_menu.tagContextMenuItems
import app.ui.dialogs.MergeDialog import app.ui.dialogs.*
import app.ui.dialogs.NewBranchDialog
import app.ui.dialogs.NewTagDialog
import app.ui.dialogs.ResetBranchDialog
import app.viewmodels.LogStatus import app.viewmodels.LogStatus
import app.viewmodels.LogViewModel import app.viewmodels.LogViewModel
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@ -156,6 +153,7 @@ fun Log(
showCreateNewTag = { showLogDialog.value = LogDialog.NewTag(graphNode) }, showCreateNewTag = { showLogDialog.value = LogDialog.NewTag(graphNode) },
resetBranch = { showLogDialog.value = LogDialog.ResetBranch(graphNode) }, resetBranch = { showLogDialog.value = LogDialog.ResetBranch(graphNode) },
onMergeBranch = { ref -> showLogDialog.value = LogDialog.MergeBranch(ref) }, onMergeBranch = { ref -> showLogDialog.value = LogDialog.MergeBranch(ref) },
onRebaseBranch = { ref -> showLogDialog.value = LogDialog.RebaseBranch(ref) },
onRevCommitSelected = { onRevCommitSelected = {
onItemSelected(SelectedItem.Commit(graphNode)) onItemSelected(SelectedItem.Commit(graphNode))
} }
@ -212,6 +210,19 @@ fun LogDialogs(
onResetShowLogDialog() 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 -> { LogDialog.None -> {
} }
} }
@ -334,6 +345,7 @@ fun CommitLine(
showCreateNewTag: () -> Unit, showCreateNewTag: () -> Unit,
resetBranch: (GraphNode) -> Unit, resetBranch: (GraphNode) -> Unit,
onMergeBranch: (Ref) -> Unit, onMergeBranch: (Ref) -> Unit,
onRebaseBranch: (Ref) -> Unit,
onRevCommitSelected: (GraphNode) -> Unit, onRevCommitSelected: (GraphNode) -> Unit,
) { ) {
val commitRefs = graphNode.refs val commitRefs = graphNode.refs
@ -405,6 +417,7 @@ fun CommitLine(
onMergeBranch = { ref -> onMergeBranch(ref) }, onMergeBranch = { ref -> onMergeBranch(ref) },
onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) }, onDeleteBranch = { ref -> logViewModel.deleteBranch(ref) },
onDeleteTag = { ref -> logViewModel.deleteTag(ref) }, onDeleteTag = { ref -> logViewModel.deleteTag(ref) },
onRebaseBranch = { ref -> onRebaseBranch(ref) },
) )
} }
} }
@ -422,6 +435,7 @@ fun CommitMessage(
onCheckoutRef: (ref: Ref) -> Unit, onCheckoutRef: (ref: Ref) -> Unit,
onMergeBranch: (ref: Ref) -> Unit, onMergeBranch: (ref: Ref) -> Unit,
onDeleteBranch: (ref: Ref) -> Unit, onDeleteBranch: (ref: Ref) -> Unit,
onRebaseBranch: (ref: Ref) -> Unit,
onDeleteTag: (ref: Ref) -> Unit, onDeleteTag: (ref: Ref) -> Unit,
) { ) {
val textColor = if (selected) { val textColor = if (selected) {
@ -467,6 +481,7 @@ fun CommitMessage(
onCheckoutBranch = { onCheckoutRef(ref) }, onCheckoutBranch = { onCheckoutRef(ref) },
onMergeBranch = { onMergeBranch(ref) }, onMergeBranch = { onMergeBranch(ref) },
onDeleteBranch = { onDeleteBranch(ref) }, onDeleteBranch = { onDeleteBranch(ref) },
onRebaseBranch = { onRebaseBranch(ref) },
) )
} }
} }
@ -644,6 +659,7 @@ fun BranchChip(
onCheckoutBranch: () -> Unit, onCheckoutBranch: () -> Unit,
onMergeBranch: () -> Unit, onMergeBranch: () -> Unit,
onDeleteBranch: () -> Unit, onDeleteBranch: () -> Unit,
onRebaseBranch: () -> Unit,
color: Color, color: Color,
) { ) {
val contextMenuItemsList = { val contextMenuItemsList = {
@ -653,6 +669,7 @@ fun BranchChip(
onCheckoutBranch = onCheckoutBranch, onCheckoutBranch = onCheckoutBranch,
onMergeBranch = onMergeBranch, onMergeBranch = onMergeBranch,
onDeleteBranch = onDeleteBranch, onDeleteBranch = onDeleteBranch,
onRebaseBranch = onRebaseBranch,
) )
} }

View File

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

View File

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

View File

@ -14,6 +14,8 @@ class StatusViewModel @Inject constructor(
private val statusManager: StatusManager, private val statusManager: StatusManager,
private val branchesManager: BranchesManager, private val branchesManager: BranchesManager,
private val repositoryManager: RepositoryManager, private val repositoryManager: RepositoryManager,
private val rebaseManager: RebaseManager,
private val mergeManager: MergeManager,
) { ) {
private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf())) private val _stageStatus = MutableStateFlow<StageStatus>(StageStatus.Loaded(listOf(), listOf()))
val stageStatus: StateFlow<StageStatus> = _stageStatus 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 true to update the log only if the uncommitedChanges status has changed
return (hasNowUncommitedChanges != hadUncommitedChanges) 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 { sealed class StageStatus {