commit a3f6382b95b1085b8c2ba724827d033669b9b5b8 from: ale date: Fri Jul 24 09:04:12 2026 UTC Refactor `doCommit` to delegate explicit path list commits via `commitFilePaths`. commit - f3ca72ddd01412bbbba872218e1fdf805b1cf72d commit + a3f6382b95b1085b8c2ba724827d033669b9b5b8 blob - /dev/null blob + 6d217996761e5c7534801c216d48eb1172ee761a (mode 644) --- /dev/null +++ .kotlin/errors/errors-1784879603938.log @@ -0,0 +1,3 @@ +kotlin version: 2.4.10 +error message: Daemon compilation failed + blob - 71cbd06edba31dd52ad4b0b3547d8f20376ce481 blob + 25846216f6513e9519a5fe1eabcc603d60f9f99a --- src/main/kotlin/dev/nezzontli/gotvcs/checkin/GotCheckinEnvironment.kt +++ src/main/kotlin/dev/nezzontli/gotvcs/checkin/GotCheckinEnvironment.kt @@ -43,9 +43,21 @@ class GotCheckinEnvironment( feedback: MutableSet, ): MutableList = doCommit(changes, commitMessage) - private fun doCommit(changes: List, commitMessage: String): MutableList { + private fun doCommit(changes: List, commitMessage: String): MutableList = + commitFilePaths(changes.mapNotNull { it.afterRevision?.file ?: it.beforeRevision?.file }, commitMessage) + + /** + * Exposed for GotCommitAndSendExecutor: unlike the default "Commit" + * button (a VCS_COMMIT session, for which the platform automatically + * calls scheduleUnversionedFilesForAddition()/scheduleMissingFileForDeletion() + * before committing), a custom CommitExecutor's session is on its own + * for that -- confirmed by decompiling ChangesViewCommitWorkflowHandler, + * whose addUnversionedFiles() is a no-op unless CommitSessionInfo.isVcsCommit() + * is true. This lets that caller commit an explicit path list (tracked + * changes plus whatever it scheduled for add/deletion itself) in one go. + */ + fun commitFilePaths(filePaths: List, commitMessage: String): MutableList { val exceptions = mutableListOf() - val filePaths = changes.mapNotNull { it.afterRevision?.file ?: it.beforeRevision?.file } for ((workDir, paths) in groupByRoot(filePaths)) { try { commandLine.commit(workDir, commitMessage, paths) blob - /dev/null blob + 00dd2062abec9b198fe10c55daef4490c8bc8ce1 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/checkin/GotCommitAndSendExecutor.kt @@ -0,0 +1,114 @@ +package dev.nezzontli.gotvcs.checkin + +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUiKind +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vcs.changes.ChangeListManager +import com.intellij.openapi.vcs.changes.ChangeListManagerImpl +import com.intellij.openapi.vcs.changes.CommitContext +import com.intellij.openapi.vcs.changes.CommitExecutor +import com.intellij.openapi.vcs.changes.CommitSession +import com.intellij.vcsUtil.VcsUtil +import dev.nezzontli.gotvcs.repo.GotRepositoryManager + +const val COMMIT_AND_SEND_EXECUTOR_ID = "Got.Commit.And.Send" + +/** + * Commits exactly like the default "Commit" button (delegates to the same + * CheckinEnvironment), then opens the native Push dialog (same action as + * Ctrl+Shift+K) so the user can review and send from there. + */ +class GotCommitAndSendExecutor( + private val project: Project, + private val checkinEnvironment: GotCheckinEnvironment, +) : CommitExecutor { + + override fun getActionText(): String = "Commit and Send" + + override fun getId(): String = COMMIT_AND_SEND_EXECUTOR_ID + + override fun createCommitSession(context: CommitContext): CommitSession = session + + private val session = object : CommitSession { + // execute() runs on a background thread (same as the default commit + // action), but opening the Push dialog is a UI action and must + // happen on the EDT -- see com.intellij.execution.process + // .OSProcessHandler#checkEdtAndReadAction-style threading rules. + override fun execute(changes: MutableCollection, commitMessage: String?) { + val message = commitMessage.orEmpty() + + // Unlike the default "Commit" button, a custom CommitExecutor's + // session does NOT get scheduleUnversionedFilesForAddition() / + // scheduleMissingFileForDeletion() called automatically by the + // platform first (verified by decompiling + // ChangesViewCommitWorkflowHandler.addUnversionedFiles(), which + // is a no-op unless CommitSessionInfo.isVcsCommit() is true) -- + // and `got commit -m msg ` errors with "no changes to + // commit" for a path that hasn't been `got add`-ed yet (verified + // directly against the CLI). So new/deleted files must be + // staged here ourselves before the actual commit. + val changeListManager = ChangeListManager.getInstance(project) as ChangeListManagerImpl + val unversionedFiles = changeListManager.unversionedFilesPaths.mapNotNull { it.virtualFile } + val deletedPaths = changeListManager.deletedFiles.map { it.path } + + if (unversionedFiles.isNotEmpty()) { + checkinEnvironment.scheduleUnversionedFilesForAddition(unversionedFiles.toMutableList()) + } + if (deletedPaths.isNotEmpty()) { + checkinEnvironment.scheduleMissingFileForDeletion(deletedPaths.toMutableList()) + } + + val trackedPaths = changes.mapNotNull { it.afterRevision?.file ?: it.beforeRevision?.file } + val unversionedFilePaths = unversionedFiles.map { VcsUtil.getFilePath(it) } + val allPaths: List = (trackedPaths + unversionedFilePaths + deletedPaths).distinct() + + val errors = checkinEnvironment.commitFilePaths(allPaths, message) + if (errors.isNotEmpty()) { + val group = NotificationGroupManager.getInstance().getNotificationGroup("got") + group.createNotification( + "got commit failed", + errors.joinToString("\n") { it.message.orEmpty() }, + NotificationType.ERROR, + ).notify(project) + return + } + + // GotRepository's cached branch/revision (used to resolve the + // Push dialog's target) is normally refreshed by the platform's + // own post-commit VCS-root-dirty machinery, but that happens + // asynchronously and can race with the dialog opening right + // below -- refreshing it here ourselves, still on this + // background thread, guarantees the dialog sees the commit that + // was just made instead of possibly-stale cached state. + val vcsManager = ProjectLevelVcsManager.getInstance(project) + val repositoryManager = project.getService(GotRepositoryManager::class.java) + val affectedRoots = allPaths.mapNotNull { vcsManager.getVcsRootFor(it) }.toSet() + for (root in affectedRoots) { + repositoryManager.getRepositoryForRoot(root)?.update() + } + + ApplicationManager.getApplication().invokeLater { + val pushAction = ActionManager.getInstance().getAction("Vcs.Push") ?: return@invokeLater + val dataContext = SimpleDataContext.getProjectContext(project) + val event = AnActionEvent.createEvent( + dataContext, + Presentation(), + "GotCommitAndSend", + ActionUiKind.NONE, + null, + ) + ActionUtil.performAction(pushAction, event) + } + } + } +} blob - /dev/null blob + 304c9f39bf6f17b49d46dbe001c9067492d285bd (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/checkin/GotCommitAndSendExecutorAction.kt @@ -0,0 +1,8 @@ +package dev.nezzontli.gotvcs.checkin + +import com.intellij.openapi.vcs.changes.actions.BaseCommitExecutorAction + +/** Renders as the secondary (gray) toolbar button, next to the primary blue "Commit" button. */ +class GotCommitAndSendExecutorAction : BaseCommitExecutorAction() { + override val executorId: String = COMMIT_AND_SEND_EXECUTOR_ID +} blob - /dev/null blob + 4cd0edd93bd88e55d33348212bc95b0946dc5b72 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/log/GotCommitContentRevision.kt @@ -0,0 +1,32 @@ +package dev.nezzontli.gotvcs.log + +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.changes.ContentRevision +import com.intellij.openapi.vcs.history.VcsRevisionNumber +import dev.nezzontli.gotvcs.changes.GotRevisionNumber +import dev.nezzontli.gotvcs.cli.GotCommandLineWrapper +import java.io.File + +/** + * File content at an arbitrary historical commit (`got cat -c -P`), + * for the Log tab's per-commit diff. Unlike GotContentRevision (which is + * pinned to the work tree's current base commit), this one is parametrized + * by whichever commit the Log tab is showing -- called from background log + * loading, never from EDT-bound UI rendering, so it may shell out lazily. + */ +class GotCommitContentRevision( + private val filePath: FilePath, + private val workDir: File, + private val relativePath: String, + private val commandLine: GotCommandLineWrapper, + private val commitId: String, +) : ContentRevision { + + @Throws(VcsException::class) + override fun getContent(): String = commandLine.catAt(workDir, commitId, relativePath) + + override fun getFile(): FilePath = filePath + + override fun getRevisionNumber(): VcsRevisionNumber = GotRevisionNumber(commitId) +} blob - /dev/null blob + d283e140140370e12759cba3fbc196bfea092aab (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/log/GotFullCommitDetails.kt @@ -0,0 +1,42 @@ +package dev.nezzontli.gotvcs.log + +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 + * platform's commit-details model, shared by the Push dialog's + * outgoing-commits preview (no [changes] needed there) and the Log tab + * (which populates [changes] from `got log -P`). [getChanges] returns the + * same flat list regardless of [parent] index -- got's `-P` output diffs + * against the immediate history parent, so this isn't fully merge-aware + * for commits with more than one parent, but is correct for the common case. + */ +class GotFullCommitDetails( + private val commitObject: GotCommitObject, + private val root: VirtualFile, + private val changes: List = emptyList(), +) : 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 = changes + override fun getChanges(parent: Int): Collection = changes +} blob - /dev/null blob + 773c61207552e102eb1c5d71fe27973802e17e87 (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/log/GotVcsLogProvider.kt @@ -0,0 +1,167 @@ +package dev.nezzontli.gotvcs.log + +import com.intellij.openapi.Disposable +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.VcsKey +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Consumer +import com.intellij.vcs.log.Hash +import com.intellij.vcs.log.VcsLogProperties +import com.intellij.vcs.log.VcsLogProvider +import com.intellij.vcs.log.VcsLogRefManager +import com.intellij.vcs.log.VcsRef +import com.intellij.vcs.log.VcsUser +import com.intellij.vcs.log.impl.HashImpl +import com.intellij.vcs.log.util.VcsUserUtil +import com.intellij.vcsUtil.VcsUtil +import dev.nezzontli.gotvcs.GotVcs +import dev.nezzontli.gotvcs.cli.GotCommandLineWrapper +import dev.nezzontli.gotvcs.cli.GotChangedPath +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +/** + * Backs the platform's Log tab entirely by shelling out to `got` (no local + * index/cache beyond GotCommandLineWrapper's in-memory commit cache) -- same + * philosophy as the rest of this plugin: no daemon, no persisted state. + */ +class GotVcsLogProvider(private val commandLine: GotCommandLineWrapper = GotCommandLineWrapper()) : VcsLogProvider { + + override val supportedVcs: VcsKey = GotVcs.getKey() + + override val referenceManager: VcsLogRefManager = GotVcsLogRefManager() + + // getCurrentBranch()/getCurrentUser() are called from EDT-bound UI code + // (CurrentBranchHighlighter.update(), on every visible-pack refresh -- and + // likely an equivalent "my commits" highlighter for getCurrentUser), so + // neither can shell out to `got` synchronously -- same class of bug fixed + // earlier for GotRepository/GotBranchWidget. readAllHashes() always runs + // off the EDT (it's the Log tab's own background data loading), and + // always runs before those highlighters can fire, so precomputing both + // here and serving them from cache is safe. + private val currentBranchCache = ConcurrentHashMap() + private val currentUserCache = ConcurrentHashMap() + + override fun readAllHashes(root: VirtualFile, commitConsumer: Consumer): VcsLogProvider.LogData { + val workDir = File(root.path) + try { + val branch = commandLine.currentBranch(workDir) + if (branch != null) { + currentBranchCache[root.path] = branch + val tipId = commandLine.refs(workDir).firstOrNull { !it.isRemote && !it.isTag && it.name == branch }?.hash + val commitObject = tipId?.let { commandLine.catCommit(workDir, it) } + if (commitObject != null) { + currentUserCache[root.path] = VcsUserUtil.createUser(commitObject.authorName, commitObject.authorEmail) + } + } else { + currentBranchCache.remove(root.path) + } + } catch (e: VcsException) { + currentBranchCache.remove(root.path) + } + val ids = commandLine.allCommitIds(workDir) + val users = mutableSetOf() + for (id in ids) { + val commitObject = commandLine.catCommit(workDir, id) + users.add(VcsUserUtil.createUser(commitObject.authorName, commitObject.authorEmail)) + commitConsumer.consume(GotFullCommitDetails(commitObject, root)) + } + val refs = buildRefs(workDir, root) + return SimpleLogData(refs, users) + } + + override fun readMetadata( + root: VirtualFile, + hashes: List, + consumer: Consumer, + ) { + val workDir = File(root.path) + for (id in hashes) { + consumer.consume(GotFullCommitDetails(commandLine.catCommit(workDir, id), root)) + } + } + + override fun readFullDetails( + root: VirtualFile, + hashes: List, + commitConsumer: Consumer, + ) { + val workDir = File(root.path) + for (id in hashes) { + val commitObject = commandLine.catCommit(workDir, id) + val parentId = commitObject.parents.firstOrNull() + val changes = try { + commandLine.changedPaths(workDir, id).map { toChange(workDir, id, parentId, it) } + } catch (e: VcsException) { + emptyList() + } + commitConsumer.consume(GotFullCommitDetails(commitObject, root, changes)) + } + } + + private fun toChange(workDir: File, commitId: String, parentId: String?, changed: GotChangedPath): Change { + val filePath = VcsUtil.getFilePath(File(workDir, changed.path), false) + val before = parentId?.let { GotCommitContentRevision(filePath, workDir, changed.path, commandLine, it) } + val after = GotCommitContentRevision(filePath, workDir, changed.path, commandLine, commitId) + return when (changed.code) { + 'A' -> Change(null, after) + 'D' -> Change(before, null) + else -> Change(before, after) + } + } + + private fun buildRefs(workDir: File, root: VirtualFile): Set { + val entries = try { + commandLine.refs(workDir) + } catch (e: VcsException) { + emptyList() + } + return entries.map { entry -> + val type = when { + entry.isTag -> GotRefType.TAG + entry.isRemote -> GotRefType.REMOTE_BRANCH + else -> GotRefType.LOCAL_BRANCH + } + SimpleVcsRef(HashImpl.build(entry.hash), entry.name, type, root) + }.toSet() + } + + /** Best-effort: the tip commit's author, precomputed by readAllHashes(). got repos in practice have a single author, so this is accurate enough. */ + override fun getCurrentUser(root: VirtualFile): VcsUser = + currentUserCache[root.path] ?: VcsUserUtil.createUser("", "") + + override fun getContainingBranches(root: VirtualFile, commitHash: Hash): Collection = emptyList() + + override fun getPropertyValue(property: VcsLogProperties.VcsLogProperty): T? { + @Suppress("UNCHECKED_CAST") + return when (property) { + VcsLogProperties.LIGHTWEIGHT_BRANCHES -> true as T + else -> false as T + } + } + + override fun getCurrentBranch(root: VirtualFile): String? = currentBranchCache[root.path] + + override fun subscribeToRootRefreshEvents( + roots: Collection, + refresher: com.intellij.vcs.log.VcsLogRefresher, + ): Disposable = Disposable { } + + private class SimpleLogData( + override val refs: Set, + override val users: Set, + ) : VcsLogProvider.LogData + + private class SimpleVcsRef( + private val hash: Hash, + private val name: String, + private val type: com.intellij.vcs.log.VcsRefType, + private val root: VirtualFile, + ) : VcsRef { + override fun getCommitHash(): Hash = hash + override fun getName(): String = name + override fun getType(): com.intellij.vcs.log.VcsRefType = type + override fun getRoot(): VirtualFile = root + } +} blob - /dev/null blob + 92899efdaeb5aaf2334dd2dc3eaf9d31ce18c13c (mode 644) --- /dev/null +++ src/main/kotlin/dev/nezzontli/gotvcs/log/GotVcsLogRefManager.kt @@ -0,0 +1,47 @@ +package dev.nezzontli.gotvcs.log + +import com.intellij.vcs.log.RefGroup +import com.intellij.vcs.log.VcsLogRefManager +import com.intellij.vcs.log.VcsRef +import com.intellij.vcs.log.VcsRefType +import java.io.DataInput +import java.io.DataOutput + +/** + * got has no notion of ref grouping/favorites beyond plain + * branches/remote-branches/tags, so this is a flat, ungrouped manager: one + * [RefGroup] per ref, ordered by name. + */ +class GotVcsLogRefManager : VcsLogRefManager { + + private val nameComparator = Comparator.comparing { it.name } + + override fun getBranchLayoutComparator(): Comparator = nameComparator + + override fun getLabelsOrderComparator(): Comparator = nameComparator + + override fun groupForBranchFilter(refs: MutableCollection): List = + refs.sortedWith(nameComparator).map { SingleRefGroup(it) } + + override fun groupForTable(refs: MutableCollection, compact: Boolean, showTagNames: Boolean): List = + refs.sortedWith(nameComparator).map { SingleRefGroup(it) } + + override fun serialize(out: DataOutput, type: VcsRefType) { + out.writeByte((type as? GotRefType)?.ordinal ?: GotRefType.LOCAL_BRANCH.ordinal) + } + + override fun deserialize(input: DataInput): VcsRefType { + val ordinal = input.readByte().toInt() + return GotRefType.entries.getOrElse(ordinal) { GotRefType.LOCAL_BRANCH } + } + + override fun isFavorite(ref: VcsRef): Boolean = false + + override fun setFavorite(ref: VcsRef, favorite: Boolean) = Unit + + private class SingleRefGroup(private val ref: VcsRef) : RefGroup { + override fun getName(): String = ref.name + override fun getRefs(): List = listOf(ref) + override fun getColors(): List = listOf(ref.type.backgroundColor) + } +} blob - 6109655e214c356044f7d12f5641baa37d9d2164 (mode 644) blob + /dev/null --- src/main/kotlin/dev/nezzontli/gotvcs/push/GotFullCommitDetails.kt +++ /dev/null @@ -1,39 +0,0 @@ -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() -}