123456module cli78import json9import time10import src.history1112pub fn cmd_log(raw []string) int {13 args := parse('log', raw, Options{14 values: ['since', 'session']15 flags: ['json']16 }) or { return usage_error(err.msg()) }17 store := open_store() or { return refuse(args.json(), err.msg()) }18 defer {19 store.close()20 }21 mut cutoff := i64(0)22 if expr := args.value_or_none('since') {23 cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }24 }25 session := args.value('session')26 head_id := history.head_id(store)27 mut shown := []history.Save{}28 for save in history.line(store) or { return refuse(args.json(), err.msg()) } {29 if cutoff > 0 && save.created_at < cutoff {30 continue31 }32 if session != '' && save.session != session {33 continue34 }35 shown << save36 }37 if args.json() {38 emit_json(json.encode(LogJson{39 head_id: head_id40 saves: shown.map(to_json(it))41 }))42 return 043 }44 if shown.len == 0 {45 ok('no saves in that range')46 return 047 }48 for save in shown {49 println(log_line(save, save.id == head_id))50 }51 return 052}5354struct SaveJson {55 id string56 parent ?string57 message string58 timestamp i6459 machine string60 agent ?string61 session ?string62 is_checkpoint bool63 restores ?string64 replayed_from ?string65}6667struct LogJson {68 head_id string69 saves []SaveJson70}7172fn to_json(save history.Save) SaveJson {73 return SaveJson{74 id: save.id75 parent: optional(save.parent)76 message: save.message77 timestamp: save.created_at78 machine: save.machine79 agent: optional(save.agent)80 session: optional(save.session)81 is_checkpoint: save.is_checkpoint82 restores: optional(save.restores)83 replayed_from: optional(save.replayed_from)84 }85}8687fn optional(value string) ?string {88 if value == '' {89 return none90 }91 return value92}9394fn log_line(save history.Save, is_head bool) string {95 marker := if is_head { amber('▸') } else { ' ' }96 mut line := ' ${marker} ${dim('[' + save.id + ']')} ${stamp(save.created_at)} ${save.message}'97 mut tags := []string{}98 if save.agent != '' || save.session != '' {99 mut inner := []string{}100 if save.agent != '' {101 inner << save.agent102 }103 if save.session != '' {104 inner << save.session105 }106 tags << '[${inner.join(' · ')}]'107 }108 if save.is_checkpoint {109 tags << '◐'110 }111 if save.restores != '' {112 tags << '↩ ${save.restores}'113 }114 if save.replayed_from != '' {115 tags << 'replayed'116 }117 if tags.len > 0 {118 line += ' ${dim(tags.join(' '))}'119 }120 return line121}122123124125pub fn parse_since(expr string) !i64 {126 now := time.now().unix()127 text := expr.trim_space().to_lower()128 match text {129 'today' { return start_of_day(now) }130 'yesterday' { return start_of_day(now) - 86400 }131 'last week' { return now - 7 * 86400 }132 'last month' { return now - 30 * 86400 }133 'last year' { return now - 365 * 86400 }134 else {}135 }136137 if date := parse_date(text) {138 return date139 }140 parts := text.split(' ')141 if parts.len >= 2 {142 amount := parts[0].int()143 if amount > 0 {144 if unit := unit_seconds(parts[1]) {145 return now - i64(amount) * unit146 }147 }148 return error('cannot understand "--since ${expr}"')149 }150 days := text.int()151 if days > 0 {152 return now - i64(days) * 86400153 }154 return error('cannot understand "--since ${expr}"')155}156157fn unit_seconds(unit string) ?i64 {158 return match unit {159 'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }160 'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }161 'd', 'day', 'days' { i64(86400) }162 'w', 'week', 'weeks' { i64(7 * 86400) }163 'month', 'months' { i64(30 * 86400) }164 'y', 'year', 'years' { i64(365 * 86400) }165 else { none }166 }167}168169fn parse_date(text string) ?i64 {170 parts := text.split('-')171 if parts.len != 3 || parts[0].len != 4 {172 return none173 }174 if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {175 return none176 }177 return midnight(parts[0].int(), parts[1].int(), parts[2].int())178}179180fn start_of_day(unix_seconds i64) i64 {181 moment := time.unix(unix_seconds).local()182 return midnight(moment.year, moment.month, moment.day)183}184185186fn midnight(year int, month int, day int) i64 {187 return time.Time{188 year: year189 month: month190 day: day191 is_local: true192 }.unix()193}