Skip to content

feat(gitarchive): add Git archive support - #702

Draft
pcfreak30 wants to merge 1 commit into
developfrom
feat/git-archive-mvp
Draft

pcfreak30 wants to merge 1 commit into
developfrom
feat/git-archive-mvp

Conversation

@pcfreak30

@pcfreak30 pcfreak30 commented Sep 13, 2026 •

Copy link
Copy Markdown
Member

Adds a Git archive workflow to pinner that binds local repositories, publishes
pushed refs as encrypted Sia objects, and serves them back through a git remote
helper.

Adds pinner git subcommands for watch, status, ls, show, unwatch, and doctor,
with an optional post-push hook that publishes refs on every push.


This pull request adds Git archive hosting on Sia, enabling users to back up and share Git repositories through the pinner CLI. The implementation introduces a full Git remote-helper protocol (via a git-remote-pinner entry point) plus a new pinner git command tree for managing archives.

Key Additions

Git archive domain (internal/core/gitarchive)

  • Session: resolves the active vault profile, loads its app key, and opens a profile-local SQLite cache with dedicated tables (git_repos, git_objects, git_binds).
  • Object metadata format (internal/core/objmeta): a card schema for non-vault Sia objects (git.pack, git.tip, git.share) that deliberately fails the vault file parser to avoid vault sync interference.
  • Upload/Download/Scan: uploads packs, tips, and share documents; verifies payload digests; ingests git cards into local bookkeeping.
  • Lineage fingerprinting: computes root-commit sets to match a local repo to an existing archive locker.
  • Local helpers: creates private bare mirrors for browsing archives, manages the archive remote, and installs/removes a post-push hook.

Remote-helper protocol (internal/cli/gitremote)

  • A single binary pinner dispatches by argv‑0: git-remote-pinner runs the remote helper; pinner runs the CLI.
  • Protocol engine implements capabilities, list, fetch, and push using go‑git in-process pack encoding/ingestion.
  • Pluggable Store interface with three implementations: a go‑git bare repo (for testing), a session‑backed store with a real Sia backend behind a build tag, and a read‑only share store serving pre‑signed URLs without a profile.

pinner git CLI commands

  • watch – bind a repository to a locker and publish all local branches.
  • status – compare local branches against archived refs.
  • ls – list known lockers.
  • show – browse archived refs, commits, and file trees via go‑git.
  • share – mint pre‑signed bearer URLs for the archive tip and packs.
  • unwatch – unbind a repository, removing its remote and locker registration.
  • doctor – diagnose and repair bindings, remotes, and object state.

Testing

Extensive unit tests cover the protocol engine, go‑git store round‑trips, share URL flows, digest verification, generation-based concurrency control (CAS), and CLI command registration.

The feature is designed to operate incrementally: the real Sia backend is compiled in only with the gitarchive_sia build tag; without it, data operations return a clear "Sia backend not wired" sentinel.

@kody-ai

kody-ai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

​

@kody-ai

kody-ai Bot commented Sep 13, 2026

Copy link
Copy Markdown

kody code-review Kody Rules medium

The publishTip GORM update in session_store_sia.go runs without .WithContext(ctx), leaving it with no timeout and risking an unbounded query against the profile cache DB. Add .WithContext(ctx) to the update chain, or resolve a context-with-timeout following the pattern used by other gitarchive queries.

Kody rule violation: Disallow GORM queries without timeout

