diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt index ce05a14..e50a9f3 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/branches/GetTrackingBranchUseCase.kt @@ -9,11 +9,15 @@ import javax.inject.Inject class GetTrackingBranchUseCase @Inject constructor() { operator fun invoke(git: Git, ref: Ref): TrackingBranch? { + return this.invoke(git, ref.simpleName) + } + + operator fun invoke(git: Git, refName: String): 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") + val remote: String? = config.getString("branch", refName, "remote") + val branch: String? = config.getString("branch", refName, "merge") if (remote != null && branch != null) { return TrackingBranch(remote, branch.removePrefix(BranchesConstants.UPSTREAM_BRANCH_CONFIG_PREFIX)) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt index 8732510..f7ab394 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/git/remote_operations/PushBranchUseCase.kt @@ -1,28 +1,70 @@ package com.jetpackduba.gitnuro.git.remote_operations +import com.jetpackduba.gitnuro.git.branches.GetTrackingBranchUseCase import com.jetpackduba.gitnuro.git.isRejected import com.jetpackduba.gitnuro.git.statusMessage +import com.jetpackduba.gitnuro.preferences.AppSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.ProgressMonitor +import org.eclipse.jgit.transport.RefLeaseSpec import org.eclipse.jgit.transport.RefSpec import javax.inject.Inject class PushBranchUseCase @Inject constructor( private val handleTransportUseCase: HandleTransportUseCase, + private val getTrackingBranchUseCase: GetTrackingBranchUseCase, + private val appSettings: AppSettings, ) { suspend operator fun invoke(git: Git, force: Boolean, pushTags: Boolean) = withContext(Dispatchers.IO) { - val currentBranchRefSpec = git.repository.fullBranch + val currentBranch = git.repository.fullBranch + val tracking = getTrackingBranchUseCase(git, git.repository.branch) + val refSpecStr = if (tracking != null) { + "$currentBranch:${Constants.R_HEADS}${tracking.branch}" + } else { + currentBranch + } val pushResult = git .push() - .setRefSpecs(RefSpec(currentBranchRefSpec)) + .setRefSpecs(RefSpec(refSpecStr)) + .run { + if (tracking != null) { + setRemote(tracking.remote) + } else { + this + } + } .setForce(force) - .apply { - if (pushTags) + .run { + if (force && appSettings.pushWithLease) { + + if (tracking != null) { + val remoteBranchName = "${Constants.R_REMOTES}$remote/${tracking.branch}" + + val remoteBranchRef = git.repository.findRef(remoteBranchName) + if (remoteBranchRef != null) { + return@run setRefLeaseSpecs( + RefLeaseSpec( + "${Constants.R_HEADS}${tracking.branch}", + remoteBranchRef.objectId.name + ) + ) + } + } + } + + return@run this + } + .run { + if (pushTags) { setPushTags() + } else { + this + } } .setTransportConfigCallback { handleTransportUseCase(it, git) } .setProgressMonitor(object : ProgressMonitor { @@ -35,14 +77,28 @@ class PushBranchUseCase @Inject constructor( }) .call() - val results = - pushResult.map { it.remoteUpdates.filter { remoteRefUpdate -> remoteRefUpdate.status.isRejected } } - .flatten() + val results = pushResult + .map { + it.remoteUpdates.filter { remoteRefUpdate -> remoteRefUpdate.status.isRejected } + } + .flatten() if (results.isNotEmpty()) { val error = StringBuilder() results.forEach { result -> - error.append(result.statusMessage) + val statusMessage = result.statusMessage + val extraMessage = if (statusMessage == "Ref rejected, old object id in remote has changed.") { + "Force push can't be completed without fetching first the remote changes." + } else + null + + error.append(statusMessage) + + if (extraMessage != null) { + error.append("\n") + error.append(extraMessage) + } + error.append("\n") } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt index 4d972c9..91b675a 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/preferences/AppSettings.kt @@ -7,10 +7,7 @@ import com.jetpackduba.gitnuro.theme.ColorsScheme import com.jetpackduba.gitnuro.theme.Theme import com.jetpackduba.gitnuro.viewmodels.TextDiffType import com.jetpackduba.gitnuro.viewmodels.textDiffTypeFromValue -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.* import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import java.io.File @@ -37,6 +34,7 @@ private const val PREF_TERMINAL_PATH = "terminalPath" private const val PREF_GIT_FF_MERGE = "gitFFMerge" private const val PREF_GIT_PULL_REBASE = "gitPullRebase" +private const val PREF_GIT_PUSH_WITH_LEASE = "gitPushWithLease" private const val DEFAULT_COMMITS_LIMIT = 1000 private const val DEFAULT_COMMITS_LIMIT_ENABLED = true @@ -62,6 +60,9 @@ class AppSettings @Inject constructor() { private val _pullRebaseFlow = MutableStateFlow(pullRebase) val pullRebaseFlow = _pullRebaseFlow.asStateFlow() + private val _pushWithLeaseFlow = MutableStateFlow(pushWithLease) + val pushWithLeaseFlow: StateFlow = _pushWithLeaseFlow.asStateFlow() + private val _commitsLimitFlow = MutableSharedFlow() val commitsLimitFlow = _commitsLimitFlow.asSharedFlow() @@ -164,6 +165,15 @@ class AppSettings @Inject constructor() { _pullRebaseFlow.value = value } + var pushWithLease: Boolean + get() { + return preferences.getBoolean(PREF_GIT_PUSH_WITH_LEASE, true) + } + set(value) { + preferences.putBoolean(PREF_GIT_PUSH_WITH_LEASE, value) + _pushWithLeaseFlow.value = value + } + val commitsLimit: Int get() { return preferences.getInt(PREF_COMMITS_LIMIT, DEFAULT_COMMITS_LIMIT) 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 dc0d308..352af57 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 @@ -219,6 +219,7 @@ private fun CommitsHistory(settingsViewModel: SettingsViewModel) { @Composable private fun RemoteActions(settingsViewModel: SettingsViewModel) { val pullRebase by settingsViewModel.pullRebaseFlow.collectAsState() + val pushWithLease by settingsViewModel.pushWithLeaseFlow.collectAsState() SettingToggle( title = "Pull with rebase as default", @@ -228,6 +229,15 @@ private fun RemoteActions(settingsViewModel: SettingsViewModel) { settingsViewModel.pullRebase = value } ) + + SettingToggle( + title = "Force push with lease", + subtitle = "Check if the local version remote branch is up to date to avoid accidentally overriding unintended commits", + value = pushWithLease, + onValueChanged = { value -> + settingsViewModel.pushWithLease = value + } + ) } @Composable diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index 52c2d34..37f3b98 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -24,6 +24,7 @@ class SettingsViewModel @Inject constructor( val themeState = appSettings.themeState val ffMergeFlow = appSettings.ffMergeFlow val pullRebaseFlow = appSettings.pullRebaseFlow + val pushWithLeaseFlow = appSettings.pushWithLeaseFlow val commitsLimitEnabledFlow = appSettings.commitsLimitEnabledFlow val swapUncommitedChangesFlow = appSettings.swapUncommitedChangesFlow val terminalPathFlow = appSettings.terminalPathFlow @@ -58,6 +59,12 @@ class SettingsViewModel @Inject constructor( appSettings.pullRebase = value } + var pushWithLease: Boolean + get() = appSettings.pushWithLease + set(value) { + appSettings.pushWithLease = value + } + var theme: Theme get() = appSettings.theme set(value) {