diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/BranchesConstants.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/BranchesConstants.kt new file mode 100644 index 0000000..0a43ae0 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/BranchesConstants.kt @@ -0,0 +1,8 @@ +package com.jetpackduba.gitnuro.git.branches + +object BranchesConstants { + /** + * Prefix added before the upstream branch name in .git/config + */ + const val UPSTREAM_BRANCH_CONFIG_PREFIX = "refs/heads/" +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt new file mode 100644 index 0000000..ce05a14 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt @@ -0,0 +1,26 @@ +package com.jetpackduba.gitnuro.git.branches + +import com.jetpackduba.gitnuro.extensions.simpleName +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.lib.Repository +import javax.inject.Inject + +class GetTrackingBranchUseCase @Inject constructor() { + operator fun invoke(git: Git, ref: Ref): TrackingBranch? { + val repository: Repository = git.repository + + val config: Config = repository.config + val remote: String? = config.getString("branch", ref.simpleName, "remote") + val branch: String? = config.getString("branch", ref.simpleName, "merge") + + if (remote != null && branch != null) { + return TrackingBranch(remote, branch.removePrefix(BranchesConstants.UPSTREAM_BRANCH_CONFIG_PREFIX)) + } + + return null + } +} + +data class TrackingBranch(val remote: String, val branch: String) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/SetTrackingBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/SetTrackingBranchUseCase.kt new file mode 100644 index 0000000..ff7e2dc --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/SetTrackingBranchUseCase.kt @@ -0,0 +1,31 @@ +package com.jetpackduba.gitnuro.git.branches + +import com.jetpackduba.gitnuro.extensions.simpleName +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Config +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.lib.StoredConfig +import javax.inject.Inject + +class SetTrackingBranchUseCase @Inject constructor() { + operator fun invoke(git: Git, ref: Ref, remoteName: String?, remoteBranch: Ref?) { + val repository: Repository = git.repository + val config: StoredConfig = repository.config + + if (remoteName == null || remoteBranch == null) { + config.unset("branch", ref.simpleName, "remote") + config.unset("branch", ref.simpleName, "merge") + } else { + config.setString("branch", ref.simpleName, "remote", remoteName) + config.setString( + "branch", + ref.simpleName, + "merge", + BranchesConstants.UPSTREAM_BRANCH_CONFIG_PREFIX + remoteBranch.simpleName + ) + } + + config.save() + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Theme.kt b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Theme.kt index a682ad2..7e9f114 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/theme/Theme.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/theme/Theme.kt @@ -80,19 +80,16 @@ val Colors.isDark: Boolean get() = !this.isLight -enum class Theme(val displayName: String) : DropDownOption { +enum class Theme(val displayName: String) { LIGHT("Light"), DARK("Dark"), DARK_GRAY("Dark gray"), CUSTOM("Custom"); - - override val optionName: String - get() = displayName } val themeLists = listOf( - Theme.LIGHT, - Theme.DARK, - Theme.DARK_GRAY, - Theme.CUSTOM, + DropDownOption(Theme.LIGHT, Theme.LIGHT.displayName), + DropDownOption(Theme.DARK, Theme.DARK.displayName), + DropDownOption(Theme.DARK_GRAY, Theme.DARK_GRAY.displayName), + DropDownOption(Theme.CUSTOM, Theme.CUSTOM.displayName), ) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt index ef1e0e4..ead2643 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/SidePanel.kt @@ -18,6 +18,7 @@ import com.jetpackduba.gitnuro.extensions.simpleName import com.jetpackduba.gitnuro.theme.onBackgroundSecondary import com.jetpackduba.gitnuro.ui.components.* import com.jetpackduba.gitnuro.ui.context_menu.* +import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.EditRemotesDialog import com.jetpackduba.gitnuro.viewmodels.sidepanel.* import org.eclipse.jgit.lib.Ref @@ -42,6 +43,7 @@ fun SidePanel( val submodulesState by submodulesViewModel.submodules.collectAsState() var showEditRemotesDialog by remember { mutableStateOf(false) } + val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf(null) } Column { FilterTextField( @@ -62,6 +64,7 @@ fun SidePanel( localBranches( branchesState = branchesState, branchesViewModel = branchesViewModel, + onChangeDefaultUpstreamBranch = { setBranchToChangeUpstream(it) } ) remotes( @@ -95,6 +98,14 @@ fun SidePanel( }, ) } + + if (branchToChangeUpstream != null) { + SetDefaultUpstreamBranchDialog( + viewModel = gitnuroDynamicViewModel(), + branch = branchToChangeUpstream, + onClose = { setBranchToChangeUpstream(null) } + ) + } } @Composable @@ -123,6 +134,7 @@ fun FilterTextField(value: String, onValueChange: (String) -> Unit, modifier: Mo fun LazyListScope.localBranches( branchesState: BranchesState, branchesViewModel: BranchesViewModel, + onChangeDefaultUpstreamBranch: (Ref) -> Unit, ) { val isExpanded = branchesState.isExpanded val branches = branchesState.branches @@ -157,6 +169,7 @@ fun LazyListScope.localBranches( onRebaseBranch = { branchesViewModel.rebaseBranch(branch) }, onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) }, onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) }, + onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(branch) } ) } } @@ -344,6 +357,7 @@ private fun Branch( onDeleteBranch: () -> Unit, onPushToRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit, + onChangeDefaultUpstreamBranch: () -> Unit, ) { ContextMenu( items = { @@ -358,6 +372,7 @@ private fun Branch( onRebaseBranch = onRebaseBranch, onPushToRemoteBranch = onPushToRemoteBranch, onPullFromRemoteBranch = onPullFromRemoteBranch, + onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch ) } ) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/FilterDropdown.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/FilterDropdown.kt new file mode 100644 index 0000000..3795b07 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/FilterDropdown.kt @@ -0,0 +1,106 @@ +package com.jetpackduba.gitnuro.ui.components + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.AppIcons +import com.jetpackduba.gitnuro.extensions.lowercaseContains +import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption + +@Preview +@Composable +fun FilterDropdownPreview() { + val items = listOf( + DropDownOption("","Test1"), + DropDownOption("","Test2"), + DropDownOption("","Test3"), + DropDownOption("","Test4"), + ) + + FilterDropdown( + dropdownItems = items, + currentOption = items[0], + onOptionSelected = {} + ) +} + +@Composable +fun FilterDropdown( + dropdownItems: List>, + currentOption: DropDownOption?, + width: Dp = 240.dp, + onOptionSelected: (DropDownOption) -> Unit, +) { + var showDropdown by remember { mutableStateOf(false) } + var filter by remember { mutableStateOf("") } + val filterFocusRequester = remember { FocusRequester() } + val filteredDropdownItems = remember(filter, dropdownItems) { dropdownItems.filter { it.optionName.lowercaseContains(filter) } } + + Box { + OutlinedButton( + onClick = { showDropdown = true }, + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background), + modifier = Modifier.width(width) + ) { + Text( + text = currentOption?.optionName ?: "", + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onBackground, + modifier = Modifier.weight(1f), + maxLines = 1 + ) + + Icon( + painter = painterResource(AppIcons.DROPDOWN), + contentDescription = null, + tint = MaterialTheme.colors.onBackground, + ) + } + + DropdownMenu( + expanded = showDropdown, + onDismissRequest = { showDropdown = false }, + modifier = Modifier.width(width), + ) { + DropdownMenuItem( + onClick = {}, + modifier = Modifier.fillMaxWidth() + ) { + AdjustableOutlinedTextField( + value = filter, + onValueChange = { filter = it }, + modifier = Modifier.focusable(showDropdown) + .focusRequester(filterFocusRequester) + ) + } + + for (dropDownOption in filteredDropdownItems) { + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + onClick = { + onOptionSelected(dropDownOption) + showDropdown = false + } + ) { + Text(dropDownOption.optionName, style = MaterialTheme.typography.body2) + } + } + } + + LaunchedEffect(showDropdown) { + if (showDropdown) { + filterFocusRequester.requestFocus() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt index eeb57cf..ef3131d 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/components/RepositoriesTabPanel.kt @@ -262,4 +262,10 @@ inline fun gitnuroViewModel(): T { return remember(tab) { tab.tabViewModelsHolder.viewModels[T::class] as T } +} +@Composable +inline fun gitnuroDynamicViewModel(): T { + val tab = LocalTabScope.current + + return tab.tabViewModelsHolder.dynamicViewModel(T::class) as T } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt index 2a7e35c..ca82816 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/context_menu/BranchContextMenu.kt @@ -18,6 +18,7 @@ fun branchContextMenuItems( onDeleteRemoteBranch: () -> Unit = {}, onPushToRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit, + onChangeDefaultUpstreamBranch: () -> Unit, ): List { return mutableListOf().apply { if (!isCurrentBranch) { @@ -68,6 +69,16 @@ fun branchContextMenuItems( ) ) } + + if(isLocal) { + add( + ContextMenuElement.ContextTextEntry( + label = "Change default upstream branch", + onClick = onChangeDefaultUpstreamBranch + ), + ) + } + if (!isLocal) { add( ContextMenuElement.ContextTextEntry( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SetDefaultUpstreamBranchDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SetDefaultUpstreamBranchDialog.kt new file mode 100644 index 0000000..6a50b89 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/SetDefaultUpstreamBranchDialog.kt @@ -0,0 +1,196 @@ +package com.jetpackduba.gitnuro.ui.dialogs + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.jetpackduba.gitnuro.AppIcons +import com.jetpackduba.gitnuro.extensions.simpleName +import com.jetpackduba.gitnuro.git.remotes.RemoteInfo +import com.jetpackduba.gitnuro.ui.components.FilterDropdown +import com.jetpackduba.gitnuro.ui.components.PrimaryButton +import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption +import com.jetpackduba.gitnuro.viewmodels.ChangeDefaultUpstreamBranchViewModel +import com.jetpackduba.gitnuro.viewmodels.SetDefaultUpstreamBranchState +import org.eclipse.jgit.lib.ObjectIdRef +import org.eclipse.jgit.lib.Ref + +@Preview +@Composable +fun SetDefaultUpstreamBranchDialogPreview() { + SetDefaultUpstreamBranchDialogView( + state = SetDefaultUpstreamBranchState.Loaded( + ObjectIdRef.PeeledNonTag(null, "TestBranch", null), + null, + emptyList(), + null, + null + ), + onClose = {}, + setSelectedRemote = {}, + setSelectedBranch = {}, + changeDefaultUpstreamBranch = {} + ) +} + +@Composable +fun SetDefaultUpstreamBranchDialog( + viewModel: ChangeDefaultUpstreamBranchViewModel, + branch: Ref, + onClose: () -> Unit, +) { + LaunchedEffect(branch) { + viewModel.init(branch) + } + + val setDefaultUpstreamBranchState = viewModel.setDefaultUpstreamBranchState.collectAsState().value + LaunchedEffect(setDefaultUpstreamBranchState) { + if (setDefaultUpstreamBranchState is SetDefaultUpstreamBranchState.UpstreamChanged) { + onClose() + } + } + + MaterialDialog(onCloseRequested = onClose) { + SetDefaultUpstreamBranchDialogView( + state = setDefaultUpstreamBranchState, + onClose = onClose, + setSelectedRemote = { viewModel.setSelectedRemote(it) }, + setSelectedBranch = { viewModel.setSelectedBranch(it) }, + changeDefaultUpstreamBranch = { viewModel.changeDefaultUpstreamBranch() } + ) + } +} + + +@Composable +private fun SetDefaultUpstreamBranchDialogView( + state: SetDefaultUpstreamBranchState, + onClose: () -> Unit, + setSelectedRemote: (RemoteInfo) -> Unit, + setSelectedBranch: (Ref) -> Unit, + changeDefaultUpstreamBranch: () -> Unit, +) { + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painterResource(AppIcons.BRANCH), + contentDescription = null, + modifier = Modifier + .padding(bottom = 16.dp) + .size(64.dp), + tint = MaterialTheme.colors.onBackground, + ) + + Text( + text = "Change upstream branch", + modifier = Modifier + .padding(bottom = 8.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.SemiBold, + ) + + Text( + text = "Set the upstream remote branch", + modifier = Modifier + .padding(bottom = 16.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + + if (state is SetDefaultUpstreamBranchState.Loaded) { + + val remotesDropDown = + state.remotes.map { DropDownOption(it, it.remoteConfig.name) } + + val selectedRemote = state.selectedRemote + val selectedRemoteOption = if (selectedRemote != null) { + DropDownOption(selectedRemote, selectedRemote.remoteConfig.name) + } else { + null + } + + val selectedBranch = state.selectedBranch + val selectedBranchOption = if (selectedBranch != null) { + DropDownOption(selectedBranch, selectedBranch.simpleName) + } else { + null + } + + val branchesDropDown = remember(selectedRemote) { + selectedRemote?.branchesList?.map { ref -> + DropDownOption(ref, ref.simpleName) + } + } + + Text( + text = "Remote", + modifier = Modifier + .padding(top = 8.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) + + FilterDropdown( + remotesDropDown, + selectedRemoteOption, + width = 400.dp, + onOptionSelected = { setSelectedRemote(it.value) } + ) + + + Text( + text = "Branch", + modifier = Modifier + .padding(top = 8.dp), + color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) + + FilterDropdown( + branchesDropDown ?: emptyList(), + selectedBranchOption, + width = 400.dp, + onOptionSelected = { setSelectedBranch(it.value) } + ) + } + } + Row( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.End) + ) { + PrimaryButton( + text = "Cancel", + modifier = Modifier.padding(end = 8.dp), + onClick = onClose, + backgroundColor = Color.Transparent, + textColor = MaterialTheme.colors.onBackground, + ) + PrimaryButton( + onClick = { + changeDefaultUpstreamBranch() + }, + text = "Change" + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index db1b8a9..7440095 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -24,7 +24,6 @@ import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel import com.jetpackduba.gitnuro.ui.dialogs.ErrorDialog import com.jetpackduba.gitnuro.ui.dialogs.MaterialDialog import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption -import com.jetpackduba.gitnuro.ui.dropdowns.ScaleDropDown import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -168,9 +167,9 @@ fun UiSettings(settingsViewModel: SettingsViewModel) { title = "Theme", subtitle = "Select the UI theme between light and dark mode", dropDownOptions = themeLists, - currentOption = currentTheme, - onOptionSelected = { theme -> - settingsViewModel.theme = theme + currentOption = DropDownOption(currentTheme, currentTheme.displayName), + onOptionSelected = { themeDropDown -> + settingsViewModel.theme = themeDropDown.value } ) @@ -199,12 +198,12 @@ fun UiSettings(settingsViewModel: SettingsViewModel) { var options by remember { mutableStateOf( listOf( - ScaleDropDown(1f, "100%"), - ScaleDropDown(1.25f, "125%"), - ScaleDropDown(1.5f, "150%"), - ScaleDropDown(2f, "200%"), - ScaleDropDown(2.5f, "250%"), - ScaleDropDown(3f, "300%"), + DropDownOption(1f, "100%"), + DropDownOption(1.25f, "125%"), + DropDownOption(1.5f, "150%"), + DropDownOption(2f, "200%"), + DropDownOption(2.5f, "250%"), + DropDownOption(3f, "300%"), ) ) } @@ -221,7 +220,7 @@ fun UiSettings(settingsViewModel: SettingsViewModel) { if (matchingOption == null) { // Scale that we haven't taken in consideration // Create a new scale and add it to the options list - matchingOption = ScaleDropDown(scaleUi, "${(scaleUi * 100).toInt()}%") + matchingOption = DropDownOption(scaleUi, "${(scaleUi * 100).toInt()}%") val newOptions = options.toMutableList() newOptions.add(matchingOption) newOptions.sortBy { it.value } @@ -275,12 +274,12 @@ fun Category( @Composable -fun SettingDropDown( +fun SettingDropDown( title: String, subtitle: String, - dropDownOptions: List, - onOptionSelected: (T) -> Unit, - currentOption: T, + dropDownOptions: List>, + onOptionSelected: (DropDownOption) -> Unit, + currentOption: DropDownOption, ) { var showThemeDropdown by remember { mutableStateOf(false) } Row( diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/DropDownOption.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/DropDownOption.kt index 3b2d4bb..f4d4778 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/DropDownOption.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/DropDownOption.kt @@ -1,5 +1,3 @@ package com.jetpackduba.gitnuro.ui.dropdowns -interface DropDownOption { - val optionName: String -} \ No newline at end of file +data class DropDownOption(val value: T, val optionName: String) \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/ScaleDropDown.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/ScaleDropDown.kt deleted file mode 100644 index 19b7e91..0000000 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dropdowns/ScaleDropDown.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.jetpackduba.gitnuro.ui.dropdowns - -class ScaleDropDown(val value: Float, override val optionName: String) : DropDownOption \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt index d9b86d2..1b61a21 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/Log.kt @@ -48,11 +48,13 @@ import com.jetpackduba.gitnuro.theme.* import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.components.AvatarImage import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn +import com.jetpackduba.gitnuro.ui.components.gitnuroDynamicViewModel import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel import com.jetpackduba.gitnuro.ui.context_menu.* import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog import com.jetpackduba.gitnuro.ui.dialogs.ResetBranchDialog +import com.jetpackduba.gitnuro.ui.dialogs.SetDefaultUpstreamBranchDialog import com.jetpackduba.gitnuro.viewmodels.LogSearch import com.jetpackduba.gitnuro.viewmodels.LogStatus import com.jetpackduba.gitnuro.viewmodels.LogViewModel @@ -436,6 +438,7 @@ fun MessagesList( onRebaseBranch = onRebase, onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) }, onRevCommitSelected = { logViewModel.selectLogLine(graphNode) }, + onChangeDefaultUpstreamBranch = { onShowLogDialog(LogDialog.ChangeDefaultBranch(it)) } ) } @@ -587,6 +590,14 @@ fun LogDialogs( LogDialog.None -> { } + + is LogDialog.ChangeDefaultBranch -> { + SetDefaultUpstreamBranchDialog( + viewModel = gitnuroDynamicViewModel(), + branch = showLogDialog.ref, + onClose = { onResetShowLogDialog() }, + ) + } } } @@ -772,6 +783,7 @@ fun CommitLine( onRebaseBranch: (Ref) -> Unit, onRevCommitSelected: () -> Unit, onRebaseInteractive: () -> Unit, + onChangeDefaultUpstreamBranch: (Ref) -> Unit, ) { val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name @@ -828,6 +840,7 @@ fun CommitLine( onRebaseBranch = { ref -> onRebaseBranch(ref) }, onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) }, onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) }, + onChangeDefaultUpstreamBranch = { ref -> onChangeDefaultUpstreamBranch(ref) }, ) } } @@ -849,6 +862,7 @@ fun CommitMessage( onDeleteTag: (ref: Ref) -> Unit, onPushRemoteBranch: (ref: Ref) -> Unit, onPullRemoteBranch: (ref: Ref) -> Unit, + onChangeDefaultUpstreamBranch: (ref: Ref) -> Unit, ) { Row( modifier = Modifier.fillMaxSize(), @@ -884,6 +898,7 @@ fun CommitMessage( onRebaseBranch = { onRebaseBranch(ref) }, onPullRemoteBranch = { onPullRemoteBranch(ref) }, onPushRemoteBranch = { onPushRemoteBranch(ref) }, + onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(ref) }, ) } } @@ -1092,6 +1107,7 @@ fun BranchChip( onRebaseBranch: () -> Unit, onPushRemoteBranch: () -> Unit, onPullRemoteBranch: () -> Unit, + onChangeDefaultUpstreamBranch: () -> Unit, color: Color, ) { val contextMenuItemsList = { @@ -1107,6 +1123,7 @@ fun BranchChip( onRebaseBranch = onRebaseBranch, onPushToRemoteBranch = onPushRemoteBranch, onPullFromRemoteBranch = onPullRemoteBranch, + onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch, ) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/LogDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/LogDialog.kt index 1ca8899..5fd6410 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/LogDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/log/LogDialog.kt @@ -1,10 +1,12 @@ package com.jetpackduba.gitnuro.ui.log import com.jetpackduba.gitnuro.git.graph.GraphNode +import org.eclipse.jgit.lib.Ref sealed class LogDialog { object None : LogDialog() data class NewBranch(val graphNode: GraphNode) : LogDialog() data class NewTag(val graphNode: GraphNode) : LogDialog() data class ResetBranch(val graphNode: GraphNode) : LogDialog() + data class ChangeDefaultBranch(val ref: Ref) : LogDialog() } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SetDefaultUpstreamBranchViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SetDefaultUpstreamBranchViewModel.kt new file mode 100644 index 0000000..94a59e0 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SetDefaultUpstreamBranchViewModel.kt @@ -0,0 +1,111 @@ +package com.jetpackduba.gitnuro.viewmodels + +import com.jetpackduba.gitnuro.extensions.simpleName +import com.jetpackduba.gitnuro.git.RefreshType +import com.jetpackduba.gitnuro.git.TabState +import com.jetpackduba.gitnuro.git.branches.GetRemoteBranchesUseCase +import com.jetpackduba.gitnuro.git.branches.GetTrackingBranchUseCase +import com.jetpackduba.gitnuro.git.branches.SetTrackingBranchUseCase +import com.jetpackduba.gitnuro.git.branches.TrackingBranch +import com.jetpackduba.gitnuro.git.remotes.GetRemotesUseCase +import com.jetpackduba.gitnuro.git.remotes.RemoteInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.eclipse.jgit.lib.Ref +import javax.inject.Inject + +class ChangeDefaultUpstreamBranchViewModel @Inject constructor( + private val tabState: TabState, + private val getRemoteBranchesUseCase: GetRemoteBranchesUseCase, + private val getRemotesUseCase: GetRemotesUseCase, + private val getTrackingBranchUseCase: GetTrackingBranchUseCase, + private val setTrackingBranchUseCase: SetTrackingBranchUseCase, +) { + private val _setDefaultUpstreamBranchState = + MutableStateFlow(SetDefaultUpstreamBranchState.Loading) + val setDefaultUpstreamBranchState: StateFlow = + _setDefaultUpstreamBranchState + + fun init(branch: Ref) = tabState.runOperation( + refreshType = RefreshType.NONE + ) { git -> + _setDefaultUpstreamBranchState.value = SetDefaultUpstreamBranchState.Loading + + val trackingBranch = getTrackingBranchUseCase(git, branch) + val remoteBranches = getRemoteBranchesUseCase(git) + val remotes = getRemotesUseCase(git, remoteBranches) + + var remote: RemoteInfo? = null + var remoteBranch: Ref? = null + + if (trackingBranch != null) { + remote = remotes.firstOrNull { it.remoteConfig.name == trackingBranch.remote } + remoteBranch = remote?.branchesList?.firstOrNull { it.simpleName == trackingBranch.branch } + } + + _setDefaultUpstreamBranchState.value = + SetDefaultUpstreamBranchState.Loaded( + branch = branch, + trackingBranch = trackingBranch, + remotes = remotes, + selectedRemote = remote, + selectedBranch = remoteBranch + ) + } + + fun changeDefaultUpstreamBranch() = tabState.runOperation( + refreshType = RefreshType.NONE, + ) { git -> + val state = _setDefaultUpstreamBranchState.value + + if (state is SetDefaultUpstreamBranchState.Loaded) { + setTrackingBranchUseCase( + git = git, + ref = state.branch, + remoteName = state.selectedRemote?.remoteConfig?.name, + remoteBranch = state.selectedBranch + ) + + _setDefaultUpstreamBranchState.value = SetDefaultUpstreamBranchState.UpstreamChanged + } + } + + fun setSelectedBranch(branchOption: Ref) { + val state = _setDefaultUpstreamBranchState.value + + if (state is SetDefaultUpstreamBranchState.Loaded) { + _setDefaultUpstreamBranchState.value = state.copy(selectedBranch = branchOption) + } + } + + fun setSelectedRemote(remote: RemoteInfo) { + val state = setDefaultUpstreamBranchState.value + val remoteConfig = remote.remoteConfig + + if (state is SetDefaultUpstreamBranchState.Loaded) { + val branch = if (remoteConfig.name == state.trackingBranch?.remote) { + remote.branchesList.firstOrNull { it.simpleName == state.trackingBranch?.branch } + } else { + null + } + + _setDefaultUpstreamBranchState.value = state.copy( + selectedRemote = remote, + selectedBranch = branch, + ) + } + } +} + +sealed interface SetDefaultUpstreamBranchState { + object Loading : SetDefaultUpstreamBranchState + data class Loaded( + val branch: Ref, + val trackingBranch: TrackingBranch?, + val remotes: List, + val selectedRemote: RemoteInfo?, + val selectedBranch: Ref?, + ) : SetDefaultUpstreamBranchState + + object UpstreamChanged : SetDefaultUpstreamBranchState +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt index df12782..c068999 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/TabViewModelsHolder.kt @@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.di.TabScope import com.jetpackduba.gitnuro.viewmodels.sidepanel.SidePanelViewModel import javax.inject.Inject import javax.inject.Provider +import kotlin.reflect.KClass @TabScope class TabViewModelsHolder @Inject constructor( @@ -19,7 +20,8 @@ class TabViewModelsHolder @Inject constructor( private val rebaseInteractiveViewModelProvider: Provider, private val historyViewModelProvider: Provider, private val authorViewModelProvider: Provider, -) { + private val changeDefaultUpstreamBranchViewModelProvider: Provider, + ) { val viewModels = mapOf( logViewModel::class to logViewModel, sidePanelViewModel::class to sidePanelViewModel, @@ -29,4 +31,16 @@ class TabViewModelsHolder @Inject constructor( cloneViewModel::class to cloneViewModel, settingsViewModel::class to settingsViewModel, ) + + // TODO Call this when required + fun dynamicViewModel(type: KClass<*>): Any { + return when(type) { + DiffViewModel::class -> diffViewModelProvider.get() + RebaseInteractiveViewModel::class -> rebaseInteractiveViewModelProvider.get() + HistoryViewModel::class -> historyViewModelProvider.get() + AuthorViewModel::class -> authorViewModelProvider.get() + ChangeDefaultUpstreamBranchViewModel::class -> changeDefaultUpstreamBranchViewModelProvider.get() + else -> throw NotImplementedError("View model provider not implemented") + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt index f9d7e79..5ad58d9 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/sidepanel/BranchesViewModel.kt @@ -52,7 +52,6 @@ class BranchesViewModel @AssistedInject constructor( ) init { - tabScope.launch { tabState.refreshFlowFiltered(RefreshType.ALL_DATA) { diff --git a/src/test/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCaseTest.kt b/src/test/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCaseTest.kt new file mode 100644 index 0000000..38f9767 --- /dev/null +++ b/src/test/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCaseTest.kt @@ -0,0 +1,54 @@ +package com.jetpackduba.gitnuro.git.branches + +import io.mockk.every +import io.mockk.mockk +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.lib.Ref +import org.eclipse.jgit.lib.Repository +import org.eclipse.jgit.lib.StoredConfig +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GetTrackingBranchUseCaseTest { + private val gitMock: Git = mockk(relaxed = true) + private val repositoryMock: Repository = mockk(relaxed = true) + private val configMock: StoredConfig = mockk(relaxed = true) + private val refMock: Ref = mockk(relaxed = true) + + private val localBranchName = "feature-branch" + private val remoteName = "origin" + private val remoteBranchFullName = "refs/heads/main" + private val remoteBranchShortName = "main" + private val objectId = ObjectId.zeroId() + + @BeforeEach + fun setUp() { + every { gitMock.repository } returns repositoryMock + every { refMock.name } returns "refs/heads/$localBranchName" + every { refMock.objectId } returns objectId + every { repositoryMock.config } returns configMock + } + + @Test + fun `invoke returns null when remote not found`() { + every { configMock.getString("branch", localBranchName, "remote") } returns null + every { configMock.getString("branch", localBranchName, "merge") } returns null + + val result = GetTrackingBranchUseCase()(gitMock, refMock) + + assertNull(result) + } + + @Test + fun `invoke returns tracking branch when local and remote branches exist`() { + every { configMock.getString("branch", localBranchName, "remote") } returns remoteName + every { configMock.getString("branch", localBranchName, "merge") } returns remoteBranchFullName + + val result = GetTrackingBranchUseCase()(gitMock, refMock) + + assertEquals(TrackingBranch(remoteName, remoteBranchShortName), result) + } +} \ No newline at end of file