feat(status): added status command and object storage

This commit is contained in:
darwincereska
2025-11-06 20:43:21 -05:00
parent 9778ad6647
commit 9b3c20cf2c
12 changed files with 813 additions and 22 deletions

1
.notevc/metadata.json Normal file
View File

@@ -0,0 +1 @@
{"version":"1.0.0","created":"2025-11-06T22:16:55.863743Z","head":null}

1
.notevc/timeline.json Normal file
View File

@@ -0,0 +1 @@
[]

View File

@@ -8,36 +8,36 @@
- [x] Create `Repository.kt` class
- [x] Implement `.notevc` directory initialization
- [ ] Create `ObjectStore.kt` for content storage
- [ ] Implement content hashing `HashUtils.kt`
- [ ] Create `NoteSnapshot` data class
- [ ] Implement `Timeline.kt` for version tracking
- [ ] Add `RepoMetadata` and configuration
- [x] Create `ObjectStore.kt` for content storage
- [x] Implement content hashing `HashUtils.kt`
- [x] Create `NoteSnapshot` data class
- [x] Implement `Timeline.kt` for version tracking
- [x] Add `RepoMetadata` and configuration
# File Operations
- [ ] Implement markdown file scanning
- [ ] Create file change detection logic
- [ ] Add file content reading/writing utilities
- [ ] Implement path resolution and validation
- [ ] Add file timestamp tracking
- [x] Implement markdown file scanning
- [x] Create file change detection logic
- [x] Add file content reading/writing utilities
- [x] Implement path resolution and validation
- [x] Add file timestamp tracking
- [ ] Create backup and restore mechanisms
# Core Commands
## Init Command
- [ ] `notevc init` - Initialize repository
- [ ] Create `.notevc` directory structure
- [ ] Generate initial metadata file
- [ ] Handle existing repository detection
- [x] `notevc init` - Initialize repository
- [x] Create `.notevc` directory structure
- [x] Generate initial metadata file
- [x] Handle existing repository detection
## Status Command
- [ ] `notevc status` - Show file changes
- [ ] Compare current files with last snapshot
- [ ] Display added/modified/deleted files
- [ ] Show clean working directory message
- [x] `notevc status` - Show file changes
- [x] Compare current files with last snapshot
- [x] Display added/modified/deleted files
- [x] Show clean working directory message
## Commit command

View File

