Gitnuro/src/main/kotlin/com/jetpackduba/gitnuro/git/TabState.kt
Abdelilah El Aissaoui 7a7eb3ad93
Refactored repository watcher
Refactored to use a rust implementation instead of the java impl, because the java impl has been unrelible in linux and macos
2023-07-14 14:28:29 +02:00

309 lines
10 KiB
Kotlin

package com.jetpackduba.gitnuro.git
import com.jetpackduba.gitnuro.di.TabScope
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.git.log.FindCommitUseCase
import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.managers.ErrorsManager
import com.jetpackduba.gitnuro.managers.newErrorNow
import com.jetpackduba.gitnuro.ui.SelectedItem
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
private const val TAG = "TabState"
interface ProcessingInfo {
fun changeSubtitle(newSubtitle: String)
fun changeIsCancellable(newIsCancellable: Boolean)
}
sealed interface ProcessingState {
object None : ProcessingState
data class Processing(
val title: String,
val subtitle: String,
val isCancellable: Boolean,
) : ProcessingState
}
@TabScope
class TabState @Inject constructor(
val errorsManager: ErrorsManager,
private val scope: CoroutineScope,
private val findCommitUseCase: FindCommitUseCase,
) {
private val _selectedItem = MutableStateFlow<SelectedItem>(SelectedItem.UncommitedChanges)
val selectedItem: StateFlow<SelectedItem> = _selectedItem
private val _taskEvent = MutableSharedFlow<TaskEvent>()
val taskEvent: SharedFlow<TaskEvent> = _taskEvent
private var unsafeGit: Git? = null
val git: Git
get() {
val unsafeGit = this.unsafeGit
if (unsafeGit == null) {
throw CancellationException("Repository not available")
} else
return unsafeGit
}
private val _refreshData = MutableSharedFlow<RefreshType>()
val refreshData: SharedFlow<RefreshType> = _refreshData
/**
* Property that indicates if a git operation is running
*/
@set:Synchronized
var operationRunning = false
private var currentJob: Job? = null
private val _processing = MutableStateFlow<ProcessingState>(ProcessingState.None)
val processing: StateFlow<ProcessingState> = _processing
fun initGit(git: Git) {
this.unsafeGit = git
}
@Synchronized
fun safeProcessing(
showError: Boolean = true,
refreshType: RefreshType,
// TODO Eventually the title and subtitles should be mandatory but for now the default it's empty to slowly
// migrate the code that uses this function
title: String = "",
subtitle: String = "",
// TODO For now have it always as false because the data refresh is cancelled even when the git process couldn't be cancelled
isCancellable: Boolean = false,
refreshEvenIfCrashes: Boolean = false,
refreshEvenIfCrashesInteractive: ((Exception) -> Boolean)? = null,
callback: suspend ProcessingInfo.(git: Git) -> Unit
): Job {
val job = scope.launch(Dispatchers.IO) {
var hasProcessFailed = false
var refreshEvenIfCrashesInteractiveResult = false
operationRunning = true
val processingInfo: ProcessingInfo = object : ProcessingInfo {
override fun changeSubtitle(newSubtitle: String) {
_processing.update { processingState ->
if (processingState is ProcessingState.Processing) {
processingState.copy(subtitle = newSubtitle)
} else {
ProcessingState.Processing(
title = title,
isCancellable = isCancellable,
subtitle = newSubtitle
)
}
}
}
override fun changeIsCancellable(newIsCancellable: Boolean) {
_processing.update { processingState ->
if (processingState is ProcessingState.Processing) {
processingState.copy(isCancellable = newIsCancellable)
} else {
ProcessingState.Processing(
title = title,
isCancellable = newIsCancellable,
subtitle = subtitle
)
}
}
}
}
try {
delayedStateChange(
delayMs = 300,
onDelayTriggered = {
_processing.update { processingState ->
if (processingState is ProcessingState.None) {
ProcessingState.Processing(title, subtitle, isCancellable)
} else {
processingState
}
}
}
) {
processingInfo.callback(git)
}
} catch (ex: Exception) {
hasProcessFailed = true
ex.printStackTrace()
refreshEvenIfCrashesInteractiveResult = refreshEvenIfCrashesInteractive?.invoke(ex) ?: false
val containsCancellation = exceptionContainsCancellation(ex)
if (showError && !containsCancellation)
errorsManager.addError(newErrorNow(ex, null, ex.message.orEmpty()))
printError(TAG, ex.message.orEmpty(), ex)
} finally {
_processing.value = ProcessingState.None
operationRunning = false
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes || refreshEvenIfCrashesInteractiveResult)) {
_refreshData.emit(refreshType)
}
}
}
this.currentJob = job
return job
}
private fun exceptionContainsCancellation(ex: Throwable?): Boolean {
return when (ex) {
null -> false
ex.cause -> false
is CancellationException -> true
else -> exceptionContainsCancellation(ex.cause)
}
}
fun safeProcessingWithoutGit(
showError: Boolean = true,
// TODO Eventually the title and subtitles should be mandatory but for now the default it's empty to slowly
// migrate the code that uses this function
title: String = "",
subtitle: String = "",
isCancellable: Boolean = false,
callback: suspend CoroutineScope.() -> Unit
): Job {
val job = scope.launch(Dispatchers.IO) {
_processing.value = ProcessingState.Processing(title, subtitle, isCancellable)
operationRunning = true
try {
this.callback()
} catch (ex: Exception) {
ex.printStackTrace()
val containsCancellation = exceptionContainsCancellation(ex)
if (showError && !containsCancellation)
errorsManager.addError(newErrorNow(ex, null, ex.localizedMessage))
printError(TAG, ex.message.orEmpty(), ex)
} finally {
_processing.value = ProcessingState.None
operationRunning = false
}
}
this.currentJob = job
return job
}
fun runOperation(
showError: Boolean = false,
refreshType: RefreshType,
refreshEvenIfCrashes: Boolean = false,
block: suspend (git: Git) -> Unit
) = scope.launch(Dispatchers.IO) {
var hasProcessFailed = false
operationRunning = true
try {
block(git)
} catch (ex: Exception) {
ex.printStackTrace()
hasProcessFailed = true
if (showError)
errorsManager.addError(newErrorNow(ex, null, ex.localizedMessage))
printError(TAG, ex.message.orEmpty(), ex)
} finally {
if (refreshType != RefreshType.NONE && (!hasProcessFailed || refreshEvenIfCrashes))
_refreshData.emit(refreshType)
operationRunning = false
}
}
suspend fun refreshData(refreshType: RefreshType) {
_refreshData.emit(refreshType)
}
suspend fun newSelectedStash(stash: RevCommit) {
newSelectedItem(SelectedItem.Stash(stash))
}
suspend fun noneSelected() {
newSelectedItem(SelectedItem.None)
}
fun newSelectedRef(objectId: ObjectId?) = runOperation(
refreshType = RefreshType.NONE,
) { git ->
if (objectId == null) {
newSelectedItem(SelectedItem.None)
} else {
val commit = findCommitUseCase(git, objectId)
if (commit == null) {
newSelectedItem(SelectedItem.None)
} else {
val newSelectedItem = SelectedItem.Ref(commit)
newSelectedItem(newSelectedItem)
_taskEvent.emit(TaskEvent.ScrollToGraphItem(newSelectedItem))
}
}
}
suspend fun newSelectedItem(selectedItem: SelectedItem, scrollToItem: Boolean = false) {
_selectedItem.value = selectedItem
if (scrollToItem) {
_taskEvent.emit(TaskEvent.ScrollToGraphItem(selectedItem))
}
}
suspend fun emitNewTaskEvent(taskEvent: TaskEvent) {
_taskEvent.emit(taskEvent)
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun refreshFlowFiltered(vararg filters: RefreshType, callback: suspend (RefreshType) -> Unit) {
refreshData
.filter { refreshType ->
filters.contains(refreshType)
}.collect {
try {
callback(it)
} catch (ex: Exception) {
ex.printStackTrace()
errorsManager.addError(newErrorNow(ex, null, ex.localizedMessage))
}
}
}
fun cancelCurrentTask() {
currentJob?.cancel()
}
}
enum class RefreshType {
NONE,
ALL_DATA,
REPO_STATE,
ONLY_LOG,
STASHES,
SUBMODULES,
UNCOMMITED_CHANGES,
UNCOMMITED_CHANGES_AND_LOG,
REMOTES,
REBASE_INTERACTIVE_STATE,
}