@@ -623,6 +623,24 @@ pub fn render_age(time: i64) -> String {
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)
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
867+ /// show the absolute commit date column (YYYY-MM-DD)
869+ /// show the number of files changed column
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,
@@ -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"));
912+ let author = match opts.field("author")? {
913+ Value::Bool(b) => b,
914+ _ => return Err(Crash::new("treeWith: author must be a Bool")),
916+ let date = match opts.field("date")? {
917+ Value::Bool(b) => b,
918+ _ => return Err(Crash::new("treeWith: date must be a Bool")),
920+ let files = match opts.field("files")? {
921+ Value::Bool(b) => b,
922+ _ => return Err(Crash::new("treeWith: files must be a Bool")),
885 let pal = Palette {
924 let pal = Palette {
886 on: color_enabled(&color),
925 on: color_enabled(&color),
@@ -892,6 +931,9 @@ pub fn tree_with(interp: &mut Interp, opts: &Value, repo: &Value) -> Result<Valu
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>,
@@ -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,
1032 detail_marks,
1077 detail_marks,
1033 children: infos,
1078 children: infos,
@@ -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,
@@ -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>,
1508fn build_row_text(
1557fn build_row_text(
@@ -1597,16 +1646,35 @@ fn build_row_text(
1598 _ => (String::new(), String::new(), String::new()),
1647 _ => (String::new(), String::new(), String::new()),
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 {
1654+ if let Some(m) = info.meta.as_ref() {
1655+ meta_extra.push(render_date(m.time));
1659+ meta_extra.push(format!("{} files", info.nfiles));
1662+ if let Some(m) = info.meta.as_ref() {
1663+ meta_extra.push(m.author.clone());
1600 RowText {
1668 RowText {
1608 initials: init,
1675 initials: init,
1609 initials_author: author,
1676 initials_author: author,
@@ -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))));
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(
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));
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));
1791+ line.push_str(&meta.join(" "));
1716 let _ = pl;
1793 let _ = pl;
1717 lines.push(line);
1794 lines.push(line);