Added change upstream branch option

This commit is contained in:
Abdelilah El Aissaoui 2023-04-23 03:13:12 +02:00
parent 5fd29fbc39
commit 91094a8771
No known key found for this signature in database
GPG Key ID: 7587FC860F594869
18 changed files with 618 additions and 31 deletions

View File

@ -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/"
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -80,19 +80,16 @@ val Colors.isDark: Boolean
get() = !this.isLight get() = !this.isLight
enum class Theme(val displayName: String) : DropDownOption { enum class Theme(val displayName: String) {
LIGHT("Light"), LIGHT("Light"),
DARK("Dark"), DARK("Dark"),
DARK_GRAY("Dark gray"), DARK_GRAY("Dark gray"),
CUSTOM("Custom"); CUSTOM("Custom");
override val optionName: String
get() = displayName
} }
val themeLists = listOf( val themeLists = listOf(
Theme.LIGHT, DropDownOption(Theme.LIGHT, Theme.LIGHT.displayName),
Theme.DARK, DropDownOption(Theme.DARK, Theme.DARK.displayName),
Theme.DARK_GRAY, DropDownOption(Theme.DARK_GRAY, Theme.DARK_GRAY.displayName),
Theme.CUSTOM, DropDownOption(Theme.CUSTOM, Theme.CUSTOM.displayName),
) )

View File

@ -18,6 +18,7 @@ import com.jetpackduba.gitnuro.extensions.simpleName
import com.jetpackduba.gitnuro.theme.onBackgroundSecondary import com.jetpackduba.gitnuro.theme.onBackgroundSecondary
import com.jetpackduba.gitnuro.ui.components.* import com.jetpackduba.gitnuro.ui.components.*
import com.jetpackduba.gitnuro.ui.context_menu.* 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.ui.dialogs.EditRemotesDialog
import com.jetpackduba.gitnuro.viewmodels.sidepanel.* import com.jetpackduba.gitnuro.viewmodels.sidepanel.*
import org.eclipse.jgit.lib.Ref import org.eclipse.jgit.lib.Ref
@ -42,6 +43,7 @@ fun SidePanel(
val submodulesState by submodulesViewModel.submodules.collectAsState() val submodulesState by submodulesViewModel.submodules.collectAsState()
var showEditRemotesDialog by remember { mutableStateOf(false) } var showEditRemotesDialog by remember { mutableStateOf(false) }
val (branchToChangeUpstream, setBranchToChangeUpstream) = remember { mutableStateOf<Ref?>(null) }
Column { Column {
FilterTextField( FilterTextField(
@ -62,6 +64,7 @@ fun SidePanel(
localBranches( localBranches(
branchesState = branchesState, branchesState = branchesState,
branchesViewModel = branchesViewModel, branchesViewModel = branchesViewModel,
onChangeDefaultUpstreamBranch = { setBranchToChangeUpstream(it) }
) )
remotes( remotes(
@ -95,6 +98,14 @@ fun SidePanel(
}, },
) )
} }
if (branchToChangeUpstream != null) {
SetDefaultUpstreamBranchDialog(
viewModel = gitnuroDynamicViewModel(),
branch = branchToChangeUpstream,
onClose = { setBranchToChangeUpstream(null) }
)
}
} }
@Composable @Composable
@ -123,6 +134,7 @@ fun FilterTextField(value: String, onValueChange: (String) -> Unit, modifier: Mo
fun LazyListScope.localBranches( fun LazyListScope.localBranches(
branchesState: BranchesState, branchesState: BranchesState,
branchesViewModel: BranchesViewModel, branchesViewModel: BranchesViewModel,
onChangeDefaultUpstreamBranch: (Ref) -> Unit,
) { ) {
val isExpanded = branchesState.isExpanded val isExpanded = branchesState.isExpanded
val branches = branchesState.branches val branches = branchesState.branches
@ -157,6 +169,7 @@ fun LazyListScope.localBranches(
onRebaseBranch = { branchesViewModel.rebaseBranch(branch) }, onRebaseBranch = { branchesViewModel.rebaseBranch(branch) },
onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) }, onPushToRemoteBranch = { branchesViewModel.pushToRemoteBranch(branch) },
onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) }, onPullFromRemoteBranch = { branchesViewModel.pullFromRemoteBranch(branch) },
onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(branch) }
) )
} }
} }
@ -344,6 +357,7 @@ private fun Branch(
onDeleteBranch: () -> Unit, onDeleteBranch: () -> Unit,
onPushToRemoteBranch: () -> Unit, onPushToRemoteBranch: () -> Unit,
onPullFromRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit,
onChangeDefaultUpstreamBranch: () -> Unit,
) { ) {
ContextMenu( ContextMenu(
items = { items = {
@ -358,6 +372,7 @@ private fun Branch(
onRebaseBranch = onRebaseBranch, onRebaseBranch = onRebaseBranch,
onPushToRemoteBranch = onPushToRemoteBranch, onPushToRemoteBranch = onPushToRemoteBranch,
onPullFromRemoteBranch = onPullFromRemoteBranch, onPullFromRemoteBranch = onPullFromRemoteBranch,
onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch
) )
} }
) { ) {

View File

@ -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 <T> FilterDropdown(
dropdownItems: List<DropDownOption<T>>,
currentOption: DropDownOption<T>?,
width: Dp = 240.dp,
onOptionSelected: (DropDownOption<T>) -> 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()
}
}
}
}

