FNNW5IEAXQ43WKB6QSQB7DFLG3Y3T5FYPXIUX7KQ2URR2GU3QLTAC
com.github.jonathanxd.dracon.cmd.PijulCmd
package com.github.jonathanxd.dracon.vfs
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.impl.FileStatusProvider
import com.intellij.openapi.vfs.VirtualFile
class PijulVirtualFileStatusProvider(val project: Project): FileStatusProvider {
override fun getFileStatus(virtualFile: VirtualFile): FileStatus {
TODO("Not yet implemented")
}
}
package com.github.jonathanxd.dracon.vfs
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
object DraconVfsUtil {
fun refreshVfs(roots: List<VirtualFile>) {
VfsUtil.markDirtyAndRefresh(false, true, false, *roots.toTypedArray());
}
}
package com.github.jonathanxd.dracon.vcs
object NotificationIds {
const val FAILED_TO_INIT = "pijul.init.failed"
}
package com.github.jonathanxd.dracon.vcs
import com.github.jonathanxd.dracon.vfs.DraconVfsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.vcsUtil.VcsUtil
object DraconVcsUtil {
fun refreshAfterInit(project: Project, root: VirtualFile) {
DraconVfsUtil.refreshVfs(listOf(root))
val manager = ProjectLevelVcsManager.getInstance(project)
manager.directoryMappings = VcsUtil.addMapping(manager.directoryMappings, root.path, "Pijul")
VcsDirtyScopeManager.getInstance(project).dirDirtyRecursively(root)
}
}
package com.github.jonathanxd.dracon.util
import java.nio.file.Files
import java.nio.file.Path
fun Path.existsOrNull(): Path? =
if (Files.exists(this)) this
else null
fun Path.isDirectoryOrNull(): Path? =
if (Files.isDirectory(this)) this
else null
fun Path.isRegularFileOrNull(): Path? =
if (Files.isRegularFile(this)) this
else null
fun Path.isExecutableOrNull(): Path? =
if (Files.isExecutable(this)) this
else null
package com.github.jonathanxd.dracon.util
import com.intellij.openapi.util.text.StringUtil
import com.intellij.xml.util.XmlStringUtil
fun String.wrapInHml() =
XmlStringUtil.wrapInHtml(this)
fun String.escapeXml() =
StringUtil.escapeXmlEntities(this)
package com.github.jonathanxd.dracon.util
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flowOn
import java.io.InputStream
fun InputStream.linesToFlow() = bufferedReader().lineSequence().asFlow().flowOn(Dispatchers.IO)
package com.github.jonathanxd.dracon.roots
import com.github.jonathanxd.dracon.PijulVcs
import com.github.jonathanxd.dracon.pijul.Pijul
import com.intellij.openapi.vcs.VcsKey
import com.intellij.openapi.vcs.VcsRootChecker
import java.nio.file.Paths
class PijulRootChecker : VcsRootChecker() {
override fun getSupportedVcs(): VcsKey = PijulVcs.KEY
override fun isRoot(path: String): Boolean =
Pijul.isPijulRepository(Paths.get(path))
override fun isVcsDir(dirName: String): Boolean =
dirName.equals(".pijul", ignoreCase = true)
}
package com.github.jonathanxd.dracon.pijul
/**
* An interface which provides introspection of [Pijul] `pristine` channels data.
*/
interface PijulPristine {
}
package com.github.jonathanxd.dracon.pijul
sealed class StatusCode(val code: Int)
object SuccessStatusCode : StatusCode(0)
data class NonZeroExitStatusCode(val exitCode: Int, val message: String) : StatusCode(exitCode)
data class PijulOperationResult<R>(val operation: String,
val statusCode: StatusCode,
val result: R?)
package com.github.jonathanxd.dracon.pijul
import kotlinx.coroutines.flow.Flow
class PijulExecution(
val regularStream: Flow<String>,
val errorStream: Flow<String>,
val status: Flow<Int>
)
package com.github.jonathanxd.dracon.pijul
/**
* An interface which provides introspection of [Pijul] `changes` data.
*/
interface PijulChanges {
}
package com.github.jonathanxd.dracon.pijul
import com.github.jonathanxd.dracon.util.existsOrNull
import com.github.jonathanxd.dracon.util.isDirectoryOrNull
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.vcsUtil.VcsUtil
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
val PIJUL_DIR = ".pijul"
val PIJUL_INSTANCE get() = service<Pijul>()
fun pijul(project: Project): Pijul = project.service()
/**
* Base interface for Pijul-IntelliJ communication.
*
* This interface provides all methods for communication with [Pijul], as well some basic implementations
* for operations that does not requires communication with [Pijul] backend.
*
* However, some functionalities are not provided through this interface, for example, Pijul changes are stored in binary
* format in the `.pijul/changes` directory and channels (like branches in git) are stored in `.pijul/pristine` directory,
* as binary data as well, introspecting this data is done through [PijulChanges] and [PijulPristine] interfaces.
*/
interface Pijul {
companion object {
fun isPijulRepository(root: Path) =
root.resolve(PIJUL_DIR).existsOrNull()?.isDirectoryOrNull() != null
}
fun findPijulDirectory(root: VirtualFile): VirtualFile? {
var dir: Path? = Paths.get(VcsUtil.getFilePath(root).path)
while (dir != null) {
if (isPijulRepository(dir)) {
return LocalFileSystem.getInstance().findFileByNioFile(dir)
}
dir = dir.parent
}
return null
}
fun isUnderPijul(root: VirtualFile): Boolean = this.findPijulDirectory(root) != null
@RequiresBackgroundThread
fun init(project: Project, root: VirtualFile): PijulOperationResult<Unit>
@RequiresBackgroundThread
fun add(project: Project, root: VirtualFile, paths: List<FilePath>): PijulOperationResult<Unit>
@RequiresBackgroundThread
fun fileStatus(project: Project, file: VirtualFile): PijulOperationResult<FileStatus>
}
package com.github.jonathanxd.dracon.log
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.ProjectLevelVcsManager
fun draconConsoleWriter(project: Project): DraconConsoleWriter =
project.service()
@Service
class DraconConsoleWriter(val project: Project) {
fun logCommand(@NlsSafe command: String, @NlsSafe args: List<String>, @NlsSafe message: String) {
this.log("$ $command ${args.joinToString(separator = " ")} > $message", ConsoleViewContentType.NORMAL_OUTPUT)
}
fun logCommandError(@NlsSafe command: String, @NlsSafe args: List<String>, @NlsSafe message: String) {
this.log("$ $command ${args.joinToString(separator = " ")} > $message", ConsoleViewContentType.ERROR_OUTPUT)
}
fun log(@NlsSafe message: String) {
this.log(message, ConsoleViewContentType.NORMAL_OUTPUT)
}
fun logError(@NlsSafe message: String) {
this.log(message, ConsoleViewContentType.ERROR_OUTPUT)
}
fun log(@NlsSafe message: String, contentType: ConsoleViewContentType) {
ProjectLevelVcsManager.getInstance(this.project).addMessageToConsoleWindow(message, contentType)
}
}
package com.github.jonathanxd.dracon.i18n
import com.github.jonathanxd.dracon.util.escapeXml
import com.github.jonathanxd.dracon.util.wrapInHml
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
import java.util.function.Supplier
const val BUNDLE = "messages.DraconBundle"
object DraconBundle : DynamicBundle(BUNDLE) {
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): @Nls String {
return this.getMessage(key, *params)
}
fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): Supplier<String> {
return this.getLazyMessage(key, *params)
}
object Init {
val title
get() = message("init.title")
val description
get() = message("init.description")
val error
get() = message("init.error")
fun alreadyUnderPijul(arg: Any) =
message("init.warning.already.under.pijul", arg)
fun alreadyUnderPijulForHtml(arg: String) =
message("init.warning.already.under.pijul", arg.escapeXml()).wrapInHml()
}
object Dracon {
val nameSupplier
get() = DraconBundle.messagePointer("dracon.vcs.name")
val mnemonic
get() = DraconBundle.message("dracon.vcs.name.with.mnemonic")
val refreshing
get() = message("dracon.refresh")
}
}
package com.github.jonathanxd.dracon.executor
import com.intellij.dvcs.ui.DvcsBundle
import com.intellij.openapi.util.Key
import com.intellij.openapi.vcs.changes.CommitContext
import com.intellij.openapi.vcs.changes.CommitExecutor
import com.intellij.openapi.vcs.changes.CommitSession
import com.intellij.vcs.commit.commitProperty
import org.jetbrains.annotations.Nls
private val IS_PUSH_AFTER_COMMIT_KEY = Key.create<Boolean>("Pijul.Commit.IsPushAfterCommit")
internal var CommitContext.isPushAfterCommit: Boolean by commitProperty(IS_PUSH_AFTER_COMMIT_KEY)
class PijulCommitAndPushExecutor : CommitExecutor {
@Nls
override fun getActionText(): String = DvcsBundle.message("action.commit.and.push.text")
override fun useDefaultAction(): Boolean = false
override fun getId(): String = ID
override fun createCommitSession(commitContext: CommitContext): CommitSession {
commitContext.isPushAfterCommit = true
return CommitSession.VCS_COMMIT
}
companion object {
internal const val ID = "Pijul.Commit.And.Push.Executor"
}
}
package com.github.jonathanxd.dracon.cmd
import com.github.jonathanxd.dracon.log.draconConsoleWriter
import com.github.jonathanxd.dracon.pijul.*
import com.github.jonathanxd.dracon.util.existsOrNull
import com.github.jonathanxd.dracon.util.isExecutableOrNull
import com.github.jonathanxd.dracon.util.isRegularFileOrNull
import com.github.jonathanxd.dracon.util.linesToFlow
import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.vcsUtil.VcsFileUtil
import com.intellij.vcsUtil.VcsUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
/**
* Command-line based implementation of [Pijul] interface.
*
* An IPC (Inter Process Communication) version of [Pijul] could be implemented in the future,
* however, an command-line based is enough for now.
*/
class PijulCmd(val project: Project) : Pijul {
@RequiresBackgroundThread
override fun init(project: Project, root: VirtualFile): PijulOperationResult<Unit> {
val path = Paths.get(VcsUtil.getFilePath(root).path)
val execution = this.execPijul(project, path, listOf("init", path.toString()))
return this.doExecution("init", execution)
}
@RequiresBackgroundThread
override fun add(project: Project, root: VirtualFile, paths: List<FilePath>): PijulOperationResult<Unit> {
val path = Paths.get(VcsUtil.getFilePath(root).path)
val results = mutableListOf<PijulOperationResult<Unit>>()
if (paths.isEmpty()) {
val execution = this.execPijul(project, path, listOf("add", "-r") + root.path)
results + this.doExecution("add", execution)
} else {
for (pathList in VcsFileUtil.chunkPaths(root, paths)) {
val execution = this.execPijul(project, path, listOf("add", "-r") + pathList)
results + this.doExecution("add", execution)
}
}
for (result in results) {
if (result.statusCode !is SuccessStatusCode)
return result
}
return PijulOperationResult("add", SuccessStatusCode, Unit)
}
@RequiresBackgroundThread
override fun fileStatus(project: Project, file: VirtualFile): PijulOperationResult<FileStatus> {
}
fun doExecution(name: String, execution: PijulExecution): PijulOperationResult<Unit> {
val status = runBlocking(Dispatchers.IO) {
execution.status.first()
}
return if (status == 0) {
PijulOperationResult(name, SuccessStatusCode, Unit)
} else {
val error = runBlocking(Dispatchers.IO) {
execution.errorStream.toList().joinToString("\n")
}
PijulOperationResult(name, NonZeroExitStatusCode(status, error), Unit)
}
}
/**
* TODO: Support configurable Pijul path.
*
* Executes a pijul command and waits for the output status
*/
@RequiresBackgroundThread
private fun execPijul(project: Project,
dir: Path,
args: List<String>): PijulExecution {
val process = ProcessBuilder()
.command(listOf(this.findPijul()) + args)
.directory(dir.toFile())
.start()
val input = process.inputStream
val error = process.errorStream
return PijulExecution(
input.linesToFlow().onEach {
draconConsoleWriter(project).logCommand("pijul", args, it)
},
error.linesToFlow().onEach {
draconConsoleWriter(project).logCommandError("pijul", args, it)
},
flow {
while (process.isAlive)
delay(1000L)
val exit = process.exitValue()
if (exit == 0) {
draconConsoleWriter(project).logCommand("pijul", args, "<Exit status> $exit")
} else {
draconConsoleWriter(project).logCommandError("pijul", args, "<Exit status> $exit")
}
emit(exit)
}.flowOn(Dispatchers.IO)
)
}
private fun findPijul(): String {
// Works in Windows, Linux and MacOS when Pijul is installed with cargo.
val localPijul = this.findCargoPijul()
// For Homebrew/Linuxbrew installations
val brewPijul = this.findBrewPijul()
// For *nix only.
val usrBinPijul = Paths.get(File.pathSeparator, "usr", "bin", "pijul").asPijulExecutableStringOrNull()
val usrLocalBinPijul = Paths.get(File.pathSeparator, "usr", "local", "bin", "pijul").asPijulExecutableStringOrNull()
val binPijul = Paths.get(File.pathSeparator, "bin", "pijul").asPijulExecutableStringOrNull()
// For Windows only.
// Windows could download Pijul binaries from https://github.com/boringcactus/pijul-windows-builds/releases/latest
// However, Dracon plugin could not detect Pijul outside from these directories, for the cases where
// Pijul is not in these locations, a search through $PATH variable will be made, so if the $PATH is correctly
// configured to point to Pijul executable, then it will be found, this applies to all OSes.
val programFiles = Paths.get("C:"+ File.pathSeparator, "Program Files", "pijul", "pijul.exe").asPijulExecutableStringOrNull()
val programFiles86 = Paths.get("C:"+ File.pathSeparator, "Program Files (x86)", "pijul", "pijul.exe").asPijulExecutableStringOrNull()
return localPijul
?: brewPijul
?: usrBinPijul
?: usrLocalBinPijul
?: binPijul
?: programFiles
?: programFiles86
?: this.findExecutableOnPath("pijul")
?: "pijul"
}
private fun Path.asPijulExecutableStringOrNull(): String? =
this.existsOrNull()?.isRegularFileOrNull()?.isExecutableOrNull()?.toAbsolutePath()?.toString()
private fun findCargoPijul(): String? {
return System.getProperty("user.dir")?.let {
Paths.get(it, ".cargo", "bin", "pijul")
}?.asPijulExecutableStringOrNull()
}
private fun findBrewPijul(): String? {
return System.getenv("HOMEBREW_PREFIX")?.ifBlank { null }?.ifEmpty { null }?.let {
Paths.get(it, "bin", "pijul")
}?.asPijulExecutableStringOrNull()
}
fun findExecutableOnPath(name: String): String? {
for (dirname in System.getenv("PATH").split(File.pathSeparator)) {
val path = Paths.get(dirname, name)
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
return path.toAbsolutePath().toString()
}
}
return null
}
}
package com.github.jonathanxd.dracon.actions
import com.github.jonathanxd.dracon.NAME
import com.github.jonathanxd.dracon.pijulVcs
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.AbstractVcs
import com.intellij.openapi.vcs.actions.StandardVcsGroup
class PijulMenu : StandardVcsGroup() {
override fun getVcs(project: Project): AbstractVcs =
pijulVcs(project)
override fun getVcsName(project: Project): String = NAME
}
package com.github.jonathanxd.dracon.actions
import com.github.jonathanxd.dracon.i18n.DraconBundle
import com.github.jonathanxd.dracon.pijul.NonZeroExitStatusCode
import com.github.jonathanxd.dracon.pijul.pijul
import com.github.jonathanxd.dracon.util.wrapInHml
import com.github.jonathanxd.dracon.vcs.DraconVcsUtil
import com.github.jonathanxd.dracon.vcs.NotificationIds
import com.github.jonathanxd.dracon.vfs.DraconVfsUtil
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.VcsNotifier
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
import com.intellij.vcsUtil.VcsUtil
class PijulInit: DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val project = e.getData(CommonDataKeys.PROJECT) ?: ProjectManager.getInstance().defaultProject
val fcd = FileChooserDescriptorFactory.createSingleFileDescriptor()
fcd.isShowFileSystemRoots = true
fcd.title = DraconBundle.Init.title
fcd.description = DraconBundle.Init.description
fcd.isHideIgnored = false
val baseDir = e.getData(CommonDataKeys.VIRTUAL_FILE)?.let { if (it.isDirectory) null else it } ?: project.baseDir
FileChooser.chooseFile(fcd, project, baseDir) { root ->
if (pijul(project).isUnderPijul(root)) {
val dialog = Messages.showYesNoCancelDialog(
project,
DraconBundle.Init.alreadyUnderPijulForHtml(root.presentableUrl),
DraconBundle.Init.title,
Messages.getWarningIcon()
)
if (dialog != Messages.YES) {
return@chooseFile
}
}
object : Task.Backgroundable(project, DraconBundle.Dracon.refreshing) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = true
val init = pijul(project).init(project, root)
if (init.statusCode is NonZeroExitStatusCode) {
VcsNotifier
.getInstance(this.project)
.notifyError(
NotificationIds.FAILED_TO_INIT,
DraconBundle.Init.error,
init.statusCode.message.wrapInHml(),
true
)
}
if (project.isDefault) {
return
}
// Refresh VCS?
DraconVcsUtil.refreshAfterInit(project, root)
}
}.queue()
}
}
}
package com.github.jonathanxd.dracon.actions
import com.github.jonathanxd.dracon.executor.PijulCommitAndPushExecutor
import com.intellij.dvcs.ui.DvcsBundle
import com.intellij.dvcs.commit.getCommitAndPushActionName
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.vcs.VcsDataKeys
import com.intellij.openapi.vcs.changes.actions.BaseCommitExecutorAction
import com.intellij.vcs.commit.CommitWorkflowHandler
import com.intellij.vcs.commit.NonModalCommitWorkflowHandler
class PijulCommitAndPushExecutorAction : BaseCommitExecutorAction() {
init {
templatePresentation.setText(DvcsBundle.messagePointer("action.commit.and.push.text"))
}
override fun update(e: AnActionEvent) {
// update presentation before synchronizing its state with button
val workflowHandler = e.getData(VcsDataKeys.COMMIT_WORKFLOW_HANDLER)
if (workflowHandler != null) {
e.presentation.text = workflowHandler.getCommitAndPushActionName()
}
super.update(e)
}
override val executorId: String = PijulCommitAndPushExecutor.ID
}
package com.github.jonathanxd.dracon.actions
import com.github.jonathanxd.dracon.pijul.pijul
import com.github.jonathanxd.dracon.pijulVcs
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.AbstractVcs
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.changes.actions.ScheduleForAdditionActionExtension
import com.intellij.openapi.vfs.VirtualFile
class PijulAddExtension : ScheduleForAdditionActionExtension {
override fun getSupportedVcs(project: Project): AbstractVcs = pijulVcs(project)
override fun isStatusForAddition(status: FileStatus): Boolean {
// TODO: Change check after fully integrated.
return true/*status === FileStatus.MODIFIED ||
status === FileStatus.MERGED_WITH_CONFLICTS ||
status === FileStatus.ADDED ||
status === FileStatus.DELETED ||
status === FileStatus.IGNORED*/
}
override fun doAddFiles(project: Project, vcsRoot: VirtualFile, paths: List<FilePath>, containsIgnored: Boolean) {
pijul(project).add(project, vcsRoot, paths)
}
}
package com.github.jonathanxd.dracon
import com.github.jonathanxd.dracon.i18n.DraconBundle
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.AbstractVcs
import com.intellij.openapi.vcs.ProjectLevelVcsManager
import com.intellij.openapi.vcs.VcsType
import com.intellij.openapi.vfs.VirtualFile
const val NAME = "Pijul"
const val ID = "pijul"
val DISPLAY_NAME_SUPPLIER = DraconBundle.Dracon.nameSupplier
fun pijulVcs(project: Project): PijulVcs {
val vcs = ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME) as PijulVcs
ProgressManager.checkCanceled()
return vcs
}
class PijulVcs(project: Project) : AbstractVcs(project, NAME) {
override fun getDisplayName(): String =
DISPLAY_NAME_SUPPLIER.get()
override fun getType(): VcsType =
VcsType.distributed
override fun isVersionedDirectory(dir: VirtualFile?): Boolean {
return super.isVersionedDirectory(dir)
}
override fun getShortNameWithMnemonic(): String =
DraconBundle.Dracon.mnemonic
companion object {
val KEY = createKey(NAME)
}
}