Comment on lines +71 to +75
store := gitremote.NewSessionStoreFromSession(session).ForLocker(bind.Locker)
remoteRefs, err := store.List(ctx, false)
if err != nil {
return fmt.Errorf("git status: list archive refs: %w", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

The git status command fails unconditionally when store.List returns ErrSiaNotWired, preventing the local/archive comparison in builds without the gitarchive_sia tag. Mirror the sibling handling in git_show.go:83 and git_doctor.go:117 by checking errors.Is(err, gitremote.ErrSiaNotWired) and treating the remote as empty instead of returning the error.

remoteRefs, err := store.List(ctx, false)
if err != nil && !errors.Is(err, gitremote.ErrSiaNotWired) {
	return fmt.Errorf("git status: list archive refs: %w", err)
}
Prompt for LLM

File internal/cli/git_status.go:

Line 71 to 75:

The `git status` command fails unconditionally when `store.List` returns ErrSiaNotWired, preventing the local/archive comparison in builds without the `gitarchive_sia` tag. Mirror the sibling handling in git_show.go:83 and git_doctor.go:117 by checking `errors.Is(err, gitremote.ErrSiaNotWired)` and treating the remote as empty instead of returning the error.

Suggested Code:

remoteRefs, err := store.List(ctx, false)
if err != nil && !errors.Is(err, gitremote.ErrSiaNotWired) {
	return fmt.Errorf("git status: list archive refs: %w", err)
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Comment thread internal/cli/git_watch.go
Comment on lines +88 to +102
chosen, err := resolveLocker(cc, session, abs, key)
if err != nil {
return err
}

// Record the locker (git_repos) and the bind (git_binds) before touching
// the archive remote so a later failure still leaves a recoverable state.

if _, err := gitarchive.EnsureRepo(session.DB, chosen, gitarchive.DefaultLockerName(abs), key); err != nil {
return err
}
bind, err := gitarchive.AddBind(session.DB, chosen, abs)
if err != nil {
return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

AddBind upserts by locker with a unique index, so watching a second checkout with the same root-commit lineage overwrites the first checkout's RepoPath, orphaning the original bind and leaving it reported as "not bound to a git archive." Key the bind row on repo_path with locker as a non-unique column, and adjust AddBind/FindBindByRepo/DeleteBind to use repo path lookups.

// key the bind row on repo_path so multiple checkouts can share a locker:
func AddBind(db *gorm.DB, locker, repoPath string) (GitBind, error) {
	bind := GitBind{Locker: locker, Remote: RemoteName, RepoPath: repoPath}
	err := db.Where("repo_path = ?", repoPath).First(&existing).Error
	if err == nil {
		existing.Locker = locker
		existing.Remote = RemoteName
		...
Prompt for LLM

File internal/cli/git_watch.go:

Line 88 to 102:

AddBind upserts by locker with a unique index, so watching a second checkout with the same root-commit lineage overwrites the first checkout's RepoPath, orphaning the original bind and leaving it reported as "not bound to a git archive." Key the bind row on repo_path with locker as a non-unique column, and adjust AddBind/FindBindByRepo/DeleteBind to use repo path lookups.

Suggested Code:

// key the bind row on repo_path so multiple checkouts can share a locker:
func AddBind(db *gorm.DB, locker, repoPath string) (GitBind, error) {
	bind := GitBind{Locker: locker, Remote: RemoteName, RepoPath: repoPath}
	err := db.Where("repo_path = ?", repoPath).First(&existing).Error
	if err == nil {
		existing.Locker = locker
		existing.Remote = RemoteName
		...

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Comment on lines +147 to +158
if len(data) > 0 {
uo, err := gitarchive.UploadPack(ctx, s.session, s.locker,
fmt.Sprintf("%s:%s:%d", dstRef, srcHash, gen), false, bytes.NewReader(data))
if err != nil {
return err
}
newPacks = append(newPacks, gitarchive.PackRef{Key: uo.Key.String(), Full: false, Digest: uo.Digest})
}

// Gen CAS: publish must not move a generation that changed after we read it.
if err := s.casPublish(ctx, oldKey); err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

SessionStore.Upload persists the pack to Sia and records a git_objects row before checking the CAS in casPublish, leaving orphaned objects and stale DB rows when the CAS rejects a concurrent publish. Move the casPublish check before the upload so the pack and row are only persisted after the CAS passes.

if err := s.casPublish(ctx, oldKey); err != nil {
	return err
}
if len(data) > 0 {
	uo, err := gitarchive.UploadPack(ctx, s.session, s.locker, ...)
	if err != nil { return err }
	newPacks = append(newPacks, gitarchive.PackRef{...})
}
Prompt for LLM

File internal/cli/gitremote/session_store_sia.go:

Line 147 to 158:

SessionStore.Upload persists the pack to Sia and records a git_objects row before checking the CAS in casPublish, leaving orphaned objects and stale DB rows when the CAS rejects a concurrent publish. Move the casPublish check before the upload so the pack and row are only persisted after the CAS passes.

Suggested Code:

if err := s.casPublish(ctx, oldKey); err != nil {
	return err
}
if len(data) > 0 {
	uo, err := gitarchive.UploadPack(ctx, s.session, s.locker, ...)
	if err != nil { return err }
	newPacks = append(newPacks, gitarchive.PackRef{...})
}

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

@github-actions

Copy link
Copy Markdown

Code Coverage Report

Total Coverage: 55.2%

Generated from commit: b1f087d
Repository: LumeWeb/pinner-cli

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant