Started arch refactor

This commit is contained in:
Abdelilah El Aissaoui 2022-08-20 03:19:31 +02:00
parent cbcb13d730
commit 270951fe66
33 changed files with 844 additions and 653 deletions

View File

@ -1,61 +0,0 @@
package app.git
import app.models.AuthorInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.storage.file.FileBasedConfig
import javax.inject.Inject
class AuthorManager @Inject constructor() {
suspend fun loadAuthor(git: Git) = withContext(Dispatchers.IO) {
val config = git.repository.config
val globalConfig = git.repository.config.baseConfig
val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs)
repoConfig.load()
val globalName = globalConfig.getString("user", null, "name")
val globalEmail = globalConfig.getString("user", null, "email")
val name = repoConfig.getString("user", null, "name")
val email = repoConfig.getString("user", null, "email")
return@withContext AuthorInfo(
globalName,
globalEmail,
name,
email,
)
}
suspend fun saveAuthorInfo(git: Git, newAuthorInfo: AuthorInfo) = withContext(Dispatchers.IO) {
val config = git.repository.config
val globalConfig = git.repository.config.baseConfig
val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs)
repoConfig.load()
if (globalConfig is FileBasedConfig) {
globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
globalConfig.save()
}
config.setStringProperty("user", null, "name", newAuthorInfo.name)
config.setStringProperty("user", null, "email", newAuthorInfo.email)
config.save()
}
private fun FileBasedConfig.setStringProperty(
section: String,
subsection: String?,
name: String,
value: String?,
) {
if (value == null) {
unset(section, subsection, name)
} else {
setString(section, subsection, name, value)
}
}
}

View File

@ -1,95 +0,0 @@
package app.git
import app.extensions.isBranch
import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.CreateBranchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class BranchesManager @Inject constructor() {
/**
* Returns the current branch in [Ref]. If the repository is new, the current branch will be null.
*/
suspend fun currentBranchRef(git: Git): Ref? {
val branchList = getBranches(git)
val branchName = git
.repository
.fullBranch
var branchFound = branchList.firstOrNull {
it.name == branchName
}
if (branchFound == null) {
branchFound = branchList.firstOrNull {
it.objectId.name == branchName // Try to get the HEAD
}
}
return branchFound
}
suspend fun getBranches(git: Git) = withContext(Dispatchers.IO) {
return@withContext git
.branchList()
.call()
}
suspend fun createBranch(git: Git, branchName: String) = withContext(Dispatchers.IO) {
git
.checkout()
.setCreateBranch(true)
.setName(branchName)
.call()
}
suspend fun createBranchOnCommit(git: Git, branch: String, revCommit: RevCommit) = withContext(Dispatchers.IO) {
git
.checkout()
.setCreateBranch(true)
.setName(branch)
.setStartPoint(revCommit)
.call()
}
suspend fun deleteBranch(git: Git, branch: Ref) = withContext(Dispatchers.IO) {
git
.branchDelete()
.setBranchNames(branch.name)
.setForce(true) // TODO Should it be forced?
.call()
}
suspend fun deleteLocallyRemoteBranches(git: Git, branches: List<String>) = withContext(Dispatchers.IO) {
git
.branchDelete()
.setBranchNames(*branches.toTypedArray())
.setForce(true)
.call()
}
suspend fun remoteBranches(git: Git) = withContext(Dispatchers.IO) {
git
.branchList()
.setListMode(ListBranchCommand.ListMode.REMOTE)
.call()
}
suspend fun checkoutRef(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
git.checkout().apply {
setName(ref.name)
if (ref.isBranch && ref.name.startsWith("refs/remotes/")) {
setCreateBranch(true)
setName(ref.simpleName)
setStartPoint(ref.objectId.name)
setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
}
call()
}
}
}

View File

@ -0,0 +1,34 @@
package app.git
import org.eclipse.jgit.transport.RemoteRefUpdate
import java.io.File
sealed class CloneStatus {
object None : CloneStatus()
data class Cloning(val taskName: String, val progress: Int, val total: Int) : CloneStatus()
object Cancelling : CloneStatus()
data class Fail(val reason: String) : CloneStatus()
data class Completed(val repoDir: File) : CloneStatus()
}
val RemoteRefUpdate.Status.isRejected: Boolean
get() {
return this == RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ||
this == RemoteRefUpdate.Status.REJECTED_NODELETE ||
this == RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED ||
this == RemoteRefUpdate.Status.REJECTED_OTHER_REASON
}
val RemoteRefUpdate.statusMessage: String
get() {
return when (this.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> "Failed to push some refs to ${this.remoteName}. " +
"Updates were rejected because the remote contains work that you do not have locally. Pulling changes from remote may help."
RemoteRefUpdate.Status.REJECTED_NODELETE -> "Could not delete ref because the remote doesn't support deleting refs."
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED -> "Ref rejected, old object id in remote has changed."
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> this.message ?: "Push rejected for unknown reasons."
else -> ""
}
}

View File

