Add color for series, status, and log

Josh Triplett created

Change summary

Cargo.lock  |  11 ++++
Cargo.toml  |   2 
src/main.rs | 137 ++++++++++++++++++++++++++++++++++++++----------------
3 files changed, 109 insertions(+), 41 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2,8 +2,10 @@
 name = "git-series"
 version = "0.8.2"
 dependencies = [
+ "ansi_term 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "colorparse 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "git2 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -56,6 +58,15 @@ dependencies = [
  "gcc 0.3.31 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "colorparse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ansi_term 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "gcc"
 version = "0.3.31"

Cargo.toml 🔗

@@ -8,8 +8,10 @@ repository = "https://github.com/git-series/git-series"
 description = "Track patch series in git"
 
 [dependencies]
+ansi_term = "0.7.4"
 chrono = "0.2.22"
 clap = "2.7.0"
+colorparse = "1"
 git2 = "0.4.4"
 isatty = "0.1.1"
 quick-error = "1.0"

src/main.rs 🔗

@@ -1,6 +1,8 @@
+extern crate ansi_term;
 extern crate chrono;
 #[macro_use]
 extern crate clap;
+extern crate colorparse;
 extern crate git2;
 extern crate isatty;
 #[macro_use]
@@ -14,6 +16,7 @@ use std::fs::File;
 use std::io::Read;
 use std::io::Write as IoWrite;
 use std::process::Command;
+use ansi_term::Style;
 use chrono::offset::TimeZone;
 use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand};
 use git2::{Config, Commit, Diff, ObjectType, Oid, Reference, Repository, TreeBuilder};
