package org.iwakura.larcjb.changes import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.FilePath import com.intellij.openapi.vcs.FileStatus import com.intellij.openapi.vcs.ProjectLevelVcsManager import com.intellij.openapi.vcs.VcsKey import com.intellij.openapi.vcs.changes.* import com.intellij.openapi.vcs.history.VcsRevisionNumber import com.intellij.openapi.vfs.VirtualFile import com.intellij.vcsUtil.VcsUtil import org.iwakura.larcjb.LarcExecutable import org.iwakura.larcjb.LarcVcs /** * Provides file change information by parsing `larc status` output. */ class LarcChangeProvider( private val project: Project, private val executable: LarcExecutable ) : ChangeProvider { private val log = Logger.getInstance(LarcChangeProvider::class.java) private val vcsKey: VcsKey by lazy { val vcs = ProjectLevelVcsManager.getInstance(project).findVcsByName(LarcVcs.NAME) vcs?.keyInstanceMethod ?: throw IllegalStateException("Larc VCS not found") } override fun getChanges( dirtyScope: VcsDirtyScope, builder: ChangelistBuilder, progress: ProgressIndicator, addGate: ChangeListManagerGate ) { val roots = dirtyScope.affectedContentRoots for (root in roots) { progress.checkCanceled() collectChangesForRoot(root, builder) } } private fun collectChangesForRoot(root: VirtualFile, builder: ChangelistBuilder) { val result = executable.status(root) if (!result.isSuccess) { log.warn("Failed to get status for ${root.path}: ${result.stderr}") return } parseStatusOutput(result.stdout, root, builder) } /** * Parse larc status output format: * * Untracked files: * newfile.txt * * Staged changes: * added: file1.txt * modified: file2.txt * deleted: file3.txt * * Unstaged changes: * modified: file4.txt * deleted: file5.txt */ private fun parseStatusOutput(output: String, root: VirtualFile, builder: ChangelistBuilder) { var section = Section.NONE val rootPath = root.path for (line in output.lines()) { val trimmed = line.trim() when { trimmed.isEmpty() -> continue trimmed.startsWith("Untracked files:") -> section = Section.UNTRACKED trimmed.startsWith("Staged changes:") -> section = Section.STAGED trimmed.startsWith("Unstaged changes:") -> section = Section.UNSTAGED trimmed.startsWith("No changes") -> return else -> { when (section) { Section.UNTRACKED -> { val filePath = VcsUtil.getFilePath("$rootPath/$trimmed") builder.processUnversionedFile(filePath) } Section.STAGED -> { processChange(trimmed, rootPath, builder, staged = true) } Section.UNSTAGED -> { processChange(trimmed, rootPath, builder, staged = false) } Section.NONE -> { /* skip header lines */ } } } } } } private fun processChange( line: String, rootPath: String, builder: ChangelistBuilder, staged: Boolean ) { val parts = line.split(":", limit = 2) if (parts.size != 2) return val changeType = parts[0].trim().lowercase() val fileName = parts[1].trim() val filePath = VcsUtil.getFilePath("$rootPath/$fileName") val change = when (changeType) { "added" -> { Change(null, CurrentContentRevision(filePath), FileStatus.ADDED) } "modified" -> { Change( LarcContentRevision(filePath, executable, rootPath), CurrentContentRevision(filePath), FileStatus.MODIFIED ) } "deleted" -> { Change( LarcContentRevision(filePath, executable, rootPath), null, FileStatus.DELETED ) } else -> null } if (change != null) { builder.processChange(change, vcsKey) } } override fun isModifiedDocumentTrackingRequired(): Boolean = false private enum class Section { NONE, UNTRACKED, STAGED, UNSTAGED } } /** * Content revision for current working copy */ class CurrentContentRevision(private val filePath: FilePath) : ContentRevision { override fun getContent(): String? { val file = filePath.virtualFile ?: return null return String(file.contentsToByteArray(), Charsets.UTF_8) } override fun getFile(): FilePath = filePath override fun getRevisionNumber(): VcsRevisionNumber = VcsRevisionNumber.NULL } /** * Content revision for larc committed version */ class LarcContentRevision( private val filePath: FilePath, private val executable: LarcExecutable, private val rootPath: String ) : ContentRevision { override fun getContent(): String? { /* TODO(kroot): implement fetching content from last committed revision */ return null } override fun getFile(): FilePath = filePath override fun getRevisionNumber(): VcsRevisionNumber = VcsRevisionNumber.NULL }