View File

@ -262,4 +262,10 @@ inline fun <reified T> gitnuroViewModel(): T {
return remember(tab) { return remember(tab) {
tab.tabViewModelsHolder.viewModels[T::class] as T tab.tabViewModelsHolder.viewModels[T::class] as T
} }
}
@Composable
inline fun <reified T> gitnuroDynamicViewModel(): T {
val tab = LocalTabScope.current
return tab.tabViewModelsHolder.dynamicViewModel(T::class) as T
} }

View File

@ -18,6 +18,7 @@ fun branchContextMenuItems(
onDeleteRemoteBranch: () -> Unit = {}, onDeleteRemoteBranch: () -> Unit = {},
onPushToRemoteBranch: () -> Unit, onPushToRemoteBranch: () -> Unit,
onPullFromRemoteBranch: () -> Unit, onPullFromRemoteBranch: () -> Unit,
onChangeDefaultUpstreamBranch: () -> Unit,
): List<ContextMenuElement> { ): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().apply { return mutableListOf<ContextMenuElement>().apply {
if (!isCurrentBranch) { if (!isCurrentBranch) {
@ -68,6 +69,16 @@ fun branchContextMenuItems(
) )
) )
} }
if(isLocal) {
add(
ContextMenuElement.ContextTextEntry(
label = "Change default upstream branch",
onClick = onChangeDefaultUpstreamBranch
),
)
}
if (!isLocal) { if (!isLocal) {
add( add(
ContextMenuElement.ContextTextEntry( ContextMenuElement.ContextTextEntry(

View File

@ -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"
)
}
}
}

View File

@ -24,7 +24,6 @@ import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.dialogs.ErrorDialog import com.jetpackduba.gitnuro.ui.dialogs.ErrorDialog
import com.jetpackduba.gitnuro.ui.dialogs.MaterialDialog import com.jetpackduba.gitnuro.ui.dialogs.MaterialDialog
import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption import com.jetpackduba.gitnuro.ui.dropdowns.DropDownOption
import com.jetpackduba.gitnuro.ui.dropdowns.ScaleDropDown
import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel import com.jetpackduba.gitnuro.viewmodels.SettingsViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -168,9 +167,9 @@ fun UiSettings(settingsViewModel: SettingsViewModel) {
title = "Theme", title = "Theme",
subtitle = "Select the UI theme between light and dark mode", subtitle = "Select the UI theme between light and dark mode",
dropDownOptions = themeLists, dropDownOptions = themeLists,
currentOption = currentTheme, currentOption = DropDownOption(currentTheme, currentTheme.displayName),
onOptionSelected = { theme -> onOptionSelected = { themeDropDown ->
settingsViewModel.theme = theme settingsViewModel.theme = themeDropDown.value
} }
) )
@ -199,12 +198,12 @@ fun UiSettings(settingsViewModel: SettingsViewModel) {
var options by remember { var options by remember {
mutableStateOf( mutableStateOf(
listOf( listOf(
ScaleDropDown(1f, "100%"), DropDownOption(1f, "100%"),
ScaleDropDown(1.25f, "125%"), DropDownOption(1.25f, "125%"),
ScaleDropDown(1.5f, "150%"), DropDownOption(1.5f, "150%"),
ScaleDropDown(2f, "200%"), DropDownOption(2f, "200%"),
ScaleDropDown(2.5f, "250%"), DropDownOption(2.5f, "250%"),
ScaleDropDown(3f, "300%"), DropDownOption(3f, "300%"),
) )
) )
} }
@ -221,7 +220,7 @@ fun UiSettings(settingsViewModel: SettingsViewModel) {
if (matchingOption == null) { // Scale that we haven't taken in consideration if (matchingOption == null) { // Scale that we haven't taken in consideration
// Create a new scale and add it to the options list // 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() val newOptions = options.toMutableList()
newOptions.add(matchingOption) newOptions.add(matchingOption)
newOptions.sortBy { it.value } newOptions.sortBy { it.value }
@ -275,12 +274,12 @@ fun Category(
@Composable @Composable
fun <T : DropDownOption> SettingDropDown( fun <T> SettingDropDown(
title: String, title: String,
subtitle: String, subtitle: String,
dropDownOptions: List<T>, dropDownOptions: List<DropDownOption<T>>,
onOptionSelected: (T) -> Unit, onOptionSelected: (DropDownOption<T>) -> Unit,
currentOption: T, currentOption: DropDownOption<T>,
) { ) {
var showThemeDropdown by remember { mutableStateOf(false) } var showThemeDropdown by remember { mutableStateOf(false) }
Row( Row(

View File

@ -1,5 +1,3 @@
package com.jetpackduba.gitnuro.ui.dropdowns package com.jetpackduba.gitnuro.ui.dropdowns
interface DropDownOption { data class DropDownOption<T>(val value: T, val optionName: String)
val optionName: String
}

View File

@ -1,3 +0,0 @@
package com.jetpackduba.gitnuro.ui.dropdowns
class ScaleDropDown(val value: Float, override val optionName: String) : DropDownOption

View File

@ -48,11 +48,13 @@ import com.jetpackduba.gitnuro.theme.*
import com.jetpackduba.gitnuro.ui.SelectedItem import com.jetpackduba.gitnuro.ui.SelectedItem
import com.jetpackduba.gitnuro.ui.components.AvatarImage import com.jetpackduba.gitnuro.ui.components.AvatarImage
import com.jetpackduba.gitnuro.ui.components.ScrollableLazyColumn 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.components.gitnuroViewModel
import com.jetpackduba.gitnuro.ui.context_menu.* import com.jetpackduba.gitnuro.ui.context_menu.*
import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog import com.jetpackduba.gitnuro.ui.dialogs.NewBranchDialog
import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog import com.jetpackduba.gitnuro.ui.dialogs.NewTagDialog
import com.jetpackduba.gitnuro.ui.dialogs.ResetBranchDialog 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.LogSearch
import com.jetpackduba.gitnuro.viewmodels.LogStatus import com.jetpackduba.gitnuro.viewmodels.LogStatus
import com.jetpackduba.gitnuro.viewmodels.LogViewModel import com.jetpackduba.gitnuro.viewmodels.LogViewModel
@ -436,6 +438,7 @@ fun MessagesList(
onRebaseBranch = onRebase, onRebaseBranch = onRebase,
onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) }, onRebaseInteractive = { logViewModel.rebaseInteractive(graphNode) },
onRevCommitSelected = { logViewModel.selectLogLine(graphNode) }, onRevCommitSelected = { logViewModel.selectLogLine(graphNode) },
onChangeDefaultUpstreamBranch = { onShowLogDialog(LogDialog.ChangeDefaultBranch(it)) }
) )
} }
@ -587,6 +590,14 @@ fun LogDialogs(
LogDialog.None -> { LogDialog.None -> {
} }
is LogDialog.ChangeDefaultBranch -> {
SetDefaultUpstreamBranchDialog(
viewModel = gitnuroDynamicViewModel(),
branch = showLogDialog.ref,
onClose = { onResetShowLogDialog() },
)
}
} }
} }
@ -772,6 +783,7 @@ fun CommitLine(
onRebaseBranch: (Ref) -> Unit, onRebaseBranch: (Ref) -> Unit,
onRevCommitSelected: () -> Unit, onRevCommitSelected: () -> Unit,
onRebaseInteractive: () -> Unit, onRebaseInteractive: () -> Unit,
onChangeDefaultUpstreamBranch: (Ref) -> Unit,
) { ) {
val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name val isLastCommitOfCurrentBranch = currentBranch?.objectId?.name == graphNode.id.name
@ -828,6 +840,7 @@ fun CommitLine(
onRebaseBranch = { ref -> onRebaseBranch(ref) }, onRebaseBranch = { ref -> onRebaseBranch(ref) },
onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) }, onPushRemoteBranch = { ref -> logViewModel.pushToRemoteBranch(ref) },
onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) }, onPullRemoteBranch = { ref -> logViewModel.pullFromRemoteBranch(ref) },
onChangeDefaultUpstreamBranch = { ref -> onChangeDefaultUpstreamBranch(ref) },
) )
} }
} }
@ -849,6 +862,7 @@ fun CommitMessage(
onDeleteTag: (ref: Ref) -> Unit, onDeleteTag: (ref: Ref) -> Unit,
onPushRemoteBranch: (ref: Ref) -> Unit, onPushRemoteBranch: (ref: Ref) -> Unit,
onPullRemoteBranch: (ref: Ref) -> Unit, onPullRemoteBranch: (ref: Ref) -> Unit,
onChangeDefaultUpstreamBranch: (ref: Ref) -> Unit,
) { ) {
Row( Row(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -884,6 +898,7 @@ fun CommitMessage(
onRebaseBranch = { onRebaseBranch(ref) }, onRebaseBranch = { onRebaseBranch(ref) },
onPullRemoteBranch = { onPullRemoteBranch(ref) }, onPullRemoteBranch = { onPullRemoteBranch(ref) },
onPushRemoteBranch = { onPushRemoteBranch(ref) }, onPushRemoteBranch = { onPushRemoteBranch(ref) },
onChangeDefaultUpstreamBranch = { onChangeDefaultUpstreamBranch(ref) },
) )
} }
} }
@ -1092,6 +1107,7 @@ fun BranchChip(
onRebaseBranch: () -> Unit, onRebaseBranch: () -> Unit,
onPushRemoteBranch: () -> Unit, onPushRemoteBranch: () -> Unit,
onPullRemoteBranch: () -> Unit, onPullRemoteBranch: () -> Unit,
onChangeDefaultUpstreamBranch: () -> Unit,
color: Color, color: Color,
) { ) {
val contextMenuItemsList = { val contextMenuItemsList = {
@ -1107,6 +1123,7 @@ fun BranchChip(
onRebaseBranch = onRebaseBranch, onRebaseBranch = onRebaseBranch,
onPushToRemoteBranch = onPushRemoteBranch, onPushToRemoteBranch = onPushRemoteBranch,
onPullFromRemoteBranch = onPullRemoteBranch, onPullFromRemoteBranch = onPullRemoteBranch,
onChangeDefaultUpstreamBranch = onChangeDefaultUpstreamBranch,
) )
} }

