| /* |
| * 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 |
| * <http://creativecommons.org/publicdomain/zero/1.0/>. |
| */ |
| |
| #![deny(warnings)] |
| |
| use git2::{Error, ErrorCode, Repository, StatusOptions, SubmoduleIgnore}; |
| use std::str; |
| use std::time::Duration; |
| use structopt::StructOpt; |
| |
| #[derive(StructOpt)] |
| struct Args { |
| arg_spec: Vec<String>, |
| #[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<String>, |
| #[structopt(name = "opt-files", long = "ignore-submodules")] |
| /// setting for ignoring submodules [all] |
| flag_ignore_submodules: Option<String>, |
| #[structopt(name = "dir", long = "git-dir")] |
| /// git directory to analyze |
| flag_git_dir: Option<String>, |
| #[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 <file>...\" 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{} <file>...\" to update what will be committed) |
| # (use \"git checkout -- <file>...\" 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 <file>...\" 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 <file>...\" 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::from_args(); |
| match run(&args) { |
| Ok(()) => {} |
| Err(e) => println!("error: {}", e), |
| } |
| } |