@ -5,6 +5,7 @@ import app.di.RawFileManagerFactory
import app.exceptions.MissingDiffEntryException import app.exceptions.MissingDiffEntryException
import app.extensions.fullData import app.extensions.fullData
import app.extensions.isMerging import app.extensions.isMerging
import app.git.branches.GetCurrentBranchUseCase
import app.git.diff.DiffResult import app.git.diff.DiffResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -28,7 +29,7 @@ import javax.inject.Inject
class DiffManager @Inject constructor( class DiffManager @Inject constructor(
private val rawFileManagerFactory: RawFileManagerFactory, private val rawFileManagerFactory: RawFileManagerFactory,
private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory, private val hunkDiffGeneratorFactory: HunkDiffGeneratorFactory,
private val branchesManager: BranchesManager, private val getCurrentBranchUseCase: GetCurrentBranchUseCase,
private val repositoryManager: RepositoryManager, private val repositoryManager: RepositoryManager,
) { ) {
suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) { suspend fun diffFormat(git: Git, diffEntryType: DiffEntryType): DiffResult = withContext(Dispatchers.IO) {
@ -57,7 +58,7 @@ class DiffManager @Inject constructor(
.setCached(cached).apply { .setCached(cached).apply {
val repositoryState = repositoryManager.getRepositoryState(git) val repositoryState = repositoryManager.getRepositoryState(git)
if ( if (
branchesManager.currentBranchRef(git) == null && getCurrentBranchUseCase(git) == null &&
!repositoryState.isMerging && !repositoryState.isMerging &&
!repositoryState.isRebasing && !repositoryState.isRebasing &&
cached cached

View File

@ -1,6 +1,7 @@
package app.git package app.git
import app.exceptions.UncommitedChangesDetectedException import app.exceptions.UncommitedChangesDetectedException
import app.git.branches.GetCurrentBranchUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@ -16,7 +17,7 @@ import org.eclipse.jgit.revwalk.RevWalk
import javax.inject.Inject import javax.inject.Inject
class RebaseManager @Inject constructor( class RebaseManager @Inject constructor(
private val branchesManager: BranchesManager, private val getCurrentBranchUseCase: GetCurrentBranchUseCase,
) { ) {
suspend fun rebaseBranch(git: Git, ref: Ref) = withContext(Dispatchers.IO) { suspend fun rebaseBranch(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
@ -127,7 +128,7 @@ class RebaseManager @Inject constructor(
} }
private suspend fun markCurrentBranchAsStart(revWalk: RevWalk, git: Git) { private suspend fun markCurrentBranchAsStart(revWalk: RevWalk, git: Git) {
val currentBranch = branchesManager.currentBranchRef(git) ?: throw Exception("Null current branch") val currentBranch = getCurrentBranchUseCase(git) ?: throw Exception("Null current branch")
val refTarget = revWalk.parseAny(currentBranch.leaf.objectId) val refTarget = revWalk.parseAny(currentBranch.leaf.objectId)
if (refTarget is RevCommit) if (refTarget is RevCommit)

View File

@ -1,297 +0,0 @@
package app.git
import app.credentials.GSessionManager
import app.credentials.HttpCredentialsProvider
import app.extensions.remoteName
import app.extensions.simpleName
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.ProgressMonitor
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.*
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
class RemoteOperationsManager @Inject constructor(
private val sessionManager: GSessionManager
) {
suspend fun pull(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
val pullResult = git
.pull()
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) {
var message = "Pull failed"
if (rebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
suspend fun pullFromBranch(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
val pullResult = git
.pull()
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) {
var message = "Pull failed"
if (rebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
suspend fun fetchAll(git: Git) = withContext(Dispatchers.IO) {
val remotes = git.remoteList().call()
for (remote in remotes) {
git.fetch()
.setRemote(remote.name)
.setRefSpecs(remote.fetchRefSpecs)
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
}
}
suspend fun push(git: Git, force: Boolean, pushTags: Boolean) = withContext(Dispatchers.IO) {
val currentBranchRefSpec = git.repository.fullBranch
val pushResult = git
.push()
.setRefSpecs(RefSpec(currentBranchRefSpec))
.setForce(force)
.apply {
if (pushTags)
setPushTags()
}
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.call()
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)
error.append("\n")
}
throw Exception(error.toString())
}
}
suspend fun pushToBranch(git: Git, force: Boolean, pushTags: Boolean, remoteBranch: Ref) =
withContext(Dispatchers.IO) {
val currentBranchRefSpec = git.repository.fullBranch
val pushResult = git
.push()
.setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}"))
.setRemote(remoteBranch.remoteName)
.setForce(force)
.apply {
if (pushTags)
setPushTags()
}
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.call()
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)
error.append("\n")
}
throw Exception(error.toString())
}
}
private fun handleTransportCredentials(transport: Transport?) {
if (transport is SshTransport) {
transport.sshSessionFactory = sessionManager.generateSshSessionFactory()
} else if (transport is HttpTransport) {
transport.credentialsProvider = HttpCredentialsProvider()
}
}
suspend fun deleteBranch(git: Git, ref: Ref) = withContext(Dispatchers.IO) {
val branchSplit = ref.name.split("/").toMutableList()
val remoteName = branchSplit[2] // Remote name
repeat(3) {
branchSplit.removeAt(0)
}
val branchName = "refs/heads/${branchSplit.joinToString("/")}"
val refSpec = RefSpec()
.setSource(null)
.setDestination(branchName)
val pushResult = git.push()
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.setRefSpecs(refSpec)
.setRemote(remoteName)
.call()
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)
error.append("\n")
}
throw Exception(error.toString())
}
git
.branchDelete()
.setBranchNames(ref.name)
.call()
}
private val RemoteRefUpdate.Status.isRejected: Boolean
get() {
return this == RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ||
this == RemoteRefUpdate.Status.REJECTED_NODELETE ||
this == RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED ||
this == RemoteRefUpdate.Status.REJECTED_OTHER_REASON
}
private val RemoteRefUpdate.statusMessage: String
get() {
return when (this.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> "Failed to push some refs to ${this.remoteName}. " +
"Updates were rejected because the remote contains work that you do not have locally. Pulling changes from remote may help."
RemoteRefUpdate.Status.REJECTED_NODELETE -> "Could not delete ref because the remote doesn't support deleting refs."
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED -> "Ref rejected, old object id in remote has changed."
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> this.message ?: "Push rejected for unknown reasons."
else -> ""
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun clone(directory: File, url: String): Flow<CloneStatus> = callbackFlow {
var lastTitle: String = ""
var lastTotalWork = 0
var progress = 0
try {
ensureActive()
trySend(CloneStatus.Cloning("Starting...", progress, lastTotalWork))
Git.cloneRepository()
.setDirectory(directory)
.setURI(url)
.setProgressMonitor(
object : ProgressMonitor {
override fun start(totalTasks: Int) {
println("ProgressMonitor Start with total tasks of: $totalTasks")
}
override fun beginTask(title: String?, totalWork: Int) {
println("ProgressMonitor Begin task with title: $title")
lastTitle = title.orEmpty()
lastTotalWork = totalWork
progress = 0
trySend(CloneStatus.Cloning(lastTitle, progress, lastTotalWork))
}
override fun update(completed: Int) {
println("ProgressMonitor Update $completed")
ensureActive()
progress += completed
trySend(CloneStatus.Cloning(lastTitle, progress, lastTotalWork))
}
override fun endTask() {
println("ProgressMonitor End task")
}
override fun isCancelled(): Boolean {
return !isActive
}
}
)
.setTransportConfigCallback {
handleTransportCredentials(it)
}
.call()
ensureActive()
trySend(CloneStatus.Completed(directory))
channel.close()
} catch (ex: Exception) {
if (ex.cause?.cause is CancellationException) {
println("Clone cancelled")
} else {
trySend(CloneStatus.Fail(ex.localizedMessage))
}
channel.close()
}
awaitClose()
}
}
sealed class CloneStatus {
object None : CloneStatus()
data class Cloning(val taskName: String, val progress: Int, val total: Int) : CloneStatus()
object Cancelling : CloneStatus()
data class Fail(val reason: String) : CloneStatus()
data class Completed(val repoDir: File) : CloneStatus()
}

View File

@ -0,0 +1,31 @@
package app.git.author
import app.models.AuthorInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.storage.file.FileBasedConfig
import javax.inject.Inject
class LoadAuthorUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): AuthorInfo = withContext(Dispatchers.IO) {
val config = git.repository.config
val globalConfig = git.repository.config.baseConfig
val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs)
repoConfig.load()
val globalName = globalConfig.getString("user", null, "name")
val globalEmail = globalConfig.getString("user", null, "email")
val name = repoConfig.getString("user", null, "name")
val email = repoConfig.getString("user", null, "email")
return@withContext AuthorInfo(
globalName,
globalEmail,
name,
email,
)
}
}

View File

@ -0,0 +1,40 @@
package app.git.author
import app.models.AuthorInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.storage.file.FileBasedConfig
import javax.inject.Inject
class SaveAuthorUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, newAuthorInfo: AuthorInfo) = withContext(Dispatchers.IO) {
val config = git.repository.config
val globalConfig = git.repository.config.baseConfig
val repoConfig = FileBasedConfig((config as FileBasedConfig).file, git.repository.fs)
repoConfig.load()
if (globalConfig is FileBasedConfig) {
globalConfig.setStringProperty("user", null, "name", newAuthorInfo.globalName)
globalConfig.setStringProperty("user", null, "email", newAuthorInfo.globalEmail)
globalConfig.save()
}
config.setStringProperty("user", null, "name", newAuthorInfo.name)
config.setStringProperty("user", null, "email", newAuthorInfo.email)
config.save()
}
}
private fun FileBasedConfig.setStringProperty(
section: String,
subsection: String?,
name: String,
value: String?,
) {
if (value == null) {
unset(section, subsection, name)
} else {
setString(section, subsection, name, value)
}
}

View File

@ -0,0 +1,25 @@
package app.git.branches
import app.extensions.isBranch
import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.CreateBranchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class CheckoutRefUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, ref: Ref): Unit = withContext(Dispatchers.IO) {
git.checkout().apply {
setName(ref.name)
if (ref.isBranch && ref.name.startsWith("refs/remotes/")) {
setCreateBranch(true)
setName(ref.simpleName)
setStartPoint(ref.objectId.name)
setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK)
}
call()
}
}
}

View File

@ -0,0 +1,19 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.revwalk.RevCommit
import javax.inject.Inject
class CreateBranchOnCommitUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, branch: String, revCommit: RevCommit): Ref = withContext(Dispatchers.IO) {
git
.checkout()
.setCreateBranch(true)
.setName(branch)
.setStartPoint(revCommit)
.call()
}
}

