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

View File

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

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) {
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 = {},
onPushToRemoteBranch: () -> Unit,
onPullFromRemoteBranch: () -> Unit,
onChangeDefaultUpstreamBranch: () -> Unit,
): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().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(

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.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 <T : DropDownOption> SettingDropDown(
fun <T> SettingDropDown(
title: String,
subtitle: String,
dropDownOptions: List<T>,
onOptionSelected: (T) -> Unit,
currentOption: T,
dropDownOptions: List<DropDownOption<T>>,
onOptionSelected: (DropDownOption<T>) -> Unit,
currentOption: DropDownOption<T>,
) {
var showThemeDropdown by remember { mutableStateOf(false) }
Row(

View File

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

View File

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

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 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<RebaseInteractiveViewModel>,
private val historyViewModelProvider: Provider<HistoryViewModel>,
private val authorViewModelProvider: Provider<AuthorViewModel>,
) {
private val changeDefaultUpstreamBranchViewModelProvider: Provider<ChangeDefaultUpstreamBranchViewModel>,
) {
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")
}
}
}

View File

@ -52,7 +52,6 @@ class BranchesViewModel @AssistedInject constructor(
)
init {
tabScope.launch {
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)
}
}