feat: add branch filtering
This commit is contained in:
parent
1d5085b689
commit
81d3c9450b
@ -0,0 +1,34 @@
|
|||||||
|
package com.jetpackduba.gitnuro.extensions
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Ref
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predicate for filtering branches
|
||||||
|
*/
|
||||||
|
typealias BranchFilter = (ref: Ref) -> Boolean
|
||||||
|
|
||||||
|
fun Ref.matchesAll(filters: List<BranchFilter>): Boolean {
|
||||||
|
return filters.all { filter -> filter(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches only branches from the specified remote name.
|
||||||
|
*/
|
||||||
|
class OriginFilter(
|
||||||
|
private val remoteName: String
|
||||||
|
) : BranchFilter {
|
||||||
|
override fun invoke(ref: Ref): Boolean {
|
||||||
|
return ref.name.startsWith("refs/remotes/$remoteName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches only branches that contain a specific keyword in its name.
|
||||||
|
*/
|
||||||
|
class BranchNameContainsFilter(
|
||||||
|
private val keyword: String
|
||||||
|
) : BranchFilter {
|
||||||
|
override fun invoke(ref: Ref): Boolean {
|
||||||
|
return ref.name.contains(keyword)
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,8 @@ class TabState @Inject constructor(
|
|||||||
val selectedItem: StateFlow<SelectedItem> = _selectedItem
|
val selectedItem: StateFlow<SelectedItem> = _selectedItem
|
||||||
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
private val _taskEvent = MutableSharedFlow<TaskEvent>()
|
||||||
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
|
||||||
|
private val _branchFilterKeyword = MutableStateFlow("")
|
||||||
|
val branchFilterKeyword: StateFlow<String> = _branchFilterKeyword
|
||||||
|
|
||||||
private var unsafeGit: Git? = null
|
private var unsafeGit: Git? = null
|
||||||
val git: Git
|
val git: Git
|
||||||
@ -160,6 +162,10 @@ class TabState @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newBranchFilter(keyword: String) = runOperation(refreshType = RefreshType.BRANCH_FILTER) {
|
||||||
|
_branchFilterKeyword.value = keyword
|
||||||
|
}
|
||||||
|
|
||||||
private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
|
private fun findCommit(git: Git, objectId: ObjectId): RevCommit {
|
||||||
return git.repository.parseCommit(objectId)
|
return git.repository.parseCommit(objectId)
|
||||||
}
|
}
|
||||||
@ -202,6 +208,7 @@ enum class RefreshType {
|
|||||||
UNCOMMITED_CHANGES,
|
UNCOMMITED_CHANGES,
|
||||||
UNCOMMITED_CHANGES_AND_LOG,
|
UNCOMMITED_CHANGES_AND_LOG,
|
||||||
REMOTES,
|
REMOTES,
|
||||||
|
BRANCH_FILTER
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Processing {
|
enum class Processing {
|
||||||
|
43
src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt
Normal file
43
src/main/kotlin/com/jetpackduba/gitnuro/ui/BranchFilter.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.jetpackduba.gitnuro.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jetpackduba.gitnuro.ui.components.AdjustableOutlinedTextField
|
||||||
|
import com.jetpackduba.gitnuro.ui.components.gitnuroViewModel
|
||||||
|
import com.jetpackduba.gitnuro.viewmodels.BranchFilterViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BranchFilter(
|
||||||
|
branchFilterViewModel: BranchFilterViewModel = gitnuroViewModel()
|
||||||
|
) {
|
||||||
|
val filterKeyword by branchFilterViewModel.keyword.collectAsState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
AdjustableOutlinedTextField(
|
||||||
|
value = filterKeyword,
|
||||||
|
onValueChange = { branchFilterViewModel.newBranchFilter(keyword = it) },
|
||||||
|
hint = "Filter Branches",
|
||||||
|
textStyle = MaterialTheme.typography.caption,
|
||||||
|
leadingIconResourcePath = "branch_filter.svg",
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 8.dp)
|
||||||
|
.defaultMinSize(minHeight = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -303,6 +303,7 @@ fun MainContentView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
|
BranchFilter()
|
||||||
Branches()
|
Branches()
|
||||||
Remotes()
|
Remotes()
|
||||||
Tags()
|
Tags()
|
||||||
|
@ -7,10 +7,7 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextFieldColors
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -20,6 +17,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.takeOrElse
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -46,6 +44,7 @@ fun AdjustableOutlinedTextField(
|
|||||||
shape: Shape = RoundedCornerShape(4.dp),
|
shape: Shape = RoundedCornerShape(4.dp),
|
||||||
backgroundColor: Color = MaterialTheme.colors.background,
|
backgroundColor: Color = MaterialTheme.colors.background,
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
leadingIconResourcePath: String = "",
|
||||||
) {
|
) {
|
||||||
val textColor = textStyle.color.takeOrElse {
|
val textColor = textStyle.color.takeOrElse {
|
||||||
colors.textColor(enabled).value
|
colors.textColor(enabled).value
|
||||||
@ -83,20 +82,39 @@ fun AdjustableOutlinedTextField(
|
|||||||
)
|
)
|
||||||
.padding(horizontal = 12.dp),
|
.padding(horizontal = 12.dp),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.wrapContentHeight(),
|
||||||
|
) {
|
||||||
|
if (leadingIconResourcePath.isNotEmpty()) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(leadingIconResourcePath),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.size(16.dp),
|
||||||
|
tint = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
modifier = Modifier.wrapContentHeight(),
|
||||||
) {
|
) {
|
||||||
innerTextField()
|
innerTextField()
|
||||||
|
if (value.isEmpty() && hint.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
hint,
|
||||||
|
style = textStyle.copy(color = MaterialTheme.colors.onBackgroundSecondary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (value.isEmpty() && hint.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
hint,
|
|
||||||
color = MaterialTheme.colors.onBackgroundSecondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 12.dp, top = 12.dp),
|
|
||||||
style = MaterialTheme.typography.body2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.git.TabState
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BranchFilterViewModel @Inject constructor(
|
||||||
|
private val tabState: TabState
|
||||||
|
) {
|
||||||
|
val keyword: StateFlow<String> = tabState.branchFilterKeyword
|
||||||
|
|
||||||
|
fun newBranchFilter(keyword: String) {
|
||||||
|
tabState.newBranchFilter(keyword)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.jetpackduba.gitnuro.viewmodels
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
|
import com.jetpackduba.gitnuro.extensions.BranchNameContainsFilter
|
||||||
import com.jetpackduba.gitnuro.git.RefreshType
|
import com.jetpackduba.gitnuro.git.RefreshType
|
||||||
import com.jetpackduba.gitnuro.git.TabState
|
import com.jetpackduba.gitnuro.git.TabState
|
||||||
import com.jetpackduba.gitnuro.git.branches.*
|
import com.jetpackduba.gitnuro.git.branches.*
|
||||||
@ -41,8 +42,7 @@ class BranchesViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
tabScope.launch {
|
tabScope.launch {
|
||||||
tabState.refreshFlowFiltered(RefreshType.ALL_DATA)
|
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.BRANCH_FILTER) {
|
||||||
{
|
|
||||||
refresh(tabState.git)
|
refresh(tabState.git)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,11 @@ class BranchesViewModel @Inject constructor(
|
|||||||
private suspend fun loadBranches(git: Git) {
|
private suspend fun loadBranches(git: Git) {
|
||||||
_currentBranch.value = getCurrentBranchUseCase(git)
|
_currentBranch.value = getCurrentBranchUseCase(git)
|
||||||
|
|
||||||
val branchesList = getBranchesUseCase(git).toMutableList()
|
val branchNameFilter = BranchNameContainsFilter(keyword = tabState.branchFilterKeyword.value)
|
||||||
|
|
||||||
|
val branchesList = getBranchesUseCase(git)
|
||||||
|
.filter(branchNameFilter)
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
// set selected branch as the first one always
|
// set selected branch as the first one always
|
||||||
val selectedBranch = branchesList.find { it.name == _currentBranch.value?.name }
|
val selectedBranch = branchesList.find { it.name == _currentBranch.value?.name }
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package com.jetpackduba.gitnuro.viewmodels
|
package com.jetpackduba.gitnuro.viewmodels
|
||||||
|
|
||||||
import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException
|
import com.jetpackduba.gitnuro.exceptions.InvalidRemoteUrlException
|
||||||
|
import com.jetpackduba.gitnuro.extensions.BranchNameContainsFilter
|
||||||
|
import com.jetpackduba.gitnuro.extensions.OriginFilter
|
||||||
|
import com.jetpackduba.gitnuro.extensions.matchesAll
|
||||||
import com.jetpackduba.gitnuro.git.RefreshType
|
import com.jetpackduba.gitnuro.git.RefreshType
|
||||||
import com.jetpackduba.gitnuro.git.TabState
|
import com.jetpackduba.gitnuro.git.TabState
|
||||||
import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase
|
import com.jetpackduba.gitnuro.git.branches.DeleteLocallyRemoteBranchesUseCase
|
||||||
@ -36,8 +39,7 @@ class RemotesViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
tabScope.launch {
|
tabScope.launch {
|
||||||
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES)
|
tabState.refreshFlowFiltered(RefreshType.ALL_DATA, RefreshType.REMOTES, RefreshType.BRANCH_FILTER) {
|
||||||
{
|
|
||||||
refresh(tabState.git)
|
refresh(tabState.git)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,9 +53,14 @@ class RemotesViewModel @Inject constructor(
|
|||||||
getRemotesUseCase(git, allRemoteBranches)
|
getRemotesUseCase(git, allRemoteBranches)
|
||||||
|
|
||||||
val remoteInfoList = remotes.map { remoteConfig ->
|
val remoteInfoList = remotes.map { remoteConfig ->
|
||||||
val remoteBranches = allRemoteBranches.filter { branch ->
|
val filters = listOf(
|
||||||
branch.name.startsWith("refs/remotes/${remoteConfig.name}")
|
OriginFilter(remoteName = remoteConfig.name),
|
||||||
}
|
BranchNameContainsFilter(keyword = tabState.branchFilterKeyword.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
val remoteBranches = allRemoteBranches
|
||||||
|
.filter { it.matchesAll(filters) }
|
||||||
|
|
||||||
RemoteInfo(remoteConfig, remoteBranches)
|
RemoteInfo(remoteConfig, remoteBranches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ class TabViewModel @Inject constructor(
|
|||||||
val errorsManager: ErrorsManager = tabState.errorsManager
|
val errorsManager: ErrorsManager = tabState.errorsManager
|
||||||
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
val selectedItem: StateFlow<SelectedItem> = tabState.selectedItem
|
||||||
var diffViewModel: DiffViewModel? = null
|
var diffViewModel: DiffViewModel? = null
|
||||||
|
val branchFilterKeyword: StateFlow<String> = tabState.branchFilterKeyword
|
||||||
|
|
||||||
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
|
var rebaseInteractiveViewModel: RebaseInteractiveViewModel? = null
|
||||||
private set
|
private set
|
||||||
|
@ -17,6 +17,7 @@ class TabViewModelsHolder @Inject constructor(
|
|||||||
commitChangesViewModel: CommitChangesViewModel,
|
commitChangesViewModel: CommitChangesViewModel,
|
||||||
cloneViewModel: CloneViewModel,
|
cloneViewModel: CloneViewModel,
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
|
branchFilterViewModel: BranchFilterViewModel,
|
||||||
// Dynamic VM
|
// Dynamic VM
|
||||||
private val diffViewModelProvider: Provider<DiffViewModel>,
|
private val diffViewModelProvider: Provider<DiffViewModel>,
|
||||||
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
|
private val rebaseInteractiveViewModelProvider: Provider<RebaseInteractiveViewModel>,
|
||||||
@ -35,6 +36,7 @@ class TabViewModelsHolder @Inject constructor(
|
|||||||
commitChangesViewModel::class to commitChangesViewModel,
|
commitChangesViewModel::class to commitChangesViewModel,
|
||||||
cloneViewModel::class to cloneViewModel,
|
cloneViewModel::class to cloneViewModel,
|
||||||
settingsViewModel::class to settingsViewModel,
|
settingsViewModel::class to settingsViewModel,
|
||||||
|
branchFilterViewModel::class to branchFilterViewModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
3
src/main/resources/branch_filter.svg
Normal file
3
src/main/resources/branch_filter.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
|
||||||
|
<path d="M10 18v-2h4v2Zm-4-5v-2h12v2ZM3 8V6h18v2Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 127 B |
Loading…
Reference in New Issue
Block a user