on master help sign in
Add date, files, and author data columns to tree
dc38b51iko
config.j+8−4
@@ -154,7 +154,7 @@ diff : Blob -> Blob -> Text -- unified diff from the first to the
154difft : Path -> Blob -> Blob -> Text -- difftastic's rendering; needs difft on PATH.
154difft : Path -> Blob -> Blob -> Text -- difftastic's rendering; needs difft on PATH.
155 -- Options via DFT_* environment variables.
155 -- Options via DFT_* environment variables.
156treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text
156treeWith : { detail : Int, margin : Bool, elide : Bool, icons : Bool, color : Text
157− , lanes : Int }
157+ , lanes : Int, author : Bool, date : Bool, files : Bool }
158 -> Repo -> Text -- the history as a tree; see the spec
158 -> Repo -> Text -- the history as a tree; see the spec
159
159
160-- Every subvalue of a given shape, however deeply nested. A shape is a type
160-- Every subvalue of a given shape, however deeply nested. A shape is a type
@@ -573,10 +573,14 @@ log = \repo ->
573-- The default `tree` shows every commit, sorted by time (parents always above
573-- The default `tree` shows every commit, sorted by time (parents always above
574-- their children); `treeCompact` folds uninteresting runs and far subtrees.
574-- their children); `treeCompact` folds uninteresting runs and far subtrees.
575tree : Repo -> Text
575tree : Repo -> Text
576−tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4 })
576+tree = treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
577
577
578treeCompact : Repo -> Text
578treeCompact : Repo -> Text
579−treeCompact = treeWith ({ detail = 1, margin = false, elide = true, icons = false, color = "auto", lanes = 4 })
579+treeCompact = treeWith ({ detail = 1, margin = false, elide = true, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
580
580
581treeFull : Repo -> Text
581treeFull : Repo -> Text
582−treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4 })
582+treeFull = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = false, date = false, files = false })
583+
584+-- The full tree plus the extra data columns: date, files changed, author.
585+treeData : Repo -> Text
586+treeData = treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = "auto", lanes = 4, author = true, date = true, files = true })
specs/tree.md+10−2
@@ -18,12 +18,16 @@ columns are at fixed positions, independent of how deep a commit is.
18, elide : Bool -- fold uninteresting runs and collapse far subtrees
18, elide : Bool -- fold uninteresting runs and collapse far subtrees
19, icons : Bool -- pictographic glyph set
19, icons : Bool -- pictographic glyph set
20, color : Text -- "auto", "always", "never"
20, color : Text -- "auto", "always", "never"
21−, lanes : Int } -- lane columns, including the trunk lane; ≥ 1
21+, lanes : Int -- lane columns, including the trunk lane; ≥ 1
22+, author : Bool -- full author name column
23+, date : Bool -- absolute commit date column (YYYY-MM-DD)
24+, files : Bool } -- number of files changed column
22```
25```
23
26
24Missing fields crash. `lanes` is the *fixed* width of the rails area in
27Missing fields crash. `lanes` is the *fixed* width of the rails area in
25lanes; it does not adapt to the history (see §Overflow). The reference
28lanes; it does not adapt to the history (see §Overflow). The reference
26−`config.j` sets `lanes = 4`.
29+`config.j` sets `lanes = 4`. The `author`, `date`, and `files` columns are off
30+by default; `treeData` enables all three.
27
31
28#### Trunk
32#### Trunk
29
33
@@ -155,6 +159,10 @@ Columns, left to right, at fixed offsets:
155 a label; names separated by two spaces.
159 a label; names separated by two spaces.
1568. **Margin** (`margin = true`): age and author initials, right-aligned, as
1608. **Margin** (`margin = true`): age and author initials, right-aligned, as
157 before.
161 before.
162+9. **Data columns** (off unless enabled): `date` shows the commit's absolute
163+ date `YYYY-MM-DD`; `files` shows the number of files changed as `n files`;
164+ `author` shows the full author name. They appear in that order, grey,
165+ immediately before the margin in the same right-aligned block.
158
166
159Columns 4–8 start at the same offset on every row. When stdout is a terminal
167Columns 4–8 start at the same offset on every row. When stdout is a terminal
160and a row would exceed its width, the message is cut and ends with `…` so that
168and a row would exceed its width, the message is cut and ends with `…` so that
src/render.rs+82−5
@@ -623,6 +623,24 @@ pub fn render_age(time: i64) -> String {
623 }
623 }
624}
624}
625
625
626+/// Format unix-seconds `time` as an absolute date `YYYY-MM-DD` (UTC), using
627+/// the civil-from-days algorithm (no external date crate).
628+pub fn render_date(time: i64) -> String {
629+ let days = time.div_euclid(86_400);
630+ // Howard Hinnant's civil_from_days
631+ let z = days + 719_468;
632+ let era = z.div_euclid(146_097);
633+ let doe = z.rem_euclid(146_097);
634+ let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
635+ let y = yoe + era * 400;
636+ let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
637+ let mp = (5 * doy + 2) / 153;
638+ let d = doy - (153 * mp + 2) / 5 + 1;
639+ let m = if mp < 10 { mp + 3 } else { mp - 9 };
640+ let y = if m <= 2 { y + 1 } else { y };
641+ format!("{:04}-{:02}-{:02}", y, m, d)
642+}
643+
626/// the repo value in context (the loaded repo), for resolving ids to commits
644/// the repo value in context (the loaded repo), for resolving ids to commits
627fn top_of(interp: &Interp, _x: &Value) -> Result<Value, Crash> {
645fn top_of(interp: &Interp, _x: &Value) -> Result<Value, Crash> {
628 let old = interp.old_repo.borrow().clone();
646 let old = interp.old_repo.borrow().clone();
@@ -844,6 +862,12 @@ pub struct TreeOptions {
844 pub icons: bool,
862 pub icons: bool,
845 pub color: String,
863 pub color: String,
846 pub lanes: i64,
864 pub lanes: i64,
865+ /// show the full author name column
866+ pub author: bool,
867+ /// show the absolute commit date column (YYYY-MM-DD)
868+ pub date: bool,
869+ /// show the number of files changed column
870+ pub files: bool,
847}
871}
848
872
849fn default_tree_options(_interp: &Interp) -> Result<TreeOptions, Crash> {
873fn default_tree_options(_interp: &Interp) -> Result<TreeOptions, Crash> {
@@ -854,6 +878,9 @@ fn default_tree_options(_interp: &Interp) -> Result<TreeOptions, Crash> {
854 icons: false,
878 icons: false,
855 color: "auto".into(),
879 color: "auto".into(),
856 lanes: 4,
880 lanes: 4,
881+ author: false,
882+ date: false,
883+ files: false,
857 })
884 })
858}
885}
859
886
@@ -882,6 +909,18 @@ pub fn tree_with(interp: &mut Interp, opts: &Value, repo: &Value) -> Result<Valu
882 if lanes < 1 {
909 if lanes < 1 {
883 return Err(Crash::new("treeWith: lanes must be at least 1"));
910 return Err(Crash::new("treeWith: lanes must be at least 1"));
884 }
911 }
912+ let author = match opts.field("author")? {
913+ Value::Bool(b) => b,
914+ _ => return Err(Crash::new("treeWith: author must be a Bool")),
915+ };
916+ let date = match opts.field("date")? {
917+ Value::Bool(b) => b,
918+ _ => return Err(Crash::new("treeWith: date must be a Bool")),
919+ };
920+ let files = match opts.field("files")? {
921+ Value::Bool(b) => b,
922+ _ => return Err(Crash::new("treeWith: files must be a Bool")),
923+ };
885 let pal = Palette {
924 let pal = Palette {
886 on: color_enabled(&color),
925 on: color_enabled(&color),
887 };
926 };
@@ -892,6 +931,9 @@ pub fn tree_with(interp: &mut Interp, opts: &Value, repo: &Value) -> Result<Valu
892 icons,
931 icons,
893 color,
932 color,
894 lanes,
933 lanes,
934+ author,
935+ date,
936+ files,
895 };
937 };
896 let text = tree_render(interp, &o, repo, &pal, true)?;
938 let text = tree_render(interp, &o, repo, &pal, true)?;
897 Ok(Value::text(text))
939 Ok(Value::text(text))
@@ -909,6 +951,7 @@ struct CommitInfo {
909 is_child_of_ancestor: bool,
951 is_child_of_ancestor: bool,
910 meta: Option<crate::domain::MetaInfo>,
952 meta: Option<crate::domain::MetaInfo>,
911 size: Option<usize>, // lines added+removed against parent
953 size: Option<usize>, // lines added+removed against parent
954+ nfiles: usize, // number of files in this commit's snapshot
912 detail_marks: Vec<(String, char)>,
955 detail_marks: Vec<(String, char)>,
913 children: Vec<CommitInfo>,
956 children: Vec<CommitInfo>,
914}
957}
@@ -961,6 +1004,7 @@ fn build_info(
961 .collect::<Result<_, _>>()?;
1004 .collect::<Result<_, _>>()?;
962 let conflict = has_conflict(commit)?;
1005 let conflict = has_conflict(commit)?;
963 let files = commit.field("files")?;
1006 let files = commit.field("files")?;
1007+ let nfiles = files.as_list()?.len();
964 let (empty, size, detail_marks) = match parent_files {
1008 let (empty, size, detail_marks) = match parent_files {
965 Some(pf) => {
1009 Some(pf) => {
966 let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
1010 let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
@@ -1029,6 +1073,7 @@ fn build_info(
1029 is_child_of_ancestor: parent_is_ancestor,
1073 is_child_of_ancestor: parent_is_ancestor,
1030 meta,
1074 meta,
1031 size,
1075 size,
1076+ nfiles,
1032 detail_marks,
1077 detail_marks,
1033 children: infos,
1078 children: infos,
1034 })
1079 })
@@ -1361,6 +1406,7 @@ fn clone_info(info: &CommitInfo, children: Vec<CommitInfo>) -> CommitInfo {
1361 is_child_of_ancestor: info.is_child_of_ancestor,
1406 is_child_of_ancestor: info.is_child_of_ancestor,
1362 meta: info.meta.clone(),
1407 meta: info.meta.clone(),
1363 size: info.size,
1408 size: info.size,
1409+ nfiles: info.nfiles,
1364 detail_marks: info.detail_marks.clone(),
1410 detail_marks: info.detail_marks.clone(),
1365 children,
1411 children,
1366 }
1412 }
@@ -1503,6 +1549,9 @@ struct RowText {
1503 age: String,
1549 age: String,
1504 initials: String,
1550 initials: String,
1505 initials_author: String,
1551 initials_author: String,
1552+ // extra right-side columns, already joined in display order (§Step 4):
1553+ // date, files changed, full author name — only the enabled ones, plain
1554+ meta_extra: Vec<String>,
1506}
1555}
1507
1556
1508fn build_row_text(
1557fn build_row_text(
@@ -1597,16 +1646,35 @@ fn build_row_text(
1597 }
1646 }
1598 _ => (String::new(), String::new(), String::new()),
1647 _ => (String::new(), String::new(), String::new()),
1599 };
1648 };
1649+ // extra columns (§Step 4): date, files changed, full author name
1650+ let mut meta_extra: Vec<String> = Vec::new();
1651+ if let Some(info) = c {
1652+ if info.id != ROOT_ID {
1653+ if opts.date {
1654+ if let Some(m) = info.meta.as_ref() {
1655+ meta_extra.push(render_date(m.time));
1656+ }
1657+ }
1658+ if opts.files {
1659+ meta_extra.push(format!("{} files", info.nfiles));
1660+ }
1661+ if opts.author {
1662+ if let Some(m) = info.meta.as_ref() {
1663+ meta_extra.push(m.author.clone());
1664+ }
1665+ }
1666+ }
1667+ }
1600 RowText {
1668 RowText {
1601 gutter,
1669 gutter,
1602 id,
1670 id,
1603−
1604 bar,
1671 bar,
1605 msg,
1672 msg,
1606 labels,
1673 labels,
1607 age,
1674 age,
1608 initials: init,
1675 initials: init,
1609 initials_author: author,
1676 initials_author: author,
1677+ meta_extra,
1610 }
1678 }
1611}
1679}
1612
1680
@@ -1701,7 +1769,10 @@ fn draw_rows(
1701 line.push_str(&" ".repeat(want));
1769 line.push_str(&" ".repeat(want));
1702 line.push_str(&pal.green(&pad_right(&t.labels, width(&t.labels))));
1770 line.push_str(&pal.green(&pad_right(&t.labels, width(&t.labels))));
1703 }
1771 }
1772+ // metadata block: extra columns (date / files / author) then the margin
1773+ // (age + initials), right-aligned together past the message+labels edge
1704− if opts.margin && !t.age.is_empty() {
1774+ let has_margin = opts.margin && !t.age.is_empty();
1775+ if !t.meta_extra.is_empty() || has_margin {
1705 let cur = if label_col && !t.labels.is_empty() {
1776 let cur = if label_col && !t.labels.is_empty() {
1706 msg_end_max + 2 + width(&t.labels)
1777 msg_end_max + 2 + width(&t.labels)
1707 } else {
1778 } else {
@@ -1709,9 +1780,15 @@ fn draw_rows(
1709 };
1780 };
1710 let want = 2 + lab_end_max.saturating_sub(cur);
1781 let want = 2 + lab_end_max.saturating_sub(cur);
1711 line.push_str(&" ".repeat(want));
1782 line.push_str(&" ".repeat(want));
1783+ let mut meta: Vec<String> = Vec::new();
1784+ for e in &t.meta_extra {
1785+ meta.push(pal.grey(4, e));
1786+ }
1787+ if has_margin {
1712− line.push_str(&pal.grey(4, &t.age));
1788+ meta.push(pal.grey(4, &t.age));
1713− line.push_str(" ");
1714− line.push_str(&pal.author(&t.initials_author, &t.initials));
1789+ meta.push(pal.author(&t.initials_author, &t.initials));
1790+ }
1791+ line.push_str(&meta.join(" "));
1715 }
1792 }
1716 let _ = pl;
1793 let _ = pl;
1717 lines.push(line);
1794 lines.push(line);
tests/tree.rs+10−10
@@ -114,7 +114,7 @@ fn now() -> i64 {
114}
114}
115
115
116const OPTS: &str =
116const OPTS: &str =
117− "{ detail = 2, margin = true, elide = false, icons = false, color = \"never\", lanes = 4 }";
117+ "{ detail = 2, margin = true, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false }";
118
118
119#[test]
119#[test]
120fn tree_glyphs_basic() {
120fn tree_glyphs_basic() {
@@ -160,7 +160,7 @@ fn tree_focus_marker_and_glyph() {
160 let text = tree_text(
160 let text = tree_text(
161 &mut i,
161 &mut i,
162 &cfg,
162 &cfg,
163− "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4 })",
163+ "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
164 repo,
164 repo,
165 );
165 );
166 assert!(text.contains("▶"), "{}", text);
166 assert!(text.contains("▶"), "{}", text);
@@ -207,7 +207,7 @@ fn tree_conflict_and_empty_glyphs() {
207 let text = tree_text(
207 let text = tree_text(
208 &mut i,
208 &mut i,
209 &cfg,
209 &cfg,
210− "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4 })",
210+ "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
211 repo,
211 repo,
212 );
212 );
213 assert!(text.contains("⊗"), "{}", text);
213 assert!(text.contains("⊗"), "{}", text);
@@ -228,7 +228,7 @@ fn tree_icons_set() {
228 let text = tree_text(
228 let text = tree_text(
229 &mut i,
229 &mut i,
230 &cfg,
230 &cfg,
231− "treeWith ({ detail = 1, margin = false, elide = false, icons = true, color = \"never\", lanes = 4 })",
231+ "treeWith ({ detail = 1, margin = false, elide = false, icons = true, color = \"never\", lanes = 4, author = false, date = false, files = false })",
232 repo,
232 repo,
233 );
233 );
234 assert!(text.contains("🌱"), "{}", text);
234 assert!(text.contains("🌱"), "{}", text);
@@ -248,7 +248,7 @@ fn tree_labels_column_only_when_present() {
248 let text = tree_text(
248 let text = tree_text(
249 &mut i,
249 &mut i,
250 &cfg,
250 &cfg,
251− "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4 })",
251+ "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
252 repo,
252 repo,
253 );
253 );
254 assert!(text.contains("feat"), "{}", text);
254 assert!(text.contains("feat"), "{}", text);
@@ -292,7 +292,7 @@ fn tree_color_never_has_no_ansi() {
292 let text = tree_text(
292 let text = tree_text(
293 &mut i,
293 &mut i,
294 &cfg,
294 &cfg,
295− "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4 })",
295+ "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
296 repo,
296 repo,
297 );
297 );
298 assert!(!text.contains('\x1b'), "ANSI found in color=never output");
298 assert!(!text.contains('\x1b'), "ANSI found in color=never output");
@@ -311,7 +311,7 @@ fn tree_color_always_has_ansi() {
311 let text = tree_text(
311 let text = tree_text(
312 &mut i,
312 &mut i,
313 &cfg,
313 &cfg,
314− "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"always\", lanes = 4 })",
314+ "treeWith ({ detail = 1, margin = false, elide = false, icons = false, color = \"always\", lanes = 4, author = false, date = false, files = false })",
315 repo,
315 repo,
316 );
316 );
317 assert!(text.contains('\x1b'), "no ANSI in color=always output");
317 assert!(text.contains('\x1b'), "no ANSI in color=always output");
@@ -327,7 +327,7 @@ fn tree_minted_commits_have_blank_margin() {
327 let text = tree_text(
327 let text = tree_text(
328 &mut i,
328 &mut i,
329 &cfg,
329 &cfg,
330− "treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = \"never\", lanes = 4 })",
330+ "treeWith ({ detail = 2, margin = true, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
331 repo,
331 repo,
332 );
332 );
333 assert!(text.contains("minted"), "{}", text);
333 assert!(text.contains("minted"), "{}", text);
@@ -350,7 +350,7 @@ fn detail_line_changed_paths() {
350 let text = tree_text(
350 let text = tree_text(
351 &mut i,
351 &mut i,
352 &cfg,
352 &cfg,
353− "treeWith ({ detail = 2, margin = false, elide = false, icons = false, color = \"never\", lanes = 4 })",
353+ "treeWith ({ detail = 2, margin = false, elide = false, icons = false, color = \"never\", lanes = 4, author = false, date = false, files = false })",
354 repo,
354 repo,
355 );
355 );
356 assert!(text.contains("+ new"), "{}", text);
356 assert!(text.contains("+ new"), "{}", text);
@@ -616,7 +616,7 @@ fn tree_worked_example() {
616 let text = tree_text(
616 let text = tree_text(
617 &mut i,
617 &mut i,
618 &cfg,
618 &cfg,
619− "treeWith ({ detail = 2, margin = true, elide = true, icons = false, color = \"never\", lanes = 3 })",
619+ "treeWith ({ detail = 2, margin = true, elide = true, icons = false, color = \"never\", lanes = 3, author = false, date = false, files = false })",
620 repo,
620 repo,
621 );
621 );
622 let expected = "\
622 let expected = "\