yagit

Yet another static site generator for Git 🙀️

Commit
f7f14df2dcca84e69ffbcb15747b658560942fc3
Parent
c750233bfdf43a55783ebe9c089be7f0f8866fce
Author
Pablo <pablo-pie@riseup.net>
Date

Redesigned the log messages

Also cleaned-up the log system

Diffstats

4 files changed, 161 insertions, 246 deletions

Status Name Changes Insertions Deletions
Modified .gitignore 2 files changed 0 4
Modified src/command.rs 2 files changed 9 12
Modified src/log.rs 2 files changed 67 153
Modified src/main.rs 2 files changed 85 77
diff --git a/.gitignore b/.gitignore
@@ -3,10 +3,6 @@
 /site
 Cargo.lock
 
-<<<<<<< HEAD
 /profiling
-=======
-flamegraph.svg
->>>>>>> 899488f (Minor changes in Cargo.toml to allow profiling)
 perf.data
 perf.data.old
diff --git a/src/command.rs b/src/command.rs
@@ -33,10 +33,7 @@ pub enum SubCmd {
 }
 
 impl Cmd {
-  pub fn parse() -> Result<(Self, String), ()> {
-    let mut args = env::args();
-    let program_name = args.next().unwrap();
-
+  pub fn parse(args: &mut env::Args, program_name: &str) -> Result<Self, ()> {
     let mut flags = Flags::EMPTY;
     let tag = loop {
       match args.next() {
@@ -53,17 +50,17 @@ impl Cmd {
 
         Some(arg) if arg.starts_with("--") => {
           errorln!("Unknown flag {arg:?}");
-          usage(&program_name, None);
+          usage(program_name, None);
           return Err(());
         }
         Some(arg) => {
           errorln!("Unknown subcommand {arg:?}");
-          usage(&program_name, None);
+          usage(program_name, None);
           return Err(());
         }
         None => {
           errorln!("No subcommand provided");
-          usage(&program_name, None);
+          usage(program_name, None);
           return Err(());
         }
       }
@@ -78,7 +75,7 @@ impl Cmd {
           name
         } else {
           errorln!("No repository name providade");
-          usage(&program_name, Some(tag));
+          usage(program_name, Some(tag));
           return Err(());
         };
 
@@ -89,7 +86,7 @@ impl Cmd {
           name
         } else {
           errorln!("No repository name providade");
-          usage(&program_name, Some(tag));
+          usage(program_name, Some(tag));
           return Err(());
         };
 
@@ -97,7 +94,7 @@ impl Cmd {
           dsc
         } else {
           errorln!("No description providade");
-          usage(&program_name, Some(tag));
+          usage(program_name, Some(tag));
           return Err(());
         };
 
@@ -107,10 +104,10 @@ impl Cmd {
 
     if args.next().is_some() {
       warnln!("Additional command line arguments provided. Ignoring trailing arguments...");
-      usage(&program_name, Some(tag));
+      usage(program_name, Some(tag));
     }
 
-    Ok((Self { sub_cmd, flags, }, program_name))
+    Ok(Self { sub_cmd, flags, })
   }
 }
 
diff --git a/src/log.rs b/src/log.rs
@@ -3,16 +3,19 @@
 //! This implementation is NOT thread safe, since yagit is only expected to run
 //! on my single-threaded server.
 #![allow(static_mut_refs)]
-use std::{io::{self, Write}, fmt::Arguments};
 
-const BOLD_WHITE:  &str = "\u{001b}[1;37m";
-const BOLD_BLUE:   &str = "\u{001b}[1;34m";
+use std::{io::{self, Write}, fmt::Arguments, time::Duration};
+
 const BOLD_RED:    &str = "\u{001b}[1;31m";
+const BOLD_GREEN:  &str = "\u{001b}[1;32m";
 const BOLD_YELLOW: &str = "\u{001b}[1;33m";
+const BOLD_BLUE:   &str = "\u{001b}[1;34m";
+const BOLD_CYAN:   &str = "\u{001b}[1;36m";
+const BOLD_WHITE:  &str = "\u{001b}[1;37m";
 const UNDERLINE:   &str = "\u{001b}[4m";
 const RESET:       &str = "\u{001b}[0m";
 
-static mut NEEDS_NEWLINE: bool = false;
+const PROGRAM_VERSION: &str = env!("CARGO_PKG_VERSION");
 static mut COUNTER: Counter = Counter {
   total: 0,
   count: 0,
@@ -33,120 +36,65 @@ struct Counter {
   current_repo_name: String,
 }
 
-pub(crate) fn log(level: Level, args: &Arguments<'_>, newline: bool) {
+pub(crate) fn log(level: Level, args: &Arguments<'_>) {
   match level {
-    Level::Error => unsafe {
-      let mut stderr = io::stderr();
-
-      if NEEDS_NEWLINE {
-        let _ = writeln!(stderr);
-      }
-
-      let _ = write!(stderr, "{BOLD_RED}ERROR:{RESET} ");
-      if newline {
-        let _ = writeln!(stderr, "{}", args);
-        // shouldn't print the job counter because we are about to die
-      } else {
-        let _ = write!(stderr, "{}", args);
-        let _ = stderr.flush();
-      }
-    }
-    Level::Info => unsafe {
-      let mut stdout = io::stdout();
-
-      if NEEDS_NEWLINE {
-        let _ = writeln!(stdout);
-      }
-
-      let _ = write!(stdout, "{BOLD_BLUE}INFO:{RESET} ");
-      if newline {
-        let _ = writeln!(stdout, "{}", args);
-        log_job_counter();
-      } else {
-        let _ = write!(stdout, "{}", args);
-        let _ = stdout.flush();
-      }
+    Level::Error => {
+      eprint!("{BOLD_RED}    Error{RESET} ");
+      eprintln!("{}", args);
+      // shouldn't print the job counter because we are about to die
     }
-    Level::Warn => unsafe {
-      let mut stdout = io::stdout();
-
-      if NEEDS_NEWLINE {
-        let _ = writeln!(stdout);
-      }
-
-      let _ = write!(stdout, "{BOLD_YELLOW}WARNING:{RESET} ");
-      if newline {
-        let _ = writeln!(stdout, "{}", args);
-        log_job_counter();
-      } else {
-        let _ = write!(stdout, "{}", args);
-      }
-      if !newline { let _ = stdout.flush(); }
+    Level::Info => {
+      print!("{BOLD_BLUE}     Info{RESET} ");
+      println!("{}", args);
+      log_current_job();
     }
-    Level::Usage => unsafe {
-      let mut stdout = io::stdout();
-
-      if NEEDS_NEWLINE {
-        let _ = writeln!(stdout);
-      }
-
-      let _ = write!(stdout, "{BOLD_YELLOW}USAGE:{RESET} ");
-      if newline {
-        let _ = writeln!(stdout, "{}", args);
-        let _ = writeln!(stdout, "       For more information check the {UNDERLINE}yagit{RESET} man page.");
-        log_job_counter();
-      } else {
-        let _ = write!(stdout, "{}", args);
-      }
-      if !newline { let _ = stdout.flush(); }
+    Level::Warn => {
+      print!("{BOLD_YELLOW}  Warning{RESET} ");
+      println!("{}", args);
+      log_current_job();
     }
-  }
-
-  if !newline {
-    unsafe {
-      NEEDS_NEWLINE = true;
+    Level::Usage => {
+      print!("{BOLD_YELLOW}    Usage{RESET} ");
+      println!("{}", args);
+      println!("          For more information check the {UNDERLINE}yagit(1){RESET} man page.");
+      log_current_job();
     }
   }
 }
 
-pub fn info_done(args: Option<&Arguments<'_>>) {
-  let mut stdout = io::stdout();
-  let _ = match args {
-    Some(args) => {
-      writeln!(stdout, " {}", args)
-    }
-    None => {
-      writeln!(stdout, " done!")
-    }
-  };
-  unsafe {
-    NEEDS_NEWLINE = false;
-  }
-}
-
-pub fn job_counter_start(total: usize) {
+pub fn set_job_count(total: usize) {
   unsafe {
     COUNTER.total = total;
+    COUNTER.count = 0;
   }
 }
 
-pub fn job_counter_increment(repo_name: &str) {
+/// Logs a message telling the user the system has started rendering a job
+pub fn render_start(repo_name: &str) {
   unsafe {
     COUNTER.count += 1;
     COUNTER.current_repo_name.clear();
     COUNTER.current_repo_name.push_str(repo_name);
 
-    log_job_counter();
+    log_current_job();
+  }
+}
 
-    // deinit the counter when we reach the total
-    if COUNTER.count == COUNTER.total {
-      COUNTER.total = 0;
-      COUNTER.count = 0;
-    }
+/// Logs a message telling the user the system has finished rendering a job
+pub fn render_done() {
+  unsafe {
+    debug_assert!(COUNTER.count > 0);
+
+    let space_padding = "... [/]".len() + 2 * crate::log_floor(COUNTER.total);
+    println!(
+      "{BOLD_GREEN} Rendered{RESET} {name}{empty:space_padding$}",
+      name  = COUNTER.current_repo_name,
+      empty = "",
+    );
   }
 }
 
-fn log_job_counter() {
+fn log_current_job() {
   unsafe {
     if COUNTER.count == 0 {
       return;
@@ -156,76 +104,23 @@ fn log_job_counter() {
 
     let _ = write!(
       stdout,
-      "{BOLD_BLUE}->{RESET} {BOLD_WHITE}{count:>padding$}/{total}{RESET} {name}...",
+      "{BOLD_CYAN}Rendering{RESET} {name}... {BOLD_WHITE}[{count:>padding$}/{total}]{RESET}\r",
       count = COUNTER.count,
       total = COUNTER.total,
       padding = crate::log_floor(COUNTER.total),
       name = COUNTER.current_repo_name,
     );
     let _ = stdout.flush();
-    NEEDS_NEWLINE = true;
   }
 }
 
 #[macro_export]
-macro_rules! info {
-  // info!("a {} event", "log");
-  ($($arg:tt)+) => ({
-    $crate::log::log(
-      $crate::log::Level::Info,
-      &std::format_args!($($arg)+),
-      false,
-    );
-  });
-}
-
-#[macro_export]
 macro_rules! infoln {
   // infoln!("a {} event", "log");
   ($($arg:tt)+) => ({
     $crate::log::log(
       $crate::log::Level::Info,
       &std::format_args!($($arg)+),
-      true,
-    );
-  });
-}
-
-#[macro_export]
-macro_rules! info_done {
-  // info_done!();
-  () => ({
-    $crate::log::info_done(None);
-  });
-
-  // info_done!("terminator");
-  ($($arg:tt)+) => ({
-    $crate::log::info_done(Some(&std::format_args!($($arg)+)));
-  });
-}
-
-#[macro_export]
-macro_rules! job_counter_start {
-  ($total:expr) => ({
-    $crate::log::job_counter_start($total as usize);
-  });
-}
-
-#[macro_export]
-macro_rules! job_counter_increment {
-  ($repo_name:expr) => ({
-    $crate::log::job_counter_increment(&$repo_name);
-  });
-}
-
-#[macro_export]
-macro_rules! error {
-  // error!("a {} event", "log");
-  ($($arg:tt)+) => ({
-    $crate::log::log(
-      $crate::log::Level::Error,
-      &std::format_args!($($arg)+),
-      false,
     );
   });
 }
@@ -237,7 +132,6 @@ macro_rules! errorln {
     $crate::log::log(
       $crate::log::Level::Error,
       &std::format_args!($($arg)+),
-      true,
     );
   });
 }
@@ -249,7 +143,6 @@ macro_rules! warnln {
     $crate::log::log(
       $crate::log::Level::Warn,
       &std::format_args!($($arg)+),
-      true,
     );
   });
 }
@@ -261,7 +154,28 @@ macro_rules! usageln {
     $crate::log::log(
       $crate::log::Level::Usage,
       &std::format_args!($($arg)+),
-      true,
     );
   });
 }
+
+pub fn finished(duration: Duration) {
+  let duration = duration.as_millis() / 100;
+  let secs  = duration / 10;
+  let dsecs = duration % 10;
+
+  println!("{BOLD_GREEN} Finished{RESET} Rendering took {secs}.{dsecs}s");
+}
+
+#[cfg(target_arch = "x86_64")]
+pub fn version(program_name: &str) {
+  if is_x86_feature_detected!("ssse3") {
+    infoln!("Running {BOLD_WHITE}{program_name} {PROGRAM_VERSION}{RESET} (SIMD optimizations enabled)");
+  } else {
+    infoln!("Running {BOLD_WHITE}{program_name} {PROGRAM_VERSION}{RESET}");
+  }
+}
+
+#[cfg(not(target_arch = "x86_64"))]
+pub fn version(program_name: &str) {
+  infoln!("Running {BOLD_WHITE}{program_name} {PROGRAM_VERSION}{RESET}");
+}
diff --git a/src/main.rs b/src/main.rs
@@ -3,10 +3,11 @@ use std::{
   fs::{self, File},
   path::{Path, PathBuf},
   mem,
+  env,
   fmt::{self, Display},
   ffi::OsStr,
   collections::HashMap,
-  time::{Duration, SystemTime},
+  time::{Duration, SystemTime, Instant},
   process::ExitCode,
   os::unix::fs::PermissionsExt,
   cmp,
@@ -221,10 +222,10 @@ struct Readme {
 }
 
 struct RepoRenderer<'repo> {
-  pub name:        String,
-  pub description: Option<String>,
+  pub name:        &'repo str,
+  pub description: Option<&'repo str>,
 
-  pub repo:   Repository,
+  pub repo:   &'repo Repository,
   pub head:   Tree<'repo>,
   pub branch: String,
 
@@ -239,7 +240,7 @@ struct RepoRenderer<'repo> {
 }
 
 impl<'repo> RepoRenderer<'repo> {
-  fn new(repo: RepoInfo, flags: Flags) -> Result<Self, ()> {
+  fn new(repo: &'repo RepoInfo, flags: Flags) -> Result<Self, ()> {
     let (head, branch) = {
       match repo.repo.head() {
         Ok(head) => unsafe {
@@ -326,10 +327,10 @@ impl<'repo> RepoRenderer<'repo> {
     };
 
     Ok(Self {
-      name: repo.name,
-      description: repo.description,
+      name: &repo.name,
+      description: repo.description.as_deref(),
 
-      repo: repo.repo,
+      repo: &repo.repo,
       head,
       branch,
 
@@ -361,28 +362,28 @@ impl<'repo> RepoRenderer<'repo> {
   ) -> io::Result<()> {
     render_header(f, title)?;
     writeln!(f, "<main>")?;
-    writeln!(f, "<h1>{title}</h1>", title = Escaped(&self.name))?;
-    if let Some(ref description) = self.description {
+    writeln!(f, "<h1>{title}</h1>", title = Escaped(self.name))?;
+    if let Some(description) = self.description {
       writeln!(f, "<p>\n{d}\n</p>", d = Escaped(description.trim()))?;
     }
     writeln!(f, "<nav>")?;
     writeln!(f, "<ul>")?;
     writeln!(f, "<li{class}><a href=\"/{root}{name}/index.html\">summary</a></li>",
                 root = self.output_root,
-                name = Escaped(&self.name),
+                name = Escaped(self.name),
                 class = if matches!(title, PageTitle::Summary { .. }) { " class=\"nav-selected\"" } else { "" })?;
     writeln!(f, "<li{class}><a href=\"/{root}{name}/{COMMIT_SUBDIR}/index.html\">log</a></li>",
                 root = self.output_root,
-                name = Escaped(&self.name),
+                name = Escaped(self.name),
                 class = if matches!(title, PageTitle::Log { .. } | PageTitle::Commit { .. }) { " class=\"nav-selected\"" } else { "" })?;
     writeln!(f, "<li{class}><a href=\"/{root}{name}/{TREE_SUBDIR}/index.html\">tree</a></li>",
                 root = self.output_root,
-                name = Escaped(&self.name),
+                name = Escaped(self.name),
                 class = if matches!(title, PageTitle::TreeEntry { .. }) { " class=\"nav-selected\"" } else { "" })?;
     if self.license.is_some() {
       writeln!(f, "<li{class}><a href=\"/{root}{name}/license.html\">license</a></li>",
                   root = self.output_root,
-                  name = Escaped(&self.name),
+                  name = Escaped(self.name),
                   class = if matches!(title, PageTitle::License { .. }) { " class=\"nav-selected\"" } else { "" })?;
     }
     writeln!(f, "</ul>")?;
@@ -426,7 +427,7 @@ impl<'repo> RepoRenderer<'repo> {
     blob_stack: &mut Vec<(Blob<'repo>, Mode, PathBuf)>,
   ) -> io::Result<()> {
     let mut blobs_path = self.output_path.clone();
-    blobs_path.push(&self.name);
+    blobs_path.push(self.name);
     blobs_path.push(BLOB_SUBDIR);
     blobs_path.extend(&parent);
 
@@ -435,7 +436,7 @@ impl<'repo> RepoRenderer<'repo> {
     }
 
     let mut index_path = self.output_path.clone();
-    index_path.push(&self.name);
+    index_path.push(self.name);
     index_path.push(TREE_SUBDIR);
     index_path.extend(&parent);
 
@@ -456,7 +457,7 @@ impl<'repo> RepoRenderer<'repo> {
 
     self.render_header(
       &mut f,
-      PageTitle::TreeEntry { repo_name: &self.name, path: &parent },
+      PageTitle::TreeEntry { repo_name: self.name, path: &parent },
     )?;
     writeln!(&mut f, "<div class=\"table-container\">")?;
     writeln!(&mut f, "<table>")?;
@@ -479,13 +480,13 @@ impl<'repo> RepoRenderer<'repo> {
       match entry.kind() {
         Some(ObjectType::Blob) => {
           let blob = entry
-            .to_object(&self.repo)
+            .to_object(self.repo)
             .unwrap()
             .peel_to_blob()
             .unwrap();
 
           let mut blob_path = self.output_path.clone();
-          blob_path.push(&self.name);
+          blob_path.push(self.name);
           blob_path.push(BLOB_SUBDIR);
           blob_path.extend(&path);
 
@@ -506,7 +507,7 @@ impl<'repo> RepoRenderer<'repo> {
             &mut f,
             "<tr><td><a href=\"/{root}{name}/{TREE_SUBDIR}/{path}.html\">{path}</a></td></tr>",
             root = self.output_root,
-            name = Escaped(&self.name),
+            name = Escaped(self.name),
             path = Escaped(&path.to_string_lossy()),
           )?;
 
@@ -520,7 +521,7 @@ impl<'repo> RepoRenderer<'repo> {
         }
         Some(ObjectType::Tree) => {
           let subtree = entry
-            .to_object(&self.repo)
+            .to_object(self.repo)
             .unwrap()
             .peel_to_tree()
             .unwrap();
@@ -529,7 +530,7 @@ impl<'repo> RepoRenderer<'repo> {
             &mut f,
             "<tr><td><a href=\"/{root}{name}/{TREE_SUBDIR}/{path}/index.html\" class=\"subtree\">{path}/</a></td></tr>",
             root = self.output_root,
-            name = Escaped(&self.name),
+            name = Escaped(self.name),
             path = Escaped(&path.to_string_lossy()),
           )?;
 
@@ -593,7 +594,7 @@ impl<'repo> RepoRenderer<'repo> {
     last_commit_time: &HashMap<Oid, SystemTime>,
   ) -> io::Result<()> {
     let mut page_path = self.output_path.clone();
-    page_path.push(&self.name);
+    page_path.push(self.name);
     page_path.push(TREE_SUBDIR);
     page_path.extend(&path);
     let page_path = format!("{}.html", page_path.to_string_lossy());
@@ -622,7 +623,7 @@ impl<'repo> RepoRenderer<'repo> {
 
     self.render_header(
       &mut f,
-      PageTitle::TreeEntry { repo_name: &self.name, path: &path },
+      PageTitle::TreeEntry { repo_name: self.name, path: &path },
     )?;
 
     writeln!(&mut f, "<div class=\"table-container\">")?;
@@ -644,7 +645,7 @@ impl<'repo> RepoRenderer<'repo> {
     writeln!(&mut f, "<tr>")?;
     writeln!(&mut f, "<td><a href=\"/{root}{name}/{BLOB_SUBDIR}/{path}\">{path}</a></td>",
                      root = self.output_root,
-                     name = Escaped(&self.name),
+                     name = Escaped(self.name),
                      path = Escaped(&path.to_string_lossy()))?;
     writeln!(&mut f, "<td align=\"right\">{}</td>", FileSize(blob.size()))?;
     writeln!(&mut f, "<td align=\"right\">{}</td>", mode)?;
@@ -704,7 +705,7 @@ impl<'repo> RepoRenderer<'repo> {
 
     // ========================================================================
     let mut index_path = self.output_path.clone();
-    index_path.push(&self.name);
+    index_path.push(self.name);
     index_path.push(COMMIT_SUBDIR);
 
     if !index_path.is_dir() {
@@ -721,7 +722,7 @@ impl<'repo> RepoRenderer<'repo> {
       }
     };
 
-    self.render_header(&mut f, PageTitle::Log { repo_name: &self.name })?;
+    self.render_header(&mut f, PageTitle::Log { repo_name: self.name })?;
     writeln!(&mut f, "<div class=\"article-list\">")?;
 
     for commit in &commits {
@@ -746,7 +747,7 @@ impl<'repo> RepoRenderer<'repo> {
         &mut f,
         "<span class=\"commit-heading\"><a href=\"/{root}{name}/{COMMIT_SUBDIR}/{id}.html\">{shorthand_id}</a> &mdash; {author}</span>",
         root = self.output_root,
-        name = Escaped(&self.name),
+        name = Escaped(self.name),
       )?;
       writeln!(&mut f, "<time datetime=\"{datetime}\">{date}</time>",
                        datetime  = DateTime(time), date = Date(time))?;
@@ -779,7 +780,7 @@ impl<'repo> RepoRenderer<'repo> {
     last_commit_time: &mut HashMap<Oid, SystemTime>,
   ) -> io::Result<()> {
     let mut path = self.output_path.clone();
-    path.push(&self.name);
+    path.push(self.name);
     path.push(COMMIT_SUBDIR);
     path.push(format!("{}.html", commit.id()));
     let should_skip = !self.full_build && path.exists();
@@ -905,7 +906,7 @@ impl<'repo> RepoRenderer<'repo> {
 
     self.render_header(
       &mut f,
-      PageTitle::Commit { repo_name: &self.name, summary }
+      PageTitle::Commit { repo_name: self.name, summary }
     )?;
 
     writeln!(&mut f, "<article class=\"commit\">")?;
@@ -914,7 +915,7 @@ impl<'repo> RepoRenderer<'repo> {
     writeln!(&mut f, "<dt>Commit</dt>")?;
     writeln!(&mut f, "<dd><a href=\"/{root}{name}/{COMMIT_SUBDIR}/{id}.html\">{id}<a/><dd>",
                      root = self.output_root,
-                     name = Escaped(&self.name), id = commit.id())?;
+                     name = Escaped(self.name), id = commit.id())?;
 
     if let Ok(ref parent) = commit.parent(0) {
       writeln!(&mut f, "<dt>Parent</dt>")?;
@@ -922,7 +923,7 @@ impl<'repo> RepoRenderer<'repo> {
         &mut f,
         "<dd><a href=\"/{root}{name}/{COMMIT_SUBDIR}/{id}.html\">{id}<a/><dd>",
         root = self.output_root,
-        name = Escaped(&self.name),
+        name = Escaped(self.name),
         id = parent.id()
       )?;
     }
@@ -1024,7 +1025,7 @@ impl<'repo> RepoRenderer<'repo> {
             &mut f,
             "<pre><b>diff --git /dev/null b/<a href=\"/{root}{name}/{TREE_SUBDIR}/{new_path}.html\">{new_path}</a></b>",
             root = self.output_root,
-            name = Escaped(&self.name),
+            name = Escaped(self.name),
             new_path = delta_info.new_path.to_string_lossy(),
           )?;
         }
@@ -1040,7 +1041,7 @@ impl<'repo> RepoRenderer<'repo> {
             &mut f,
             "<pre><b>diff --git a/<a id=\"d#{delta_id}\" href=\"/{root}{name}/{TREE_SUBDIR}/{new_path}.html\">{old_path}</a> b/<a href=\"/{root}{name}/{TREE_SUBDIR}/{new_path}.html\">{new_path}</a></b>",
             root = self.output_root,
-            name = Escaped(&self.name),
+            name = Escaped(self.name),
             new_path = delta_info.new_path.to_string_lossy(),
             old_path = delta_info.old_path.to_string_lossy(),
           )?;
@@ -1131,7 +1132,7 @@ impl<'repo> RepoRenderer<'repo> {
 
   fn render_summary(&self) -> io::Result<()> {
     let mut path = self.output_path.clone();
-    path.push(&self.name);
+    path.push(self.name);
 
     fs::create_dir_all(&path)?;
     path.push("index.html");
@@ -1145,7 +1146,7 @@ impl<'repo> RepoRenderer<'repo> {
     };
 
     // ========================================================================
-    self.render_header(&mut f, PageTitle::Summary { repo_name: &self.name })?;
+    self.render_header(&mut f, PageTitle::Summary { repo_name: self.name })?;
 
     writeln!(&mut f, "<ul>")?;
     writeln!(&mut f, "<li>refs: {branch}</li>",
@@ -1153,7 +1154,7 @@ impl<'repo> RepoRenderer<'repo> {
     writeln!(
       &mut f,
       "<li>git clone: <a href=\"git://git.pablopie.xyz/{name}\">git://git.pablopie.xyz/{name}</a></li>",
-      name = Escaped(&self.name),
+      name = Escaped(self.name),
     )?;
     writeln!(&mut f, "</ul>")?;
 
@@ -1178,7 +1179,7 @@ impl<'repo> RepoRenderer<'repo> {
 
   pub fn render_license(&self, license: &str) -> io::Result<()> {
     let mut path = self.output_path.clone();
-    path.push(&self.name);
+    path.push(self.name);
     path.push("license.html");
 
     let mut f = match File::create(&path) {
@@ -1190,7 +1191,7 @@ impl<'repo> RepoRenderer<'repo> {
     };
 
     // ========================================================================
-    self.render_header(&mut f, PageTitle::License { repo_name: &self.name })?;
+    self.render_header(&mut f, PageTitle::License { repo_name: self.name })?;
     writeln!(&mut f, "<section id=\"license\">")?;
     writeln!(&mut f, "<pre>{}</pre>", Escaped(license))?;
     writeln!(&mut f, "</section>")?;
@@ -1560,8 +1561,13 @@ fn getuser<'a>() -> Cow<'a, str> {
 }
 
 fn main() -> ExitCode {
-  #[allow(unused_variables)]
-  let (cmd, program_name) = if let Ok(cmd) = Cmd::parse() {
+  let mut args = env::args();
+  let program_name = args.next().unwrap();
+
+  let start = Instant::now();
+  log::version(&program_name);
+
+  let cmd = if let Ok(cmd) = Cmd::parse(&mut args, &program_name) {
     cmd
   } else {
     return ExitCode::FAILURE;
@@ -1592,32 +1598,31 @@ fn main() -> ExitCode {
         return ExitCode::FAILURE;
       };
 
-      info!("Updating global repository index...");
+      let n_repos = repos.len();
+      infoln!("Updating pages for git repositories in {repos_dir:?}");
+      log::set_job_count(n_repos+1); // tasks: render index + render each repo
+
+      log::render_start("repository index");
       if render_index(&repos, cmd.flags.private()).is_err() {
         return ExitCode::FAILURE;
       }
-      info_done!();
+      log::render_done();
 
-      let n_repos = repos.len();
-      job_counter_start!(n_repos);
-      infoln!("Updating pages for git repositories in {repos_dir:?}...");
       for repo in repos {
-        job_counter_increment!(repo.name);
-
-        let renderer = RepoRenderer::new(repo, cmd.flags);
+        let renderer = RepoRenderer::new(&repo, cmd.flags);
         let renderer = if let Ok(renderer) = renderer {
           renderer
         } else {
           return ExitCode::FAILURE;
         };
 
+        log::render_start(&repo.name);
         if let Err(e) = renderer.render() {
           errorln!("Failed rendering pages for {name:?}: {e}",
                    name = renderer.name);
           return ExitCode::FAILURE;
         }
-        info_done!();
-
+        log::render_done();
       }
     }
     SubCmd::Render { repo_name } => {
@@ -1627,40 +1632,44 @@ fn main() -> ExitCode {
         return ExitCode::FAILURE;
       };
 
-      info!("Updating global repository index...");
-      if let Err(e) = render_index(&repos, cmd.flags.private()) {
-        errorln!("Failed rendering global repository index: {e}");
-      }
-      info_done!();
-
       let mut repo = None;
-      for r in repos {
+      for r in &repos {
         if *r.name == *repo_name {
           repo = Some(r);
           break;
         }
       }
 
-      if let Some(repo) = repo {
-        info!("Updating pages for {name:?}...", name = repo.name);
-
-        let renderer = RepoRenderer::new(repo, cmd.flags);
-        let renderer = if let Ok(renderer) = renderer {
-          renderer
-        } else {
-          return ExitCode::FAILURE;
-        };
-
-        if let Err(e) = renderer.render() {
-          errorln!("Failed rendering pages for {name:?}: {e}",
-                   name = renderer.name);
-        }
+      if repo.is_none() {
+        errorln!("Couldnt' find repository {repo_name:?} in {repos_dir:?}");
+        return ExitCode::FAILURE;
+      }
+      let repo = repo.unwrap();
 
-        info_done!();
+      let renderer = RepoRenderer::new(repo, cmd.flags);
+      let renderer = if let Ok(renderer) = renderer {
+        renderer
       } else {
-        errorln!("Couldnt' find repository {repo_name:?} in {repos_dir:?}");
         return ExitCode::FAILURE;
+      };
+
+      infoln!("Updating pages for git repository {repo_name:?}");
+      log::set_job_count(2); // tasks: render index + render repo
+
+      log::render_start("repository index");
+      if let Err(e) = render_index(&repos, cmd.flags.private()) {
+        errorln!("Failed rendering global repository index: {e}");
       }
+      log::render_done();
+
+      log::render_start(&repo.name);
+
+      if let Err(e) = renderer.render() {
+        errorln!("Failed rendering pages for {name:?}: {e}",
+          name = renderer.name);
+      }
+
+      log::render_done();
     }
     SubCmd::Init { repo_name, description } => {
       let mut repo_path = if cmd.flags.private() {
@@ -1673,7 +1682,7 @@ fn main() -> ExitCode {
       let mut opts = RepositoryInitOptions::new();
       opts.bare(false).no_reinit(true);
 
-      info!("Initializing empty {repo_name:?} repository in {repo_path:?}...");
+      infoln!("Initializing empty {repo_name:?} repository in {repo_path:?}");
 
       if let Err(e) = Repository::init_opts(&repo_path, &opts) {
         errorln!("Couldn't initialize {repo_name:?}: {e}", e = e.message());
@@ -1684,10 +1693,9 @@ fn main() -> ExitCode {
         .is_err() {
         return ExitCode::FAILURE;
       }
-
-      info_done!();
     }
   }
 
+  log::finished(start.elapsed());
   ExitCode::SUCCESS
 }