@@ -6,7 +6,7 @@ import io.notevc.commands.*
fun main(args: Array<String>) {
// Args logic
when (args.firstOrNull()) {
"init" -> {
"init", "i" -> {
val initCommand = InitCommand()
val result = initCommand.execute(args.getOrNull(1))
@@ -20,12 +20,22 @@ fun main(args: Array<String>) {
println("Not implemented yet")
}
"status" -> {
println("Not implemented yet")
"status", "st" -> {
val statusCommand = StatusCommand()
val result = statusCommand.execute()
result.fold(
onSuccess = { output -> println(output) },
onFailure = { error -> println("Error: ${error.message}") }
)
}
"version", "--version", "-v" -> {
println("notevc version ${Repository.VERSION}")
}
else -> {
println("Usage: notevc init|commit|status|version")
}
}
}

View File

@@ -12,7 +12,8 @@ class InitCommand {
repo.init().fold(
onSuccess = {
Result.success("Initialized notevc repository in ${repo.path.toAbsolutePath()}")
val absolutePath = repo.path.toAbsolutePath().toString()
Result.success("Initialized notevc repository in $absolutePath")
},
onFailure = {
error -> Result.failure(error)

View File

@@ -1 +1,164 @@
package io.notevc.commands
import io.notevc.core.*
import io.notevc.utils.FileUtils
import io.notevc.core.Repository.Companion.NOTEVC_DIR
import java.time.Instant
class StatusCommand {
fun execute(): Result<String> {
return try {
val repo = Repository.find()
?: return Result.failure(Exception("Not in notevc repository. Run `notevc init` first."))
val status = getRepositoryStatus(repo)
val output = formatStatusOutput(status)
Result.success(output)
}
catch (e: Exception) {
Result.failure(e)
}
}
private fun getRepositoryStatus(repo: Repository): RepositoryStatus {
val objectStore = ObjectStore(repo.path.resolve("$NOTEVC_DIR/objects"))
val blockStore = BlockStore(objectStore, repo.path.resolve("$NOTEVC_DIR/blocks"))
val blockParser = BlockParser()
// Find all markdown files in the repository
val currentFiles = FileUtils.findMarkdownFiles(repo.path)
val trackedFiles = blockStore.getTrackedFiles()
val fileStatuses = mutableListOf<FileStatus>()
// Check tracked files (files that have previous snapshots)
trackedFiles.forEach { filePath ->
val fullPath = repo.path.resolve(filePath)
// File exists - check for changes
val currentContent = java.nio.file.Files.readString(fullPath)
val parsedFile = blockParser.parseFile(currentContent, filePath)
// Get latest snapshot for comparison
val latestSnapshot = blockStore.getLatestBlockSnapshot(filePath)
if (latestSnapshot != null) {
// Create snapshot to compare
val currentSnapshot = createCurrentSnapshot(parsedFile, objectStore)
val changes = blockStore.compareBlocks(latestSnapshot, currentSnapshot)
if (changes.isNotEmpty()) {
fileStatuses.add(FileStatus(
path = filePath,
type = FileStatusType.MODIFIED,
blockChanges = changes
))
}
} else {
// File was deleted
fileStatuses.add(FileStatus(
path = filePath,
type = FileStatusType.DELETED
))
}
}
// Check for untracked files (new files)
currentFiles.forEach { filePath ->
val relativePath = repo.path.relativize(filePath).toString()
if (relativePath !in trackedFiles) {
val content = java.nio.file.Files.readString(filePath)
val parsedFile = blockParser.parseFile(content, relativePath)
fileStatuses.add(FileStatus(
path = relativePath,
type = FileStatusType.UNTRACKED,
blockCount = parsedFile.blocks.size,
))
}
}
return RepositoryStatus(fileStatuses)
}
private fun createCurrentSnapshot(parsedFile: ParsedFile, objectStore: ObjectStore): BlockSnapshot {
return BlockSnapshot(
filePath = parsedFile.path,
timestamp = Instant.now().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 formatStatusOutput(status: RepositoryStatus): String {
if (status.files.isEmpty()) return "Working directory clean - no changes detected"
val output = StringBuilder()
val grouped = status.files.groupBy { it.type }
// Modified files
grouped[FileStatusType.MODIFIED]?.let { modifiedFiles ->
output.appendLine("Modified files:")
modifiedFiles.forEach { fileStatus ->
output.appendLine(" ${fileStatus.path}")
fileStatus.blockChanges?.forEach { change ->
val symbol = when (change.type) {
BlockChangeType.MODIFIED -> "M"
BlockChangeType.ADDED -> "A"
BlockChangeType.DELETED -> "D"
}
val heading = change.heading.replace(Regex("^#+\\s*"), "").trim()
output.appendLine("―― $symbol $heading")
}
}
}
// Untracked files
grouped[FileStatusType.UNTRACKED]?.let { untrackedFiles ->
output.appendLine("Untracked files:")
untrackedFiles.forEach { fileStatus ->
output.appendLine("―― ${fileStatus.path} (${fileStatus.blockCount} blocks)")
}
output.appendLine()
}
// Deleted files
grouped[FileStatusType.DELETED]?.let { deletedFiles ->
output.appendLine("Deleted files:")
deletedFiles.forEach { fileStatus ->
output.appendLine("―― ${fileStatus.path}")
}
output.appendLine()
}
return output.toString().trim()
}
}
// Data classes for status information
data class RepositoryStatus(
val files: List<FileStatus>
)
data class FileStatus(
val path: String,
val type: FileStatusType,
val blockChanges: List<BlockChange>? = null,
val blockCount: Int? = null
)
enum class FileStatusType {
MODIFIED, // File has changes
UNTRACKED, // File was not yet commited
DELETED // File was removed
}

View File

@@ -0,0 +1,155 @@
package io.notevc.core
import kotlinx.serialization.Serializable
class BlockParser {
// Parse markdown file into blocks based on headings
fun parseFile(content: String, filePath: String): ParsedFile {
val lines = content.lines()
val blocks = mutableListOf<Block>()
val frontMatter = extractFrontMatter(lines)
var currentBlock: MutableList<String>? = null
var currentHeading: String? = null
var blockIndex = 0
val contentLines = if (frontMatter != null) {
lines.drop(frontMatter.endLine + 1)
} else lines
for (line in contentLines) {
when {
line.startsWith("#") -> {
// Save previous block if exists
if (currentBlock != null && currentHeading != null) {
blocks.add(Block(
id = generateBlockId(filePath, currentHeading, blockIndex),
heading = currentHeading,
content = currentBlock.joinToString("\n"),
type = BlockType.HEADING_SECTION,
order = blockIndex++
))
}
// Start new block
currentHeading = line
currentBlock = mutableListOf(line)
}
else -> {
// Add to current block or create content-only block
if (currentBlock != null) currentBlock.add(line)
else {
// Content before any heading
currentBlock = mutableListOf()
currentHeading = "<!-- Content -->"
currentBlock.add(line)
}
}
}
}
// Save final block
if (currentBlock != null && currentHeading != null) {
blocks.add(Block(
id = generateBlockId(filePath, currentHeading, blockIndex),
heading = currentHeading,
content = currentBlock.joinToString("\n"),
type = BlockType.HEADING_SECTION,
order = blockIndex
))
}
return ParsedFile(
path = filePath,
frontMatter = frontMatter,
blocks = blocks
)
}
// Extract YAML front matter
private fun extractFrontMatter(lines: List<String>): FrontMatter? {
if (lines.isEmpty() || lines[0] != "---") return null
val endIndex = lines.drop(1).indexOfFirst { it == "---"}
if (endIndex == -1) return null
val yamlLines = lines.subList(1, endIndex + 1)
val properties = mutableMapOf<String, String>()
yamlLines.forEach { line ->
val colonIndex = line.indexOf(":")
if (colonIndex != -1) {
val key = line.take(colonIndex).trim()
val value = line.substring(colonIndex + 1).trim().removeSurrounding("\"")
properties[key] = value
}
}
return FrontMatter(
properties = properties,
endLine = endIndex + 1
)
}
// Generate stable block id
private fun generateBlockId(filePath: String, heading: String, order: Int): String {
val cleanHeading = heading.replace(Regex("^#+\\s*"), "").trim()
val baseId = "$filePath:$cleanHeading:$order"
return io.notevc.utils.HashUtils.sha256(baseId).take(12)
}
// Reconstruct file from blocks
fun reconstructFile(parsedFile: ParsedFile): String {
val result = StringBuilder()
// Add front matter if exists
parsedFile.frontMatter?.let { fm ->
result.appendLine("---")
fm.properties.forEach { (key, value) ->
result.appendLine("$key: \"$value\"")
}
result.appendLine("---")
result.appendLine()
}
// Add blocks in order
parsedFile.blocks.sortedBy { it.order }.forEach { block ->
result.appendLine(block.content)
if (block != parsedFile.blocks.last()) {
result.appendLine()
}
}
return result.toString()
}
}
@Serializable
data class ParsedFile(
val path: String,
val frontMatter: FrontMatter?,
val blocks: List<Block>
)
@Serializable
data class Block(
val id: String, // Stable block identifier
val heading: String, // The heading text
val content: String, // Full block content including heading
val type: BlockType,
val order: Int // Order within file
)
@Serializable
data class FrontMatter(
val properties: Map<String, String>,
val endLine: Int
) {
val isEnabled: Boolean get() = properties["enabled"]?.lowercase() == "true"
val isAutomatic: Boolean get() = properties["automatic"]?.lowercase() == "true"
}
enum class BlockType {
HEADING_SECTION, // # Heading with content
CONTENT_ONLY // Content without heading
}

View File

@@ -0,0 +1,283 @@
package io.notevc.core
import io.notevc.utils.HashUtils
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import kotlin.io.path.*
class BlockStore(
private val objectStore: ObjectStore,
private val blocksDir: Path
) {
private val json = Json { prettyPrint = true }
// Store blocks from a parsed file and return block snapshot
fun storeBlocks(parsedFile: ParsedFile, timestamp: Instant): BlockSnapshot {
val blockStates = parsedFile.blocks.map { block ->
val contentHash = objectStore.storeContent(block.content)
BlockState(
id = block.id,
heading = block.heading,
contentHash = contentHash,
type = block.type,
order = block.order
)
}
val snapshot = BlockSnapshot(
filePath = parsedFile.path,
timestamp = timestamp.toString(),
blocks = blockStates,
frontMatter = parsedFile.frontMatter
)
// Store block snapshot with time-based structure (yyyy/mm/dd)
storeBlockSnapshot(snapshot)
return snapshot
}
// Get blocks for a file at a specific time
fun getBlocksAtTime(filePath: String, timestamp: Instant): List<Block>? {
val snapshot = getLatestBlockSnapshotBefore(filePath, timestamp)
return snapshot?.let { reconstructBlocks(it) }
}
// Get current blocks for a file
fun getCurrentBlocks(filePath: String): List<Block>? {
val snapshot = getLatestBlockSnapshot(filePath)
return snapshot?.let { reconstructBlocks(it) }
}
// Compare blocks between two snapshots
fun compareBlocks(oldSnapshot: BlockSnapshot?, newSnapshot: BlockSnapshot): List<BlockChange> {
val changes = mutableListOf<BlockChange>()
val oldBlocks = oldSnapshot?.blocks?.associateBy { it.id } ?: emptyMap()
val newBlocks = newSnapshot.blocks.associateBy { it.id }
// Find added and modified blocks
newBlocks.forEach { (id, newBlock) ->
val oldBlock = oldBlocks[id]
when {
oldBlock == null -> {
changes.add(BlockChange(
blockId = id,
type = BlockChangeType.ADDED,
heading = newBlock.heading,
newHash = newBlock.contentHash
))
}
oldBlock.contentHash != newBlock.contentHash -> {
changes.add(BlockChange(
blockId = id,
type = BlockChangeType.MODIFIED,
heading = newBlock.heading,
oldHash = oldBlock.contentHash,
newHash = newBlock.contentHash
))
}
}
}
// Find deleted blocks
oldBlocks.forEach { (id, oldBlock) ->
if (id !in newBlocks) {
changes.add(BlockChange(
blockId = id,
type = BlockChangeType.DELETED,
heading = oldBlock.heading,
oldHash = oldBlock.contentHash
))
}
}
return changes
}
private fun storeBlockSnapshot(snapshot: BlockSnapshot) {
val datePath = getDatePath(Instant.parse(snapshot.timestamp))
val timeString: String = getTimeString(Instant.parse(snapshot.timestamp))
Files.createDirectories(blocksDir.resolve(datePath))
val filename = "blocks-$timeString-${snapshot.filePath.replace("/","_")}.json"
val snapshotPath: Path = blocksDir.resolve(datePath).resolve(filename)
Files.writeString(snapshotPath, json.encodeToString(snapshot))
}
private fun reconstructBlocks(snapshot: BlockSnapshot): List<Block> {
return snapshot.blocks.map { blockState ->
val content = objectStore.getContent(blockState.contentHash)
?: throw IllegalStateException("Missing content for block ${blockState.id}")
Block(
id = blockState.id,
heading = blockState.heading,
content = content,
type = blockState.type,
order = blockState.order
)
}
}
fun getLatestBlockSnapshot(filePath: String): BlockSnapshot? {
// Implementation to find latest snapshot for file
// Walk through time directories and find most recent
if (!blocksDir.exists()) return null
// Walk through all time directories to find snapshot for this file
val snapshots = mutableListOf<Pair<BlockSnapshot, Instant>>()
Files.walk(blocksDir)
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
.filter { filePath.replace("/","_") in it.fileName.toString()}
.forEach { snapshotFile ->
try {
val content = Files.readString(snapshotFile)
val snapshot = json.decodeFromString<BlockSnapshot>(content)
val timestamp = Instant.parse(snapshot.timestamp)
snapshots.add(snapshot to timestamp)
}
catch (e: Exception) {
// Skip corrupted snapshots
}
}
// Return the most recent snapshot
return snapshots
.maxByOrNull { it.second }
?.first
}
fun getLatestBlockSnapshotBefore(filePath: String, timestamp: Instant): BlockSnapshot? {
if (!blocksDir.exists()) return null
val snapshots = mutableListOf<Pair<BlockSnapshot, Instant>>()
Files.walk(blocksDir)
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
.filter { filePath.replace("/","_") in it.fileName.toString()}
.forEach { snapshotFile ->
try {
val content = Files.readString(snapshotFile)
val snapshot = json.decodeFromString<BlockSnapshot>(content)
val snapshotTime = Instant.parse(snapshot.timestamp)
// Only include snapshots before the given timestamp
if (snapshotTime.isBefore(timestamp)) {
snapshots.add(snapshot to snapshotTime)
}
} catch (e: Exception) {
// Skip corrupted snapshots
}
}
// Return the most recent snapshot before the timestamp
return snapshots
.maxByOrNull { it.second }
?.first
}
// Get all snapshots for specific file
fun getSnapshotForFile(filePath: String): List<BlockSnapshot> {
if (!blocksDir.exists()) return emptyList()
val snapshots = mutableListOf<Pair<BlockSnapshot, Instant>>()
Files.walk(blocksDir)
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
.filter { filePath.replace("/","_") in it.fileName.toString() }
.forEach { snapshotFile ->
try {
val content = Files.readString(snapshotFile)
val snapshot = json.decodeFromString<BlockSnapshot>(content)
val timestamp = Instant.parse(snapshot.timestamp)
snapshots.add(snapshot to timestamp)
}
catch (e: Exception) {
// Skip corrupted snapshots
}
}
return snapshots
.sortedByDescending { it.second }
.map { it.first }
}
// Check if any snapshots exist for a file
fun hasSnapshots(filePath: String): Boolean {
if (!blocksDir.exists()) return false
return Files.walk(blocksDir)
.filter { it.isRegularFile() && it.fileName.toString().startsWith("blocks-") }
.anyMatch { filePath.replace("/", "_") in it.fileName.toString() }
}
// Get all files that have snapshots
fun getTrackedFiles(): List<String> {
if (!blocksDir.exists()) return emptyList()
val files = mutableSetOf<String>()
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)
files.add(snapshot.filePath)
}
catch (e: Exception) {
// Skip corrupted snapshots
}
}
return files.toList()
}
private fun getDatePath(timestamp: Instant): String {
val date = java.time.LocalDateTime.ofInstant(timestamp, java.time.ZoneId.systemDefault())
return "${date.year}/${date.monthValue.toString().padStart(2, '0')}/${date.dayOfMonth.toString().padStart(2, '0')}"
}
private fun getTimeString(timestamp: Instant): String {
val time = java.time.LocalDateTime.ofInstant(timestamp, java.time.ZoneId.systemDefault())
return "${time.hour.toString().padStart(2, '0')}-${time.minute.toString().padStart(2, '0')}-${time.second.toString().padStart(2, '0')}"
}
}
@Serializable
data class BlockSnapshot(
val filePath: String,
val timestamp: String,
val blocks: List<BlockState>,
val frontMatter: FrontMatter?
)
@Serializable
data class BlockState(
val id: String,
val heading: String,
val contentHash: String,
val type: BlockType,
val order: Int
)
@Serializable
data class BlockChange(
val blockId: String,
val type: BlockChangeType,
val heading: String,
val oldHash: String? = null,
val newHash: String? = null
)
enum class BlockChangeType {
ADDED, MODIFIED, DELETED
}

View File

@@ -1 +1,74 @@
package io.notevc.core
import java.nio.file.Files
import io.notevc.utils.HashUtils
import java.nio.file.Path
import kotlin.io.path.*
class ObjectStore(private val objectsDir: Path) {
// Store content and return its hash
// Uses git-like storage: objects/ab/cdef123... (first 2 characters as directory)
fun storeContent(content: String): String {
val hash = HashUtils.sha256(content)
val objectPath = getObjectPath(hash)
// Only store if it doesn't already exist
if (!objectPath.exists()) {
Files.createDirectories(objectPath.parent)
Files.writeString(objectPath, content)
}
return hash
}
// Retrieve content by hash
fun getContent(hash: String): String? {
val objectPath = getObjectPath(hash)
return if (objectPath.exists()) {
Files.readString(objectPath)
} else null
}
// Check if content exists
fun hasContent(hash: String): Boolean = getObjectPath(hash).exists()
// Get all stored object hashes
fun getAllHashes(): List<String> {
if (!objectsDir.exists()) return emptyList()
return Files.walk(objectsDir, 2)
.filter { it.isRegularFile() }
.map { path ->
val parent = path.parent.fileName.toString()
val filename = path.fileName.toString()
parent + filename
}
.toList()
}
// Get storage statistics
fun getStats(): ObjectStoreStats {
val hashes = getAllHashes()
val totalSize = hashes.sumOf { hash ->
getObjectPath(hash).fileSize()
}
return ObjectStoreStats(
objectCount = hashes.size,
totalSize = totalSize
)
}
// Convert hash to file path
private fun getObjectPath(hash: String): Path {
require(hash.length >= 3) { "Hash too short: $hash" }
val dir = hash.take(2)
val filename = hash.drop(2)
return objectsDir.resolve(dir).resolve(filename)
}
}
data class ObjectStoreStats(
val objectCount: Int,
val totalSize: Long
)

View File

@@ -12,6 +12,7 @@ import java.time.Instant
class Repository private constructor(private val rootPath: Path) {
private val notevcDir = rootPath.resolve(NOTEVC_DIR)
private val objectStore = ObjectStore(notevcDir.resolve("objects"))
val path: Path get() = rootPath
val isInitialized: Boolean get() = notevcDir.exists()

View File

@@ -1,3 +1,83 @@
package io.notevc.utils
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import kotlin.io.path.*
import io.notevc.core.Repository.Companion.NOTEVC_DIR
object FileUtils {
// Find all markdown files in directory (recursively)
fun findMarkdownFiles(rootPath: Path): List<Path> {
if (!rootPath.exists() || !rootPath.isDirectory()) return emptyList()
return Files.walk(rootPath)
.filter { path ->
path.isRegularFile() &&
path.extension.lowercase() == "md" &&
!isInNotevcDir(path, rootPath)
}
.toList()
}
// Get file state information
fun getFileState(filePath: Path, rootPath: Path): FileState {
val content = Files.readString(filePath)
val contentHash = HashUtils.sha256(content)
val relativePath = rootPath.relativize(filePath).toString()
return FileState(
path = relativePath,
contentHash = contentHash,
size = filePath.fileSize(),
lastModified = filePath.getLastModifiedTime().toInstant()
)
}
// Get current state of all markdown files
fun getCurrentFileStates(rootPath: Path): Map<String, FileState> {
return findMarkdownFiles(rootPath)
.associate { filePath ->
val fileState = getFileState(filePath, rootPath)
fileState.path to fileState
}
}
// Create relative path string for display
fun getDisplayPath(filePath: Path, rootPath: Path): String = rootPath.relativize(filePath).toString()
// Ensure directory exists
fun ensureDirectoryExists(path: Path) {
if (!path.exists()) {
Files.createDirectories(path)
}
}
// Check if path is inside .notevc directory
private fun isInNotevcDir(filePath: Path, rootPath: Path): Boolean {
val relativePath = rootPath.relativize(filePath)
return relativePath.toString().startsWith(NOTEVC_DIR)
}
}
// Represents the state of a file at a point in time
data class FileState(
val path: String, // Relative path from repo root
val contentHash: String, // SHA-256 of content
val size: Long, // File size in bytes
val lastModified: Instant // Last modification time
)
// Represents a change to a file
data class FileChange(
val path: String,
val type: ChangeType,
val oldHash: String? = null,
val newHash: String? = null
)
enum class ChangeType {
ADDED, // New file
MODIFIED, // Content changed
DELETED // File removed
}

View File

@@ -1 +1,24 @@
package io.notevc.utils
import java.security.MessageDigest
object HashUtils {
// Generate SHA-256 hash of string content
fun sha256(content: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(content.toByteArray())
return hashBytes.joinToString("") { "%02x" .format(it) }
}
// Generate SHA-256 hash of file content
fun sha256File(filePath: java.nio.file.Path): String {
val content = java.nio.file.Files.readString(filePath)
return sha256(content)
}
// Generate short hash (first 8 characters) for display
fun shortHash(hash: String): String = hash.take(8)
// Validate hash format
fun isValidHash(hash: String): Boolean = hash.matches(Regex("^[a-f0-9]{64}$"))
}