Skip to content

Instantly share code, notes, and snippets.

@havenwood
Created April 22, 2026 16:04
Show Gist options
  • Select an option

  • Save havenwood/d99e0a7bf3b07cbd3e3cdc40c4420e63 to your computer and use it in GitHub Desktop.

Select an option

Save havenwood/d99e0a7bf3b07cbd3e3cdc40c4420e63 to your computer and use it in GitHub Desktop.
Single-file version control implemented in a single Ruby file

Single-file version control. Lighter than RCS.

cp rev.rb /usr/local/bin/rev

Requires Ruby, diff and patch. Colored output when git is available.

rev save mylib.rb initial version
# edit mylib.rb...
rev save -m "fix the thing"
rev log
rev diff 1
rev show 1
rev restore 1

File is required on first save, auto-detected after. Specify the file when tracking multiple files.

save [file] [message]

Snapshot. Use -m MSG or positional args.

log [file]

Show revision history.

diff [file] [rev]

Diff working copy against a revision. Defaults to latest.

show [file] rev

Print a revision to stdout.

restore [file] rev

Overwrite working file with a revision.

.rev/<filename>/
  base        # first version (immutable)
  snapshot    # latest version
  log         # one line per rev: timestamp message
  1.patch     # forward diff r1 -> r2
  2.patch     # forward diff r2 -> r3

First and latest versions are full copies. Intermediate revisions are reconstructed by applying patches forward from base.

MIT

#!/usr/bin/env ruby
# frozen_string_literal: true
require "optparse"
require "open3"
require "tempfile"
module Rev
DIR = ".rev"
VERSION = "0.1.0"
class << self
def run
opts = {}
parser = ARGV.options do
it.banner = "Usage: #{it.program_name} <command> [options] [file]\n\nCommands: save, log, diff, show, restore"
it.version = VERSION
it.on "-m", "--message=MSG", "Save message"
end.freeze
parser.permute! into: opts
case ARGV.shift
when "save"
file = detect_or_init ARGV
save file, opts.fetch(:message) { ARGV.join " " }
when "log" then log detect(ARGV)
when "diff" then diff detect(ARGV), Integer(ARGV.shift, exception: false)
when "show" then show detect(ARGV), Integer(ARGV.fetch(0) { abort "rev required" })
when "restore" then restore detect(ARGV), Integer(ARGV.fetch(0) { abort "rev required" })
else abort parser.to_s
end
rescue Errno::ENOENT => e
abort "#{$PROGRAM_NAME}: #{e.message.sub(/.* @ \w+ - /, "not found: ")}"
end
private
def file_dir(file) = "#{DIR}/#{file}"
def log_path(file) = "#{file_dir file}/log"
def base_path(file) = "#{file_dir file}/base"
def snapshot_path(file) = "#{file_dir file}/snapshot"
def patch_path(file, rev) = "#{file_dir file}/#{rev}.patch"
def tracked_files
return [] unless Dir.exist? DIR
Dir.children(DIR).select { File.directory? "#{DIR}/#{it}" }
end
def detect argv
file = argv.shift if argv.first && !argv.first.start_with?("-") && File.exist?(argv.first.to_s)
file ||= case tracked_files.size
when 0 then abort "no tracked files"
when 1 then tracked_files.first
else abort "multiple tracked files: #{tracked_files.join ", "}\nspecify one"
end
file
end
def detect_or_init argv
file = argv.shift if argv.first && !argv.first.start_with?("-") && File.exist?(argv.first.to_s)
return file if file
case tracked_files.size
when 0 then abort "#{$PROGRAM_NAME}: file required"
when 1 then tracked_files.first
else abort "multiple tracked files: #{tracked_files.join ", "}\nspecify one"
end
end
def latest file
File.readlines(log_path(file)).size
rescue Errno::ENOENT
0
end
def save file, msg
dir = file_dir file
Dir.mkdir DIR unless Dir.exist? DIR
Dir.mkdir dir unless Dir.exist? dir
rev = (latest file).succ
snap = snapshot_path file
if File.exist? snap
patch, = Open3.capture2 "diff", "-u", snap, file
File.write patch_path(file, rev - 1), patch unless patch.empty?
else
IO.copy_stream file, base_path(file)
end
IO.copy_stream file, snap
File.open(log_path(file), "a") { it.puts "#{Time.now.strftime "%FT%T"} #{msg}" }
warn "r#{rev}"
end
def reconstruct file, rev
cur = latest file
return File.read snapshot_path(file) if rev == cur
return File.read base_path(file) if rev == 1
abort "r#{rev} not found" unless rev >= 1 && rev < cur
Tempfile.create("rev") do |tmp|
IO.copy_stream base_path(file), tmp.path
1.upto(rev - 1) do |r|
p = patch_path file, r
abort "r#{rev} not found" unless File.exist? p
system "patch", "-s", tmp.path, p, [:out, :err] => File::NULL
end
File.read tmp.path
end
end
def log file
lp = log_path file
abort "no history" unless File.exist? lp
File.foreach(lp).with_index(1) do |l, rev|
time, msg = l.chomp.split " ", 2
puts "r#{rev} #{time} #{msg}".rstrip
end
end
def diff file, rev = nil
abort "no history" unless File.exist? log_path(file)
rev ||= latest file
if rev == latest(file)
run_diff snapshot_path(file), file
else
content = reconstruct file, rev
Tempfile.create("rev") do |tmp|
tmp.write content
tmp.flush
run_diff tmp.path, file
end
end
end
def run_diff a, b
exec "git", "diff", "--no-index", "--", a, b
rescue Errno::ENOENT
exec "diff", "-u", a, b
end
def show(file, rev) = print reconstruct(file, rev)
def restore file, rev
File.write file, reconstruct(file, rev)
warn "r#{rev}"
end
end
end
Rev.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment