Implemented merge & rebase
This commit is contained in:
parent
ca1eeb2d11
commit
42fec7c591
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
31
src/main/kotlin/app/git/MergeManager.kt
Normal file
31
src/main/kotlin/app/git/MergeManager.kt
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
36
src/main/kotlin/app/git/RebaseManager.kt
Normal file
36
src/main/kotlin/app/git/RebaseManager.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
83
src/main/kotlin/app/ui/dialogs/RebaseDialog.kt
Normal file
83
src/main/kotlin/app/ui/dialogs/RebaseDialog.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user