Skip to content
iyulabPublic

About

Lightweight single-file version control tool — snapshot, diff, and restore any file without Git, powered by an embedded go-git library and a single binary.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

Repository files navigation

Oops - Simple File Versioning for Everyone 🎯

Oops! Made a mistake? No worries - you can always go back!

A single binary with zero runtime dependencies. No Git installation required.

Installation

go install github.com/iyulab/oops@latest

Or download from GitHub Releases.

Quick Start

oops save essay.txt "first draft"      # 📸 Save a version (starts versioning the file)
# ... write something ...
oops save essay.txt "added conclusion" # 📸 Save another
oops history essay.txt                 # 📜 View all versions
oops changes essay.txt                 # 🔍 What changed since the last save?
oops back essay.txt 1                  # ⏪ Go back to version #1
oops oops! essay.txt                   # ↩️  Made a mistake? Back to the last save

Going back never loses work: if the file has changes that were not saved, oops keeps them as an automatic version first, so you can return to them too.

Commands

Command Also Description
oops save <file> [message] start, track, commit, snap 📸 Save a version (the first save starts versioning)
oops history <file> log, list 📜 List versions
oops changes <file> [from] [to] diff 🔍 Compare the file with the latest version, with #from, or #from with #to
oops back <file> <version> checkout, restore ⏪ Go back to a version
oops oops! <file> undo ↩️ Go back to the latest version
oops cat <file> <version> 📄 Print a version (--out <path> writes it to a file)
oops now <file> status ℹ️ Does the file match a saved version?
oops files ls 📁 List versioned files
oops mv <from> <to> 🚚 Move a file and keep its versions
oops done <file> untrack 🗑️ Stop versioning a file and delete its versions
oops tag <file> [version] --meta k=v mark 🏷️ Add metadata to a version (the latest by default) and keep it from being pruned
oops find --where k=v 🔎 List the versions in the store that carry it
oops rewind --where k=v ⏮️ Put every file back to the version that carries it
oops prune 🧹 Remove old automatic versions
oops gc 🧹 Remove the versions of files that no longer exist
oops config ⚙️ Default storage mode
oops update 🔄 Update oops

Saved and Automatic Versions

A version is either saved (manual, the default) or automatic (auto):

oops save notes.md "before the big edit"   # saved - kept until you delete it
oops save notes.md --auto                  # automatic - may be removed by prune

Versions made by back to keep unsaved changes are saved versions. prune only ever removes automatic versions, and never a file's newest version by age.

Putting Several Files Back Together

Versions carry metadata, so a group of files can share a tag and go back as one:

oops save a.txt --auto && oops save b.txt --auto     # a version of each (unchanged files keep their latest)
oops tag a.txt --meta checkpoint=before-rewrite      # tag the latest version of each file…
oops tag b.txt --meta checkpoint=before-rewrite
# … edit and save a.txt and b.txt as you like …
oops find   --where checkpoint=before-rewrite        # which versions carry it
oops rewind --where checkpoint=before-rewrite --dry-run   # what would go back
oops rewind --where checkpoint=before-rewrite             # put all of them back
  • tag works on a version that already exists, so a file that did not change since you saved it can be part of the group. A tagged version is a saved one: prune never removes it.
  • rewind locks every affected history for the whole call, keeps each file's unsaved changes as a saved version first (as back does), and leaves files that carry no such version alone.
  • A file with more than one matching version is an error (ambiguous, exit 8) unless you say --pick earliest (the state from before a series of changes began) or --pick latest. Nothing is written in that case.
  • If a restore fails, rewind stops there, exits 9 and reports what came back, the file it stopped at and what it did not reach; every file that came back has its own safety version.

Keeping the Store Small

oops prune --max-age 30d                  # automatic versions older than 30 days
oops prune --max-size 500MB               # keep the store under 500 MB
oops prune --max-age 30d --dry-run        # see what would go
oops gc                                   # versions of files that were deleted

