diff --git a/src/main/kotlin/app/git/GitManager.kt b/src/main/kotlin/app/git/GitManager.kt index 58df60a..e585ba6 100644 --- a/src/main/kotlin/app/git/GitManager.kt +++ b/src/main/kotlin/app/git/GitManager.kt @@ -15,6 +15,7 @@ import app.AppStateManager import app.app.Error import app.app.ErrorsManager import app.app.newErrorNow +import org.eclipse.jgit.lib.ObjectId import java.io.File import javax.inject.Inject @@ -238,6 +239,27 @@ class GitManager @Inject constructor( statusManager.stageAll(safeGit) } + fun checkoutCommit(revCommit: RevCommit) = managerScope.launch { + safeProcessing { + logManager.checkoutCommit(safeGit, revCommit) + coLoadLog() + } + } + + fun createBranchOnCommit(branch: String, revCommit: RevCommit) = managerScope.launch { + safeProcessing { + logManager.createBranchOnCommit(safeGit, branch, revCommit) + coLoadLog() + } + } + + fun createTagOnCommit(tag: String, revCommit: RevCommit) = managerScope.launch { + safeProcessing { + logManager.createTagOnCommit(safeGit, tag, revCommit) + coLoadLog() + } + } + var onRepositoryChanged: (path: String?) -> Unit = {} private suspend fun safeProcessing(callback: suspend () -> Unit) { diff --git a/src/main/kotlin/app/git/LogManager.kt b/src/main/kotlin/app/git/LogManager.kt index c26ff31..994abf2 100644 --- a/src/main/kotlin/app/git/LogManager.kt +++ b/src/main/kotlin/app/git/LogManager.kt @@ -9,7 +9,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.Constants -import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.revwalk.RevCommit import javax.inject.Inject @@ -47,6 +48,31 @@ class LogManager @Inject constructor( _logStatus.value = loadedStatus } + + suspend fun checkoutCommit(git: Git, revCommit: RevCommit) = withContext(Dispatchers.IO) { + git + .checkout() + .setName(revCommit.name) + .call() + } + + suspend fun createBranchOnCommit(git: Git, branch: String, revCommit: RevCommit) = withContext(Dispatchers.IO) { + git + .checkout() + .setCreateBranch(true) + .setName(branch) + .setStartPoint(revCommit) + .call() + } + + suspend fun createTagOnCommit(git: Git, tag: String, revCommit: RevCommit) = withContext(Dispatchers.IO) { + git + .tag() + .setAnnotated(true) + .setName(tag) + .setObjectId(revCommit) + .call() + } } diff --git a/src/main/kotlin/app/ui/Log.kt b/src/main/kotlin/app/ui/Log.kt index 79414b3..221e3c2 100644 --- a/src/main/kotlin/app/ui/Log.kt +++ b/src/main/kotlin/app/ui/Log.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.res.painterResource @@ -25,6 +24,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.DialogManager import app.extensions.simpleName import app.extensions.toSmartSystemString import app.git.GitManager @@ -35,6 +35,8 @@ import app.theme.headerText import app.theme.primaryTextColor import app.theme.secondaryTextColor import app.ui.components.ScrollableLazyColumn +import app.ui.dialogs.NewBranchDialog +import app.ui.dialogs.NewTagDialog import org.eclipse.jgit.lib.ObjectIdRef import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.revwalk.RevCommit @@ -59,8 +61,12 @@ private const val CANVAS_MIN_WIDTH = 100 @Composable fun Log( gitManager: GitManager, + dialogManager: DialogManager, onRevCommitSelected: (RevCommit) -> Unit, onUncommitedChangesSelected: () -> Unit, + onCheckoutCommit: (graphNode: GraphNode) -> Unit, + onCreateBranchOnCommit: (branchName: String, graphNode: GraphNode) -> Unit, + onCreateTagOnCommit: (tagName: String, graphNode: GraphNode) -> Unit, selectedIndex: MutableState = remember { mutableStateOf(-1) } ) { val logStatusState = gitManager.logStatus.collectAsState() @@ -184,38 +190,86 @@ fun Log( itemsIndexed(items = commitList) { index, item -> val commitRefs = item.refs - Row( - modifier = Modifier - .height(40.dp) - .fillMaxWidth() - .clickable { - selectedIndex.value = index - selectedUncommited.value = false - onRevCommitSelected(item) - }, + Box(modifier = Modifier + .clickable { + selectedIndex.value = index + selectedUncommited.value = false + onRevCommitSelected(item) + } ) { - CommitsGraphLine( - modifier = Modifier - .width(graphWidth), - plotCommit = item - ) - DividerLog( - modifier = Modifier - .draggable( - rememberDraggableState { - weightMod += it - }, - Orientation.Horizontal + + ContextMenuArea( + items = { + listOf( + ContextMenuItem( + label = "Checkout commit", + onClick = { onCheckoutCommit(item) } + ), + ContextMenuItem( + label = "Create branch", + onClick = { + dialogManager.show { + NewBranchDialog( + onReject = { + dialogManager.dismiss() + }, + onAccept = { branchName -> + onCreateBranchOnCommit(branchName, item) + dialogManager.dismiss() + } + ) + } + } + ), + ContextMenuItem( + label = "Create tag", + onClick = { + dialogManager.show { + NewTagDialog( + onReject = { + dialogManager.dismiss() + }, + onAccept = { branchName -> + onCreateTagOnCommit(branchName, item) + dialogManager.dismiss() + } + ) + } + } + ), + ) + }, + ) { + Row( + modifier = Modifier + .height(40.dp) + .fillMaxWidth(), + ) { + CommitsGraphLine( + modifier = Modifier + .width(graphWidth), + plotCommit = item ) - ) - CommitMessage( - modifier = Modifier.weight(1f), - commit = item, - selected = selectedIndex.value == index, - refs = commitRefs, - ) + DividerLog( + modifier = Modifier + .draggable( + rememberDraggableState { + weightMod += it + }, + Orientation.Horizontal + ) + ) + + CommitMessage( + modifier = Modifier.weight(1f), + commit = item, + selected = selectedIndex.value == index, + refs = commitRefs, + ) + } + } } } } @@ -404,7 +458,7 @@ fun UncommitedChangesGraphLine( @Composable fun RefChip(modifier: Modifier = Modifier, ref: Ref) { val icon = remember(ref) { - if(ref is ObjectIdRef.PeeledTag) { + if (ref is ObjectIdRef.PeeledTag) { "tag.svg" } else "branch.svg" diff --git a/src/main/kotlin/app/ui/RepositoryOpen.kt b/src/main/kotlin/app/ui/RepositoryOpen.kt index fdf7c93..4513775 100644 --- a/src/main/kotlin/app/ui/RepositoryOpen.kt +++ b/src/main/kotlin/app/ui/RepositoryOpen.kt @@ -91,10 +91,18 @@ fun RepositoryOpenPage(gitManager: GitManager, dialogManager: DialogManager) { null -> { Log( gitManager = gitManager, + dialogManager = dialogManager, selectedIndex = selectedIndexCommitLog, + onCheckoutCommit = { graphNode -> + gitManager.checkoutCommit(graphNode) + }, + onCreateBranchOnCommit = { branch, graphNode -> + gitManager.createBranchOnCommit(branch, graphNode) + }, + onCreateTagOnCommit = { tag, graphNode -> + gitManager.createTagOnCommit(tag, graphNode) + }, onRevCommitSelected = { commit -> - // TODO Move all this code to tree manager - selectedRevCommit = commit uncommitedChangesSelected = false }, diff --git a/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt b/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt new file mode 100644 index 0000000..a4b7e37 --- /dev/null +++ b/src/main/kotlin/app/ui/dialogs/NewTagDialog.kt @@ -0,0 +1,75 @@ +package app.ui.dialogs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusOrder +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun NewTagDialog( + onReject: () -> Unit, + onAccept: (tagName: String) -> Unit +) { + var tagField by remember { mutableStateOf("") } + val tagFieldFocusRequester = remember { FocusRequester() } + val buttonFieldFocusRequester = remember { FocusRequester() } + + Column( + modifier = Modifier + .background(MaterialTheme.colors.background), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + OutlinedTextField( + modifier = Modifier + .focusOrder(tagFieldFocusRequester) { + this.next = buttonFieldFocusRequester + } + .width(300.dp), + value = tagField, + singleLine = true, + label = { Text("New tag name", fontSize = 14.sp) }, + textStyle = TextStyle(fontSize = 14.sp), + onValueChange = { + tagField = it + }, + ) + Row( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.End) + ) { + TextButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { + onReject() + } + ) { + Text("Cancel") + } + Button( + modifier = Modifier.focusOrder(buttonFieldFocusRequester) { + this.previous = tagFieldFocusRequester + this.next = tagFieldFocusRequester + }, + enabled = tagField.isNotEmpty(), + onClick = { + onAccept(tagField) + } + ) { + Text("Create tag") + } + } + } + + LaunchedEffect(Unit) { + tagFieldFocusRequester.requestFocus() + } +} \ No newline at end of file