/* * libgit2 "status" example - shows how to use the status APIs * * Written by the libgit2 contributors * * To the extent possible under law, the author(s) have dedicated all copyright * and related and neighboring rights to this software to the public domain * worldwide. This software is distributed without any warranty. * * You should have received a copy of the CC0 Public Domain Dedication along * with this software. If not, see * . */ #![deny(warnings)] use clap::Parser; use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; use std::str; use std::time::Duration; #[derive(Parser)] struct Args { arg_spec: Vec, #[structopt(name = "long", long)] /// show longer statuses (default) _flag_long: bool, /// show short statuses #[structopt(name = "short", long)] flag_short: bool, #[structopt(name = "porcelain", long)] /// ?? flag_porcelain: bool, #[structopt(name = "branch", short, long)] /// show branch information flag_branch: bool, #[structopt(name = "z", short)] /// ?? flag_z: bool, #[structopt(name = "ignored", long)] /// show ignored files as well flag_ignored: bool, #[structopt(name = "opt-modules", long = "untracked-files")] /// setting for showing untracked files [no|normal|all] flag_untracked_files: Option, #[structopt(name = "opt-files", long = "ignore-submodules")] /// setting for ignoring submodules [all] flag_ignore_submodules: Option, #[structopt(name = "dir", long = "git-dir")] /// git directory to analyze flag_git_dir: Option, #[structopt(name = "repeat", long)] /// repeatedly show status, sleeping inbetween flag_repeat: bool, #[structopt(name = "list-submodules", long)] /// show submodules flag_list_submodules: bool, } #[derive(Eq, PartialEq)] enum Format { Long, Short, Porcelain, } fn run(args: &Args) -> Result<(), Error> { let path = args.flag_git_dir.clone().unwrap_or_else(|| ".".to_string()); let repo = Repository::open(&path)?; if repo.is_bare() { return Err(Error::from_str("cannot report status on bare repository")); } let mut opts = StatusOptions::new(); opts.include_ignored(args.flag_ignored); match args.flag_untracked_files.as_ref().map(|s| &s[..]) { Some("no") => { opts.include_untracked(false); } Some("normal") => { opts.include_untracked(true); } Some("all") => { opts.include_untracked(true).recurse_untracked_dirs(true); } Some(_) => return Err(Error::from_str("invalid untracked-files value")), None => {} } match args.flag_ignore_submodules.as_ref().map(|s| &s[..]) { Some("all") => { opts.exclude_submodules(true); } Some(_) => return Err(Error::from_str("invalid ignore-submodules value")), None => {} } opts.include_untracked(!args.flag_ignored); for spec in &args.arg_spec { opts.pathspec(spec); } loop { if args.flag_repeat { println!("\u{1b}[H\u{1b}[2J"); } let statuses = repo.statuses(Some(&mut opts))?; if args.flag_branch { show_branch(&repo, &args.format())?; } if args.flag_list_submodules { print_submodules(&repo)?; } if args.format() == Format::Long { print_long(&statuses); } else { print_short(&repo, &statuses); } if args.flag_repeat { std::thread::sleep(Duration::new(10, 0)); } else { return Ok(()); } } } fn show_branch(repo: &Repository, format: &Format) -> Result<(), Error> { let head = match repo.head() { Ok(head) => Some(head), Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { None } Err(e) => return Err(e), }; let head = head.as_ref().and_then(|h| h.shorthand()); if format == &Format::Long { println!( "# On branch {}", head.unwrap_or("Not currently on any branch") ); } else { println!("## {}", head.unwrap_or("HEAD (no branch)")); } Ok(()) } fn print_submodules(repo: &Repository) -> Result<(), Error> { let modules = repo.submodules()?; println!("# Submodules"); for sm in &modules { println!( "# - submodule '{}' at {}", sm.name().unwrap(), sm.path().display() ); } Ok(()) } // This function print out an output similar to git's status command in long // form, including the command-line hints. fn print_long(statuses: &git2::Statuses) { let mut header = false; let mut rm_in_workdir = false; let mut changes_in_index = false; let mut changed_in_workdir = false; // Print index changes for entry in statuses .iter() .filter(|e| e.status() != git2::Status::CURRENT) { if entry.status().contains(git2::Status::WT_DELETED) { rm_in_workdir = true; } let istatus = match entry.status() { s if s.contains(git2::Status::INDEX_NEW) => "new file: ", s if s.contains(git2::Status::INDEX_MODIFIED) => "modified: ", s if s.contains(git2::Status::INDEX_DELETED) => "deleted: ", s if s.contains(git2::Status::INDEX_RENAMED) => "renamed: ", s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange:", _ => continue, }; if !header { println!( "\ # Changes to be committed: # (use \"git reset HEAD ...\" to unstage) #" ); header = true; } let old_path = entry.head_to_index().unwrap().old_file().path(); let new_path = entry.head_to_index().unwrap().new_file().path(); match (old_path, new_path) { (Some(old), Some(new)) if old != new => { println!("#\t{} {} -> {}", istatus, old.display(), new.display()); } (old, new) => { println!("#\t{} {}", istatus, old.or(new).unwrap().display()); } } } if header { changes_in_index = true; println!("#"); } header = false; // Print workdir changes to tracked files for entry in statuses.iter() { // With `Status::OPT_INCLUDE_UNMODIFIED` (not used in this example) // `index_to_workdir` may not be `None` even if there are no differences, // in which case it will be a `Delta::Unmodified`. if entry.status() == git2::Status::CURRENT || entry.index_to_workdir().is_none() { continue; } let istatus = match entry.status() { s if s.contains(git2::Status::WT_MODIFIED) => "modified: ", s if s.contains(git2::Status::WT_DELETED) => "deleted: ", s if s.contains(git2::Status::WT_RENAMED) => "renamed: ", s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange:", _ => continue, }; if !header { println!( "\ # Changes not staged for commit: # (use \"git add{} ...\" to update what will be committed) # (use \"git checkout -- ...\" to discard changes in working directory) #\ ", if rm_in_workdir { "/rm" } else { "" } ); header = true; } let old_path = entry.index_to_workdir().unwrap().old_file().path(); let new_path = entry.index_to_workdir().unwrap().new_file().path(); match (old_path, new_path) { (Some(old), Some(new)) if old != new => { println!("#\t{} {} -> {}", istatus, old.display(), new.display()); } (old, new) => { println!("#\t{} {}", istatus, old.or(new).unwrap().display()); } } } if header { changed_in_workdir = true; println!("#"); } header = false; // Print untracked files for entry in statuses .iter() .filter(|e| e.status() == git2::Status::WT_NEW) { if !header { println!( "\ # Untracked files # (use \"git add ...\" to include in what will be committed) #" ); header = true; } let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); println!("#\t{}", file.display()); } header = false; // Print ignored files for entry in statuses .iter() .filter(|e| e.status() == git2::Status::IGNORED) { if !header { println!( "\ # Ignored files # (use \"git add -f ...\" to include in what will be committed) #" ); header = true; } let file = entry.index_to_workdir().unwrap().old_file().path().unwrap(); println!("#\t{}", file.display()); } if !changes_in_index && changed_in_workdir { println!( "no changes added to commit (use \"git add\" and/or \ \"git commit -a\")" ); } } // This version of the output prefixes each path with two status columns and // shows submodule status information. fn print_short(repo: &Repository, statuses: &git2::Statuses) { for entry in statuses .iter() .filter(|e| e.status() != git2::Status::CURRENT) { let mut istatus = match entry.status() { s if s.contains(git2::Status::INDEX_NEW) => 'A', s if s.contains(git2::Status::INDEX_MODIFIED) => 'M', s if s.contains(git2::Status::INDEX_DELETED) => 'D', s if s.contains(git2::Status::INDEX_RENAMED) => 'R', s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T', _ => ' ', }; let mut wstatus = match entry.status() { s if s.contains(git2::Status::WT_NEW) => { if istatus == ' ' { istatus = '?'; } '?' } s if s.contains(git2::Status::WT_MODIFIED) => 'M', s if s.contains(git2::Status::WT_DELETED) => 'D', s if s.contains(git2::Status::WT_RENAMED) => 'R', s if s.contains(git2::Status::WT_TYPECHANGE) => 'T', _ => ' ', }; if entry.status().contains(git2::Status::IGNORED) { istatus = '!'; wstatus = '!'; } if istatus == '?' && wstatus == '?' { continue; } let mut extra = ""; // A commit in a tree is how submodules are stored, so let's go take a // look at its status. // // TODO: check for GIT_FILEMODE_COMMIT let status = entry.index_to_workdir().and_then(|diff| { let ignore = SubmoduleIgnore::Unspecified; diff.new_file() .path_bytes() .and_then(|s| str::from_utf8(s).ok()) .and_then(|name| repo.submodule_status(name, ignore).ok()) }); if let Some(status) = status { if status.contains(git2::SubmoduleStatus::WD_MODIFIED) { extra = " (new commits)"; } else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED) || status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED) { extra = " (modified content)"; } else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) { extra = " (untracked content)"; } } let (mut a, mut b, mut c) = (None, None, None); if let Some(diff) = entry.head_to_index() { a = diff.old_file().path(); b = diff.new_file().path(); } if let Some(diff) = entry.index_to_workdir() { a = a.or_else(|| diff.old_file().path()); b = b.or_else(|| diff.old_file().path()); c = diff.new_file().path(); } match (istatus, wstatus) { ('R', 'R') => println!( "RR {} {} {}{}", a.unwrap().display(), b.unwrap().display(), c.unwrap().display(), extra ), ('R', w) => println!( "R{} {} {}{}", w, a.unwrap().display(), b.unwrap().display(), extra ), (i, 'R') => println!( "{}R {} {}{}", i, a.unwrap().display(), c.unwrap().display(), extra ), (i, w) => println!("{}{} {}{}", i, w, a.unwrap().display(), extra), } } for entry in statuses .iter() .filter(|e| e.status() == git2::Status::WT_NEW) { println!( "?? {}", entry .index_to_workdir() .unwrap() .old_file() .path() .unwrap() .display() ); } } impl Args { fn format(&self) -> Format { if self.flag_short { Format::Short } else if self.flag_porcelain || self.flag_z { Format::Porcelain } else { Format::Long } } } fn main() { let args = Args::parse(); match run(&args) { Ok(()) => {} Err(e) => println!("error: {}", e), } }