feat(release): first release
This commit is contained in:
@@ -6,6 +6,7 @@ plugins {
|
||||
application
|
||||
id("com.github.gmazzo.buildconfig") version "4.1.2"
|
||||
id("com.gradleup.shadow") version "9.2.2"
|
||||
id("org.graalvm.buildtools.native") version "0.9.28"
|
||||
}
|
||||
|
||||
group = "io.notevc"
|
||||
@@ -34,10 +35,49 @@ application {
|
||||
mainClass.set("io.notevc.NoteVCKt")
|
||||
}
|
||||
|
||||
|
||||
|
||||
tasks.shadowJar {
|
||||
archiveClassifier.set("") // This should remove the -all suffix
|
||||
manifest {
|
||||
attributes(mapOf("Main-Class" to "io.notevc.NoteVCKt"))
|
||||
}
|
||||
}
|
||||
|
||||
graalvmNative {
|
||||
binaries {
|
||||
named("main") {
|
||||
imageName.set("notevc")
|
||||
mainClass.set("io.notevc.NoteVCKt")
|
||||
|
||||
// Force it to use the shadowJar
|
||||
classpath.from(tasks.shadowJar.get().outputs.files)
|
||||
|
||||
buildArgs.addAll(listOf(
|
||||
"--no-fallback",
|
||||
"-H:+ReportExceptionStackTraces",
|
||||
"-H:+UnlockExperimentalVMOptions",
|
||||
|
||||
"--initialize-at-build-time=kotlin",
|
||||
"--initialize-at-build-time=kotlinx",
|
||||
"--initialize-at-build-time=io.notevc",
|
||||
|
||||
"--enable-monitoring=heapdump,jfr",
|
||||
"-H:IncludeResources=.*\\.json",
|
||||
"-H:IncludeResources=.*\\.properties"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
tasks.nativeCompile {
|
||||
dependsOn(tasks.shadowJar)
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(tasks.shadowJar)
|
||||
}
|
||||
@@ -46,3 +86,6 @@ tasks.jar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
tasks.startScripts {
|
||||
dependsOn(tasks.shadowJar)
|
||||
}
|
||||
|
||||
6
notevc.rb
Normal file
6
notevc.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class Notevc < Formula
|
||||
desc "Version control for markdown files"
|
||||
homepage "https://github.com/darwincereska/notevc"
|
||||
version "1.0.0"
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.notevc
|
||||
|
||||
import io.notevc.core.Repository
|
||||
import io.notevc.commands.*
|
||||
import io.notevc.utils.ColorUtils
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// Args logic
|
||||
@@ -12,7 +13,18 @@ fun main(args: Array<String>) {
|
||||
|
||||
result.fold(
|
||||
onSuccess = { message -> println(message) },
|
||||
onFailure = { error -> println("Error: ${error.message}") }
|
||||
onFailure = { error -> println("${ColorUtils.error("Error:")} ${error.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
"log" -> {
|
||||
val logArgs = args.drop(1)
|
||||
val logCommand = LogCommand()
|
||||
val result = logCommand.execute(logArgs)
|
||||
|
||||
result.fold(
|
||||
onSuccess = { output -> println(output) },
|
||||
onFailure = { error -> println("${ColorUtils.error("Error:")} ${error.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +35,7 @@ fun main(args: Array<String>) {
|
||||
|
||||
result.fold(
|
||||
onSuccess = { output -> println(output) },
|
||||
onFailure = { error -> println("Error: ${error.message}") }
|
||||
onFailure = { error -> println("${ColorUtils.error("Error:")} ${error.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +45,7 @@ fun main(args: Array<String>) {
|
||||
|
||||
result.fold(
|
||||
onSuccess = { output -> println(output) },
|
||||
onFailure = { error -> println("Error: ${error.message}") }
|
||||
onFailure = { error -> println("${ColorUtils.error("Error:")} ${error.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +53,17 @@ fun main(args: Array<String>) {
|
||||
println("notevc version ${Repository.VERSION}")
|
||||
}
|
||||
|
||||
"restore" -> {
|
||||
val restoreArgs = args.drop(1)
|
||||
val restoreCommand = RestoreCommand()
|
||||
val result = restoreCommand.execute(restoreArgs)
|
||||
|
||||
result.fold(
|
||||
onSuccess = { output -> println(output) },
|
||||
onFailure = { error -> println("${ColorUtils.error("Error:")} ${error.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
println("Usage: notevc init|commit|status|version")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
import io.notevc.utils.ColorUtils
|
||||
import kotlin.io.path.*
|
||||
|
||||
class CommitCommand {
|
||||
@@ -97,9 +98,9 @@ class CommitCommand {
|
||||
updateRepositoryHead(repo, commitHash, timestamp, message)
|
||||
|
||||
return buildString {
|
||||
appendLine("Created commit $commitHash")
|
||||
appendLine("Message: $message")
|
||||
appendLine("File: $relativePath (${snapshot.blocks.size} blocks)")
|
||||
appendLine("${ColorUtils.success("Created commit")} ${ColorUtils.hash(commitHash)}")
|
||||
appendLine("${ColorUtils.bold("Message:")} $message")
|
||||
appendLine("${ColorUtils.bold("File:")} ${ColorUtils.filename(relativePath)} ${ColorUtils.dim("(${snapshot.blocks.size} blocks)")}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,13 +161,16 @@ class CommitCommand {
|
||||
updateRepositoryHead(repo, commitHash, timestamp, message)
|
||||
|
||||
return buildString {
|
||||
appendLine("Created commit $commitHash")
|
||||
appendLine("Message: $message")
|
||||
appendLine("Files committed: ${changedFiles.size}")
|
||||
appendLine("Total blocks: $totalBlocksStored")
|
||||
appendLine("${ColorUtils.success("Created commit")} ${ColorUtils.hash(commitHash)}")
|
||||
appendLine("${ColorUtils.bold("Message:")} $message")
|
||||
appendLine("${ColorUtils.bold("Files committed:")} ${changedFiles.size}")
|
||||
appendLine("${ColorUtils.bold("Total blocks:")} $totalBlocksStored")
|
||||
appendLine()
|
||||
changedFiles.forEach { fileInfo ->
|
||||
appendLine(" $fileInfo")
|
||||
val parts = fileInfo.split(" (")
|
||||
val filename = parts[0]
|
||||
val blockInfo = if (parts.size > 1) " (${parts[1]}" else ""
|
||||
appendLine(" ${ColorUtils.filename(filename)}${ColorUtils.dim(blockInfo)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.notevc.commands
|
||||
|
||||
import io.notevc.core.Repository
|
||||
import java.nio.file.Path
|
||||
import io.notevc.utils.ColorUtils
|
||||
|
||||
class InitCommand {
|
||||
fun execute(path: String?): Result<String> {
|
||||
@@ -13,7 +14,7 @@ class InitCommand {
|
||||
repo.init().fold(
|
||||
onSuccess = {
|
||||
val absolutePath = repo.path.toAbsolutePath().toString()
|
||||
Result.success("Initialized notevc repository in $absolutePath")
|
||||
Result.success("${ColorUtils.success("Initialized notevc repository")} in ${ColorUtils.filename(repo.path.toAbsolutePath().toString())}")
|
||||
},
|
||||
onFailure = {
|
||||
error -> Result.failure(error)
|
||||
|
||||
@@ -1 +1,262 @@
|
||||
package io.notevc.commands
|
||||
|
||||
import io.notevc.core.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
import io.notevc.utils.ColorUtils
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.io.path.*
|
||||
|
||||
class LogCommand {
|
||||
|
||||
fun execute(args: List<String>): Result<String> {
|
||||
return try {
|
||||
val repo = Repository.find()
|
||||
?: return Result.failure(Exception("Not in a notevc repository. Run 'notevc init' first."))
|
||||
|
||||
val options = parseArgs(args)
|
||||
val logOutput = generateLog(repo, options)
|
||||
|
||||
Result.success(logOutput)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseArgs(args: List<String>): LogOptions {
|
||||
var maxCount: Int? = null
|
||||
var since: String? = null
|
||||
var oneline = false
|
||||
var showFiles = false
|
||||
var targetFile: String? = null
|
||||
|
||||
var i = 0
|
||||
while (i < args.size) {
|
||||
when (args[i]) {
|
||||
"--max-count", "-n" -> {
|
||||
if (i + 1 < args.size) {
|
||||
maxCount = args[i + 1].toIntOrNull()
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
"--since" -> {
|
||||
if (i + 1 < args.size) {
|
||||
since = args[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
"--oneline" -> {
|
||||
oneline = true
|
||||
i++
|
||||
}
|
||||
"--file", "-f" -> {
|
||||
if (i + 1 < args.size) {
|
||||
targetFile = args[i + 1]
|
||||
showFiles = true
|
||||
i += 2
|
||||
} else {
|
||||
showFiles = true
|
||||
i++
|
||||
}
|
||||
}
|
||||
else -> i++
|
||||
}
|
||||
}
|
||||
|
||||
return LogOptions(maxCount, since, oneline, showFiles, targetFile)
|
||||
}
|
||||
|
||||
private fun generateLog(repo: Repository, options: LogOptions): String {
|
||||
val timelineFile = repo.path.resolve("${Repository.NOTEVC_DIR}/timeline.json")
|
||||
|
||||
if (!timelineFile.exists()) {
|
||||
return "No commits yet"
|
||||
}
|
||||
|
||||
val content = Files.readString(timelineFile)
|
||||
if (content.trim() == "[]") {
|
||||
return "No commits yet"
|
||||
}
|
||||
|
||||
val commits = Json.decodeFromString<List<CommitEntry>>(content)
|
||||
|
||||
// Filter commits based on options
|
||||
val filteredCommits = filterCommits(commits, options)
|
||||
|
||||
return if (options.oneline) {
|
||||
formatOnelineLog(filteredCommits, repo, options)
|
||||
} else {
|
||||
formatDetailedLog(filteredCommits, repo, options)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterCommits(commits: List<CommitEntry>, options: LogOptions): List<CommitEntry> {
|
||||
var filtered = commits
|
||||
|
||||
// Filter by date if --since is provided
|
||||
options.since?.let { sinceStr ->
|
||||
val sinceTime = parseSinceTime(sinceStr)
|
||||
filtered = filtered.filter { commit ->
|
||||
val commitTime = Instant.parse(commit.timestamp)
|
||||
commitTime.isAfter(sinceTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit count if --max-count is provided
|
||||
options.maxCount?.let { count ->
|
||||
filtered = filtered.take(count)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
private fun parseSinceTime(since: String): Instant {
|
||||
return when {
|
||||
since.endsWith("h") -> {
|
||||
val hours = since.dropLast(1).toLongOrNull() ?: 1
|
||||
Instant.now().minusSeconds(hours * 3600)
|
||||
}
|
||||
since.endsWith("d") -> {
|
||||
val days = since.dropLast(1).toLongOrNull() ?: 1
|
||||
Instant.now().minusSeconds(days * 24 * 3600)
|
||||
}
|
||||
since.endsWith("w") -> {
|
||||
val weeks = since.dropLast(1).toLongOrNull() ?: 1
|
||||
Instant.now().minusSeconds(weeks * 7 * 24 * 3600)
|
||||
}
|
||||
else -> {
|
||||
try {
|
||||
Instant.parse(since)
|
||||
} catch (e: Exception) {
|
||||
Instant.now().minusSeconds(24 * 3600)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatOnelineLog(commits: List<CommitEntry>, repo: Repository, options: LogOptions): String {
|
||||
return commits.joinToString("\n") { commit ->
|
||||
val fileInfo = if (options.showFiles) {
|
||||
val stats = getCommitStats(repo, commit, options.targetFile)
|
||||
ColorUtils.dim(" (${stats.filesChanged} files, ${stats.totalBlocks} blocks)")
|
||||
} else ""
|
||||
|
||||
"${ColorUtils.hash(commit.hash)} ${commit.message}$fileInfo"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDetailedLog(commits: List<CommitEntry>, repo: Repository, options: LogOptions): String {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
.withZone(ZoneId.systemDefault())
|
||||
|
||||
return commits.joinToString("\n\n") { commit ->
|
||||
val timestamp = Instant.parse(commit.timestamp)
|
||||
val formattedDate = formatter.format(timestamp)
|
||||
|
||||
buildString {
|
||||
appendLine("${ColorUtils.bold("commit")} ${ColorUtils.hash(commit.hash)}")
|
||||
appendLine("${ColorUtils.bold("Author:")} ${ColorUtils.author(commit.author)}")
|
||||
appendLine("${ColorUtils.bold("Date:")} ${ColorUtils.date(formattedDate)}")
|
||||
|
||||
if (options.showFiles) {
|
||||
val stats = getCommitStats(repo, commit, options.targetFile)
|
||||
appendLine("${ColorUtils.info("Files changed:")} ${stats.filesChanged}, ${ColorUtils.info("Total blocks:")} ${stats.totalBlocks}")
|
||||
|
||||
if (stats.fileDetails.isNotEmpty()) {
|
||||
appendLine()
|
||||
stats.fileDetails.forEach { (file, blocks) ->
|
||||
appendLine(" ${ColorUtils.filename(file)} ${ColorUtils.dim("(${blocks.size} blocks)")}")
|
||||
blocks.forEach { block ->
|
||||
val heading = block.heading.replace(Regex("^#+\\s*"), "").trim()
|
||||
appendLine(" - ${ColorUtils.hash(block.id.take(8))}: ${ColorUtils.heading(heading)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendLine()
|
||||
appendLine(" ${commit.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCommitStats(repo: Repository, commit: CommitEntry, targetFile: String?): CommitStats {
|
||||
val blockStore = BlockStore(
|
||||
ObjectStore(repo.path.resolve("${Repository.NOTEVC_DIR}/objects")),
|
||||
repo.path.resolve("${Repository.NOTEVC_DIR}/blocks")
|
||||
)
|
||||
|
||||
// Find all block snapshots for this commit timestamp
|
||||
val commitTime = Instant.parse(commit.timestamp)
|
||||
val snapshots = findSnapshotsForCommit(repo, commitTime, targetFile)
|
||||
|
||||
val fileDetails = mutableMapOf<String, List<BlockState>>()
|
||||
var totalBlocks = 0
|
||||
|
||||
snapshots.forEach { snapshot ->
|
||||
if (targetFile == null || snapshot.filePath == targetFile) {
|
||||
fileDetails[snapshot.filePath] = snapshot.blocks
|
||||
totalBlocks += snapshot.blocks.size
|
||||
}
|
||||
}
|
||||
|
||||
return CommitStats(
|
||||
filesChanged = fileDetails.size,
|
||||
totalBlocks = totalBlocks,
|
||||
fileDetails = fileDetails
|
||||
)
|
||||
}
|
||||
|
||||
private fun findSnapshotsForCommit(repo: Repository, commitTime: Instant, targetFile: String?): List<BlockSnapshot> {
|
||||
val blocksDir = repo.path.resolve("${Repository.NOTEVC_DIR}/blocks")
|
||||
if (!blocksDir.exists()) return emptyList()
|
||||
|
||||
val snapshots = mutableListOf<BlockSnapshot>()
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
// Look for snapshots around the commit time (within 1 minute)
|
||||
val timeRange = 60L // seconds
|
||||
|
||||
Files.walk(blocksDir)
|
||||
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
|
||||
.forEach { snapshotFile ->
|
||||
try {
|
||||
val content = Files.readString(snapshotFile)
|
||||
val snapshot = json.decodeFromString<BlockSnapshot>(content)
|
||||
val snapshotTime = Instant.parse(snapshot.timestamp)
|
||||
|
||||
// Check if snapshot is within time range of commit
|
||||
val timeDiff = kotlin.math.abs(commitTime.epochSecond - snapshotTime.epochSecond)
|
||||
if (timeDiff <= timeRange) {
|
||||
if (targetFile == null || snapshot.filePath == targetFile) {
|
||||
snapshots.add(snapshot)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Skip corrupted snapshots
|
||||
}
|
||||
}
|
||||
|
||||
return snapshots
|
||||
}
|
||||
}
|
||||
|
||||
data class LogOptions(
|
||||
val maxCount: Int? = null,
|
||||
val since: String? = null,
|
||||
val oneline: Boolean = false,
|
||||
val showFiles: Boolean = false,
|
||||
val targetFile: String? = null
|
||||
)
|
||||
|
||||
data class CommitStats(
|
||||
val filesChanged: Int,
|
||||
val totalBlocks: Int,
|
||||
val fileDetails: Map<String, List<BlockState>>
|
||||
)
|
||||
|
||||
@@ -1 +1,249 @@
|
||||
package io.notevc.commands
|
||||
|
||||
import io.notevc.core.*
|
||||
import io.notevc.utils.ColorUtils
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Files
|
||||
import java.time.Instant
|
||||
import kotlin.io.path.*
|
||||
|
||||
class RestoreCommand {
|
||||
|
||||
fun execute(args: List<String>): Result<String> {
|
||||
return try {
|
||||
val options = parseArgs(args)
|
||||
|
||||
if (options.commitHash.isBlank()) {
|
||||
return Result.failure(Exception("Commit hash is required"))
|
||||
}
|
||||
|
||||
val repo = Repository.find()
|
||||
?: return Result.failure(Exception("Not in a notevc repository. Run 'notevc init' first."))
|
||||
|
||||
val result = when {
|
||||
options.blockHash != null && options.targetFile != null -> {
|
||||
restoreSpecificBlock(repo, options.commitHash, options.blockHash, options.targetFile)
|
||||
}
|
||||
options.targetFile != null -> {
|
||||
restoreSpecificFile(repo, options.commitHash, options.targetFile)
|
||||
}
|
||||
else -> {
|
||||
restoreEntireRepository(repo, options.commitHash)
|
||||
}
|
||||
}
|
||||
|
||||
Result.success(result)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseArgs(args: List<String>): RestoreOptions {
|
||||
if (args.isEmpty()) {
|
||||
return RestoreOptions("", null, null)
|
||||
}
|
||||
|
||||
val commitHash = args[0]
|
||||
var blockHash: String? = null
|
||||
var targetFile: String? = null
|
||||
|
||||
var i = 1
|
||||
while (i < args.size) {
|
||||
when (args[i]) {
|
||||
"--block", "-b" -> {
|
||||
if (i + 1 < args.size) {
|
||||
blockHash = args[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Assume it's the target file
|
||||
targetFile = args[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RestoreOptions(commitHash, blockHash, targetFile)
|
||||
}
|
||||
|
||||
private fun restoreSpecificBlock(repo: Repository, commitHash: String, blockHash: String, targetFile: String): String {
|
||||
val objectStore = ObjectStore(repo.path.resolve("${Repository.NOTEVC_DIR}/objects"))
|
||||
val blockStore = BlockStore(objectStore, repo.path.resolve("${Repository.NOTEVC_DIR}/blocks"))
|
||||
val blockParser = BlockParser()
|
||||
|
||||
// Find the commit
|
||||
val commit = findCommit(repo, commitHash)
|
||||
?: throw Exception("Commit ${ColorUtils.hash(commitHash)} not found")
|
||||
|
||||
// Find the block snapshot for this file at the commit time
|
||||
val commitTime = Instant.parse(commit.timestamp)
|
||||
val snapshot = blockStore.getBlocksAtTime(targetFile, commitTime)
|
||||
?: throw Exception("No snapshot found for ${ColorUtils.filename(targetFile)} at commit ${ColorUtils.hash(commitHash)}")
|
||||
|
||||
// Find the specific block
|
||||
val targetBlock = snapshot.find { it.id.startsWith(blockHash) }
|
||||
?: throw Exception("Block ${ColorUtils.hash(blockHash)} not found in ${ColorUtils.filename(targetFile)} at commit ${ColorUtils.hash(commitHash)}")
|
||||
|
||||
// Read current file
|
||||
val filePath = repo.path.resolve(targetFile)
|
||||
if (!filePath.exists()) {
|
||||
throw Exception("File ${ColorUtils.filename(targetFile)} does not exist")
|
||||
}
|
||||
|
||||
val currentContent = Files.readString(filePath)
|
||||
val currentParsedFile = blockParser.parseFile(currentContent, targetFile)
|
||||
|
||||
// Find the block to replace in current file
|
||||
val currentBlockIndex = currentParsedFile.blocks.indexOfFirst { it.id.startsWith(blockHash) }
|
||||
if (currentBlockIndex == -1) {
|
||||
throw Exception("Block ${ColorUtils.hash(blockHash)} not found in current ${ColorUtils.filename(targetFile)}")
|
||||
}
|
||||
|
||||
// Replace the block
|
||||
val updatedBlocks = currentParsedFile.blocks.toMutableList()
|
||||
updatedBlocks[currentBlockIndex] = targetBlock
|
||||
|
||||
val updatedParsedFile = currentParsedFile.copy(blocks = updatedBlocks)
|
||||
val restoredContent = blockParser.reconstructFile(updatedParsedFile)
|
||||
|
||||
// Write the updated file
|
||||
Files.writeString(filePath, restoredContent)
|
||||
|
||||
val blockHeading = targetBlock.heading.replace(Regex("^#+\\s*"), "").trim()
|
||||
return "${ColorUtils.success("Restored block")} ${ColorUtils.hash(blockHash.take(8))} ${ColorUtils.heading("\"$blockHeading\"")} in ${ColorUtils.filename(targetFile)} from commit ${ColorUtils.hash(commitHash)}"
|
||||
}
|
||||
|
||||
private fun restoreSpecificFile(repo: Repository, commitHash: String, targetFile: String): String {
|
||||
val objectStore = ObjectStore(repo.path.resolve("${Repository.NOTEVC_DIR}/objects"))
|
||||
val blockStore = BlockStore(objectStore, repo.path.resolve("${Repository.NOTEVC_DIR}/blocks"))
|
||||
val blockParser = BlockParser()
|
||||
|
||||
// Find the commit
|
||||
val commit = findCommit(repo, commitHash)
|
||||
?: throw Exception("Commit ${ColorUtils.hash(commitHash)} not found")
|
||||
|
||||
// Find the block snapshot for this file at the commit time
|
||||
val commitTime = Instant.parse(commit.timestamp)
|
||||
val blocks = blockStore.getBlocksAtTime(targetFile, commitTime)
|
||||
?: throw Exception("No snapshot found for ${ColorUtils.filename(targetFile)} at commit ${ColorUtils.hash(commitHash)}")
|
||||
|
||||
// Reconstruct the file from blocks
|
||||
val parsedFile = ParsedFile(
|
||||
path = targetFile,
|
||||
frontMatter = null, // TODO: Get front matter from snapshot
|
||||
blocks = blocks
|
||||
)
|
||||
|
||||
val restoredContent = blockParser.reconstructFile(parsedFile)
|
||||
|
||||
// Write the restored file
|
||||
val filePath = repo.path.resolve(targetFile)
|
||||
Files.createDirectories(filePath.parent)
|
||||
Files.writeString(filePath, restoredContent)
|
||||
|
||||
return "${ColorUtils.success("Restored file")} ${ColorUtils.filename(targetFile)} ${ColorUtils.dim("(${blocks.size} blocks)")} from commit ${ColorUtils.hash(commitHash)}"
|
||||
}
|
||||
|
||||
private fun restoreEntireRepository(repo: Repository, commitHash: String): String {
|
||||
val objectStore = ObjectStore(repo.path.resolve("${Repository.NOTEVC_DIR}/objects"))
|
||||
val blockStore = BlockStore(objectStore, repo.path.resolve("${Repository.NOTEVC_DIR}/blocks"))
|
||||
val blockParser = BlockParser()
|
||||
|
||||
// Find the commit
|
||||
val commit = findCommit(repo, commitHash)
|
||||
?: throw Exception("Commit ${ColorUtils.hash(commitHash)} not found")
|
||||
|
||||
val commitTime = Instant.parse(commit.timestamp)
|
||||
|
||||
// Get all files that were tracked at this commit
|
||||
val trackedFiles = getTrackedFilesAtCommit(repo, commitTime)
|
||||
|
||||
if (trackedFiles.isEmpty()) {
|
||||
throw Exception("No files found at commit ${ColorUtils.hash(commitHash)}")
|
||||
}
|
||||
|
||||
var restoredFiles = 0
|
||||
var totalBlocks = 0
|
||||
|
||||
trackedFiles.forEach { filePath ->
|
||||
val blocks = blockStore.getBlocksAtTime(filePath, commitTime)
|
||||
if (blocks != null) {
|
||||
val parsedFile = ParsedFile(
|
||||
path = filePath,
|
||||
frontMatter = null, // TODO: Get front matter from snapshot
|
||||
blocks = blocks
|
||||
)
|
||||
|
||||
val restoredContent = blockParser.reconstructFile(parsedFile)
|
||||
val fullPath = repo.path.resolve(filePath)
|
||||
|
||||
Files.createDirectories(fullPath.parent)
|
||||
Files.writeString(fullPath, restoredContent)
|
||||
|
||||
restoredFiles++
|
||||
totalBlocks += blocks.size
|
||||
}
|
||||
}
|
||||
|
||||
return buildString {
|
||||
appendLine("${ColorUtils.success("Restored repository")} to commit ${ColorUtils.hash(commitHash)}")
|
||||
appendLine("${ColorUtils.bold("Files restored:")} $restoredFiles")
|
||||
appendLine("${ColorUtils.bold("Total blocks:")} $totalBlocks")
|
||||
appendLine("${ColorUtils.bold("Commit message:")} ${commit.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun findCommit(repo: Repository, commitHash: String): CommitEntry? {
|
||||
val timelineFile = repo.path.resolve("${Repository.NOTEVC_DIR}/timeline.json")
|
||||
|
||||
if (!timelineFile.exists()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val content = Files.readString(timelineFile)
|
||||
if (content.trim() == "[]") {
|
||||
return null
|
||||
}
|
||||
|
||||
val commits = Json.decodeFromString<List<CommitEntry>>(content)
|
||||
return commits.find { it.hash.startsWith(commitHash) }
|
||||
}
|
||||
|
||||
private fun getTrackedFilesAtCommit(repo: Repository, commitTime: Instant): List<String> {
|
||||
val blocksDir = repo.path.resolve("${Repository.NOTEVC_DIR}/blocks")
|
||||
if (!blocksDir.exists()) return emptyList()
|
||||
|
||||
val files = mutableSetOf<String>()
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val timeRange = 60L // seconds
|
||||
|
||||
Files.walk(blocksDir)
|
||||
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
|
||||
.forEach { snapshotFile ->
|
||||
try {
|
||||
val content = Files.readString(snapshotFile)
|
||||
val snapshot = json.decodeFromString<BlockSnapshot>(content)
|
||||
val snapshotTime = Instant.parse(snapshot.timestamp)
|
||||
|
||||
val timeDiff = kotlin.math.abs(commitTime.epochSecond - snapshotTime.epochSecond)
|
||||
if (timeDiff <= timeRange) {
|
||||
files.add(snapshot.filePath)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Skip corrupted snapshots
|
||||
}
|
||||
}
|
||||
|
||||
return files.toList()
|
||||
}
|
||||
}
|
||||
|
||||
data class RestoreOptions(
|
||||
val commitHash: String,
|
||||
val blockHash: String?,
|
||||
val targetFile: String?
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.notevc.commands
|
||||
|
||||
import io.notevc.core.*
|
||||
import io.notevc.utils.FileUtils
|
||||
import io.notevc.utils.ColorUtils
|
||||
import io.notevc.core.Repository.Companion.NOTEVC_DIR
|
||||
import java.time.Instant
|
||||
|
||||
@@ -108,35 +109,35 @@ class StatusCommand {
|
||||
|
||||
// Modified files
|
||||
grouped[FileStatusType.MODIFIED]?.let { modifiedFiles ->
|
||||
output.appendLine("Modified files:")
|
||||
output.appendLine(ColorUtils.bold("Modified files:"))
|
||||
modifiedFiles.forEach { fileStatus ->
|
||||
output.appendLine("― ${fileStatus.path}")
|
||||
output.appendLine(" ${ColorUtils.filename(fileStatus.path)}")
|
||||
fileStatus.blockChanges?.forEach { change ->
|
||||
val symbol = when (change.type) {
|
||||
BlockChangeType.MODIFIED -> "M"
|
||||
BlockChangeType.ADDED -> "A"
|
||||
BlockChangeType.DELETED -> "D"
|
||||
BlockChangeType.MODIFIED -> ColorUtils.modified("M")
|
||||
BlockChangeType.ADDED -> ColorUtils.added("+")
|
||||
BlockChangeType.DELETED -> ColorUtils.deleted("-")
|
||||
}
|
||||
val heading = change.heading.replace(Regex("^#+\\s*"), "").trim()
|
||||
output.appendLine("―― $symbol $heading")
|
||||
output.appendLine(" $symbol ${ColorUtils.heading(heading)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Untracked files
|
||||
grouped[FileStatusType.UNTRACKED]?.let { untrackedFiles ->
|
||||
output.appendLine("Untracked files:")
|
||||
output.appendLine(ColorUtils.bold("Untracked files:"))
|
||||
untrackedFiles.forEach { fileStatus ->
|
||||
output.appendLine("―― ${fileStatus.path} (${fileStatus.blockCount} blocks)")
|
||||
output.appendLine(" ${ColorUtils.untracked(fileStatus.path)} ${ColorUtils.dim("${fileStatus.blockCount} blocks)")}")
|
||||
}
|
||||
output.appendLine()
|
||||
}
|
||||
|
||||
// Deleted files
|
||||
grouped[FileStatusType.DELETED]?.let { deletedFiles ->
|
||||
output.appendLine("Deleted files:")
|
||||
output.appendLine(ColorUtils.bold("Deleted files:"))
|
||||
deletedFiles.forEach { fileStatus ->
|
||||
output.appendLine("―― ${fileStatus.path}")
|
||||
output.appendLine(" ${ColorUtils.deleted(fileStatus.path)}")
|
||||
}
|
||||
output.appendLine()
|
||||
}
|
||||
|
||||
67
src/main/kotlin/io/notevc/utils/ColorUtils.kt
Normal file
67
src/main/kotlin/io/notevc/utils/ColorUtils.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
package io.notevc.utils
|
||||
|
||||
object ColorUtils {
|
||||
// ANSI color codes
|
||||
private const val RESET = "\u001B[0m"
|
||||
private const val BOLD = "\u001B[1m"
|
||||
private const val DIM = "\u001B[2m"
|
||||
|
||||
// Colors
|
||||
private const val BLACK = "\u001B[30m"
|
||||
private const val RED = "\u001B[31m"
|
||||
private const val GREEN = "\u001B[32m"
|
||||
private const val YELLOW = "\u001B[33m"
|
||||
private const val BLUE = "\u001B[34m"
|
||||
private const val MAGENTA = "\u001B[35m"
|
||||
private const val CYAN = "\u001B[36m"
|
||||
private const val WHITE = "\u001B[37m"
|
||||
|
||||
// Bright colors
|
||||
private const val BRIGHT_RED = "\u001B[91m"
|
||||
private const val BRIGHT_GREEN = "\u001B[92m"
|
||||
private const val BRIGHT_YELLOW = "\u001B[93m"
|
||||
private const val BRIGHT_BLUE = "\u001B[94m"
|
||||
private const val BRIGHT_MAGENTA = "\u001B[95m"
|
||||
private const val BRIGHT_CYAN = "\u001B[96m"
|
||||
|
||||
// Check if colors should be enabled (disable in CI/pipes)
|
||||
private val colorsEnabled = System.getenv("NO_COLOR") == null &&
|
||||
System.getenv("CI") == null &&
|
||||
System.console() != null
|
||||
|
||||
// Public color functions
|
||||
fun red(text: String): String = if (colorsEnabled) "$RED$text$RESET" else text
|
||||
fun green(text: String): String = if (colorsEnabled) "$GREEN$text$RESET" else text
|
||||
fun yellow(text: String): String = if (colorsEnabled) "$YELLOW$text$RESET" else text
|
||||
fun blue(text: String): String = if (colorsEnabled) "$BLUE$text$RESET" else text
|
||||
fun magenta(text: String): String = if (colorsEnabled) "$MAGENTA$text$RESET" else text
|
||||
fun cyan(text: String): String = if (colorsEnabled) "$CYAN$text$RESET" else text
|
||||
|
||||
fun brightRed(text: String): String = if (colorsEnabled) "$BRIGHT_RED$text$RESET" else text
|
||||
fun brightGreen(text: String): String = if (colorsEnabled) "$BRIGHT_GREEN$text$RESET" else text
|
||||
fun brightYellow(text: String): String = if (colorsEnabled) "$BRIGHT_YELLOW$text$RESET" else text
|
||||
fun brightBlue(text: String): String = if (colorsEnabled) "$BRIGHT_BLUE$text$RESET" else text
|
||||
fun brightMagenta(text: String): String = if (colorsEnabled) "$BRIGHT_MAGENTA$text$RESET" else text
|
||||
fun brightCyan(text: String): String = if (colorsEnabled) "$BRIGHT_CYAN$text$RESET" else text
|
||||
|
||||
fun bold(text: String): String = if (colorsEnabled) "$BOLD$text$RESET" else text
|
||||
fun dim(text: String): String = if (colorsEnabled) "$DIM$text$RESET" else text
|
||||
|
||||
// Semantic colors for version control
|
||||
fun success(text: String): String = brightGreen(text)
|
||||
fun error(text: String): String = brightRed(text)
|
||||
fun warning(text: String): String = brightYellow(text)
|
||||
fun info(text: String): String = brightBlue(text)
|
||||
fun hash(text: String): String = yellow(text)
|
||||
fun filename(text: String): String = cyan(text)
|
||||
fun heading(text: String): String = brightMagenta(text)
|
||||
fun author(text: String): String = green(text)
|
||||
fun date(text: String): String = dim(text)
|
||||
|
||||
// Status-specific colors
|
||||
fun added(text: String): String = brightGreen(text)
|
||||
fun modified(text: String): String = brightYellow(text)
|
||||
fun deleted(text: String): String = brightRed(text)
|
||||
fun untracked(text: String): String = red(text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user