FRFFQV7VNYKGCA7ZAOSRPC2HHYTAIZ6AGGR7A5QEV6QPAQGFDYGAC
else null
else null
fun VirtualFile.path(): Path =
this.toNioPath()
fun VirtualFile.vcsRoot(project: Project): Path? =
ProjectLevelVcsManager.getInstance(project).getVcsRootFor(this)?.toNioPath()
fun FilePath.vcsRoot(project: Project): Path? =
ProjectLevelVcsManager.getInstance(project).getVcsRootFor(this)?.toNioPath()
package com.github.jonathanxd.dracon.revision
import com.github.jonathanxd.dracon.pijul.pijul
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vfs.VirtualFile
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
import java.util.Comparator
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.relativeTo
@OptIn(ExperimentalPathApi::class)
fun loadStateInRevision(revisionHash: String,
project: Project,
root: Path,
fileToResolve: FilePath): String {
val tmpTarget = Paths.get(project.baseDir.path, ".idea", "dracon_diffs", revisionHash)
if (Files.exists(tmpTarget)) {
Files.walk(tmpTarget).use { walk ->
walk.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete)
}
}
Files.createDirectories(tmpTarget)
copyFolder(root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)
val revisions = pijul(project).latestRevisionNumber(project, tmpTarget).result
if (revisions != null && revisions.hash == revisionHash) {
val rollbackOp = pijul(project).reset(project, tmpTarget)
} else {
val resetOp = pijul(project).reset(project, tmpTarget)
val rollbackOp = pijul(project).rollbackTo(revisionHash, project, tmpTarget)
}
val relative = Paths.get(fileToResolve.path).relativeTo(root).toString()
return Files.readString(tmpTarget.resolve(relative))
}
@Throws(IOException::class)
fun copyFolder(source: Path, target: Path, vararg options: CopyOption) {
Files.walkFileTree(source, object : SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
if (dir.fileName.toString().equals(".idea", ignoreCase = true))
return FileVisitResult.SKIP_SUBTREE
Files.createDirectories(target.resolve(source.relativize(dir)))
return FileVisitResult.CONTINUE
}
@Throws(IOException::class)
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
Files.copy(file, target.resolve(source.relativize(file)), *options)
return FileVisitResult.CONTINUE
}
})
}
package com.github.jonathanxd.dracon.revision
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.RepositoryLocation
import com.intellij.openapi.vcs.history.VcsFileRevision
import com.intellij.openapi.vcs.history.VcsRevisionNumber
import java.nio.file.Path
import java.util.*
class PijulVcsFileRevision(val project: Project,
val vcsRoot: Path,
val filePath: FilePath,
val revision: PijulRevisionNumber,
val author_: String?,
val message: String?,
val branch: String?) : VcsFileRevision {
override fun loadContent(): ByteArray =
loadStateInRevision(this.revision.hash, this.project, vcsRoot, this.filePath).toByteArray(Charsets.UTF_8)
override fun getContent(): ByteArray = this.loadContent()
override fun getRevisionNumber(): VcsRevisionNumber = this.revision
override fun getRevisionDate(): Date = Date.from(this.revision.timestamp.toInstant())
override fun getAuthor(): String? = this.author_
override fun getCommitMessage(): String? = this.message
override fun getBranchName(): String? = this.branch
override fun getChangedRepositoryPath(): RepositoryLocation? = null
}
package com.github.jonathanxd.dracon.provider
import com.github.jonathanxd.dracon.log.draconConsoleWriter
import com.github.jonathanxd.dracon.pijul.diff.PijulDiffFromHistoryHandler
import com.github.jonathanxd.dracon.pijul.pijul
import com.github.jonathanxd.dracon.revision.PijulRevisionNumber
import com.github.jonathanxd.dracon.revision.PijulVcsFileRevision
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.components.Service
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.*
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.ui.ColumnInfo
import javax.swing.JComponent
@Service
class PijulHistoryProvider(val project: Project) : VcsHistoryProvider {
override fun getUICustomization(
session: VcsHistorySession?,
forShortcutRegistration: JComponent?
): VcsDependentHistoryComponents {
return VcsDependentHistoryComponents(ColumnInfo.EMPTY_ARRAY, null, null)
}
override fun getAdditionalActions(refresher: Runnable?): Array<AnAction> {
return emptyArray()
}
override fun isDateOmittable(): Boolean =
false
override fun getHelpId(): String? = null
override fun createSessionFor(filePath: FilePath): VcsHistorySession? {
// TODO: Support multiple-channels
val root = ProjectLevelVcsManager.getInstance(this.project).getVcsRootFor(filePath)!!
val channels = pijul(this.project).channel(this.project, root)
val currentChannel = channels.result?.channels?.firstOrNull { it.current }?.name
val history = pijul(this.project).log(this.project, root).map {
it.entries.map {
PijulVcsFileRevision(
this.project,
root.toNioPath(),
filePath,
PijulRevisionNumber(it.changeHash, it.date),
it.authors.map { it.name }.firstOrNull(),
it.message,
currentChannel
)
}
}
return if (history.result != null) {
PijulHistorySession(filePath, history.result.firstOrNull()?.revision, history.result)
} else {
draconConsoleWriter(this.project).logError("Failed to build history session")
null
}
}
override fun reportAppendableHistory(path: FilePath, partner: VcsAppendableHistorySessionPartner) {
val root = ProjectLevelVcsManager.getInstance(this.project).getVcsRootFor(path)!!
val emptySession = createSession(path, emptyList(), null)
partner.reportCreatedEmptySession(emptySession)
val channels = pijul(this.project).channel(this.project, root)
val currentChannel = channels.result?.channels?.firstOrNull { it.current }?.name
pijul(this.project).fileHistory(
this.project,
root,
path,
{
partner.acceptRevision(
PijulVcsFileRevision(
this.project,
root.toNioPath(),
path,
PijulRevisionNumber(it.changeHash, it.date),
it.authors.map { it.name }.firstOrNull(),
it.message,
currentChannel
)
)
},
{ partner.reportException(VcsException(it.toString())) }
)
}
override fun supportsHistoryForDirectories(): Boolean = true
override fun getHistoryDiffHandler(): DiffFromHistoryHandler =
PijulDiffFromHistoryHandler(this.project)
override fun canShowHistoryFor(file: VirtualFile): Boolean {
return true// TODO?
}
private fun createSession(
filePath: FilePath, revisions: List<VcsFileRevision>,
number: VcsRevisionNumber?
): VcsAbstractHistorySession {
return PijulHistorySession(filePath, number, revisions)
}
inner class PijulHistorySession(
val filePath: FilePath,
number: VcsRevisionNumber?,
revisions: List<VcsFileRevision>
) :
VcsAbstractHistorySession(revisions, number) {
override fun calcCurrentRevisionNumber(): VcsRevisionNumber? {
return try {
val root = ProjectLevelVcsManager.getInstance(project).getVcsRootFor(filePath)!!
val rev = pijul(project).latestRevisionNumber(project, root)
return rev.result
} catch (e: Throwable) {
draconConsoleWriter(project).logError(e.stackTraceToString())
null
}
}
override fun getHistoryAsTreeProvider(): HistoryAsTreeProvider? {
return null
}
override fun copy(): VcsHistorySession {
return createSession(filePath, revisionList, currentRevisionNumber)
}
}
/*override fun createFromCachedData(
cacheable: Boolean?,
revisions: MutableList<out VcsFileRevision>,
filePath: FilePath,
currentRevision: VcsRevisionNumber?
): VcsAbstractHistorySession {
TODO("Not yet implemented")
}
override fun getBaseVersionContent(
filePath: FilePath?,
processor: Processor<in String>?,
beforeVersionId: String?
): Boolean {
TODO("Not yet implemented")
}*/
}
package com.github.jonathanxd.dracon.pijul.diff
import com.github.jonathanxd.dracon.content.PijulContentRevision
import com.github.jonathanxd.dracon.revision.PijulVcsFileRevision
import com.github.jonathanxd.dracon.util.vcsRoot
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.Project
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vcs.changes.CurrentContentRevision
import com.intellij.openapi.vcs.history.BaseDiffFromHistoryHandler
import com.intellij.openapi.vcs.history.DiffFromHistoryHandler
import com.intellij.openapi.vcs.history.VcsFileRevision
class PijulDiffFromHistoryHandler(val project: Project) : BaseDiffFromHistoryHandler<PijulVcsFileRevision>(project) {
override fun getChangesBetweenRevisions(
path: FilePath,
rev1: PijulVcsFileRevision,
rev2: PijulVcsFileRevision?
): MutableList<Change> {
val root = path.vcsRoot(project)!!
return if (rev2 == null) {
mutableListOf(
Change(
PijulContentRevision(
root,
path,
rev1.revision,
this.project
),
CurrentContentRevision.create(path),
FileStatus.MODIFIED
)
)
} else {
mutableListOf(
Change(
PijulContentRevision(
root,
path,
rev1.revision,
this.project
),
PijulContentRevision(
root,
path,
rev2.revision,
this.project
),
FileStatus.MODIFIED
)
)
}
}
override fun getAffectedChanges(path: FilePath, rev: PijulVcsFileRevision): MutableList<Change> {
val root = path.vcsRoot(project)!!
return mutableListOf(
Change(
PijulContentRevision(
root,
path,
rev.revision,
this.project
),
CurrentContentRevision.create(path),
FileStatus.MODIFIED
)
)
}
override fun getPresentableName(revision: PijulVcsFileRevision): String =
revision.revision.hash
}
* Retrieves change history
*/
@RequiresBackgroundThread
fun log(project: Project, root: Path): PijulOperationResult<PijulLog>
/**
* Retrieves change history
*/
@RequiresBackgroundThread
fun fileHistory(project: Project,
root: VirtualFile,
file: FilePath,
consumer: (PijulLogEntry) -> Unit,
errorConsumer: (PijulOperationResult<Unit>) -> Unit): PijulOperationResult<Unit>
/**
@OptIn(ExperimentalPathApi::class)
private fun loadStateInRevision(): String {
val tmpTarget = Paths.get(this.project.baseDir.path, ".idea", "dracon_diffs", this.revision.hash)
if (Files.exists(tmpTarget)) {
Files.walk(tmpTarget).use { walk ->
walk.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete)
}
}
Files.createDirectories(tmpTarget)
copyFolder(this.root, tmpTarget, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING)
val rollbackOp = pijul(project).reset(this.project, tmpTarget)
val relative = Paths.get(filePath.path).relativeTo(this.root).toString()
return Files.readString(tmpTarget.resolve(relative))
}
@Throws(IOException::class)
fun copyFolder(source: Path, target: Path, vararg options: CopyOption) {
Files.walkFileTree(source, object : SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
if (dir.fileName.toString().equals(".idea", ignoreCase = true))
return FileVisitResult.SKIP_SUBTREE
Files.createDirectories(target.resolve(source.relativize(dir)))
return FileVisitResult.CONTINUE
}
@Throws(IOException::class)
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
Files.copy(file, target.resolve(source.relativize(file)), *options)
return FileVisitResult.CONTINUE
}
})
}
import com.github.jonathanxd.dracon.log.PijulLog
import com.github.jonathanxd.dracon.log.PijulLogEntry
import com.github.jonathanxd.dracon.log.draconConsoleWriter
import com.github.jonathanxd.dracon.log.parseChange
import com.github.jonathanxd.dracon.channel.ChannelInfo
import com.github.jonathanxd.dracon.channel.PijulChannel
import com.github.jonathanxd.dracon.log.*
val fileStatusBasedInPijulLs = this.doExecutionWithMapper("file_status_from_ls", this.execPijul(project, rootPath, listOf("ls"), delay = 10L)) {
val fileStatusBasedInPijulLs = this.doExecutionWithMapper("file_status_from_ls", this.createExecPijulOperation(project, rootPath, listOf("ls"), delay = 10L)) {
val logHashExecution = this.execPijul(project, rootPath, listOf("log", "--hash-only"), delay = 10L)
return this.log(project, rootPath)
}
override fun log(project: Project, root: Path): PijulOperationResult<PijulLog> {
val logHashExecution = this.createPainlessExecPijulOperation(project, root, listOf("log", "--hash-only"))
val change = this.doExecutionWithMapper("change-$hash", this.execPijul(project, rootPath, listOf("change", hash), delay = 10L)) {
val change = this.doExecutionWithMapper("change-$hash", this.createPainlessExecPijulOperation(project, root, listOf("change", hash))) {
@OptIn(ExperimentalPathApi::class)
override fun fileHistory(
project: Project,
root: VirtualFile,
file: FilePath,
consumer: (PijulLogEntry) -> Unit,
errorConsumer: (PijulOperationResult<Unit>) -> Unit
): PijulOperationResult<Unit> {
val rootPath = Paths.get(VcsUtil.getFilePath(root).path)
val filePath = Paths.get(file.path).relativeTo(rootPath)
val logHashExecution = this.createExecPijulOperation(project, rootPath, listOf("log", "--hash-only"), delay = 10L)
val hashes = this.doExecutionWithMapper("log--hash-only", logHashExecution) {
it.lines()
}
if (hashes.statusCode !is SuccessStatusCode) {
errorConsumer(hashes as PijulOperationResult<Unit>)
return hashes as PijulOperationResult<Unit>
} else {
val hashList = hashes.result!!
for (hash in hashList) {
if (hash.isEmpty()) {
break
}
val change = this.doExecutionWithMapper("change-$hash", this.createPainlessExecPijulOperation(project, rootPath, listOf("change", hash))) {
it.parseChange(hash)
}
if (change.statusCode !is SuccessStatusCode) {
errorConsumer(change as PijulOperationResult<Unit>)
} else if (change.result != null) {
var shouldConsume = false
var isAdd = false
for (hunk in change.result.hunks) {
if (hunk is HunkWithPath) {
if (filePath.toString().equals(hunk.resolvedPath, ignoreCase = true)) {
if (hunk is FileAddHunk) {
isAdd = true
}
shouldConsume = true
}
}
}
if (shouldConsume) {
consumer(change.result)
}
if (isAdd) {
break
}
}
}
}
return hashes as PijulOperationResult<Unit>
}
* Creates a [PijulExecution] operation that could be executed at any time. This operation uses Kotlin Coroutines
* and can be executed immediately through [doExecution] or through [doExecutionWithMapper].
*
* As this implementation depends on System Processes, a [delay] should be provided as the interval between
* [Process.isAlive] check before trying to retrieve [Process.onExit]. Bigger values yields less resource
* intensive operation, smaller values yields less input lag and better feed back but in cost of intensive
* scheduling.
*
private fun execPijul(project: Project,
dir: Path,
args: List<String>,
delay: Long = 1000L): PijulExecution {
private fun createExecPijulOperation(project: Project,
dir: Path,
args: List<String>,
delay: Long = 1000L): PijulExecution {
/**
* Creates a [PijulExecution] operation that could be executed at any time. This operation uses Kotlin Coroutines
* and can be executed immediately through [doExecution] or through [doExecutionWithMapper].
*
* This implementation does not requires a delay value to be provided, like [createExecPijulOperation] does, instead
* it uses the kotlin conversion from `CompletionStage` to `Coroutines` and awaits the process through [Process.onExit].
*
* [doExecution] and [doExecutionWithMapper] does execution by scheduling task to [Dispatchers.IO], instead of Main Thread,
* offloading the Process execution handling to a different scheduler. However, mapping operation of [doExecutionWithMapper]
* is not offloaded from the caller context.
*
*/
@RequiresBackgroundThread
private fun createPainlessExecPijulOperation(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 {
process.onExit().await()
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)
)
}
package com.github.jonathanxd.dracon.channel
data class PijulChannel(val current: Boolean, val name: String)
package com.github.jonathanxd.dracon.channel
data class ChannelInfo(val channels: List<PijulChannel>)