//! `koh status` — where things stand: the save you are on, and what the working tree has that it //! does not. module cli import json2 import os import src.events import src.fmt import src.history import src.snapshot import src.storage pub fn cmd_status(raw []string) int { args := parse('status', raw, Options{ flags: ['json'] }) or { return usage_error(err.msg()) } store := open_store() or { return refuse(args.json(), err.msg()) } defer { store.close() } head := history.head(store) or { return refuse(args.json(), 'this repo has no saves yet — run "koh save \\"first save\\""') } unfinished := snapshot.restore_pending(store) changes := compare_with_head(store, head) or { // Whatever stopped the restore may also stop this walk. reason := if unfinished { '${err.msg()} — ${snapshot.restore_unfinished}' } else { err.msg() } return refuse(args.json(), reason) } if args.json() { line := history.line(store) or { return refuse(true, err.msg()) } flags := events.latest_flags(store) or { return refuse(true, err.msg()) } head_flag := flags[head.id] or { events.Flag{} } emit_json(json2.encode(StatusJson{ head_id: head.id head_msg: head.message dirty: changes.dirty modified: changes.modified added: changes.added deleted: changes.removed ignore_changed: changes.ignore_changed restore_unfinished: unfinished saves: line.len size_bytes: os.file_size(os.join_path(store.root, storage.file_name)) head_flagged: head_flag.flagged unsafe_paths: changes.unsafe_paths })) return 0 } saves := history.save_count(store) or { return refuse(false, err.msg()) } print_status(head, changes, unfinished, int(saves)) return 0 } // print_status is the plain-text report: the save you are on, then each changed path with a // mark, then the counts. fn print_status(head history.Save, changes Changes, unfinished bool, saves int) { note('at [${head.id}] ${fmt.fit(head.message.all_before('\n'), 50)} · ${ago(head.created_at)} · ${fmt.plural(saves, 'save')}') if unfinished { warn(snapshot.restore_unfinished) } for path in changes.unsafe_paths { warn('not saved: unsafe name ${path}') } if !changes.dirty { ok('no changes') if changes.ignore_changed { note('${snapshot.ignore_file} changed since [${head.id}]') } return } for path in changes.added { println(' ${fmt.green('+')} ${path}') } for path in changes.modified { println(' ${fmt.amber('~')} ${path}') } for path in changes.removed { println(' ${fmt.red('-')} ${path}') } note('${changes.added.len} added · ${changes.modified.len} modified · ${changes.removed.len} removed') if changes.ignore_changed { warn('${snapshot.ignore_file} changed since [${head.id}] — the next save records the new rules') } } pub struct Changes { pub: dirty bool added []string modified []string removed []string ignore_changed bool unsafe_paths []string } struct StatusJson { head_id string head_msg string dirty bool modified []string added []string deleted []string ignore_changed bool restore_unfinished bool saves int size_bytes u64 head_flagged bool unsafe_paths []string } pub fn compare_with_head(store &storage.Store, head history.Save) !Changes { before := snapshot.index_files(history.files_of(store, head.id)!) mut hashing := reading() walked := snapshot.walk_report(store, store.root, snapshot.load(store.root), mut hashing)! after := snapshot.index_files(walked.files) mut added := []string{} mut modified := []string{} mut removed := []string{} for path in after.keys() { if path !in before { added << path } else if before[path].hash != after[path].hash || before[path].executable != after[path].executable { modified << path } } for path in before.keys() { if path !in after { removed << path } } added.sort() modified.sort() removed.sort() ignore_changed := snapshot.ignore_file in added || snapshot.ignore_file in modified return Changes{ dirty: added.len + modified.len + removed.len > 0 added: added modified: modified removed: removed ignore_changed: ignore_changed unsafe_paths: walked.unsafe_paths } }