//! `koh diff` — unified diff of the working tree against a save, of one save //! against another, or across a session. module cli import json import src.fmt import src.history import src.snapshot import src.storage pub fn cmd_diff(raw []string) int { args := parse('diff', raw, Options{ values: ['session'] flags: ['json'] }) or { return usage_error(err.msg()) } store := open_store() or { return refuse(args.json(), err.msg()) } defer { store.close() } sides := resolve_sides(store, args) or { return refuse(args.json(), err.msg()) } deltas := snapshot.diff_trees(store, sides.from, sides.to) or { return refuse(args.json(), err.msg()) } if args.json() { emit_json(json.encode(DiffJson{ from: sides.from_name to: sides.to_name files: deltas.map(delta_json(it)) })) return 0 } if deltas.len == 0 { ok('no changes') return 0 } for delta in deltas { print(delta.patch) if !delta.patch.ends_with('\n') { println('') } } mut added := 0 mut removed := 0 for delta in deltas { added += delta.added removed += delta.removed } note('${fmt.plural(deltas.len, 'file')} changed · ${fmt.green('+' + added.str())} ${fmt.red( '-' + removed.str())}') return 0 } struct Sides { pub: from snapshot.Tree to snapshot.Tree from_name string to_name string } struct DiffJson { from string to string files []DeltaJson } struct DeltaJson { path string change string added int removed int binary bool patch string } fn delta_json(delta snapshot.Delta) DeltaJson { return DeltaJson{ path: delta.path change: delta.kind added: delta.added removed: delta.removed binary: delta.binary patch: delta.patch } } fn resolve_sides(store &storage.Store, args Args) !Sides { if expr := args.value_or_none('session') { return session_sides(store, expr)! } match args.positional.len { 0 { head := history.head(store) or { return error('this repo has no saves yet') } return Sides{ from: tree_of(store, head.id)! to: working_tree(store)! from_name: head.id to_name: 'working' } } 1 { save := history.resolve(store, args.positional_at(0))! return Sides{ from: tree_of(store, save.id)! to: working_tree(store)! from_name: save.id to_name: 'working' } } 2 { first := history.resolve(store, args.positional_at(0))! second := history.resolve(store, args.positional_at(1))! return Sides{ from: tree_of(store, first.id)! to: tree_of(store, second.id)! from_name: first.id to_name: second.id } } else { return error('diff takes one save ID, or two to compare saves') } } } // session_sides spans a whole session: from the tree just before its first // save to its last save. fn session_sides(store &storage.Store, expr string) !Sides { name := if expr == 'last' { history.last_session(store)! } else { expr } saves := history.session_saves(store, name)! if saves.len == 0 { return error('no saves in session "${name}"') } first := saves[0] last := saves[saves.len - 1] mut from := snapshot.Tree{} mut from_name := 'empty' if first.parent != '' { from = tree_of(store, first.parent)! from_name = first.parent } return Sides{ from: from to: tree_of(store, last.id)! from_name: from_name to_name: last.id } }