From 673c9a3154503657d115562b6486964de56ee457 Mon Sep 17 00:00:00 2001 From: darwincereska Date: Fri, 7 Nov 2025 16:01:35 -0500 Subject: [PATCH] feat(commit): added commit command and timeline --- .gitignore | 1 + .notevc/metadata.json | 12 +- .notevc/timeline.json | 16 +- CHECKLIST.md | 11 +- README.md | 80 +++++- images/png/Color400X500.png | Bin 0 -> 19161 bytes images/png/Color40X50.png | Bin 0 -> 4220 bytes images/png/Monochrome400X500.png | Bin 0 -> 12187 bytes images/svg/Color.svg | 19 ++ images/svg/Monochrome.svg | 5 + src/main/kotlin/io/notevc/NoteVC.kt | 9 +- .../io/notevc/commands/CommitCommand.kt | 247 ++++++++++++++++++ .../io/notevc/commands/StatusCommand.kt | 2 +- src/main/kotlin/io/notevc/core/Repository.kt | 21 +- 14 files changed, 400 insertions(+), 23 deletions(-) create mode 100644 images/png/Color400X500.png create mode 100644 images/png/Color40X50.png create mode 100644 images/png/Monochrome400X500.png create mode 100644 images/svg/Color.svg create mode 100644 images/svg/Monochrome.svg diff --git a/.gitignore b/.gitignore index 0a28dde..8680464 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ Thumbs.db # Other *.hprof +.notevc/ diff --git a/.notevc/metadata.json b/.notevc/metadata.json index 0424d93..b6a5726 100644 --- a/.notevc/metadata.json +++ b/.notevc/metadata.json @@ -1 +1,11 @@ -{"version":"1.0.0","created":"2025-11-06T22:16:55.863743Z","head":null} \ No newline at end of file +{ + "version": "1.0.0", + "created": "2025-11-07T02:45:31.185947Z", + "head": "faa8ece0", + "lastCommit": { + "hash": "faa8ece0", + "message": "Updated readme", + "timestamp": "2025-11-07T19:12:43.784310Z", + "author": "darwin" + } +} \ No newline at end of file diff --git a/.notevc/timeline.json b/.notevc/timeline.json index 0637a08..0cbc69d 100644 --- a/.notevc/timeline.json +++ b/.notevc/timeline.json @@ -1 +1,15 @@ -[] \ No newline at end of file +[ + { + "hash": "faa8ece0", + "message": "Updated readme", + "timestamp": "2025-11-07T19:12:43.784310Z", + "author": "darwin", + "parent": "00f585ce" + }, + { + "hash": "00f585ce", + "message": "first", + "timestamp": "2025-11-07T02:45:41.270419Z", + "author": "darwin" + } +] \ No newline at end of file diff --git a/CHECKLIST.md b/CHECKLIST.md index ad9e24c..e7aa096 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -41,11 +41,11 @@ ## Commit command -- [ ] `notevc commit "message"` - Create snapshot -- [ ] Validate commit message exists -- [ ] Store changed file contents -- [ ] Create snapshot with metadata -- [ ] Update repository head pointer +- [x] `notevc commit "message"` - Create snapshot +- [x] Validate commit message exists +- [x] Store changed file contents +- [x] Create snapshot with metadata +- [x] Update repository head pointer ## Log Command @@ -111,3 +111,4 @@ - [ ] File watching for auto-commits - [ ] Export/import functionality - [ ] NeoVim Plugin + diff --git a/README.md b/README.md index b68f75e..b671d34 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,75 @@ -# NoteVC: Version Control for Markdown +# ![logo.png](images/png/Color40x50.png) NoteVC: Version Control for Markdown + + # Repository management -notevc init [path] # Initialize notevc repo -notevc status # Show changed files -notevc commit "message" # Create snapshot -notevc log [--since=time] # Show commit history + +Initialize notevc repo: +```bash +notevc init [path] +``` + +Show changed files: +```bash +notevc status +``` + +Create snapshot: +```bash +notevc commit [--file ] "message" +``` + +Show commit history: +```bash +notevc log [--since=time] +``` # Viewing changes -notevc diff [file] # Show changes since last commit -notevc diff HEAD~1 [file] # Compare with previous commit -notevc show # Show specific commit + +Show changes since last commit: +```bash +notevc diff [file] +``` + +Compare with previous commit: +```bash +notevc diff HEAD~1 [file] +``` + +Show specific commit: +```bash +notevc show +``` # Restoration -notevc restore [file] # Restore to specific version -notevc checkout # Restore entire repo state + +Restore to specific version: +```bash +notevc restore [file] +``` + +Restore to specific block: +```bash +notevc restore --block [file] +``` + +Restore entire repo state: +```bash +notevc checkout +``` # Utilities -notevc clean # Remove old snapshots -notevc gc # Garbage collect unused objects -notevc config # Show/set configuration +Remove old snapshots: +```bash +notevc clean +``` + +Garbage collect unused objects: +```bash +notevc gc +``` + +Show/set configuration: +```bash +notevc config +``` diff --git a/images/png/Color400X500.png b/images/png/Color400X500.png new file mode 100644 index 0000000000000000000000000000000000000000..5d15b5d501484e1ae48980df0179673094eb6c5c GIT binary patch literal 19161 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 literal 0 HcmV?d00001 diff --git a/images/png/Color40X50.png b/images/png/Color40X50.png new file mode 100644 index 0000000000000000000000000000000000000000..a4bebd504f096345220e6c13daa88c74cf0cb99a GIT binary patch literal 4220 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 CucUJT literal 0 HcmV?d00001 diff --git a/images/png/Monochrome400X500.png b/images/png/Monochrome400X500.png new file mode 100644 index 0000000000000000000000000000000000000000..2abd1374380e470f839895a494a18e90f3813a2f GIT binary patch literal 12187 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 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/svg/Monochrome.svg b/images/svg/Monochrome.svg new file mode 100644 index 0000000..c9bf4d6 --- /dev/null +++ b/images/svg/Monochrome.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/kotlin/io/notevc/NoteVC.kt b/src/main/kotlin/io/notevc/NoteVC.kt index 3e0c345..3600ce1 100644 --- a/src/main/kotlin/io/notevc/NoteVC.kt +++ b/src/main/kotlin/io/notevc/NoteVC.kt @@ -17,7 +17,14 @@ fun main(args: Array) { } "commit" -> { - println("Not implemented yet") + val commitArgs = args.drop(1) + val commitCommand = CommitCommand() + val result = commitCommand.execute(commitArgs) + + result.fold( + onSuccess = { output -> println(output) }, + onFailure = { error -> println("Error: ${error.message}") } + ) } "status", "st" -> { diff --git a/src/main/kotlin/io/notevc/commands/CommitCommand.kt b/src/main/kotlin/io/notevc/commands/CommitCommand.kt index a1f7d85..d9ddf47 100644 --- a/src/main/kotlin/io/notevc/commands/CommitCommand.kt +++ b/src/main/kotlin/io/notevc/commands/CommitCommand.kt @@ -1 +1,248 @@ package io.notevc.commands + +import io.notevc.core.* +import io.notevc.utils.FileUtils +import io.notevc.utils.HashUtils +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.nio.file.Files +import java.time.Instant +import kotlin.io.path.* + +class CommitCommand { + + fun execute(args: List): Result { + return try { + val (targetFile, message) = parseArgs(args) + + if (message.isBlank()) { + return Result.failure(Exception("Commit message cannot be empty")) + } + + val repo = Repository.find() + ?: return Result.failure(Exception("Not in a notevc repository. Run 'notevc init' first.")) + + val commitResult = if (targetFile != null) { + createSingleFileCommit(repo, targetFile, message) + } else { + createChangedFilesCommit(repo, message) + } + + Result.success(commitResult) + } catch (e: Exception) { + Result.failure(e) + } + } + + private fun parseArgs(args: List): Pair { + if (args.isEmpty()) { + return null to "" + } + + // Check for --file flag + val fileIndex = args.indexOf("--file") + if (fileIndex != -1 && fileIndex + 1 < args.size) { + val targetFile = args[fileIndex + 1] + val messageArgs = args.filterIndexed { index, _ -> + index != fileIndex && index != fileIndex + 1 + } + return targetFile to messageArgs.joinToString(" ") + } + + // No --file flag, all args are the message + return null to args.joinToString(" ") + } + + private fun createSingleFileCommit(repo: Repository, targetFile: String, message: 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() + val timestamp = Instant.now() + + // Resolve the target file path + val filePath = repo.path.resolve(targetFile) + if (!filePath.exists()) { + throw Exception("File not found: $targetFile") + } + + if (!targetFile.endsWith(".md")) { + throw Exception("Only markdown files (.md) are supported") + } + + val relativePath = repo.path.relativize(filePath).toString() + val content = Files.readString(filePath) + val parsedFile = blockParser.parseFile(content, relativePath) + + // Check if file is disabled + if (parsedFile.frontMatter?.isEnabled == false) { + throw Exception("File $targetFile is disabled (enabled: false in front matter)") + } + + // Check if file actually changed + val latestSnapshot = blockStore.getLatestBlockSnapshot(relativePath) + if (latestSnapshot != null) { + val currentSnapshot = createCurrentSnapshot(parsedFile, objectStore, timestamp) + val changes = blockStore.compareBlocks(latestSnapshot, currentSnapshot) + + if (changes.isEmpty()) { + return "No changes detected in $targetFile" + } + } + + // Store blocks for this file + val snapshot = blockStore.storeBlocks(parsedFile, timestamp) + val commitHash = HashUtils.sha256("$timestamp:$message:$relativePath").take(8) + + // Update repository metadata + updateRepositoryHead(repo, commitHash, timestamp, message) + + return buildString { + appendLine("Created commit $commitHash") + appendLine("Message: $message") + appendLine("File: $relativePath (${snapshot.blocks.size} blocks)") + } + } + + private fun createChangedFilesCommit(repo: Repository, message: 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() + val timestamp = Instant.now() + + // Find all markdown files + val markdownFiles = FileUtils.findMarkdownFiles(repo.path) + + if (markdownFiles.isEmpty()) { + throw Exception("No markdown files found to commit") + } + + val changedFiles = mutableListOf() + var totalBlocksStored = 0 + + // Process each file and check for changes + markdownFiles.forEach { filePath -> + val relativePath = repo.path.relativize(filePath).toString() + val content = Files.readString(filePath) + val parsedFile = blockParser.parseFile(content, relativePath) + + // Skip files with front matter disabled + if (parsedFile.frontMatter?.isEnabled == false) { + return@forEach + } + + // Check if file changed + val latestSnapshot = blockStore.getLatestBlockSnapshot(relativePath) + val hasChanges = if (latestSnapshot != null) { + val currentSnapshot = createCurrentSnapshot(parsedFile, objectStore, timestamp) + val changes = blockStore.compareBlocks(latestSnapshot, currentSnapshot) + changes.isNotEmpty() + } else { + // New file - always has changes + true + } + + if (hasChanges) { + // Store blocks for this file + val snapshot = blockStore.storeBlocks(parsedFile, timestamp) + changedFiles.add("$relativePath (${snapshot.blocks.size} blocks)") + totalBlocksStored += snapshot.blocks.size + } + } + + if (changedFiles.isEmpty()) { + return "No changes detected - working directory clean" + } + + // Create commit hash from timestamp and message + val commitHash = HashUtils.sha256("$timestamp:$message").take(8) + + // Update repository metadata + updateRepositoryHead(repo, commitHash, timestamp, message) + + return buildString { + appendLine("Created commit $commitHash") + appendLine("Message: $message") + appendLine("Files committed: ${changedFiles.size}") + appendLine("Total blocks: $totalBlocksStored") + appendLine() + changedFiles.forEach { fileInfo -> + appendLine(" $fileInfo") + } + } + } + + private fun createCurrentSnapshot(parsedFile: ParsedFile, objectStore: ObjectStore, timestamp: Instant): BlockSnapshot { + return BlockSnapshot( + filePath = parsedFile.path, + timestamp = timestamp.toString(), + blocks = parsedFile.blocks.map { block -> + BlockState( + id = block.id, + heading = block.heading, + contentHash = objectStore.storeContent(block.content), + type = block.type, + order = block.order + ) + }, + frontMatter = parsedFile.frontMatter + ) + } + + private fun updateRepositoryHead(repo: Repository, commitHash: String, timestamp: Instant, message: String) { + val metadataFile = repo.path.resolve("${Repository.NOTEVC_DIR}/metadata.json") + val timelineFile = repo.path.resolve("${Repository.NOTEVC_DIR}/timeline.json") + + // Read current timeline + val currentCommits = if (timelineFile.exists()) { + val content = Files.readString(timelineFile) + if (content.trim() == "[]") { + emptyList() + } else { + Json.decodeFromString>(content) + } + } else { + emptyList() + } + + // Create new commit entry + val newCommit = CommitEntry( + hash = commitHash, + message = message, + timestamp = timestamp.toString(), + author = System.getProperty("user.name"), + parent = currentCommits.firstOrNull()?.hash // Previous commit + ) + + val updatedCommits = listOf(newCommit) + currentCommits + + // Save timeline + val json = Json { prettyPrint = true } + Files.writeString(timelineFile, json.encodeToString(updatedCommits)) + + // Read current metadata + val currentMetadata = if (metadataFile.exists()) { + val content = Files.readString(metadataFile) + Json.decodeFromString(content) + } else { + RepoMetadata( + version = Repository.VERSION, + created = timestamp.toString(), + head = null + ) + } + + // Update with new commit + val updatedMetadata = currentMetadata.copy( + head = commitHash, + lastCommit = CommitInfo( + hash = commitHash, + message = message, + timestamp = timestamp.toString(), + author = System.getProperty("user.name") + ) + ) + + // Save updated metadata + Files.writeString(metadataFile, json.encodeToString(updatedMetadata)) + } +} diff --git a/src/main/kotlin/io/notevc/commands/StatusCommand.kt b/src/main/kotlin/io/notevc/commands/StatusCommand.kt index c9ff13b..7fb4d84 100644 --- a/src/main/kotlin/io/notevc/commands/StatusCommand.kt +++ b/src/main/kotlin/io/notevc/commands/StatusCommand.kt @@ -110,7 +110,7 @@ class StatusCommand { grouped[FileStatusType.MODIFIED]?.let { modifiedFiles -> output.appendLine("Modified files:") modifiedFiles.forEach { fileStatus -> - output.appendLine(" ${fileStatus.path}") + output.appendLine("― ${fileStatus.path}") fileStatus.blockChanges?.forEach { change -> val symbol = when (change.type) { BlockChangeType.MODIFIED -> "M" diff --git a/src/main/kotlin/io/notevc/core/Repository.kt b/src/main/kotlin/io/notevc/core/Repository.kt index 31bf56a..14fed74 100644 --- a/src/main/kotlin/io/notevc/core/Repository.kt +++ b/src/main/kotlin/io/notevc/core/Repository.kt @@ -93,7 +93,17 @@ class Repository private constructor(private val rootPath: Path) { data class RepoMetadata( val version: String, val created: String, - var head: String? + var head: String?, + val config: RepoConfig = RepoConfig(), + val lastCommit: CommitInfo? = null +) + +@Serializable +data class CommitInfo( + val hash: String, + val message: String, + val timestamp: String, + val author: String ) @Serializable @@ -102,3 +112,12 @@ data class RepoConfig( val compressionEnabled: Boolean = false, val maxSnapshots: Int = 100 ) + +@Serializable +data class CommitEntry( + val hash: String, + val message: String, + val timestamp: String, + val author: String, + val parent: String? = null +)