View File

@ -1,10 +1,12 @@
package com.jetpackduba.gitnuro.ui.log package com.jetpackduba.gitnuro.ui.log
import com.jetpackduba.gitnuro.git.graph.GraphNode import com.jetpackduba.gitnuro.git.graph.GraphNode
import org.eclipse.jgit.lib.Ref
sealed class LogDialog { sealed class LogDialog {
object None : LogDialog() object None : LogDialog()
data class NewBranch(val graphNode: GraphNode) : LogDialog() data class NewBranch(val graphNode: GraphNode) : 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 ChangeDefaultBranch(val ref: Ref) : LogDialog()
} }

View File

@ -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>(SetDefaultUpstreamBranchState.Loading)
val setDefaultUpstreamBranchState: StateFlow<SetDefaultUpstreamBranchState> =
_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<RemoteInfo>,
val selectedRemote: RemoteInfo?,
val selectedBranch: Ref?,
) : SetDefaultUpstreamBranchState
object UpstreamChanged : SetDefaultUpstreamBranchState
}

View File

@ -4,6 +4,7 @@ import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.viewmodels.sidepanel.SidePanelViewModel import com.jetpackduba.gitnuro.viewmodels.sidepanel.SidePanelViewModel
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlin.reflect.KClass
@TabScope @TabScope
class TabViewModelsHolder @Inject constructor( class TabViewModelsHolder @Inject constructor(
@ -19,7 +20,8 @@ class TabViewModelsHolder @Inject constructor(
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>, private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
private val historyViewModelProvider: Provider<HistoryViewModel>, private val historyViewModelProvider: Provider<HistoryViewModel>,
private val authorViewModelProvider: Provider<AuthorViewModel>, private val authorViewModelProvider: Provider<AuthorViewModel>,
) { private val changeDefaultUpstreamBranchViewModelProvider: Provider<ChangeDefaultUpstreamBranchViewModel>,
) {
val viewModels = mapOf( val viewModels = mapOf(
logViewModel::class to logViewModel, logViewModel::class to logViewModel,
sidePanelViewModel::class to sidePanelViewModel, sidePanelViewModel::class to sidePanelViewModel,
@ -29,4 +31,16 @@ class TabViewModelsHolder @Inject constructor(
cloneViewModel::class to cloneViewModel, cloneViewModel::class to cloneViewModel,
settingsViewModel::class to settingsViewModel, 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")
}
}
} }

View File

@ -52,7 +52,6 @@ class BranchesViewModel @AssistedInject constructor(
) )
init { init {
tabScope.launch { tabScope.launch {
tabState.refreshFlowFiltered(RefreshType.ALL_DATA) tabState.refreshFlowFiltered(RefreshType.ALL_DATA)
{ {

View File

@ -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)
}
}