View File

@ -0,0 +1,17 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class CreateBranchUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, branchName: String): Ref = withContext(Dispatchers.IO) {
git
.checkout()
.setCreateBranch(true)
.setName(branchName)
.call()
}
}

View File

@ -0,0 +1,17 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class DeleteBranchUseCase @Inject constructor() {
suspend operator fun invoke(git: Git, branch: Ref): List<String> = withContext(Dispatchers.IO) {
git
.branchDelete()
.setBranchNames(branch.name)
.setForce(true) // TODO Should it be forced?
.call()
}
}

View File

@ -0,0 +1,16 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import javax.inject.Inject
class DeleteLocallyRemoteBranches @Inject constructor() {
suspend operator fun invoke(git: Git, branches: List<String>): List<String> = withContext(Dispatchers.IO) {
git
.branchDelete()
.setBranchNames(*branches.toTypedArray())
.setForce(true)
.call()
}
}

View File

@ -0,0 +1,15 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class GetBranchesUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): List<Ref> = withContext(Dispatchers.IO) {
return@withContext git
.branchList()
.call()
}
}

View File

@ -0,0 +1,31 @@
package app.git.branches
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
/**
* Returns the current branch in [Ref]. If the repository is new, the current branch will be null.
*/
class GetCurrentBranchUseCase @Inject constructor(
private val getBranchesUseCase: GetBranchesUseCase,
) {
suspend operator fun invoke(git: Git): Ref? {
val branchList = getBranchesUseCase(git)
val branchName = git
.repository
.fullBranch
var branchFound = branchList.firstOrNull {
it.name == branchName
}
if (branchFound == null) {
branchFound = branchList.firstOrNull {
it.objectId.name == branchName // Try to get the HEAD
}
}
return branchFound
}
}

View File

@ -0,0 +1,17 @@
package app.git.branches
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.lib.Ref
import javax.inject.Inject
class GetRemoteBranchesUseCase @Inject constructor() {
suspend operator fun invoke(git: Git): List<Ref> = withContext(Dispatchers.IO) {
git
.branchList()
.setListMode(ListBranchCommand.ListMode.REMOTE)
.call()
}
}

View File

@ -0,0 +1,81 @@
package app.git.remote_operations
import app.git.CloneStatus
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.isActive
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ProgressMonitor
import java.io.File
import javax.inject.Inject
class CloneRepositoryUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(directory: File, url: String): Flow<CloneStatus> = callbackFlow {
var lastTitle: String = ""
var lastTotalWork = 0
var progress = 0
try {
ensureActive()
trySend(CloneStatus.Cloning("Starting...", progress, lastTotalWork))
Git.cloneRepository()
.setDirectory(directory)
.setURI(url)
.setProgressMonitor(
object : ProgressMonitor {
override fun start(totalTasks: Int) {
println("ProgressMonitor Start with total tasks of: $totalTasks")
}
override fun beginTask(title: String?, totalWork: Int) {
println("ProgressMonitor Begin task with title: $title")
lastTitle = title.orEmpty()
lastTotalWork = totalWork
progress = 0
trySend(CloneStatus.Cloning(lastTitle, progress, lastTotalWork))
}
override fun update(completed: Int) {
println("ProgressMonitor Update $completed")
ensureActive()
progress += completed
trySend(CloneStatus.Cloning(lastTitle, progress, lastTotalWork))
}
override fun endTask() {
println("ProgressMonitor End task")
}
override fun isCancelled(): Boolean {
return !isActive
}
}
)
.setTransportConfigCallback { handleTransportUseCase(it) }
.call()
ensureActive()
trySend(CloneStatus.Completed(directory))
channel.close()
} catch (ex: Exception) {
if (ex.cause?.cause is CancellationException) {
println("Clone cancelled")
} else {
trySend(CloneStatus.Fail(ex.localizedMessage))
}
channel.close()
}
awaitClose()
}
}

View File

@ -0,0 +1,59 @@
package app.git.remote_operations
import app.git.branches.DeleteBranchUseCase
import app.git.isRejected
import app.git.statusMessage
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.RefSpec
import javax.inject.Inject
class DeleteRemoteBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
private val deleteBranchUseCase: DeleteBranchUseCase,
) {
suspend operator fun invoke(git: Git, ref: Ref) {
val branchSplit = ref.name.split("/").toMutableList()
val remoteName = branchSplit[2] // Remote name
repeat(3) {
branchSplit.removeAt(0)
}
val branchName = "refs/heads/${branchSplit.joinToString("/")}"
val refSpec = RefSpec()
.setSource(null)
.setDestination(branchName)
val pushResults = git.push()
.setTransportConfigCallback {
handleTransportUseCase(it)
}
.setRefSpecs(refSpec)
.setRemote(remoteName)
.call()
val results = pushResults.map { pushResult ->
pushResult.remoteUpdates.filter { remoteRefUpdate ->
remoteRefUpdate.status.isRejected
}
}.flatten()
if (results.isNotEmpty()) {
val error = StringBuilder()
results.forEach { result ->
error.append(result.statusMessage)
error.append("\n")
}
throw Exception(error.toString())
}
deleteBranchUseCase(git, ref)
// git
// .branchDelete()
// .setBranchNames(ref.name)
// .call()
}
}

View File