@@ -314,16 +317,22 @@ fn series(out: &mut Output, repo: &Repository) -> Result<()> {
     refs.sort();
     refs.dedup();
 
-    let config = try!(repo.config());
+    let config = try!(try!(repo.config()).snapshot());
     try!(out.auto_pager(&config, "branch", false));
+    let color_current = try!(out.get_color(&config, "branch", "current", "green"));
+    let color_plain = try!(out.get_color(&config, "branch", "plain", "normal"));
     for name in refs.iter() {
-        let star = if Some(name) == shead_target.as_ref() { '*' } else { ' ' };
+        let (star, color) = if Some(name) == shead_target.as_ref() {
+            ('*', color_current)
+        } else {
+            (' ', color_plain)
+        };
         let new = if try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", SERIES_PREFIX, name)))).is_none() {
             " (new, no commits yet)"
         } else {
             ""
         };
-        try!(writeln!(out, "{} {}{}", star, name, new));
+        try!(writeln!(out, "{} {}{}", star, color.paint(name as &str), new));
     }
     if refs.is_empty() {
         try!(writeln!(out, "No series; use \"git series start <name>\" to start"));
@@ -622,6 +631,34 @@ impl Output {
         Ok(())
     }
 
+    // Get a color to write text with, taking git configuration into account.
+    //
+    // config: the configuration to determine the color from.
+    // command: the git command to act like.
+    // slot: the color "slot" of that git command to act like.
+    // default: the color to use if not configured.
+    fn get_color(&self, config: &Config, command: &str, slot: &str, default: &str) -> Result<Style> {
+        if !cfg!(unix) {
+            return Ok(Style::new());
+        }
+        let color_ui = try!(notfound_to_none(config.get_str("color.ui"))).unwrap_or("auto");
+        let color_cmd = try!(notfound_to_none(config.get_str(&format!("color.{}", command)))).unwrap_or(color_ui);
+        if color_cmd == "never" || Config::parse_bool(color_cmd) == Ok(false) {
+            return Ok(Style::new());
+        }
+        if self.pager.is_some() {
+            let color_pager = try!(notfound_to_none(config.get_bool(&format!("color.pager")))).unwrap_or(true);
+            if !color_pager {
+                return Ok(Style::new());
+            }
+        } else if !isatty::stdout_isatty() {
+            return Ok(Style::new());
+        }
+        let cfg = format!("color.{}.{}", command, slot);
+        let color = try!(notfound_to_none(config.get_str(&cfg))).unwrap_or(default);
+        colorparse::parse(color).map_err(|e| format!("Error parsing {}: {}", cfg, e).into())
+   }
+
     fn write_err(&mut self, msg: &str) {
         if self.include_stderr {
             if write!(self, "{}", msg).is_err() {
@@ -674,40 +711,57 @@ fn get_signature(config: &Config, which: &str) -> Result<git2::Signature<'static
     Ok(try!(git2::Signature::now(&name, &email)))
 }
 
-fn write_status(status: &mut String, diff: &Diff, heading: &str, show_hints: bool, hints: &[&str]) -> Result<bool> {
-    let mut changes = false;
-
-    try!(diff.foreach(&mut |delta, _| {
-        if !changes {
-            changes = true;
-            writeln!(status, "{}", heading).unwrap();
-            if show_hints {
-                for hint in hints {
-                    writeln!(status, "  ({})", hint).unwrap();
-                }
-            }
-            writeln!(status, "").unwrap();
-        }
-        writeln!(status, "        {:?}:   {}", delta.status(), delta.old_file().path().unwrap().to_str().unwrap()).unwrap();
-        true
-    }, None, None, None));
-
-    if changes {
-        writeln!(status, "").unwrap();
-    }
-
-    Ok(changes)
-}
-
 fn commit_status(out: &mut Output, repo: &Repository, m: &ArgMatches, do_status: bool) -> Result<()> {
-    let config = try!(repo.config());
+    let config = try!(try!(repo.config()).snapshot());
     let shead = match repo.find_reference(SHEAD_REF) {
         Err(ref e) if e.code() == git2::ErrorCode::NotFound => { println!("No series; use \"git series start <name>\" to start"); return Ok(()); }
         result => try!(result),
     };
     let series_name = try!(shead_series_name(&shead));
-    let mut status = String::new();
-    writeln!(status, "On series {}", series_name).unwrap();
+
+    if do_status {
+        try!(out.auto_pager(&config, "status", false));
+    }
+    let get_color = |out: &Output, color: &str, default: &str| {
+        if do_status {
+            out.get_color(&config, "status", color, default)
+        } else {
+            Ok(Style::new())
+        }
+    };
+    let color_normal = Style::new();
+    let color_header = try!(get_color(out, "header", "normal"));
+    let color_updated = try!(get_color(out, "updated", "green"));
+    let color_changed = try!(get_color(out, "changed", "red"));
+
+    let write_status = |status: &mut Vec<ansi_term::ANSIString>, diff: &Diff, heading: &str, color: &Style, show_hints: bool, hints: &[&str]| -> Result<bool> {
+        let mut changes = false;
+
+        try!(diff.foreach(&mut |delta, _| {
+            if !changes {
+                changes = true;
+                status.push(color_header.paint(format!("{}\n", heading.to_string())));
+                if show_hints {
+                    for hint in hints {
+                        status.push(color_header.paint(format!("  ({})\n", hint)));
+                    }
+                }
+                status.push(color_normal.paint("\n"));
+            }
+            status.push(color_normal.paint("        "));
+            status.push(color.paint(format!("{:?}:   {}\n", delta.status(), delta.old_file().path().unwrap().to_str().unwrap())));
+            true
+        }, None, None, None));
+
+        if changes {
+            status.push(color_normal.paint("\n"));
+        }
+
+        Ok(changes)
+    };
+
+    let mut status = Vec::new();
+    status.push(color_header.paint(format!("On series {}\n", series_name)));
 
     let mut internals = try!(Internals::read(repo));
     let working_tree = try!(repo.find_tree(try!(internals.working.write())));
@@ -716,7 +770,7 @@ fn commit_status(out: &mut Output, repo: &Repository, m: &ArgMatches, do_status:
     let shead_commit = match shead.resolve() {
         Ok(r) => Some(try!(peel_to_commit(r))),
         Err(ref e) if e.code() == git2::ErrorCode::NotFound => {
-            writeln!(status, "\nInitial series commit\n").unwrap();
+            status.push(color_header.paint("\nInitial series commit\n"));
             None
         }
         Err(e) => try!(Err(e)),
@@ -730,37 +784,37 @@ fn commit_status(out: &mut Output, repo: &Repository, m: &ArgMatches, do_status:
 
     let (changes, tree, diff) = if commit_all {
         let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&working_tree), None));
-        let changes = try!(write_status(&mut status, &diff, "Changes to be committed:", false, &[]));
+        let changes = try!(write_status(&mut status, &diff, "Changes to be committed:", &color_normal, false, &[]));
         if !changes {
-            writeln!(status, "nothing to commit; series unchanged").unwrap();
+            status.push(color_normal.paint("nothing to commit; series unchanged\n"));
         }
         (changes, working_tree, diff)
     } else {
         let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&staged_tree), None));
         let changes_to_be_committed = try!(write_status(&mut status, &diff,
-                "Changes to be committed:", do_status,
+                "Changes to be committed:", &color_updated, do_status,
                 &["use \"git series commit\" to commit",
                   "use \"git series unadd <file>...\" to undo add"]));
 
         let diff_not_staged = try!(repo.diff_tree_to_tree(Some(&staged_tree), Some(&working_tree), None));
         let changes_not_staged = try!(write_status(&mut status, &diff_not_staged,
-                "Changes not staged for commit:", do_status,
+                "Changes not staged for commit:", &color_changed, do_status,
                 &["use \"git series add <file>...\" to update what will be committed"]));
 
         if !changes_to_be_committed {
             if changes_not_staged {
-                writeln!(status, "no changes added to commit (use \"git series add\" or \"git series commit -a\")").unwrap();
+                status.push(color_normal.paint("no changes added to commit (use \"git series add\" or \"git series commit -a\")\n"));
             } else {
-                writeln!(status, "nothing to commit; series unchanged").unwrap();
+                status.push(color_normal.paint("nothing to commit; series unchanged\n"));
             }
         }
 
         (changes_to_be_committed, staged_tree, diff)
     };
 
+    let status = ansi_term::ANSIStrings(&status).to_string();
     if do_status || !changes {
         if do_status {
-            try!(out.auto_pager(&config, "status", false));
             try!(write!(out, "{}", status));
         } else {
             return Err(status.into());
@@ -1126,8 +1180,9 @@ fn format(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
 }
 
 fn log(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
-    let config = try!(repo.config());
+    let config = try!(try!(repo.config()).snapshot());
     try!(out.auto_pager(&config, "log", true));
+    let color_commit = try!(out.get_color(&config, "diff", "commit", "yellow"));
 
     let mut revwalk = try!(repo.revwalk());
     revwalk.simplify_first_parent();
@@ -1144,7 +1199,7 @@ fn log(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
         let first_parent_id = try!(commit.parent_id(0).map_err(|e| format!("Malformed series commit {}: {}", oid, e)));
         let first_series_commit = tree.iter().find(|entry| entry.id() == first_parent_id).is_some();
 
-        try!(writeln!(out, "commit {}", oid));
+        try!(writeln!(out, "{}", color_commit.paint(format!("commit {}", oid))));
         try!(writeln!(out, "Author: {} <{}>", author.name().unwrap(), author.email().unwrap()));
         try!(writeln!(out, "Date:   {}\n", date_822(author.when())));
         for line in commit.message().unwrap().lines() {