72 lines
2.3 KiB
Ruby
72 lines
2.3 KiB
Ruby
|
|
require "json"
|
||
|
|
require "fileutils"
|
||
|
|
|
||
|
|
module Backlog
|
||
|
|
# The global project index at ~/.cc-os/projects.json: a derived map of
|
||
|
|
# canonical repo path -> {name, tracker, remote, updated_at}, maintained as
|
||
|
|
# a side effect of config-write (issue #27). Deliberately GLOBAL state
|
||
|
|
# (distinct from per-project .cc-os/ under ADR-0027) so any session can ask
|
||
|
|
# "which tracker does project X use" without visiting its repo.
|
||
|
|
#
|
||
|
|
# Pure over an injected path: callers hand in the already-canonical repo
|
||
|
|
# path (the CLI resolves it via `git rev-parse --show-toplevel`). Read
|
||
|
|
# paths never raise — a missing or corrupt file reads as an empty index; a
|
||
|
|
# corrupt file is only overwritten when a new upsert happens. Writes are
|
||
|
|
# atomic (temp file + rename in the same directory).
|
||
|
|
class ProjectIndex
|
||
|
|
VERSION = 1
|
||
|
|
|
||
|
|
# Default index path, honoring a CC_OS_HOME override (used by tests to
|
||
|
|
# keep the real index untouched) and falling back to $HOME/.cc-os.
|
||
|
|
def self.default_path(env: ENV)
|
||
|
|
base = env["CC_OS_HOME"] || File.join(env.fetch("HOME", Dir.home), ".cc-os")
|
||
|
|
File.join(base, "projects.json")
|
||
|
|
end
|
||
|
|
|
||
|
|
def initialize(path: self.class.default_path, today: -> { Time.now.strftime("%Y-%m-%d") })
|
||
|
|
@path = path
|
||
|
|
@today = today
|
||
|
|
end
|
||
|
|
|
||
|
|
# @return [Hash] the projects map (canonical path -> row); {} when the
|
||
|
|
# file is missing or unreadable/corrupt. Never raises.
|
||
|
|
def all
|
||
|
|
data = read
|
||
|
|
data["projects"].is_a?(Hash) ? data["projects"] : {}
|
||
|
|
end
|
||
|
|
|
||
|
|
# Insert or update the row for a canonical repo path. Atomic write;
|
||
|
|
# creates the parent directory if missing.
|
||
|
|
def upsert(path:, name:, tracker:, remote:)
|
||
|
|
projects = all
|
||
|
|
projects[path] = {
|
||
|
|
"name" => name,
|
||
|
|
"tracker" => tracker,
|
||
|
|
"remote" => remote,
|
||
|
|
"updated_at" => @today.call
|
||
|
|
}
|
||
|
|
write({ "version" => VERSION, "projects" => projects })
|
||
|
|
projects[path]
|
||
|
|
end
|
||
|
|
|
||
|
|
private
|
||
|
|
|
||
|
|
def read
|
||
|
|
return {} unless File.exist?(@path)
|
||
|
|
|
||
|
|
parsed = JSON.parse(File.read(@path))
|
||
|
|
parsed.is_a?(Hash) ? parsed : {}
|
||
|
|
rescue StandardError
|
||
|
|
{}
|
||
|
|
end
|
||
|
|
|
||
|
|
def write(data)
|
||
|
|
dir = File.dirname(@path)
|
||
|
|
FileUtils.mkdir_p(dir)
|
||
|
|
temp = File.join(dir, ".projects.json.tmp.#{Process.pid}")
|
||
|
|
File.write(temp, JSON.pretty_generate(data) + "\n")
|
||
|
|
File.rename(temp, @path)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|