@ -0,0 +1,24 @@
package app.git.remote_operations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
class FetchAllBranchesUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
suspend operator fun invoke(git: Git) = withContext(Dispatchers.IO) {
val remotes = git.remoteList().call()
for (remote in remotes) {
git.fetch()
.setRemote(remote.name)
.setRefSpecs(remote.fetchRefSpecs)
.setTransportConfigCallback { handleTransportUseCase(it) }
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
}
}
}

View File

@ -0,0 +1,20 @@
package app.git.remote_operations
import app.credentials.GSessionManager
import app.credentials.HttpCredentialsProvider
import org.eclipse.jgit.transport.HttpTransport
import org.eclipse.jgit.transport.SshTransport
import org.eclipse.jgit.transport.Transport
import javax.inject.Inject
class HandleTransportUseCase @Inject constructor(
private val sessionManager: GSessionManager
) {
operator fun invoke(transport: Transport?) {
if (transport is SshTransport) {
transport.sshSessionFactory = sessionManager.generateSshSessionFactory()
} else if (transport is HttpTransport) {
transport.credentialsProvider = HttpCredentialsProvider()
}
}
}

View File

@ -0,0 +1,35 @@
package app.git.remote_operations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
class PullBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
suspend operator fun invoke(git: Git, rebase: Boolean) = withContext(Dispatchers.IO) {
val pullResult = git
.pull()
.setTransportConfigCallback { handleTransportUseCase(it) }
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) {
var message = "Pull failed"
if (rebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
}

View File

@ -0,0 +1,40 @@
package app.git.remote_operations
import app.extensions.remoteName
import app.extensions.simpleName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.CredentialsProvider
import javax.inject.Inject
class PullFromSpecificBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
suspend operator fun invoke(git: Git, rebase: Boolean, remoteBranch: Ref) = withContext(Dispatchers.IO) {
val pullResult = git
.pull()
.setTransportConfigCallback { handleTransportUseCase(it) }
.setRemote(remoteBranch.remoteName)
.setRemoteBranchName(remoteBranch.simpleName)
.setRebase(rebase)
.setCredentialsProvider(CredentialsProvider.getDefault())
.call()
if (!pullResult.isSuccessful) {
var message = "Pull failed" // TODO Remove messages from here and pass the result to a custom exception type
if (rebase) {
message = when (pullResult.rebaseResult.status) {
RebaseResult.Status.UNCOMMITTED_CHANGES -> "The pull with rebase has failed because you have got uncommited changes"
RebaseResult.Status.CONFLICTS -> "Pull with rebase has conflicts, fix them to continue"
else -> message
}
}
throw Exception(message)
}
}
}

View File

@ -0,0 +1,43 @@
package app.git.remote_operations
import app.git.isRejected
import app.git.statusMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec
import javax.inject.Inject
class PushBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
suspend operator fun invoke(git: Git, force: Boolean, pushTags: Boolean) = withContext(Dispatchers.IO) {
val currentBranchRefSpec = git.repository.fullBranch
val pushResult = git
.push()
.setRefSpecs(RefSpec(currentBranchRefSpec))
.setForce(force)
.apply {
if (pushTags)
setPushTags()
}
.setTransportConfigCallback { handleTransportUseCase(it) }
.call()
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)
error.append("\n")
}
throw Exception(error.toString())
}
}
}

View File

@ -0,0 +1,47 @@
package app.git.remote_operations
import app.extensions.remoteName
import app.extensions.simpleName
import app.git.isRejected
import app.git.statusMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.transport.RefSpec
import javax.inject.Inject
class PushToSpecificBranchUseCase @Inject constructor(
private val handleTransportUseCase: HandleTransportUseCase,
) {
suspend operator fun invoke(git: Git, force: Boolean, pushTags: Boolean, remoteBranch: Ref) =
withContext(Dispatchers.IO) {
val currentBranchRefSpec = git.repository.fullBranch
val pushResult = git
.push()
.setRefSpecs(RefSpec("$currentBranchRefSpec:${remoteBranch.simpleName}"))
.setRemote(remoteBranch.remoteName)
.setForce(force)
.apply {
if (pushTags)
setPushTags()
}
.setTransportConfigCallback { handleTransportUseCase(it) }
.call()
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)
error.append("\n")
}
throw Exception(error.toString())
}
}
}

View File

@ -1,9 +1,10 @@
package app.viewmodels package app.viewmodels
import app.extensions.nullIfEmpty import app.extensions.nullIfEmpty
import app.git.AuthorManager
import app.git.RefreshType import app.git.RefreshType
import app.git.TabState import app.git.TabState
import app.git.author.LoadAuthorUseCase
import app.git.author.SaveAuthorUseCase
import app.models.AuthorInfo import app.models.AuthorInfo
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -11,7 +12,8 @@ import javax.inject.Inject
class AuthorViewModel @Inject constructor( class AuthorViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val authorManager: AuthorManager, private val saveAuthorUseCase: SaveAuthorUseCase,
private val loadAuthorUseCase: LoadAuthorUseCase,
) { ) {
private val _authorInfo = MutableStateFlow(AuthorInfo(null, null, null, null)) private val _authorInfo = MutableStateFlow(AuthorInfo(null, null, null, null))
@ -21,7 +23,7 @@ class AuthorViewModel @Inject constructor(
refreshType = RefreshType.NONE, refreshType = RefreshType.NONE,
showError = true, showError = true,
) { git -> ) { git ->
_authorInfo.value = authorManager.loadAuthor(git) _authorInfo.value = loadAuthorUseCase(git)
} }
fun saveAuthorInfo(globalName: String, globalEmail: String, name: String, email: String) = tabState.runOperation( fun saveAuthorInfo(globalName: String, globalEmail: String, name: String, email: String) = tabState.runOperation(
@ -35,6 +37,6 @@ class AuthorViewModel @Inject constructor(
email.nullIfEmpty, email.nullIfEmpty,
) )
authorManager.saveAuthorInfo(git, newAuthorInfo) saveAuthorUseCase(git, newAuthorInfo)
} }
} }

View File