With --max-size, oops removes the oldest automatic version of the file that has the most versions first, and keeps each file's newest version until nothing else is left to remove. prune without options removes nothing.

Where Versions Live

Mode Location
Local (default) .oops/ beside the file - added to an existing .gitignore
Global (-g) ~/.oops/
Any directory --store <dir> or the OOPS_STORE environment variable

oops config --default-global makes global the default; -l overrides it.

A store holds any number of files:

<store>/
└── files/
    └── <key>/            ← one directory per file (key = hash of its path; relative to
                            the folder for a local store, so renaming the folder keeps its versions)
        ├── index.json    ← versions: number, time, kind, label, actor, metadata
        └── blobs/        ← content, one file per distinct version (gzip when useful)

For Scripts and Applications

Every command accepts --json and then prints exactly one JSON object on stdout, success or error.

oops --json save report.md "draft" --actor sync --meta batch=42
{ "saved": true, "path": "/work/report.md",
  "version": { "n": 3, "hash": "…", "size": 1204, "time": "2026-01-01T09:00:00Z",
               "kind": "manual", "actor": "sync", "label": "draft", "meta": { "batch": "42" } } }

Saving content equal to the latest version succeeds without a new version: {"saved": false, "reason": "unchanged", …}.

Command JSON
save {saved, reason?, path, version}
history {path, versions: [version…]} — filter with --kind manual|auto and --where key=value
now {path, exists, latest, current, changed} — current is 0 when no version matches
back, oops! {path, restored, savedBefore: version|null}
changes {path, from, to|null, binary, diff}
files {store, files: [{path, versions, latest}], unreadable: [{dir, error}]}
tag {path, version}
find {store, matches: [{path, version}], unreadable: [{dir, error}]}
rewind {dryRun, items: [{path, version, restored, savedBefore?}], failed: {path, code?, message}|null, notAttempted: [path…], unreadable: […]}
prune {dryRun, removed: [{path, n, reason}], freedBytes, totalBytes, overCapBytes, unreadable: [{dir, error}]} — removed lists only what was applied
gc {orphans, removed, dryRun} — a history whose index cannot be read is never an orphan
--version {version}
error {"error": {"code": "<code>", "message": "<text>"}}

Exit codes

Code Meaning error.code
0 success (including "unchanged")
1 other failure error
2 wrong arguments or flags usage
3 the file is not versioned not_tracked
4 no such version version_not_found
5 another oops process kept the lock lock_timeout
6 the file does not exist file_not_found
7 the target already has versions (mv) already_tracked
8 a file has several matching versions and --pick was not given (rewind) ambiguous
9 rewind stopped at a file (the report is printed; no error object)
  • --actor <text> and --meta key=value (repeatable) are stored on the version as given.
  • With --json, oops never asks a question: done and gc need --yes.
  • Several oops processes may work on one store at once; each waits up to --lock-timeout (10s).
  • An application that ships oops can set OOPS_NO_UPDATE=1 so oops update refuses to replace it.

Upgrading from 0.3

0.4 uses a new store format and does not read 0.3 stores (.oops/<name>.git). Restore anything you need with oops 0.3 first. Commands now take the file: oops save notes.md "message" instead of oops save "message", and one folder can hold any number of versioned files.

Use Cases

  • 📝 Writers - essays, articles, manuscripts
  • 📊 Researchers - notes, data files
  • ⚙️ Config files - when you need quick rollback
  • 🤖 Tools that change files for you - save an automatic version first, go back if the change was wrong

For software projects: use Git.

Why "Oops"?

Because everyone makes mistakes when editing files. With Oops, you can simply say "oops!" and go back to a safe state. No complex commands, no fear of losing work.

License

MIT

About

Lightweight single-file version control tool — snapshot, diff, and restore any file without Git, powered by an embedded go-git library and a single binary.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages