Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:

env:
CARGO_TERM_COLOR: always

jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check

clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets -- -D warnings

test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-targets
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
clap_mangen = "0.2"
redb = "4.1.0"
ureq = { version = "3.3.0", features = ["json"] }
base64 = "0.22"
Expand All @@ -22,6 +24,7 @@ flate2 = "1"
sha1 = "0.10"
snow = "0.9"
rand_core = { version = "0.6", features = ["getrandom"] }
rustix = { version = "1", features = ["fs", "std"] }

[dev-dependencies]
tempfile = "3"
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ BINDIR = $(PREFIX)/bin
BINARY = ivaldi
TARGET = target/release/$(BINARY)

.PHONY: build test install uninstall clean help
.PHONY: build test install install-extras uninstall clean help

## Build release binary
build:
Expand All @@ -21,6 +21,20 @@ install: build
@echo "Installed $(BINARY) to $(BINDIR)/$(BINARY)"
@echo "Run 'ivaldi --version' to verify."

## Install man pages and shell completions (bash/zsh/fish)
install-extras: build
@echo "Installing man pages to $(PREFIX)/share/man/man1..."
@install -d $(PREFIX)/share/man/man1
@$(TARGET) man --out $(PREFIX)/share/man/man1
@echo "Installing shell completions..."
@install -d $(PREFIX)/share/bash-completion/completions
@$(TARGET) completions bash > $(PREFIX)/share/bash-completion/completions/ivaldi
@install -d $(PREFIX)/share/zsh/site-functions
@$(TARGET) completions zsh > $(PREFIX)/share/zsh/site-functions/_ivaldi
@install -d $(PREFIX)/share/fish/vendor_completions.d
@$(TARGET) completions fish > $(PREFIX)/share/fish/vendor_completions.d/ivaldi.fish
@echo "Installed man pages and completions under $(PREFIX)/share"

## Uninstall ivaldi from $(PREFIX)/bin
uninstall:
@echo "Removing $(BINDIR)/$(BINARY)..."
Expand All @@ -38,6 +52,7 @@ help:
@echo " make build Build release binary"
@echo " make test Run all tests"
@echo " make install Install to $(BINDIR) (may need sudo)"
@echo " make install-extras Install man pages and bash/zsh/fish completions (may need sudo)"
@echo " make uninstall Remove from $(BINDIR) (may need sudo)"
@echo " make clean Clean build artifacts"
@echo ""
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ ivaldi config --set user.email "you@example.com"

# Daily workflow
ivaldi gather . # Stage all files
ivaldi gather -p src/main.rs # Stage only some hunks (interactive)
ivaldi seal "Add new feature" # Commit
ivaldi reseal "Better message" # Redo the last seal (message and/or staged changes)
ivaldi status # Check workspace state
ivaldi log --oneline # View history

# Going back (history is never rewritten — old seals stay recoverable)
ivaldi undo swift-eagle # New seal that removes swift-eagle's changes
ivaldi pluck gentle-otter # New seal that applies gentle-otter's changes
ivaldi rewind calm-river # Move the head back; your files stay as-is
ivaldi rewind calm-river --discard # Move the head back AND rewrite the files

# Timelines (branches)
ivaldi timeline create feature # Create timeline
ivaldi timeline switch feature # Switch (auto-shelves changes)
Expand Down Expand Up @@ -74,7 +82,11 @@ ivaldi peer known list # Servers we trust (TOFU known_peers)
| `log` | | View commit history |
| `whodidit <file>` | `blame` | Show which seal last touched each line of a file |
| `diff` | | Compare changes |
| `reset` | | Unstage files or hard reset |
| `reseal` | | Redo the most recent seal (new message and/or staged changes) |
| `reset` | | Unstage files or discard local changes |
| `rewind <seal>` | | Move the timeline head back (`--discard` to also rewrite files) |
| `undo <seal>` | | New seal that removes an earlier seal's changes |
| `pluck <seal>` | `cherry-pick` | New seal that applies another seal's changes |
| `timeline` | `tl` | Manage timelines (create/switch/list/rename/remove) |
| `butterfly` | `tl bf` | Experimental sandbox timelines |
| `fuse` | | Merge timelines (auto strategy uses MMR-based merge base) |
Expand All @@ -91,6 +103,11 @@ ivaldi peer known list # Servers we trust (TOFU known_peers)
| `sync` | | Pull remote changes (delta only) |
| `serve` | | Run an `ivaldi://` peer server for trusted users |
| `peer` | | Manage trusted peers + known servers (`trust` / `list` / `forget` / `whoami` / `known`) |
| `completions <shell>` | | Print a shell completion script (bash/zsh/fish/powershell/elvish) |

`status`, `timeline list`, and `portal list` accept `--json`, and
`log --format json` emits machine-readable history — handy for scripts and CI.
`make install-extras` installs man pages and shell completions.

## Ivaldi vs Git

Expand Down
40 changes: 40 additions & 0 deletions docs/atomic_io.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Atomic I/O Module (`atomic_io.rs`)

Atomic file replacement for repository metadata files.

## Overview

A plain `fs::write` can leave a truncated file behind if the process
crashes mid-write. Every metadata file under `.ivaldi/` goes through
`atomic_write` instead: bytes are written to a unique temp file in the
same directory, fsynced, then renamed over the destination, and the
parent directory is fsynced best-effort. Readers observe either the old
contents or the new contents — never a partial file.

```rust
pub fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()>
```

## Call sites

| File | Writer |
|------|--------|
| `.ivaldi/stage/files` | `StagingArea::save` (workspace.rs) |
| `.ivaldi/HEAD` | `forge::write_head` |
| `.ivaldi/shelves/<timeline>.shelf` | `ShelfManager::save_shelf` |
| `.ivaldi/MERGE_STATE` | `Repo::save_merge_state` |
| `.ivaldi/reviews/<id>.json` | `Repo::save_review` |
| `.ivaldi/dotfile-allowlist` | `DotfileAllowlist::save` |
| `.ivaldi/config` | `Config::save` |
| `.ivaldi/SWITCH_IN_PROGRESS` | `switch_journal::write` |

Working-tree writes (materialize/apply_changes) intentionally do NOT use
this — those are user files where plain writes are correct.

## Notes

- Temp names are `{name}.tmp.{pid}.{counter}`, mirroring `FileCas::put`,
so concurrent processes can't collide; failures clean up the temp file.
- The rename requires the parent directory to already exist.
- macOS note: `sync_all` issues `F_FULLFSYNC`; these are sub-KB files
written once per command, so the cost is negligible.
8 changes: 8 additions & 0 deletions docs/cas.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ let cas = FileCas::new(".ivaldi/objects")?;

**Idempotent:** Writing the same content twice is a no-op (skips if file exists).

**Durability — `flush()`:** `put` skips fsync on the hot path for speed.
`FileCas` tracks which shard directories were written since the last
flush, and `flush()` fsyncs exactly those (a no-op when nothing was
written). Callers flush at the points where the CAS holds the only copy
of data before a commit record references it: after building a seal tree
(`seal`/`reseal`), after the shelf capture during a timeline switch, and
at the end of bulk imports (`git_remote`, `p2p`) and `gather --patch`.

## Error Types

```rust
Expand Down
50 changes: 45 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ Command-line interface for Ivaldi VCS, built with `clap`.
| Command | Alias | Description |
|---------|-------|-------------|
| `forge` | | Initialize repository |
| `gather [files]` | | Stage files for next seal |
| `gather [files] [-p]` | | Stage files for next seal (`-p`/`--patch` picks hunks interactively) |
| `seal "msg"` | | Create sealed commit |
| `status` | | Show repository status |
| `reseal [msg]` | | Redo the most recent seal — folds in staged changes and/or a new message |
| `status [--json]` | | Show repository status |
| `whereami` | `wai` | Show current position |
| `log` | | View commit history |
| `log [--format short\|medium\|full\|json]` | | View commit history |
| `whodidit <file> [--summary]` | `blame` | Line-by-line seal attribution |
| `diff` | | Compare changes |
| `reset [files]` | | Unstage files |
| `reset [files]` / `reset --hard` | | Unstage files / discard local changes |
| `rewind <seal> [--discard]` | | Move the timeline head back to an earlier seal (`--discard` also rewrites files) |
| `undo <seal>` | | New seal that removes an earlier seal's changes |
| `pluck <seal>` | `cherry-pick` | New seal that applies another seal's changes |
| `timeline create/switch/list/rename/remove` | `tl` | Manage timelines |
| `timeline butterfly create/up/down/rm` | `tl bf` | Butterfly timelines |
| `fuse <src> to <tgt>` | | Merge timelines (auto strategy uses MMR-based merge base) |
Expand All @@ -31,6 +36,11 @@ Command-line interface for Ivaldi VCS, built with `clap`.
| `serve [--bind addr:port]` | | Run an `ivaldi://` peer server |
| `peer trust/list/forget/whoami/known` | | Manage peer pubkey allowlists + TOFU known servers |
| `review create/list/show/diff/comment/approve/request-changes/merge/close/reopen` | `rv` | Local code review system |
| `completions <shell>` | | Print a bash/zsh/fish/powershell/elvish completion script |
| `man [--out dir]` | | Generate man pages (used by `make install-extras`) |

`timeline list`, `portal list`, and `status` accept `--json` for scripting;
`log --format json` does the same for history.

## Global Flags

Expand All @@ -55,6 +65,24 @@ ivaldi gather .
ivaldi seal "Add feature"
ivaldi status

# Fixing up the most recent seal
ivaldi gather forgotten.txt
ivaldi reseal # fold staged changes in, keep the message
ivaldi reseal "better message" # and/or replace the message

# Stage only some hunks of a file
ivaldi gather -p src/main.rs # y/n per hunk; a=rest, d=skip rest, q=quit

# Going back
ivaldi undo swift-eagle # new seal that removes swift-eagle's changes
ivaldi pluck gentle-otter # new seal that applies gentle-otter's changes
ivaldi rewind calm-river # head moves back; your files stay as-is
ivaldi rewind calm-river --discard # head moves back AND files are rewritten

# Scripting
ivaldi status --json | jq '.files[].path'
ivaldi log --format json | jq '.[0].seal_name'

# Timelines
ivaldi tl create feature
ivaldi tl sw feature
Expand Down Expand Up @@ -120,8 +148,20 @@ ivaldi exclude "*.log" "build/" "node_modules/"
| `--global` | Target `~/.ivaldi/config` instead of repo-local |
| (no flag) | Launch the interactive ratatui form |

`configure` is an alias for `config`. `ivaldi config --help` lists every
known key with a description and example; `--set` validates values per
key (email shape, true/false toggles, repo specs) and rejects dotless
keys. See [config.md](config.md) for the full key reference.

The interactive form's first field is the **scope** — toggle between
repo-local and global with ←/→ or Enter; the form reloads from (and saves
to) whichever config file is selected. Below that it covers `user.name`,
`user.email`, `color.ui`, `core.autoshelf`, and (local scope only)
`portal.default`. Email and repo-spec values are validated as you type.

`ivaldi config` **no longer requires being inside an Ivaldi repo** — outside
a repo it automatically operates on the global config.
a repo it automatically operates on the global config (the scope selector
is locked to global).

## Architecture

Expand Down
Loading
Loading