@ -1,6 +1,9 @@
package app.viewmodels package app.viewmodels
import app.git.* import app.git.*
import app.git.branches.*
import app.git.remote_operations.PullFromSpecificBranchUseCase
import app.git.remote_operations.PushToSpecificBranchUseCase
import app.preferences.AppSettings import app.preferences.AppSettings
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -9,12 +12,17 @@ import org.eclipse.jgit.lib.Ref
import javax.inject.Inject import javax.inject.Inject
class BranchesViewModel @Inject constructor( class BranchesViewModel @Inject constructor(
private val branchesManager: BranchesManager,
private val rebaseManager: RebaseManager, private val rebaseManager: RebaseManager,
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager, private val pushToSpecificBranchUseCase: PushToSpecificBranchUseCase,
private val pullFromSpecificBranchUseCase: PullFromSpecificBranchUseCase,
private val tabState: TabState, private val tabState: TabState,
private val appSettings: AppSettings, private val appSettings: AppSettings,
private val getCurrentBranchUseCase: GetCurrentBranchUseCase,
private val getBranchesUseCase: GetBranchesUseCase,
private val createBranchUseCase: CreateBranchUseCase,
private val deleteBranchUseCase: DeleteBranchUseCase,
private val checkoutRefUseCase: CheckoutRefUseCase,
) : ExpandableViewModel(true) { ) : ExpandableViewModel(true) {
private val _branches = MutableStateFlow<List<Ref>>(listOf()) private val _branches = MutableStateFlow<List<Ref>>(listOf())
val branches: StateFlow<List<Ref>> val branches: StateFlow<List<Ref>>
@ -25,9 +33,9 @@ class BranchesViewModel @Inject constructor(
get() = _currentBranch get() = _currentBranch
suspend fun loadBranches(git: Git) { suspend fun loadBranches(git: Git) {
_currentBranch.value = branchesManager.currentBranchRef(git) _currentBranch.value = getCurrentBranchUseCase(git)
val branchesList = branchesManager.getBranches(git) val branchesList = getBranchesUseCase(git).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 }
@ -44,7 +52,7 @@ class BranchesViewModel @Inject constructor(
refreshType = RefreshType.ONLY_LOG, refreshType = RefreshType.ONLY_LOG,
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
) { git -> ) { git ->
branchesManager.createBranch(git, branchName) createBranchUseCase(git, branchName)
this.loadBranches(git) this.loadBranches(git)
} }
@ -57,13 +65,13 @@ class BranchesViewModel @Inject constructor(
fun deleteBranch(branch: Ref) = tabState.safeProcessing( fun deleteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.deleteBranch(git, branch) deleteBranchUseCase(git, branch)
} }
fun checkoutRef(ref: Ref) = tabState.safeProcessing( fun checkoutRef(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.checkoutRef(git, ref) checkoutRefUseCase(git, ref)
} }
suspend fun refresh(git: Git) { suspend fun refresh(git: Git) {
@ -83,7 +91,7 @@ class BranchesViewModel @Inject constructor(
fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing( fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.pushToBranch( pushToSpecificBranchUseCase(
git = git, git = git,
force = false, force = false,
pushTags = false, pushTags = false,
@ -94,7 +102,7 @@ class BranchesViewModel @Inject constructor(
fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing( fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.pullFromBranch( pullFromSpecificBranchUseCase(
git = git, git = git,
rebase = false, rebase = false,
remoteBranch = branch, remoteBranch = branch,

View File

@ -1,8 +1,8 @@
package app.viewmodels package app.viewmodels
import app.git.CloneStatus import app.git.CloneStatus
import app.git.RemoteOperationsManager
import app.git.TabState import app.git.TabState
import app.git.remote_operations.CloneRepositoryUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -14,7 +14,7 @@ import javax.inject.Inject
class CloneViewModel @Inject constructor( class CloneViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val remoteOperationsManager: RemoteOperationsManager, private val cloneRepositoryUseCase: CloneRepositoryUseCase,
) { ) {
private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None) private val _cloneStatus = MutableStateFlow<CloneStatus>(CloneStatus.None)
@ -69,7 +69,7 @@ class CloneViewModel @Inject constructor(
repoDir.mkdir() repoDir.mkdir()
} }
remoteOperationsManager.clone(repoDir, url) cloneRepositoryUseCase(repoDir, url)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.collect { newCloneStatus -> .collect { newCloneStatus ->
_cloneStatus.value = newCloneStatus _cloneStatus.value = newCloneStatus

View File

@ -4,8 +4,12 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import app.extensions.delayedStateChange import app.extensions.delayedStateChange
import app.git.* import app.git.*
import app.git.branches.*
import app.git.graph.GraphCommitList import app.git.graph.GraphCommitList
import app.git.graph.GraphNode import app.git.graph.GraphNode
import app.git.remote_operations.DeleteRemoteBranchUseCase
import app.git.remote_operations.PullFromSpecificBranchUseCase
import app.git.remote_operations.PushToSpecificBranchUseCase
import app.preferences.AppSettings import app.preferences.AppSettings
import app.ui.SelectedItem import app.ui.SelectedItem
import app.ui.log.LogDialog import app.ui.log.LogDialog
@ -34,11 +38,16 @@ private const val LOG_MIN_TIME_IN_MS_TO_SHOW_LOAD = 500L
class LogViewModel @Inject constructor( class LogViewModel @Inject constructor(
private val logManager: LogManager, private val logManager: LogManager,
private val statusManager: StatusManager, private val statusManager: StatusManager,
private val branchesManager: BranchesManager, private val getCurrentBranchUseCase: GetCurrentBranchUseCase,
private val checkoutRefUseCase: CheckoutRefUseCase,
private val createBranchOnCommitUseCase: CreateBranchOnCommitUseCase,
private val deleteBranchUseCase: DeleteBranchUseCase,
private val pushToSpecificBranchUseCase: PushToSpecificBranchUseCase,
private val pullFromSpecificBranchUseCase: PullFromSpecificBranchUseCase,
private val deleteRemoteBranchUseCase: DeleteRemoteBranchUseCase,
private val rebaseManager: RebaseManager, private val rebaseManager: RebaseManager,
private val tagsManager: TagsManager, private val tagsManager: TagsManager,
private val mergeManager: MergeManager, private val mergeManager: MergeManager,
private val remoteOperationsManager: RemoteOperationsManager,
private val tabState: TabState, private val tabState: TabState,
private val appSettings: AppSettings, private val appSettings: AppSettings,
) { ) {
@ -83,7 +92,7 @@ class LogViewModel @Inject constructor(
_logStatus.value = LogStatus.Loading _logStatus.value = LogStatus.Loading
} }
) { ) {
val currentBranch = branchesManager.currentBranchRef(git) val currentBranch = getCurrentBranchUseCase(git)
val statusSummary = statusManager.getStatusSummary( val statusSummary = statusManager.getStatusSummary(
git = git, git = git,
@ -113,7 +122,7 @@ class LogViewModel @Inject constructor(
fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing( fun pushToRemoteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.pushToBranch( pushToSpecificBranchUseCase(
git = git, git = git,
force = false, force = false,
pushTags = false, pushTags = false,
@ -124,7 +133,7 @@ class LogViewModel @Inject constructor(
fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing( fun pullFromRemoteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.pullFromBranch( pullFromSpecificBranchUseCase(
git = git, git = git,
rebase = false, rebase = false,
remoteBranch = branch, remoteBranch = branch,
@ -152,7 +161,7 @@ class LogViewModel @Inject constructor(
fun checkoutRef(ref: Ref) = tabState.safeProcessing( fun checkoutRef(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.checkoutRef(git, ref) checkoutRefUseCase(git, ref)
} }
fun cherrypickCommit(revCommit: RevCommit) = tabState.safeProcessing( fun cherrypickCommit(revCommit: RevCommit) = tabState.safeProcessing(
@ -164,7 +173,7 @@ class LogViewModel @Inject constructor(
fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing( fun createBranchOnCommit(branch: String, revCommit: RevCommit) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.createBranchOnCommit(git, branch, revCommit) createBranchOnCommitUseCase(git, branch, revCommit)
} }
fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing( fun createTagOnCommit(tag: String, revCommit: RevCommit) = tabState.safeProcessing(
@ -182,7 +191,7 @@ class LogViewModel @Inject constructor(
fun deleteBranch(branch: Ref) = tabState.safeProcessing( fun deleteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.deleteBranch(git, branch) deleteBranchUseCase(git, branch)
} }
fun deleteTag(tag: Ref) = tabState.safeProcessing( fun deleteTag(tag: Ref) = tabState.safeProcessing(
@ -196,7 +205,7 @@ class LogViewModel @Inject constructor(
} }
private suspend fun uncommitedChangesLoadLog(git: Git) { private suspend fun uncommitedChangesLoadLog(git: Git) {
val currentBranch = branchesManager.currentBranchRef(git) val currentBranch = getCurrentBranchUseCase(git)
val hasUncommitedChanges = statusManager.hasUncommitedChanges(git) val hasUncommitedChanges = statusManager.hasUncommitedChanges(git)
val statsSummary = if (hasUncommitedChanges) { val statsSummary = if (hasUncommitedChanges) {
@ -357,7 +366,7 @@ class LogViewModel @Inject constructor(
fun deleteRemoteBranch(branch: Ref) = tabState.safeProcessing( fun deleteRemoteBranch(branch: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.deleteBranch(git, branch) deleteRemoteBranchUseCase(git, branch)
} }
} }

View File

@ -1,12 +1,18 @@
package app.viewmodels package app.viewmodels
import app.git.* import app.git.*
import app.git.remote_operations.DeleteRemoteBranchUseCase
import app.git.remote_operations.FetchAllBranchesUseCase
import app.git.remote_operations.PullBranchUseCase
import app.git.remote_operations.PushBranchUseCase
import java.awt.Desktop import java.awt.Desktop
import javax.inject.Inject import javax.inject.Inject
class MenuViewModel @Inject constructor( class MenuViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val remoteOperationsManager: RemoteOperationsManager, private val pullBranchUseCase: PullBranchUseCase,
private val pushBranchUseCase: PushBranchUseCase,
private val fetchAllBranchesUseCase: FetchAllBranchesUseCase,
private val stashManager: StashManager, private val stashManager: StashManager,
private val statusManager: StatusManager, private val statusManager: StatusManager,
) { ) {
@ -14,21 +20,21 @@ class MenuViewModel @Inject constructor(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
) { git -> ) { git ->
remoteOperationsManager.pull(git, rebase) pullBranchUseCase(git, rebase)
} }
fun fetchAll() = tabState.safeProcessing( fun fetchAll() = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
) { git -> ) { git ->
remoteOperationsManager.fetchAll(git) fetchAllBranchesUseCase(git)
} }
fun push(force: Boolean = false, pushTags: Boolean = false) = tabState.safeProcessing( fun push(force: Boolean = false, pushTags: Boolean = false) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
refreshEvenIfCrashes = true, refreshEvenIfCrashes = true,
) { git -> ) { git ->
remoteOperationsManager.push(git, force, pushTags) pushBranchUseCase(git, force, pushTags)
} }
fun stash() = tabState.safeProcessing( fun stash() = tabState.safeProcessing(

View File

@ -2,6 +2,9 @@ package app.viewmodels
import app.exceptions.InvalidRemoteUrlException import app.exceptions.InvalidRemoteUrlException
import app.git.* import app.git.*
import app.git.branches.DeleteLocallyRemoteBranches
import app.git.branches.GetRemoteBranchesUseCase
import app.git.remote_operations.DeleteRemoteBranchUseCase
import app.ui.dialogs.RemoteWrapper import app.ui.dialogs.RemoteWrapper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -14,18 +17,19 @@ import javax.inject.Inject
class RemotesViewModel @Inject constructor( class RemotesViewModel @Inject constructor(
private val remotesManager: RemotesManager, private val remotesManager: RemotesManager,
private val remoteOperationsManager: RemoteOperationsManager, private val deleteRemoteBranchUseCase: DeleteRemoteBranchUseCase,
private val branchesManager: BranchesManager,
private val tabState: TabState, private val tabState: TabState,
private val getRemoteBranchesUseCase: GetRemoteBranchesUseCase,
private val deleteLocallyRemoteBranchesUseCase: DeleteLocallyRemoteBranches,
) : ExpandableViewModel() { ) : ExpandableViewModel() {
private val _remotes = MutableStateFlow<List<RemoteView>>(listOf()) private val _remotes = MutableStateFlow<List<RemoteView>>(listOf())
val remotes: StateFlow<List<RemoteView>> val remotes: StateFlow<List<RemoteView>>
get() = _remotes get() = _remotes
suspend fun loadRemotes(git: Git) = withContext(Dispatchers.IO) { private suspend fun loadRemotes(git: Git) = withContext(Dispatchers.IO) {
val remotes = git.remoteList() val remotes = git.remoteList()
.call() .call()
val allRemoteBranches = branchesManager.remoteBranches(git) val allRemoteBranches = getRemoteBranchesUseCase(git)
remotesManager.loadRemotes(git, allRemoteBranches) remotesManager.loadRemotes(git, allRemoteBranches)
val remoteInfoList = remotes.map { remoteConfig -> val remoteInfoList = remotes.map { remoteConfig ->
@ -45,7 +49,7 @@ class RemotesViewModel @Inject constructor(
fun deleteRemoteBranch(ref: Ref) = tabState.safeProcessing( fun deleteRemoteBranch(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
remoteOperationsManager.deleteBranch(git, ref) deleteRemoteBranchUseCase(git, ref)
} }
suspend fun refresh(git: Git) = withContext(Dispatchers.IO) { suspend fun refresh(git: Git) = withContext(Dispatchers.IO) {
@ -72,14 +76,14 @@ class RemotesViewModel @Inject constructor(
) { git -> ) { git ->
remotesManager.deleteRemote(git, remoteName) remotesManager.deleteRemote(git, remoteName)
val remoteBranches = branchesManager.remoteBranches(git) val remoteBranches = getRemoteBranchesUseCase(git)
val remoteToDeleteBranchesNames = remoteBranches.filter { val remoteToDeleteBranchesNames = remoteBranches.filter {
it.name.startsWith("refs/remotes/$remoteName/") it.name.startsWith("refs/remotes/$remoteName/")
}.map { }.map {
it.name it.name
} }
branchesManager.deleteLocallyRemoteBranches(git, remoteToDeleteBranchesNames) deleteLocallyRemoteBranchesUseCase(git, remoteToDeleteBranchesNames)
} }

View File

@ -1,9 +1,9 @@
package app.viewmodels package app.viewmodels
import app.git.BranchesManager
import app.git.RefreshType import app.git.RefreshType
import app.git.TabState import app.git.TabState
import app.git.TagsManager import app.git.TagsManager
import app.git.branches.CheckoutRefUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -14,8 +14,8 @@ import javax.inject.Inject
class TagsViewModel @Inject constructor( class TagsViewModel @Inject constructor(
private val tabState: TabState, private val tabState: TabState,
private val branchesManager: BranchesManager,
private val tagsManager: TagsManager, private val tagsManager: TagsManager,
private val checkoutRefUseCase: CheckoutRefUseCase,
) : ExpandableViewModel() { ) : ExpandableViewModel() {
private val _tags = MutableStateFlow<List<Ref>>(listOf()) private val _tags = MutableStateFlow<List<Ref>>(listOf())
val tags: StateFlow<List<Ref>> val tags: StateFlow<List<Ref>>
@ -30,7 +30,7 @@ class TagsViewModel @Inject constructor(
fun checkoutRef(ref: Ref) = tabState.safeProcessing( fun checkoutRef(ref: Ref) = tabState.safeProcessing(
refreshType = RefreshType.ALL_DATA, refreshType = RefreshType.ALL_DATA,
) { git -> ) { git ->
branchesManager.checkoutRef(git, ref) checkoutRefUseCase(git, ref)
} }
fun deleteTag(tag: Ref) = tabState.safeProcessing( fun deleteTag(tag: Ref) = tabState.safeProcessing(

View File

@ -3,6 +3,8 @@ package app.git
import app.credentials.GProcess import app.credentials.GProcess
import app.credentials.GRemoteSession import app.credentials.GRemoteSession
import app.credentials.GSessionManager import app.credentials.GSessionManager
import app.git.remote_operations.CloneRepositoryUseCase
import app.git.remote_operations.HandleTransportUseCase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -31,8 +33,8 @@ class BeforeRepoAllTestsExtension : BeforeAllCallback, AfterAllCallback {
// The following line registers a callback hook when the root test context is shut down // The following line registers a callback hook when the root test context is shut down
context.root.getStore(GLOBAL).put("gitnuro_tests", this) context.root.getStore(GLOBAL).put("gitnuro_tests", this)
val remoteOperationsManager = RemoteOperationsManager(GSessionManager { GRemoteSession { GProcess() } }) val cloneRepositoryUseCase = CloneRepositoryUseCase(HandleTransportUseCase(GSessionManager { GRemoteSession { GProcess() } }))
remoteOperationsManager.clone(repoDir, REPO_URL) cloneRepositoryUseCase(repoDir, REPO_URL)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.collect { newCloneStatus -> .collect { newCloneStatus ->
println("Clonning test repository: $newCloneStatus") println("Clonning test repository: $newCloneStatus")

View File

@ -1,154 +1,154 @@
package app.git //package app.git
//
import app.TestUtils.copyDir //import app.TestUtils.copyDir
import kotlinx.coroutines.runBlocking //import kotlinx.coroutines.runBlocking
import org.eclipse.jgit.api.Git //import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId //import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Repository //import org.eclipse.jgit.lib.Repository
import org.junit.jupiter.api.AfterEach //import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals //import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull //import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach //import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.extension.ExtendWith //import org.junit.jupiter.api.extension.ExtendWith
import java.io.File //import java.io.File
//
private const val DEFAULT_REMOTE = "origin" //private const val DEFAULT_REMOTE = "origin"
private const val DEFAULT_PRIMARY_BRANCH = "main" //private const val DEFAULT_PRIMARY_BRANCH = "main"
private const val DEFAULT_SECONDARY_BRANCH = "TestBranch1" //private const val DEFAULT_SECONDARY_BRANCH = "TestBranch1"
//
private const val LOCAL_PREFIX = "refs/heads" //private const val LOCAL_PREFIX = "refs/heads"
private const val DEFAULT_PRIMARY_BRANCH_FULL_NAME = "$LOCAL_PREFIX/$DEFAULT_PRIMARY_BRANCH" //private const val DEFAULT_PRIMARY_BRANCH_FULL_NAME = "$LOCAL_PREFIX/$DEFAULT_PRIMARY_BRANCH"
private const val DEFAULT_SECONDARY_BRANCH_FULL_NAME = "$LOCAL_PREFIX/$DEFAULT_SECONDARY_BRANCH" //private const val DEFAULT_SECONDARY_BRANCH_FULL_NAME = "$LOCAL_PREFIX/$DEFAULT_SECONDARY_BRANCH"
//
private const val INITIAL_LOCAL_BRANCH_COUNT = 1 //private const val INITIAL_LOCAL_BRANCH_COUNT = 1
private const val INITIAL_REMOTE_BRANCH_COUNT = 2 //private const val INITIAL_REMOTE_BRANCH_COUNT = 2
//
private const val REMOTE_PREFIX = "refs/remotes/$DEFAULT_REMOTE" //private const val REMOTE_PREFIX = "refs/remotes/$DEFAULT_REMOTE"
//
private val initialRemoteBranches = listOf( //private val initialRemoteBranches = listOf(
"$REMOTE_PREFIX/$DEFAULT_PRIMARY_BRANCH", // "$REMOTE_PREFIX/$DEFAULT_PRIMARY_BRANCH",
"$REMOTE_PREFIX/$DEFAULT_SECONDARY_BRANCH", // "$REMOTE_PREFIX/$DEFAULT_SECONDARY_BRANCH",
) //)
//
@ExtendWith(BeforeRepoAllTestsExtension::class) //@ExtendWith(BeforeRepoAllTestsExtension::class)
class BranchesManagerTest { //class BranchesManagerTest {
private lateinit var repo: Repository // private lateinit var repo: Repository
private lateinit var git: Git // private lateinit var git: Git
private lateinit var branchesManagerTestDir: File // private lateinit var branchesManagerTestDir: File
private val branchesManager = BranchesManager() // private val branchesManager = BranchesManager()
//
@BeforeEach // @BeforeEach
fun setUp() { // fun setUp() {
branchesManagerTestDir = File(tempDir, "branches_manager") // branchesManagerTestDir = File(tempDir, "branches_manager")
branchesManagerTestDir.mkdir() // branchesManagerTestDir.mkdir()
//
copyDir(repoDir.absolutePath, branchesManagerTestDir.absolutePath) // copyDir(repoDir.absolutePath, branchesManagerTestDir.absolutePath)
//
repo = RepositoryManager().openRepository(branchesManagerTestDir) // repo = RepositoryManager().openRepository(branchesManagerTestDir)
git = Git(repo) // git = Git(repo)
} // }
//
@AfterEach // @AfterEach
fun tearDown() { // fun tearDown() {
repo.close() // repo.close()
branchesManagerTestDir.deleteRecursively() // branchesManagerTestDir.deleteRecursively()
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun currentBranchRef() = runBlocking { // fun currentBranchRef() = runBlocking {
val currentBranchRef = branchesManager.currentBranchRef(Git(repo)) // val currentBranchRef = branchesManager.currentBranchRef(Git(repo))
assertEquals(currentBranchRef?.name, "refs/heads/$DEFAULT_PRIMARY_BRANCH") // assertEquals(currentBranchRef?.name, "refs/heads/$DEFAULT_PRIMARY_BRANCH")
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun getBranches() = runBlocking { // fun getBranches() = runBlocking {
val branchesManager = BranchesManager() // val branchesManager = BranchesManager()
val branches = branchesManager.getBranches(git) // val branches = branchesManager.getBranches(git)
assertEquals(branches.count(), INITIAL_LOCAL_BRANCH_COUNT) // assertEquals(branches.count(), INITIAL_LOCAL_BRANCH_COUNT)
val containsMain = branches.any { it.name == "refs/heads/$DEFAULT_PRIMARY_BRANCH" } // val containsMain = branches.any { it.name == "refs/heads/$DEFAULT_PRIMARY_BRANCH" }
assert(containsMain) { println("Error: Branch main does not exist") } // assert(containsMain) { println("Error: Branch main does not exist") }
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun checkoutRef() = runBlocking { // fun checkoutRef() = runBlocking {
val remoteBranchToCheckout = "$REMOTE_PREFIX/$DEFAULT_SECONDARY_BRANCH" // val remoteBranchToCheckout = "$REMOTE_PREFIX/$DEFAULT_SECONDARY_BRANCH"
//
var currentBranch = branchesManager.currentBranchRef(git) // var currentBranch = branchesManager.currentBranchRef(git)
assertEquals(currentBranch?.name, DEFAULT_PRIMARY_BRANCH_FULL_NAME) // assertEquals(currentBranch?.name, DEFAULT_PRIMARY_BRANCH_FULL_NAME)
//
// Checkout a remote branch // // Checkout a remote branch
var branchToCheckout = branchesManager.remoteBranches(git).first { it.name == remoteBranchToCheckout } // var branchToCheckout = branchesManager.remoteBranches(git).first { it.name == remoteBranchToCheckout }
branchesManager.checkoutRef(git, branchToCheckout) // branchesManager.checkoutRef(git, branchToCheckout)
//
currentBranch = branchesManager.currentBranchRef(git) // currentBranch = branchesManager.currentBranchRef(git)
assertEquals(DEFAULT_SECONDARY_BRANCH_FULL_NAME, currentBranch?.name) // assertEquals(DEFAULT_SECONDARY_BRANCH_FULL_NAME, currentBranch?.name)
//
// Checkout a local branch // // Checkout a local branch
branchToCheckout = branchesManager.getBranches(git).first { it.name == DEFAULT_PRIMARY_BRANCH_FULL_NAME } // branchToCheckout = branchesManager.getBranches(git).first { it.name == DEFAULT_PRIMARY_BRANCH_FULL_NAME }
branchesManager.checkoutRef(git, branchToCheckout) // branchesManager.checkoutRef(git, branchToCheckout)
currentBranch = branchesManager.currentBranchRef(git) // currentBranch = branchesManager.currentBranchRef(git)
//
assertEquals(DEFAULT_PRIMARY_BRANCH_FULL_NAME, currentBranch?.name) // assertEquals(DEFAULT_PRIMARY_BRANCH_FULL_NAME, currentBranch?.name)
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun createBranch() = runBlocking { // fun createBranch() = runBlocking {
val branchName = "test" // val branchName = "test"
branchesManager.createBranch(git, branchName) // branchesManager.createBranch(git, branchName)
//
val branches = branchesManager.getBranches(git) // val branches = branchesManager.getBranches(git)
assertEquals(INITIAL_LOCAL_BRANCH_COUNT + 1, branches.count()) // assertEquals(INITIAL_LOCAL_BRANCH_COUNT + 1, branches.count())
val containsNewBranch = branches.any { it.name == "refs/heads/$branchName" } // val containsNewBranch = branches.any { it.name == "refs/heads/$branchName" }
//
assert(containsNewBranch) { println("Error: Branch $branchName does not exist") } // assert(containsNewBranch) { println("Error: Branch $branchName does not exist") }
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun createBranchOnCommit() = runBlocking { // fun createBranchOnCommit() = runBlocking {
val branchName = "test" // val branchName = "test"
val commitId = "f66757e23dc5c43eccbe84d02c58245406c8f8f4" // val commitId = "f66757e23dc5c43eccbe84d02c58245406c8f8f4"
//
val objectId = ObjectId.fromString(commitId) // val objectId = ObjectId.fromString(commitId)
val revCommit = repo.parseCommit(objectId) // val revCommit = repo.parseCommit(objectId)
branchesManager.createBranchOnCommit(git, branchName, revCommit) // branchesManager.createBranchOnCommit(git, branchName, revCommit)
//
val branches = branchesManager.getBranches(git) // val branches = branchesManager.getBranches(git)
assertEquals(INITIAL_LOCAL_BRANCH_COUNT + 1, branches.count()) // assertEquals(INITIAL_LOCAL_BRANCH_COUNT + 1, branches.count())
val newBranch = branches.firstOrNull { it.name == "refs/heads/$branchName" } // val newBranch = branches.firstOrNull { it.name == "refs/heads/$branchName" }
//
assertNotNull(newBranch) // assertNotNull(newBranch)
assertEquals(commitId, newBranch?.objectId?.name()) // assertEquals(commitId, newBranch?.objectId?.name())
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun deleteBranch() = runBlocking { // fun deleteBranch() = runBlocking {
val branchToDeleteName = "branch_to_delete" // val branchToDeleteName = "branch_to_delete"
val currentBranch = branchesManager.currentBranchRef(git) // should be "main" // val currentBranch = branchesManager.currentBranchRef(git) // should be "main"
assertNotNull(currentBranch) // assertNotNull(currentBranch)
//
val newBranch = branchesManager.createBranch(git, branchToDeleteName) // val newBranch = branchesManager.createBranch(git, branchToDeleteName)
branchesManager.checkoutRef(git, currentBranch!!) // branchesManager.checkoutRef(git, currentBranch!!)
//
branchesManager.deleteBranch(git, newBranch) // branchesManager.deleteBranch(git, newBranch)
//
val branches = branchesManager.getBranches(git) // val branches = branchesManager.getBranches(git)
assertEquals(INITIAL_LOCAL_BRANCH_COUNT, branches.count()) // assertEquals(INITIAL_LOCAL_BRANCH_COUNT, branches.count())
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun remoteBranches() = runBlocking { // fun remoteBranches() = runBlocking {
val remoteBranches = branchesManager.remoteBranches(git) // val remoteBranches = branchesManager.remoteBranches(git)
assertEquals(remoteBranches.count(), INITIAL_REMOTE_BRANCH_COUNT) // assertEquals(remoteBranches.count(), INITIAL_REMOTE_BRANCH_COUNT)
remoteBranches.forEach { ref -> // remoteBranches.forEach { ref ->
assert(initialRemoteBranches.contains(ref.name)) // assert(initialRemoteBranches.contains(ref.name))
} // }
} // }
//
@org.junit.jupiter.api.Test // @org.junit.jupiter.api.Test
fun deleteLocallyRemoteBranches() = runBlocking { // fun deleteLocallyRemoteBranches() = runBlocking {
branchesManager.deleteLocallyRemoteBranches(git, initialRemoteBranches) // branchesManager.deleteLocallyRemoteBranches(git, initialRemoteBranches)
//
val branches = branchesManager.remoteBranches(git) // val branches = branchesManager.remoteBranches(git)
assertEquals(0, branches.count()) // assertEquals(0, branches.count())
} // }
} //}