diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5d7be49 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,286 @@ +[root] +name = "git-tree" +version = "0.1.0" +dependencies = [ + "ansi_term 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)", + "git2 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ansi_term" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "atty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "2.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cmake" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curl-sys" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "git2" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "libgit2-sys 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libgit2-sys" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cmake 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "curl-sys 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "libssh2-sys 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cmake 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libz-sys" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "openssl-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "openssl-sys" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "percent-encoding" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pkg-config" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strsim" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "term_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vcpkg" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0525564ca78e828cba1b7ee07cc1ed5ab9259311c294519114a3386f4e72f505" +"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" +"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" +"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +"checksum clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3451e409013178663435d6f15fdb212f14ee4424a3d74f979d081d0a66b6f1f2" +"checksum cmake 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "0c8a6541a55bcd72d3de4faee2d101a5a66df29790282c7f797082a7228a9b3d" +"checksum curl-sys 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d5481162dc4f424d088581db2f979fa7d4c238fe9794595de61d8d7522e277de" +"checksum gcc 0.3.53 (registry+https://github.com/rust-lang/crates.io-index)" = "e8310f7e9c890398b0e80e301c4f474e9918d2b27fca8f48486ca775fa9ffc5a" +"checksum git2 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0c1c0203d653f4140241da0c1375a404f0a397249ec818cd2076c6280c50f6fa" +"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum libc 0.2.30 (registry+https://github.com/rust-lang/crates.io-index)" = "2370ca07ec338939e356443dac2296f581453c35fe1e3a3ed06023c49435f915" +"checksum libgit2-sys 0.6.14 (registry+https://github.com/rust-lang/crates.io-index)" = "c00f6e5bc3fb2b5f87e75e8d0fd4ae6720d55f3ee23d389b7c6cae30f8db8db1" +"checksum libssh2-sys 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0db4ec23611747ef772db1c4d650f8bd762f07b461727ec998f953c614024b75" +"checksum libz-sys 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)" = "3fdd64ef8ee652185674455c1d450b83cbc8ad895625d543b5324d923f82e4d8" +"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" +"checksum openssl-probe 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d98df0270d404ccd3c050a41d579c52d1db15375168bb3471e04ec0f5f378daf" +"checksum openssl-sys 0.9.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7e3a9845a4c9fdb321931868aae5549e96bb7b979bf9af7de03603d74691b5f3" +"checksum percent-encoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de154f638187706bde41d9b4738748933d64e6b37bdbffc0b47a97d16a6ae356" +"checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" +"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" +"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209" +"checksum textwrap 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8e08afc40ae3459e4838f303e465aa50d823df8d7f83ca88108f6d3afe7edd" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb819346883532a271eb626deb43c4a1bb4c4dd47c519bd78137c3e72a4fe27" +"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9adda1d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "git-tree" +version = "0.1.0" +authors = ["Christoph Rüßler "] + +[dependencies] +ansi_term = "0.10.1" +clap = "2.26.2" +git2 = "0.6.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ce7382 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# git-tree + +git-tree is a small command line utility for showing the status of untracked +and modified files in a git repository as a tree. + +## Installation + +Because it uses `#![feature(advanced_slice_patterns, slice_patterns)]`, it +currently (2017-10) requires a nightly compiler. See +https://github.com/rust-lang/rust/issues/23121 and +https://doc.rust-lang.org/1.8.0/book/slice-patterns.html. + +Provided you have `cargo` installed, installation is as easy as + +``` +cargo install https://github.com/cruessler/git-tree +``` + +This will download the source code and compile the binary which can then be +found in `~/.cargo/bin`. If that’s in your `$PATH`, you can type `git-tree +--help` to get an overview of the available commands. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5c38afb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,243 @@ +#![feature(advanced_slice_patterns, slice_patterns)] +extern crate ansi_term; +extern crate clap; +extern crate git2; + +use ansi_term::Colour::{Blue, Fixed, Green, Red, White}; +use clap::{App, Arg}; +use git2::{Repository, Status, StatusEntry}; +use std::collections::BTreeMap; +use std::fmt; +use std::path::{Component, Components, Path}; + +#[derive(Debug)] +enum Node { + Tree(Tree), + Leaf(Leaf), +} + +#[derive(Debug)] +struct Tree { + name: String, + children: BTreeMap, +} + +#[derive(Debug)] +struct Leaf { + name: String, + status: Status, +} + +impl Tree { + fn add_entry(&mut self, entry: &StatusEntry) { + entry + .path() + .and_then(|path| Path::new(path).parent()) + .map(|parent| { + self.add_entry_at_path(entry, &mut parent.components()); + }); + } + + fn add_entry_at_path(&mut self, entry: &StatusEntry, path: &mut Components) { + match path.next() { + Some(Component::Normal(ref dir)) => { + dir.to_str().map(|dir| { + let node = self.children.entry(dir.into()).or_insert(Node::Tree(Tree { + name: dir.into(), + children: BTreeMap::new(), + })); + + if let &mut Node::Tree(ref mut node) = node { + node.add_entry_at_path(entry, path) + } + }); + } + + Some(_) => unimplemented!(), + + _ => { + entry + .path() + .map(|path| Path::new(path)) + .and_then(|path| path.file_name()) + .and_then(|file_name| file_name.to_str()) + .map(|file_name| { + let new_node = Leaf { + name: file_name.into(), + status: entry.status(), + }; + + self.children.insert(file_name.into(), Node::Leaf(new_node)) + }); + } + } + } +} + +trait Lines { + fn lines(&self) -> Vec; + + fn prepend(&self, lines: Vec, with: String) -> Vec { + lines + .into_iter() + .map(|line| { + let mut new_line = with.clone(); + new_line.push_str(&line); + new_line + }) + .collect() + } + + fn prepend_first_and_rest( + &self, + mut lines: Vec, + first_with: String, + rest_with: String, + ) -> Vec { + let (first, rest) = lines.split_at_mut(1); + + let mut first_line = first_with.clone(); + first_line.push_str(&first[0]); + + let mut lines = vec![first_line]; + + lines.extend(self.prepend(rest.into(), rest_with)); + + lines + } +} + +impl Lines for Node { + fn lines(&self) -> Vec { + match self { + &Node::Tree(ref node) => node.lines(), + &Node::Leaf(ref node) => node.lines(), + } + } +} + +impl Lines for Tree { + fn lines(&self) -> Vec { + let children = self.children.values().collect::>(); + + match &children[..] { + &[ref rest.., ref last] => { + let mut lines = vec![self.name.clone()]; + + lines.extend( + rest.iter() + .flat_map(|&node| { + // Every child’s first line gets prepended by "├── ". + // All following lines get prepended by "│ ". + + self.prepend_first_and_rest( + node.lines(), + "├── ".into(), + "│ ".into(), + ) + }) + .collect::>(), + ); + + // The last child’s first line gets prepended by "└── ". + // All following lines get prepended by " ". + lines.extend(self.prepend_first_and_rest( + last.lines(), + "└── ".into(), + " ".into(), + )); + + lines + } + + _ => vec![], + } + } +} + +// http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html +impl Lines for Leaf { + fn lines(&self) -> Vec { + let style = match self.status { + s if s.contains(git2::STATUS_WT_MODIFIED) => Red.normal(), + s if s.contains(git2::STATUS_INDEX_MODIFIED) => Red.bold(), + s if s.contains(git2::STATUS_WT_NEW) => Green.normal(), + s if s.contains(git2::STATUS_INDEX_NEW) => Green.bold(), + s if s.contains(git2::STATUS_IGNORED) => Blue.normal(), + _ => White.normal(), + }; + + let modifier_index = match self.status { + s if s.contains(git2::STATUS_INDEX_MODIFIED) => "M", + s if s.contains(git2::STATUS_INDEX_NEW) => "N", + _ => "-", + }; + + let modifier_worktree = match self.status { + s if s.contains(git2::STATUS_WT_MODIFIED) => "M", + s if s.contains(git2::STATUS_WT_NEW) => "N", + _ => "-", + }; + + let gray = Fixed(244).normal(); + + vec![ + format!( + "{}{} {}", + gray.paint(modifier_index), + gray.paint(modifier_worktree), + style.paint(self.name.as_str()) + ), + ] + } +} + +impl fmt::Display for Tree { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for l in self.lines() { + f.write_str(&l)?; + f.write_str("\n")?; + } + + Ok(()) + } +} + +fn main() { + let matches = App::new("git-tree") + .version(env!("CARGO_PKG_VERSION")) + .author("Christoph Rüßler ") + .about("tree + git status: displays git status info in a tree") + .after_help( + "git-tree searches for a git repository the same way git does, and \ + displays a tree showing untracked and modified files. The tree’s \ + root is the repository’s root. The tree’s items are colored to \ + indicate their status (green: new, red: modified, blue: ignored). \ + Changes to files in the index are shown in bold.\n\ + A column in front of each file’s name indicates changes to the \ + index and the working tree, respectively (M: modified, N: new).", + ) + .arg( + Arg::with_name("all") + .short("a") + .long("all") + .help("Include ignored files"), + ) + .get_matches(); + + let repo = Repository::discover("./").expect("Could not open repository"); + let statuses = repo.statuses(None).expect("Could not get statuses"); + + let mut root = Tree { + name: ".".into(), + children: BTreeMap::new(), + }; + + for s in statuses.iter() { + if matches.is_present("all") || !s.status().contains(git2::STATUS_IGNORED) { + root.add_entry(&s); + } + } + + println!("{}", root); +}