commit f980e76321979183bd8dd6cc67f93d08fced21db from: ale date: Fri Jul 24 05:26:14 2026 UTC Implementar deteccion real de commits pendientes de enviar Anade remoteBranchHash/outgoingCommitIds/catCommit a GotCommandLineWrapper (basados en got ref -l, got log -x y got cat ), y una clase GotFullCommitDetails que adapta un GotCommitObject a VcsFullCommitDetails. GotOutgoingCommitsProvider ahora resuelve el hash de la rama remota y lista los commits no alcanzados por ella en vez de devolver siempre una lista vacia, lo que permite que el dialogo de Push nativo detecte y habilite el boton de envio con commits reales pendientes. commit - 1723d2ac5388915746d7cd9c84279116898690a1 commit + f980e76321979183bd8dd6cc67f93d08fced21db blob - 59801df9877633c858b4d08ab598a6e0a8b2799d blob + 8e3da498cbe96662aeccc2cac5fc8e9c46a5e20e --- build.gradle.kts +++ build.gradle.kts @@ -32,6 +32,11 @@ dependencies { bundledModule("intellij.platform.vcs.dvcs.impl") // Referenced by OutgoingResult's commit list type (com.intellij.dvcs.push). bundledModule("intellij.platform.vcs.log") + // Hash/VcsUser (vcs.shared), HashImpl/VcsUserImpl (vcs.impl.shared) and + // GraphCommit (vcs.log.graph) back the outgoing-commits VcsFullCommitDetails. + bundledModule("intellij.platform.vcs.shared") + bundledModule("intellij.platform.vcs.impl.shared") + bundledModule("intellij.platform.vcs.log.graph") } } blob - 061b30b9cccd625922be97ff4080fa8d96371d65 blob + 7843f1287cfd7e333e454184ebb7c2696aa9b7c5 --- src/main/kotlin/dev/nezzontli/gotvcs/cli/GotCommandLineWrapper.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/cli/GotCommandLineWrapper.kt @@ -14,6 +14,16 @@ data class GotLogEntry(val commitId: String, val autho data class GotUpdateEntry(val code: Char, val path: String) +data class GotCommitObject( + val commitId: String, + val parents: List, + val authorName: String, + val authorEmail: String, + /** Seconds since the epoch, as stored in the commit object. */ + val authorTimestamp: Long, + val message: String, +) + /** * Central entry point for every invocation of the `got` binary. The binary * path and SSH_AUTH_SOCK are configurable in Settings > Version Control > @@ -181,4 +191,64 @@ class GotCommandLineWrapper { .map { line -> GotUpdateEntry(line[0], line.substring(3)) } .toList() } + + /** Resolves `refs/remotes//` to a commit ID via `got ref -l`, or null if it doesn't exist locally. */ + @Throws(VcsException::class) + fun remoteBranchHash(workDir: File, remote: String, branch: String): String? { + val output = run(workDir, "ref", "-l") + val prefix = "refs/remotes/$remote/$branch:" + val line = output.lineSequence().firstOrNull { it.trim().startsWith(prefix) } ?: return null + return line.substringAfter(":").trim() + } + + /** + * Commit IDs reachable from the current branch but not yet known to be on + * [remoteHash] (via `got log -x`), most recent first. If [remoteHash] is + * null (no local knowledge of the remote branch, e.g. before any fetch), + * this falls back to the full history up to [limit]. + */ + @Throws(VcsException::class) + fun outgoingCommitIds(workDir: File, remoteHash: String?, limit: Int = 200): List { + val args = mutableListOf("log", "-l", limit.toString()) + if (remoteHash != null) { + args.add("-x") + args.add(remoteHash) + } + val output = run(workDir, *args.toTypedArray()) + return output.lineSequence() + .filter { it.startsWith("commit ") } + .map { it.removePrefix("commit ").trim().substringBefore(' ') } + .filter { it != remoteHash } + .toList() + } + + private val commitAuthorPattern = Regex("""^(.*)\s+<(.+)>\s+(\d+)\s+[+-]\d{4}$""") + + /** Parses the raw commit object printed by `got cat ` (tree/parent/author/committer/message). */ + @Throws(VcsException::class) + fun catCommit(workDir: File, commitId: String): GotCommitObject { + val lines = run(workDir, "cat", commitId).lines() + val parents = mutableListOf() + var authorName = "" + var authorEmail = "" + var authorTimestamp = 0L + var messageStart = -1 + + for ((index, line) in lines.withIndex()) { + when { + line.startsWith("parent ") -> parents.add(line.removePrefix("parent ").trim()) + line.startsWith("author ") -> { + commitAuthorPattern.find(line.removePrefix("author ").trim())?.let { match -> + authorName = match.groupValues[1].trim() + authorEmail = match.groupValues[2] + authorTimestamp = match.groupValues[3].toLongOrNull() ?: 0L + } + } + line.startsWith("messagelen ") -> messageStart = index + 2 + } + } + + val message = if (messageStart in lines.indices) lines.drop(messageStart).joinToString("\n").trimEnd('\n') else "" + return GotCommitObject(commitId, parents, authorName, authorEmail, authorTimestamp, message) + } } blob - a2df57f27c8576ff4321027caeb61a6e2c2217dc blob + 632600d4ab0b4ba9d133197386db363c491e6c69 --- src/main/kotlin/dev/nezzontli/gotvcs/push/GotOutgoingCommitsProvider.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/push/GotOutgoingCommitsProvider.kt @@ -3,18 +3,35 @@ package dev.nezzontli.gotvcs.push import com.intellij.dvcs.push.OutgoingCommitsProvider import com.intellij.dvcs.push.OutgoingResult import com.intellij.dvcs.push.PushSpec +import com.intellij.dvcs.push.VcsError +import com.intellij.openapi.vcs.VcsException +import dev.nezzontli.gotvcs.cli.GotCommandLineWrapper import dev.nezzontli.gotvcs.repo.GotRepository +import java.io.File /** - * got has no cheap local equivalent of "commits not yet on the remote" - * without contacting the server first, so this reports an empty preview - * rather than an inaccurate one. The Push dialog still works; it just - * doesn't list outgoing commits ahead of time. + * Lists commits reachable from the current branch but not yet known to be on + * `refs/remotes//` locally (via `got log -x`). This reflects + * the last `got fetch`, not a live round-trip to the server, same as Git's + * own outgoing-commits preview. */ -class GotOutgoingCommitsProvider : OutgoingCommitsProvider() { +class GotOutgoingCommitsProvider(private val commandLine: GotCommandLineWrapper) : + OutgoingCommitsProvider() { + override fun getOutgoingCommits( repository: GotRepository, pushSpec: PushSpec, initial: Boolean, - ): OutgoingResult = OutgoingResult(emptyList(), emptyList()) + ): OutgoingResult { + val workDir = File(repository.root.path) + val target = pushSpec.target + return try { + val remoteHash = commandLine.remoteBranchHash(workDir, target.remoteName, target.branch) + val commits = commandLine.outgoingCommitIds(workDir, remoteHash) + .map { id -> GotFullCommitDetails(commandLine.catCommit(workDir, id), repository.root) } + OutgoingResult(commits, emptyList()) + } catch (e: VcsException) { + OutgoingResult(emptyList(), listOf(VcsError(e.message))) + } + } } blob - /dev/null blob + 6109655e214c356044f7d12f5641baa37d9d2164 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/push/GotFullCommitDetails.kt @@ -0,0 +1,39 @@ +package dev.nezzontli.gotvcs.push + +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.vcs.log.Hash +import com.intellij.vcs.log.VcsFullCommitDetails +import com.intellij.vcs.log.VcsUser +import com.intellij.vcs.log.impl.HashImpl +import com.intellij.vcs.log.util.VcsUserUtil +import dev.nezzontli.gotvcs.cli.GotCommitObject + +/** + * Adapts a [GotCommitObject] (parsed from `got cat `) to the + * Push dialog's outgoing-commits list. got has no per-file change list + * cheaply available for an arbitrary historical commit here, so + * [getChanges] is empty; the dialog only needs id/author/message/parents + * to render its commit list. + */ +class GotFullCommitDetails( + private val commitObject: GotCommitObject, + private val root: VirtualFile, +) : VcsFullCommitDetails { + + private val hash: Hash = HashImpl.build(commitObject.commitId) + private val author: VcsUser = VcsUserUtil.createUser(commitObject.authorName, commitObject.authorEmail) + + override fun getId(): Hash = hash + override fun getParents(): List = commitObject.parents.map { HashImpl.build(it) } + override fun getTimestamp(): Long = commitObject.authorTimestamp * 1000L + override fun getRoot(): VirtualFile = root + override fun getSubject(): String = commitObject.message.lineSequence().firstOrNull().orEmpty() + override fun getAuthor(): VcsUser = author + override fun getCommitter(): VcsUser = author + override fun getAuthorTime(): Long = commitObject.authorTimestamp * 1000L + override fun getCommitTime(): Long = commitObject.authorTimestamp * 1000L + override fun getFullMessage(): String = commitObject.message + override fun getChanges(): Collection = emptyList() + override fun getChanges(parent: Int): Collection = emptyList() +} blob - ae7f6076ead4322cc0a63a24d017ac0f76b98a8e blob + f893f92824fea712e2695c5cbf3f47507df21b36 --- src/main/kotlin/dev/nezzontli/gotvcs/push/GotPushSupport.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/push/GotPushSupport.kt @@ -15,7 +15,7 @@ class GotPushSupport(private val project: Project) : P private val commandLine = GotCommandLineWrapper() private val pusher = GotPusher(commandLine) - private val outgoingCommitsProvider = GotOutgoingCommitsProvider() + private val outgoingCommitsProvider = GotOutgoingCommitsProvider(commandLine) override fun getVcs(): AbstractVcs = ProjectLevelVcsManager.getInstance(project).findVcsByName(GotVcs.NAME)!! blob - ba31c5d498654310fa732f8323f263e9daa97f3b blob + 602288db61a733ba007e8129fde8ca2afbe9f7ce --- src/main/kotlin/dev/nezzontli/gotvcs/push/GotPushTarget.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/push/GotPushTarget.kt @@ -7,7 +7,7 @@ import com.intellij.dvcs.push.PushTarget * be a no-op, so [hasSomethingToPush] always returns true: `got send` itself * reports "nothing to send" if that turns out to be the case. */ -class GotPushTarget(private val remoteName: String, private val branch: String) : PushTarget { +class GotPushTarget(val remoteName: String, val branch: String) : PushTarget { override fun hasSomethingToPush(): Boolean = true override fun getPresentation(): String = "$remoteName/$branch" }