commit f2c271a1a7eb4bc7a6000c5b8ff246338d3ea016 from: ale date: Thu Jul 23 20:30:59 2026 UTC Fase 5: historial (got log) y update/fetch desde la UI GotVcsHistoryProvider + GotVcsHistorySession + GotFileRevision exponen 'got log' en el diálogo nativo de 'Show History': parsea el formato verbose por defecto (bloques separados por líneas de guiones, 'commit ', 'from:', 'date:', mensaje indentado) y usa 'got cat -c -P ' para cargar el contenido de cada revisión bajo demanda. GotUpdateEnvironment implementa 'Update Project': por cada raíz seleccionada corre 'got fetch' (best-effort, got.conf puede no tener remoto) seguido de 'got update', parseando sus códigos de estado (U/G/C/D/A/!/#) a los FileGroup estándar de IntelliJ. GotCommandLineWrapper gana log/fetch/update/catAt (cat contra un commit arbitrario, no solo :base). commit - 86002dd96cc41e1d71f2bcd80782da4e6c1f4d0e commit + f2c271a1a7eb4bc7a6000c5b8ff246338d3ea016 blob - 3a132b68a94c64d99f1a7cfa3f540b599cfc9c73 blob + 62c7c35916c9501165f4cadef5b9539a44a95a67 --- src/main/kotlin/dev/nezzontli/gotvcs/GotCommandLineWrapper.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/GotCommandLineWrapper.kt @@ -9,6 +9,10 @@ import java.nio.charset.StandardCharsets data class GotStatusEntry(val code: Char, val stagedCode: Char, val path: String) +data class GotLogEntry(val commitId: String, val author: String, val date: String, val message: String) + +data class GotUpdateEntry(val code: Char, val path: String) + /** * Centraliza todas las invocaciones al binario `got`. La ruta de abajo es la * del store de Nix resuelta al momento de escribir este código @@ -57,6 +61,10 @@ class GotCommandLineWrapper { run(workDir, "cat", "-c", ":base", "-P", relativePath) @Throws(VcsException::class) + fun catAt(workDir: File, commitId: String, relativePath: String): String = + run(workDir, "cat", "-c", commitId, "-P", relativePath) + + @Throws(VcsException::class) fun baseCommit(workDir: File): String { val output = run(workDir, "info") val line = output.lineSequence().firstOrNull { it.trimStart().startsWith("work tree base commit:") } @@ -87,4 +95,60 @@ class GotCommandLineWrapper { if (paths.isEmpty()) return run(workDir, *(arrayOf("revert", "-R") + paths)) } + + /** Historial vía `got log`, formato verbose por defecto (bloques separados por líneas de guiones). */ + @Throws(VcsException::class) + fun log(workDir: File, relativePath: String?, limit: Int = 500): List { + val args = mutableListOf("log", "-l", limit.toString()) + if (relativePath != null) args.add(relativePath) + val output = run(workDir, *args.toTypedArray()) + + val entries = mutableListOf() + var commitId: String? = null + var author: String? = null + var date: String? = null + val message = StringBuilder() + + fun flush() { + val id = commitId + if (id != null) { + entries.add(GotLogEntry(id, author.orEmpty(), date.orEmpty(), message.toString().trim('\n'))) + } + commitId = null + author = null + date = null + message.setLength(0) + } + + for (line in output.lineSequence()) { + when { + line.isBlank() && commitId == null -> Unit + line.all { it == '-' } && line.isNotEmpty() -> flush() + line.startsWith("commit ") -> commitId = line.removePrefix("commit ").trim().substringBefore(' ') + line.startsWith("from: ") -> author = line.removePrefix("from: ").trim() + line.startsWith("date: ") -> date = line.removePrefix("date: ").trim() + line.startsWith(" ") -> message.appendLine(line.trimStart(' ')) + } + } + flush() + return entries + } + + @Throws(VcsException::class) + fun fetch(workDir: File) { + run(workDir, "fetch") + } + + /** `got update`, formato de estado similar a `got status` (código + 2 espacios + path). */ + @Throws(VcsException::class) + fun update(workDir: File): List { + val output = run(workDir, "update") + return output.lineSequence() + .filter { it.isNotBlank() && it[0] != ' ' } + .mapNotNull { line -> + if (line.length < 3) return@mapNotNull null + GotUpdateEntry(line[0], line.substring(3)) + } + .toList() + } } blob - /dev/null blob + 16059e116fbd373039f1808615f71b732dd0aef5 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/GotFileRevision.kt @@ -0,0 +1,44 @@ +package dev.nezzontli.gotvcs + +import com.intellij.openapi.vcs.RepositoryLocation +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.history.VcsFileRevision +import com.intellij.openapi.vcs.history.VcsRevisionNumber +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** Una entrada de `got log` adaptada al modelo de historial de IntelliJ. */ +class GotFileRevision( + private val workDir: File, + private val relativePath: String, + private val commandLine: GotCommandLineWrapper, + private val entry: GotLogEntry, +) : VcsFileRevision { + + // Formato real de `got log`: "Thu Jul 23 05:30:12 2026 UTC". + private val dateFormat = SimpleDateFormat("EEE MMM d HH:mm:ss yyyy zzz", Locale.US) + + override fun getRevisionNumber(): VcsRevisionNumber = GotRevisionNumber(entry.commitId) + + override fun getRevisionDate(): Date = try { + dateFormat.parse(entry.date) + } catch (e: Exception) { + Date(0) + } + + override fun getAuthor(): String = entry.author + + override fun getCommitMessage(): String = entry.message + + override fun getBranchName(): String? = null + + override fun getChangedRepositoryPath(): RepositoryLocation? = null + + @Throws(VcsException::class) + override fun loadContent(): ByteArray = getContent() + + @Throws(VcsException::class) + override fun getContent(): ByteArray = commandLine.catAt(workDir, entry.commitId, relativePath).toByteArray() +} blob - 361c64c88ee0ab4cdf957b1ed9fc7d775b12f158 blob + b433f08749a4b14d51e376f06d541a91dca5ac13 --- src/main/kotlin/dev/nezzontli/gotvcs/GotVcs.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/GotVcs.kt @@ -6,7 +6,9 @@ import com.intellij.openapi.vcs.VcsKey import com.intellij.openapi.vcs.changes.ChangeProvider import com.intellij.openapi.vcs.checkin.CheckinEnvironment import com.intellij.openapi.vcs.diff.DiffProvider +import com.intellij.openapi.vcs.history.VcsHistoryProvider import com.intellij.openapi.vcs.rollback.RollbackEnvironment +import com.intellij.openapi.vcs.update.UpdateEnvironment class GotVcs(project: Project) : AbstractVcs(project, NAME) { @@ -15,6 +17,8 @@ class GotVcs(project: Project) : AbstractVcs(project, private val diffProvider = GotDiffProvider(project, commandLine) private val checkinEnvironment = GotCheckinEnvironment(project, commandLine) private val rollbackEnvironment = GotRollbackEnvironment(project, commandLine) + private val historyProvider = GotVcsHistoryProvider(project, commandLine) + private val updateEnvironment = GotUpdateEnvironment(commandLine) override fun getDisplayName(): String = NAME @@ -26,6 +30,10 @@ class GotVcs(project: Project) : AbstractVcs(project, override fun getRollbackEnvironment(): RollbackEnvironment = rollbackEnvironment + override fun getVcsHistoryProvider(): VcsHistoryProvider = historyProvider + + override fun getUpdateEnvironment(): UpdateEnvironment = updateEnvironment + companion object { const val NAME = "got" blob - /dev/null blob + 603a30cd5a03c463712bb3314ece4eb4115a207d (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/GotUpdateEnvironment.kt @@ -0,0 +1,66 @@ +package dev.nezzontli.gotvcs + +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.util.Ref +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.update.FileGroup +import com.intellij.openapi.vcs.update.SequentialUpdatesContext +import com.intellij.openapi.vcs.update.UpdateEnvironment +import com.intellij.openapi.vcs.update.UpdateSession +import com.intellij.openapi.vcs.update.UpdatedFiles +import java.io.File + +/** + * "Update Project" en IntelliJ ejecuta `got fetch` (best-effort: got.conf + * puede no tener un remoto configurado) seguido de `got update` sobre cada + * raíz seleccionada. + */ +class GotUpdateEnvironment(private val commandLine: GotCommandLineWrapper) : UpdateEnvironment { + + override fun fillGroups(updatedFiles: UpdatedFiles) = Unit + + override fun updateDirectories( + contentRoots: Array, + updatedFiles: UpdatedFiles, + progressIndicator: ProgressIndicator, + context: Ref, + ): UpdateSession { + val exceptions = mutableListOf() + val workDirs = contentRoots.map { File(it.path) }.distinct() + + for (workDir in workDirs) { + try { + commandLine.fetch(workDir) + } catch (e: VcsException) { + exceptions.add(e) + } + + try { + for (entry in commandLine.update(workDir)) { + val groupId = when (entry.code) { + 'U', 'G' -> FileGroup.UPDATED_ID + 'C', '#' -> FileGroup.MERGED_WITH_CONFLICT_ID + 'D' -> FileGroup.REMOVED_FROM_REPOSITORY_ID + 'A', '!' -> FileGroup.CREATED_ID + else -> continue + } + updatedFiles.getGroupById(groupId)?.add( + File(workDir, entry.path).path, + GotVcs.getKey(), + null, + ) + } + } catch (e: VcsException) { + exceptions.add(e) + } + } + + return GotUpdateSession(exceptions) + } + + override fun createConfigurable(files: MutableCollection): Configurable? = null + + override fun validateOptions(files: MutableCollection): Boolean = true +} blob - /dev/null blob + 6896e018526e2496a89dedb5757f2667ef065a50 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/GotUpdateSession.kt @@ -0,0 +1,13 @@ +package dev.nezzontli.gotvcs + +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.update.UpdateSession + +class GotUpdateSession(private val exceptions: MutableList) : UpdateSession { + + override fun getExceptions(): MutableList = exceptions + + override fun onRefreshFilesCompleted() = Unit + + override fun isCanceled(): Boolean = false +} blob - /dev/null blob + 1a42e9637c573a2648e53d28c8aa02ff6bd317fc (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/GotVcsHistoryProvider.kt @@ -0,0 +1,63 @@ +package dev.nezzontli.gotvcs + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.history.DiffFromHistoryHandler +import com.intellij.openapi.vcs.history.VcsAbstractHistorySession +import com.intellij.openapi.vcs.history.VcsAppendableHistorySessionPartner +import com.intellij.openapi.vcs.history.VcsDependentHistoryComponents +import com.intellij.openapi.vcs.history.VcsFileRevision +import com.intellij.openapi.vcs.history.VcsHistoryProvider +import com.intellij.openapi.vcs.history.VcsHistorySession +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.util.ui.ColumnInfo +import java.io.File + +class GotVcsHistoryProvider( + private val project: Project, + private val commandLine: GotCommandLineWrapper, +) : VcsHistoryProvider { + + override fun getUICustomization(session: VcsHistorySession, forShortcutRegistration: javax.swing.JComponent) = + VcsDependentHistoryComponents.createOnlyColumns(emptyArray>()) + + override fun getAdditionalActions(refresher: Runnable): Array = emptyArray() + + override fun isDateOmittable(): Boolean = false + + override fun getHelpId(): String? = null + + override fun supportsHistoryForDirectories(): Boolean = true + + override fun getHistoryDiffHandler(): DiffFromHistoryHandler? = null + + override fun canShowHistoryFor(virtualFile: com.intellij.openapi.vfs.VirtualFile): Boolean = true + + @Throws(VcsException::class) + override fun createSessionFor(filePath: FilePath): VcsHistorySession { + val vcsRoot = ProjectLevelVcsManager.getInstance(project).getVcsRootFor(filePath) + ?: throw VcsException("$filePath no pertenece a ningún work tree got") + val workDir = File(vcsRoot.path) + val relativePath = VfsUtilCore.getRelativePath(filePath.virtualFile ?: vcsRoot, vcsRoot) + ?: throw VcsException("No se pudo resolver la ruta relativa de $filePath en $workDir") + + val entries = commandLine.log(workDir, relativePath.ifEmpty { null }) + val revisions: List = entries.map { GotFileRevision(workDir, relativePath, commandLine, it) } + val baseRevision = GotRevisionNumber(commandLine.baseCommit(workDir)) + return GotVcsHistorySession(revisions, baseRevision) + } + + @Throws(VcsException::class) + override fun reportAppendableHistory(filePath: FilePath, partner: VcsAppendableHistorySessionPartner) { + try { + val session = createSessionFor(filePath) as VcsAbstractHistorySession + partner.reportCreatedEmptySession(session) + session.revisionList.forEach { partner.acceptRevision(it) } + } catch (e: VcsException) { + partner.reportException(e) + } + } +} blob - /dev/null blob + bbb281713e03b2340649d2950a97f3d43e1df48b (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/GotVcsHistorySession.kt @@ -0,0 +1,16 @@ +package dev.nezzontli.gotvcs + +import com.intellij.openapi.vcs.history.VcsAbstractHistorySession +import com.intellij.openapi.vcs.history.VcsFileRevision +import com.intellij.openapi.vcs.history.VcsHistorySession +import com.intellij.openapi.vcs.history.VcsRevisionNumber + +class GotVcsHistorySession( + revisions: List, + private val baseRevision: VcsRevisionNumber, +) : VcsAbstractHistorySession(revisions, baseRevision) { + + override fun calcCurrentRevisionNumber(): VcsRevisionNumber = baseRevision + + override fun copy(): VcsHistorySession = GotVcsHistorySession(revisionList, baseRevision) +}