main.rs

   1extern crate ansi_term;
   2extern crate chrono;
   3#[macro_use]
   4extern crate clap;
   5extern crate colorparse;
   6extern crate git2;
   7extern crate isatty;
   8#[macro_use]
   9extern crate quick_error;
  10extern crate tempdir;
  11
  12use std::env;
  13use std::ffi::{OsStr, OsString};
  14use std::fmt::Write as FmtWrite;
  15use std::fs::File;
  16use std::io::Read;
  17use std::io::Write as IoWrite;
  18use std::process::Command;
  19use ansi_term::Style;
  20use chrono::offset::TimeZone;
  21use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand};
  22use git2::{Config, Commit, Diff, Object, ObjectType, Oid, Reference, Repository, TreeBuilder};
  23use tempdir::TempDir;
  24
  25quick_error! {
  26    #[derive(Debug)]
  27    enum Error {
  28        Git2(err: git2::Error) {
  29            from()
  30            cause(err)
  31            display("{}", err)
  32        }
  33        IO(err: std::io::Error) {
  34            from()
  35            cause(err)
  36            display("{}", err)
  37        }
  38        Msg(msg: String) {
  39            from()
  40            from(s: &'static str) -> (s.to_string())
  41            description(msg)
  42            display("{}", msg)
  43        }
  44        Utf8Error(err: std::str::Utf8Error) {
  45            from()
  46            cause(err)
  47            display("{}", err)
  48        }
  49    }
  50}
  51
  52type Result<T> = std::result::Result<T, Error>;
  53
  54const COMMIT_MESSAGE_COMMENT: &'static str = "
  55# Please enter the commit message for your changes. Lines starting
  56# with '#' will be ignored, and an empty message aborts the commit.
  57";
  58const COVER_LETTER_COMMENT: &'static str = "
  59# Please enter the cover letter for your changes. Lines starting
  60# with '#' will be ignored, and an empty message aborts the change.
  61";
  62const REBASE_COMMENT: &'static str = "\
  63#
  64# Commands:
  65# p, pick = use commit
  66# r, reword = use commit, but edit the commit message
  67# e, edit = use commit, but stop for amending
  68# s, squash = use commit, but meld into previous commit
  69# f, fixup = like \"squash\", but discard this commit's log message
  70# x, exec = run command (the rest of the line) using shell
  71# d, drop = remove commit
  72#
  73# These lines can be re-ordered; they are executed from top to bottom.
  74#
  75# If you remove a line here THAT COMMIT WILL BE LOST.
  76#
  77# However, if you remove everything, the rebase will be aborted.
  78";
  79const SCISSOR_LINE: &'static str = "\
  80# ------------------------ >8 ------------------------";
  81const SCISSOR_COMMENT: &'static str = "\
  82# Do not touch the line above.
  83# Everything below will be removed.
  84";
  85
  86const SHELL_METACHARS: &'static str = "|&;<>()$`\\\"' \t\n*?[#~=%";
  87
  88const SERIES_PREFIX: &'static str = "refs/heads/git-series/";
  89const SHEAD_REF: &'static str = "refs/SHEAD";
  90const STAGED_PREFIX: &'static str = "refs/git-series-internals/staged/";
  91const WORKING_PREFIX: &'static str = "refs/git-series-internals/working/";
  92
  93const GIT_FILEMODE_BLOB: u32 = 0o100644;
  94const GIT_FILEMODE_COMMIT: u32 = 0o160000;
  95
  96fn zero_oid() -> Oid {
  97    Oid::from_bytes(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00").unwrap()
  98}
  99
 100fn peel_to_commit(r: Reference) -> Result<Commit> {
 101    Ok(try!(try!(r.peel(ObjectType::Commit)).into_commit().map_err(|obj| format!("Internal error: expected a commit: {}", obj.id()))))
 102}
 103
 104fn commit_obj_summarize_components(commit: &mut Commit) -> Result<(String, String)> {
 105    let short_id_buf = try!(commit.as_object().short_id());
 106    let short_id = short_id_buf.as_str().unwrap();
 107    let summary = String::from_utf8_lossy(commit.summary_bytes().unwrap());
 108    Ok((short_id.to_string(), summary.to_string()))
 109}
 110
 111fn commit_summarize_components(repo: &Repository, id: Oid) -> Result<(String, String)> {
 112    let mut commit = try!(repo.find_commit(id));
 113    commit_obj_summarize_components(&mut commit)
 114}
 115
 116fn commit_obj_summarize(commit: &mut Commit) -> Result<String> {
 117    let (short_id, summary) = try!(commit_obj_summarize_components(commit));
 118    Ok(format!("{} {}", short_id, summary))
 119}
 120
 121fn commit_summarize(repo: &Repository, id: Oid) -> Result<String> {
 122    let mut commit = try!(repo.find_commit(id));
 123    commit_obj_summarize(&mut commit)
 124}
 125
 126fn notfound_to_none<T>(result: std::result::Result<T, git2::Error>) -> Result<Option<T>> {
 127    match result {
 128        Err(ref e) if e.code() == git2::ErrorCode::NotFound => Ok(None),
 129        Err(e) => Err(e.into()),
 130        Ok(x) => Ok(Some(x)),
 131    }
 132}
 133
 134// If current_id_opt is Some, acts like reference_matching.  If current_id_opt is None, acts like
 135// reference.
 136fn reference_matching_opt<'repo>(repo: &'repo Repository, name: &str, id: Oid, force: bool, current_id_opt: Option<Oid>, log_message: &str) -> Result<Reference<'repo>> {
 137    match current_id_opt {
 138        None => Ok(try!(repo.reference(name, id, force, log_message))),
 139        Some(current_id) => Ok(try!(repo.reference_matching(name, id, force, current_id, log_message))),
 140    }
 141}
 142
 143fn parents_from_ids(repo: &Repository, mut parents: Vec<Oid>) -> Result<Vec<Commit>> {
 144    parents.sort();
 145    parents.dedup();
 146    parents.drain(..).map(|id| Ok(try!(repo.find_commit(id)))).collect::<Result<Vec<Commit>>>()
 147}
 148
 149struct Internals<'repo> {
 150    staged: TreeBuilder<'repo>,
 151    working: TreeBuilder<'repo>,
 152}
 153
 154impl<'repo> Internals<'repo> {
 155    fn read(repo: &'repo Repository) -> Result<Self> {
 156        let shead = try!(repo.find_reference(SHEAD_REF));
 157        let series_name = try!(shead_series_name(&shead));
 158        let mut internals = try!(Internals::read_series(repo, &series_name));
 159        try!(internals.update_series(repo));
 160        Ok(internals)
 161    }
 162
 163    fn read_series(repo: &'repo Repository, series_name: &str) -> Result<Self> {
 164        let committed_id = try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", SERIES_PREFIX, series_name))));
 165        let maybe_get_ref = |prefix: &str| -> Result<TreeBuilder<'repo>> {
 166            match try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", prefix, series_name)))).or(committed_id) {
 167                Some(id) => {
 168                    let c = try!(repo.find_commit(id));
 169                    let t = try!(c.tree());
 170                    Ok(try!(repo.treebuilder(Some(&t))))
 171                }
 172                None => Ok(try!(repo.treebuilder(None))),
 173            }
 174        };
 175        Ok(Internals {
 176            staged: try!(maybe_get_ref(STAGED_PREFIX)),
 177            working: try!(maybe_get_ref(WORKING_PREFIX)),
 178        })
 179    }
 180
 181    fn exists(repo: &'repo Repository, series_name: &str) -> Result<bool> {
 182        for prefix in [SERIES_PREFIX, STAGED_PREFIX, WORKING_PREFIX].iter() {
 183            let prefixed_name = format!("{}{}", prefix, series_name);
 184            if try!(notfound_to_none(repo.refname_to_id(&prefixed_name))).is_some() {
 185                return Ok(true);
 186            }
 187        }
 188        Ok(false)
 189    }
 190
 191    // Returns true if it had anything to copy.
 192    fn copy(repo: &'repo Repository, source: &str, dest: &str) -> Result<bool> {
 193        let mut copied_any = false;
 194        for prefix in [SERIES_PREFIX, STAGED_PREFIX, WORKING_PREFIX].iter() {
 195            let prefixed_source = format!("{}{}", prefix, source);
 196            if let Some(r) = try!(notfound_to_none(repo.find_reference(&prefixed_source))) {
 197                let oid = try!(r.target().ok_or(format!("Internal error: \"{}\" is a symbolic reference", prefixed_source)));
 198                let prefixed_dest = format!("{}{}", prefix, dest);
 199                try!(repo.reference(&prefixed_dest, oid, false, &format!("copied from {}", prefixed_source)));
 200                copied_any = true;
 201            }
 202        }
 203        Ok(copied_any)
 204    }
 205
 206    // Returns true if it had anything to delete.
 207    fn delete(repo: &'repo Repository, series_name: &str) -> Result<bool> {
 208        let mut deleted_any = false;
 209        for prefix in [SERIES_PREFIX, STAGED_PREFIX, WORKING_PREFIX].iter() {
 210            let prefixed_name = format!("{}{}", prefix, series_name);
 211            if let Some(mut r) = try!(notfound_to_none(repo.find_reference(&prefixed_name))) {
 212                try!(r.delete());
 213                deleted_any = true;
 214            }
 215        }
 216        Ok(deleted_any)
 217    }
 218
 219    fn update_series(&mut self, repo: &'repo Repository) -> Result<()> {
 220        let head_id = try!(repo.refname_to_id("HEAD"));
 221        try!(self.working.insert("series", head_id, GIT_FILEMODE_COMMIT as i32));
 222        Ok(())
 223    }
 224
 225    fn write(&self, repo: &'repo Repository) -> Result<()> {
 226        let config = try!(repo.config());
 227        let author = try!(get_signature(&config, "AUTHOR"));
 228        let committer = try!(get_signature(&config, "COMMITTER"));
 229
 230        let shead = try!(repo.find_reference(SHEAD_REF));
 231        let series_name = try!(shead_series_name(&shead));
 232        let maybe_commit = |prefix: &str, tb: &TreeBuilder| -> Result<()> {
 233            let tree_id = try!(tb.write());
 234            let refname = format!("{}{}", prefix, series_name);
 235            let old_commit_id = try!(notfound_to_none(repo.refname_to_id(&refname)));
 236            if let Some(id) = old_commit_id {
 237                let c = try!(repo.find_commit(id));
 238                if c.tree_id() == tree_id {
 239                    return Ok(());
 240                }
 241            }
 242            let tree = try!(repo.find_tree(tree_id));
 243            let mut parents = Vec::new();
 244            // Include all commits from tree, to keep them reachable and fetchable. Include base,
 245            // because series might not have it as an ancestor; we don't enforce that until commit.
 246            for e in tree.iter() {
 247                if e.kind() == Some(ObjectType::Commit) {
 248                    parents.push(e.id());
 249                }
 250            }
 251            let parents = try!(parents_from_ids(repo, parents));
 252            let parents_ref: Vec<&_> = parents.iter().collect();
 253            let commit_id = try!(repo.commit(None, &author, &committer, &refname, &tree, &parents_ref));
 254            try!(repo.reference_ensure_log(&refname));
 255            try!(reference_matching_opt(repo, &refname, commit_id, true, old_commit_id, &format!("commit: {}", refname)));
 256            Ok(())
 257        };
 258        try!(maybe_commit(STAGED_PREFIX, &self.staged));
 259        try!(maybe_commit(WORKING_PREFIX, &self.working));
 260        Ok(())
 261    }
 262}
 263
 264fn diff_empty(diff: &Diff) -> bool {
 265    diff.deltas().len() == 0
 266}
 267
 268fn add(repo: &Repository, m: &ArgMatches) -> Result<()> {
 269    let mut internals = try!(Internals::read(repo));
 270    for file in m.values_of_os("change").unwrap() {
 271        match try!(internals.working.get(file)) {
 272            Some(entry) => { try!(internals.staged.insert(file, entry.id(), entry.filemode())); }
 273            None => {
 274                if try!(internals.staged.get(file)).is_some() {
 275                    try!(internals.staged.remove(file));
 276                }
 277            }
 278        }
 279    }
 280    internals.write(repo)
 281}
 282
 283fn unadd(repo: &Repository, m: &ArgMatches) -> Result<()> {
 284    let shead = try!(repo.find_reference(SHEAD_REF));
 285    let started = {
 286        let shead_target = try!(shead.symbolic_target().ok_or("SHEAD not a symbolic reference"));
 287        try!(notfound_to_none(repo.find_reference(shead_target))).is_some()
 288    };
 289
 290    let mut internals = try!(Internals::read(repo));
 291    if started {
 292        let shead_commit = try!(peel_to_commit(shead));
 293        let shead_tree = try!(shead_commit.tree());
 294
 295        for file in m.values_of("change").unwrap() {
 296            match shead_tree.get_name(file) {
 297                Some(entry) => {
 298                    try!(internals.staged.insert(file, entry.id(), entry.filemode()));
 299                }
 300                None => { try!(internals.staged.remove(file)); }
 301            }
 302        }
 303    } else {
 304        for file in m.values_of("change").unwrap() {
 305            try!(internals.staged.remove(file))
 306        }
 307    }
 308    internals.write(repo)
 309}
 310
 311fn shead_series_name(shead: &Reference) -> Result<String> {
 312    let shead_target = try!(shead.symbolic_target().ok_or("SHEAD not a symbolic reference"));
 313    if !shead_target.starts_with(SERIES_PREFIX) {
 314        return Err(format!("SHEAD does not start with {}", SERIES_PREFIX).into());
 315    }
 316    Ok(shead_target[SERIES_PREFIX.len()..].to_string())
 317}
 318
 319fn series(out: &mut Output, repo: &Repository) -> Result<()> {
 320    let mut refs = Vec::new();
 321    for prefix in [SERIES_PREFIX, STAGED_PREFIX, WORKING_PREFIX].iter() {
 322        let l = prefix.len();
 323        for r in try!(repo.references_glob(&[prefix, "*"].concat())).names() {
 324            refs.push(try!(r)[l..].to_string());
 325        }
 326    }
 327    let shead_target = if let Some(shead) = try!(notfound_to_none(repo.find_reference(SHEAD_REF))) {
 328        Some(try!(shead_series_name(&shead)))
 329    } else {
 330        None
 331    };
 332    refs.extend(shead_target.clone().into_iter());
 333    refs.sort();
 334    refs.dedup();
 335
 336    let config = try!(try!(repo.config()).snapshot());
 337    try!(out.auto_pager(&config, "branch", false));
 338    let color_current = try!(out.get_color(&config, "branch", "current", "green"));
 339    let color_plain = try!(out.get_color(&config, "branch", "plain", "normal"));
 340    for name in refs.iter() {
 341        let (star, color) = if Some(name) == shead_target.as_ref() {
 342            ('*', color_current)
 343        } else {
 344            (' ', color_plain)
 345        };
 346        let new = if try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", SERIES_PREFIX, name)))).is_none() {
 347            " (new, no commits yet)"
 348        } else {
 349            ""
 350        };
 351        try!(writeln!(out, "{} {}{}", star, color.paint(name as &str), new));
 352    }
 353    if refs.is_empty() {
 354        try!(writeln!(out, "No series; use \"git series start <name>\" to start"));
 355    }
 356    Ok(())
 357}
 358
 359fn start(repo: &Repository, m: &ArgMatches) -> Result<()> {
 360    let head = try!(repo.head());
 361    let head_commit = try!(peel_to_commit(head));
 362    let head_id = head_commit.as_object().id();
 363
 364    let name = m.value_of("name").unwrap();
 365    if try!(Internals::exists(repo, name)) {
 366        return Err(format!("Series {} already exists.\nUse checkout to resume working on an existing patch series.", name).into());
 367    }
 368    let prefixed_name = &[SERIES_PREFIX, name].concat();
 369    try!(repo.reference_symbolic(SHEAD_REF, &prefixed_name, true, &format!("git series start {}", name)));
 370
 371    let internals = try!(Internals::read(repo));
 372    try!(internals.write(repo));
 373
 374    // git status parses this reflog string; the prefix must remain "checkout: moving from ".
 375    try!(repo.reference("HEAD", head_id, true, &format!("checkout: moving from {} to {} (git series start {})", head_id, head_id, name)));
 376    println!("HEAD is now detached at {}", try!(commit_summarize(&repo, head_id)));
 377    Ok(())
 378}
 379
 380fn checkout_tree(repo: &Repository, treeish: &Object) -> Result<()> {
 381    let mut conflicts = Vec::new();
 382    let mut dirty = Vec::new();
 383    let result = {
 384        let mut opts = git2::build::CheckoutBuilder::new();
 385        opts.safe();
 386        opts.notify_on(git2::CHECKOUT_NOTIFICATION_CONFLICT | git2::CHECKOUT_NOTIFICATION_DIRTY);
 387        opts.notify(|t, path, _, _, _| {
 388            let path = path.unwrap().to_owned();
 389            if t == git2::CHECKOUT_NOTIFICATION_CONFLICT {
 390                conflicts.push(path);
 391            } else if t == git2::CHECKOUT_NOTIFICATION_DIRTY {
 392                dirty.push(path);
 393            }
 394            true
 395        });
 396        if isatty::stdout_isatty() {
 397            opts.progress(|_, completed, total| {
 398                let total = total.to_string();
 399                print!("\rChecking out files: {1:0$}/{2}", total.len(), completed, total);
 400            });
 401        }
 402        repo.checkout_tree(treeish, Some(&mut opts))
 403    };
 404    match result {
 405        Err(ref e) if e.code() == git2::ErrorCode::Conflict => {
 406            let mut msg = String::new();
 407            writeln!(msg, "error: Your changes to the following files would be overwritten by checkout:").unwrap();
 408            for path in conflicts {
 409                writeln!(msg, "        {}", path.to_string_lossy()).unwrap();
 410            }
 411            writeln!(msg, "Please, commit your changes or stash them before you switch series.").unwrap();
 412            return Err(msg.into());
 413        }
 414        _ => try!(result),
 415    }
 416    println!("");
 417    if !dirty.is_empty() {
 418        let mut stderr = std::io::stderr();
 419        writeln!(stderr, "Files with changes unaffected by checkout:").unwrap();
 420        for path in dirty {
 421            writeln!(stderr, "        {}", path.to_string_lossy()).unwrap();
 422        }
 423    }
 424    Ok(())
 425}
 426
 427fn checkout(repo: &Repository, m: &ArgMatches) -> Result<()> {
 428    match repo.state() {
 429        git2::RepositoryState::Clean => (),
 430        s => { return Err(format!("{:?} in progress; cannot checkout patch series", s).into()); }
 431    }
 432    let name = m.value_of("name").unwrap();
 433    if !try!(Internals::exists(repo, name)) {
 434        return Err(format!("Series {} does not exist.\nUse \"git series start <name>\" to start a new patch series.", name).into());
 435    }
 436
 437    let internals = try!(Internals::read_series(repo, name));
 438    let new_head_id = try!(try!(internals.working.get("series")).ok_or(format!("Could not find \"series\" in \"{}\"", name))).id();
 439    let new_head = try!(repo.find_commit(new_head_id)).into_object();
 440
 441    try!(checkout_tree(repo, &new_head));
 442
 443    let head = try!(repo.head());
 444    let head_commit = try!(peel_to_commit(head));
 445    let head_id = head_commit.as_object().id();
 446    println!("Previous HEAD position was {}", try!(commit_summarize(&repo, head_id)));
 447
 448    let prefixed_name = &[SERIES_PREFIX, name].concat();
 449    try!(repo.reference_symbolic(SHEAD_REF, &prefixed_name, true, &format!("git series checkout {}", name)));
 450    try!(internals.write(repo));
 451
 452    // git status parses this reflog string; the prefix must remain "checkout: moving from ".
 453    try!(repo.reference("HEAD", new_head_id, true, &format!("checkout: moving from {} to {} (git series checkout {})", head_id, new_head_id, name)));
 454    println!("HEAD is now detached at {}", try!(commit_summarize(&repo, new_head_id)));
 455
 456    Ok(())
 457}
 458
 459fn base(repo: &Repository, m: &ArgMatches) -> Result<()> {
 460    let mut internals = try!(Internals::read(repo));
 461
 462    let current_base_id = match try!(internals.working.get("base")) {
 463        Some(entry) => entry.id(),
 464        _ => zero_oid(),
 465    };
 466
 467    if !m.is_present("delete") && !m.is_present("base") {
 468        if current_base_id.is_zero() {
 469            return Err("Patch series has no base set".into());
 470        } else {
 471            println!("{}", current_base_id);
 472            return Ok(());
 473        }
 474    }
 475
 476    let new_base_id = if m.is_present("delete") {
 477        zero_oid()
 478    } else {
 479        let base = m.value_of("base").unwrap();
 480        let base_object = try!(repo.revparse_single(base));
 481        let base_commit = try!(base_object.peel(ObjectType::Commit));
 482        let base_id = base_commit.id();
 483        let s_working_series = try!(try!(internals.working.get("series")).ok_or("Could not find entry \"series\" in working vesion of current series"));
 484        if base_id != s_working_series.id() && !try!(repo.graph_descendant_of(s_working_series.id(), base_id)) {
 485            return Err(format!("Cannot set base to {}: not an ancestor of the patch series {}", base, s_working_series.id()).into());
 486        }
 487        base_id
 488    };
 489
 490    if current_base_id == new_base_id {
 491        println!("Base unchanged");
 492        return Ok(());
 493    }
 494
 495    if !current_base_id.is_zero() {
 496        println!("Previous base was {}", try!(commit_summarize(&repo, current_base_id)));
 497    }
 498
 499    if new_base_id.is_zero() {
 500        try!(internals.working.remove("base"));
 501        try!(internals.write(repo));
 502        println!("Cleared patch series base");
 503    } else {
 504        try!(internals.working.insert("base", new_base_id, GIT_FILEMODE_COMMIT as i32));
 505        try!(internals.write(repo));
 506        println!("Set patch series base to {}", try!(commit_summarize(&repo, new_base_id)));
 507    }
 508
 509    Ok(())
 510}
 511
 512fn detach(repo: &Repository) -> Result<()> {
 513    match repo.find_reference(SHEAD_REF) {
 514        Ok(mut r) => try!(r.delete()),
 515        Err(_) => { return Err("No current patch series to detach from.".into()); }
 516    }
 517    Ok(())
 518}
 519
 520fn delete(repo: &Repository, m: &ArgMatches) -> Result<()> {
 521    let name = m.value_of("name").unwrap();
 522    if let Ok(shead) = repo.find_reference(SHEAD_REF) {
 523        let shead_target = try!(shead_series_name(&shead));
 524        if shead_target == name {
 525            return Err(format!("Cannot delete the current series \"{}\"; detach first.", name).into());
 526        }
 527    }
 528    if !try!(Internals::delete(repo, name)) {
 529        return Err(format!("Nothing to delete: series \"{}\" does not exist.", name).into());
 530    }
 531    Ok(())
 532}
 533
 534fn get_editor(config: &Config) -> Result<OsString> {
 535    if let Some(e) = env::var_os("GIT_EDITOR") {
 536        return Ok(e);
 537    }
 538    if let Ok(e) = config.get_path("core.editor") {
 539        return Ok(e.into());
 540    }
 541    let terminal_is_dumb = match env::var_os("TERM") {
 542        None => true,
 543        Some(t) => t.as_os_str() == "dumb",
 544    };
 545    if !terminal_is_dumb {
 546        if let Some(e) = env::var_os("VISUAL") {
 547            return Ok(e);
 548        }
 549    }
 550    if let Some(e) = env::var_os("EDITOR") {
 551        return Ok(e);
 552    }
 553    if terminal_is_dumb {
 554        return Err("TERM unset or \"dumb\" but EDITOR unset".into());
 555    }
 556    return Ok("vi".into());
 557}
 558
 559// Get the pager to use; with for_cmd set, get the pager for use by the
 560// specified git command.  If get_pager returns None, don't use a pager.
 561fn get_pager(config: &Config, for_cmd: &str, default: bool) -> Option<OsString> {
 562    if !isatty::stdout_isatty() {
 563        return None;
 564    }
 565    // pager.cmd can contain a boolean (if false, force no pager) or a
 566    // command-specific pager; only treat it as a command if it doesn't parse
 567    // as a boolean.
 568    let maybe_pager = config.get_path(&format!("pager.{}", for_cmd)).ok();
 569    let (cmd_want_pager, cmd_pager) = maybe_pager.map_or((default, None), |p|
 570            if let Ok(b) = Config::parse_bool(&p) {
 571                (b, None)
 572            } else {
 573                (true, Some(p))
 574            }
 575        );
 576    if !cmd_want_pager {
 577        return None;
 578    }
 579    let pager =
 580        if let Some(e) = env::var_os("GIT_PAGER") {
 581            Some(e)
 582        } else if let Some(p) = cmd_pager {
 583            Some(p.into())
 584        } else if let Ok(e) = config.get_path("core.pager") {
 585            Some(e.into())
 586        } else if let Some(e) = env::var_os("PAGER") {
 587            Some(e)
 588        } else {
 589            Some("less".into())
 590        };
 591    pager.and_then(|p| if p.is_empty() || p == OsString::from("cat") { None } else { Some(p) })
 592}
 593
 594/// Construct a Command, using the shell if the command contains shell metachars
 595fn cmd_maybe_shell<S: AsRef<OsStr>>(program: S, args: bool) -> Command {
 596    if program.as_ref().to_string_lossy().contains(|c| SHELL_METACHARS.contains(c)) {
 597        let mut cmd = Command::new("sh");
 598        cmd.arg("-c");
 599        if args {
 600            let mut program_with_args = program.as_ref().to_os_string();
 601            program_with_args.push(" \"$@\"");
 602            cmd.arg(program_with_args).arg(program);
 603        } else {
 604            cmd.arg(program);
 605        }
 606        cmd
 607    } else {
 608        Command::new(program)
 609    }
 610}
 611
 612fn run_editor<S: AsRef<OsStr>>(config: &Config, filename: S) -> Result<()> {
 613    let editor = try!(get_editor(&config));
 614    let editor_status = try!(cmd_maybe_shell(editor, true).arg(&filename).status());
 615    if !editor_status.success() {
 616        return Err(format!("Editor exited with status {}", editor_status).into());
 617    }
 618    Ok(())
 619}
 620
 621struct Output {
 622    pager: Option<std::process::Child>,
 623    include_stderr: bool,
 624}
 625
 626impl Output {
 627    fn new() -> Self {
 628        Output { pager: None, include_stderr: false }
 629    }
 630
 631    fn auto_pager(&mut self, config: &Config, for_cmd: &str, default: bool) -> Result<()> {
 632        if let Some(pager) = get_pager(config, for_cmd, default) {
 633            let mut cmd = cmd_maybe_shell(pager, false);
 634            cmd.stdin(std::process::Stdio::piped());
 635            if env::var_os("LESS").is_none() {
 636                cmd.env("LESS", "FRX");
 637            }
 638            if env::var_os("LV").is_none() {
 639                cmd.env("LV", "-c");
 640            }
 641            let child = try!(cmd.spawn());
 642            self.pager = Some(child);
 643            self.include_stderr = isatty::stderr_isatty();
 644        }
 645        Ok(())
 646    }
 647
 648    // Get a color to write text with, taking git configuration into account.
 649    //
 650    // config: the configuration to determine the color from.
 651    // command: the git command to act like.
 652    // slot: the color "slot" of that git command to act like.
 653    // default: the color to use if not configured.
 654    fn get_color(&self, config: &Config, command: &str, slot: &str, default: &str) -> Result<Style> {
 655        if !cfg!(unix) {
 656            return Ok(Style::new());
 657        }
 658        let color_ui = try!(notfound_to_none(config.get_str("color.ui"))).unwrap_or("auto");
 659        let color_cmd = try!(notfound_to_none(config.get_str(&format!("color.{}", command)))).unwrap_or(color_ui);
 660        if color_cmd == "never" || Config::parse_bool(color_cmd) == Ok(false) {
 661            return Ok(Style::new());
 662        }
 663        if self.pager.is_some() {
 664            let color_pager = try!(notfound_to_none(config.get_bool(&format!("color.pager")))).unwrap_or(true);
 665            if !color_pager {
 666                return Ok(Style::new());
 667            }
 668        } else if !isatty::stdout_isatty() {
 669            return Ok(Style::new());
 670        }
 671        let cfg = format!("color.{}.{}", command, slot);
 672        let color = try!(notfound_to_none(config.get_str(&cfg))).unwrap_or(default);
 673        colorparse::parse(color).map_err(|e| format!("Error parsing {}: {}", cfg, e).into())
 674   }
 675
 676    fn write_err(&mut self, msg: &str) {
 677        if self.include_stderr {
 678            if write!(self, "{}", msg).is_err() {
 679                write!(std::io::stderr(), "{}", msg).unwrap();
 680            }
 681        } else {
 682            write!(std::io::stderr(), "{}", msg).unwrap();
 683        }
 684    }
 685}
 686
 687impl Drop for Output {
 688    fn drop(&mut self) {
 689        if let Some(ref mut child) = self.pager {
 690            let status = child.wait().unwrap();
 691            if !status.success() {
 692                writeln!(std::io::stderr(), "Pager exited with status {}", status).unwrap();
 693            }
 694        }
 695    }
 696}
 697
 698impl IoWrite for Output {
 699    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
 700        match self.pager {
 701            Some(ref mut child) => child.stdin.as_mut().unwrap().write(buf),
 702            None => std::io::stdout().write(buf),
 703        }
 704    }
 705
 706    fn flush(&mut self) -> std::io::Result<()> {
 707        match self.pager {
 708            Some(ref mut child) => child.stdin.as_mut().unwrap().flush(),
 709            None => std::io::stdout().flush(),
 710        }
 711    }
 712}
 713
 714fn get_signature(config: &Config, which: &str) -> Result<git2::Signature<'static>> {
 715    let name_var = ["GIT_", which, "_NAME"].concat();
 716    let email_var = ["GIT_", which, "_EMAIL"].concat();
 717    let which_lc = which.to_lowercase();
 718    let name = try!(env::var(&name_var).or_else(
 719            |_| config.get_string("user.name").or_else(
 720                |_| Err(format!("Could not determine {} name: checked ${} and user.name in git config", which_lc, name_var)))));
 721    let email = try!(env::var(&email_var).or_else(
 722            |_| config.get_string("user.email").or_else(
 723                |_| env::var("EMAIL").or_else(
 724                    |_| Err(format!("Could not determine {} email: checked ${}, user.email in git config, and $EMAIL", which_lc, email_var))))));
 725    Ok(try!(git2::Signature::now(&name, &email)))
 726}
 727
 728fn commit_status(out: &mut Output, repo: &Repository, m: &ArgMatches, do_status: bool) -> Result<()> {
 729    let config = try!(try!(repo.config()).snapshot());
 730    let shead = match repo.find_reference(SHEAD_REF) {
 731        Err(ref e) if e.code() == git2::ErrorCode::NotFound => { println!("No series; use \"git series start <name>\" to start"); return Ok(()); }
 732        result => try!(result),
 733    };
 734    let series_name = try!(shead_series_name(&shead));
 735
 736    if do_status {
 737        try!(out.auto_pager(&config, "status", false));
 738    }
 739    let get_color = |out: &Output, color: &str, default: &str| {
 740        if do_status {
 741            out.get_color(&config, "status", color, default)
 742        } else {
 743            Ok(Style::new())
 744        }
 745    };
 746    let color_normal = Style::new();
 747    let color_header = try!(get_color(out, "header", "normal"));
 748    let color_updated = try!(get_color(out, "updated", "green"));
 749    let color_changed = try!(get_color(out, "changed", "red"));
 750
 751    let write_status = |status: &mut Vec<ansi_term::ANSIString>, diff: &Diff, heading: &str, color: &Style, show_hints: bool, hints: &[&str]| -> Result<bool> {
 752        let mut changes = false;
 753
 754        try!(diff.foreach(&mut |delta, _| {
 755            if !changes {
 756                changes = true;
 757                status.push(color_header.paint(format!("{}\n", heading.to_string())));
 758                if show_hints {
 759                    for hint in hints {
 760                        status.push(color_header.paint(format!("  ({})\n", hint)));
 761                    }
 762                }
 763                status.push(color_normal.paint("\n"));
 764            }
 765            status.push(color_normal.paint("        "));
 766            status.push(color.paint(format!("{:?}:   {}\n", delta.status(), delta.old_file().path().unwrap().to_str().unwrap())));
 767            true
 768        }, None, None, None));
 769
 770        if changes {
 771            status.push(color_normal.paint("\n"));
 772        }
 773
 774        Ok(changes)
 775    };
 776
 777    let mut status = Vec::new();
 778    status.push(color_header.paint(format!("On series {}\n", series_name)));
 779
 780    let mut internals = try!(Internals::read(repo));
 781    let working_tree = try!(repo.find_tree(try!(internals.working.write())));
 782    let staged_tree = try!(repo.find_tree(try!(internals.staged.write())));
 783
 784    let shead_commit = match shead.resolve() {
 785        Ok(r) => Some(try!(peel_to_commit(r))),
 786        Err(ref e) if e.code() == git2::ErrorCode::NotFound => {
 787            status.push(color_header.paint("\nInitial series commit\n"));
 788            None
 789        }
 790        Err(e) => try!(Err(e)),
 791    };
 792    let shead_tree = match shead_commit {
 793        Some(ref c) => Some(try!(c.tree())),
 794        None => None,
 795    };
 796
 797    let commit_all = m.is_present("all");
 798
 799    let (changes, tree, diff) = if commit_all {
 800        let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&working_tree), None));
 801        let changes = try!(write_status(&mut status, &diff, "Changes to be committed:", &color_normal, false, &[]));
 802        if !changes {
 803            status.push(color_normal.paint("nothing to commit; series unchanged\n"));
 804        }
 805        (changes, working_tree, diff)
 806    } else {
 807        let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&staged_tree), None));
 808        let changes_to_be_committed = try!(write_status(&mut status, &diff,
 809                "Changes to be committed:", &color_updated, do_status,
 810                &["use \"git series commit\" to commit",
 811                  "use \"git series unadd <file>...\" to undo add"]));
 812
 813        let diff_not_staged = try!(repo.diff_tree_to_tree(Some(&staged_tree), Some(&working_tree), None));
 814        let changes_not_staged = try!(write_status(&mut status, &diff_not_staged,
 815                "Changes not staged for commit:", &color_changed, do_status,
 816                &["use \"git series add <file>...\" to update what will be committed"]));
 817
 818        if !changes_to_be_committed {
 819            if changes_not_staged {
 820                status.push(color_normal.paint("no changes added to commit (use \"git series add\" or \"git series commit -a\")\n"));
 821            } else {
 822                status.push(color_normal.paint("nothing to commit; series unchanged\n"));
 823            }
 824        }
 825
 826        (changes_to_be_committed, staged_tree, diff)
 827    };
 828
 829    let status = ansi_term::ANSIStrings(&status).to_string();
 830    if do_status || !changes {
 831        if do_status {
 832            try!(write!(out, "{}", status));
 833        } else {
 834            return Err(status.into());
 835        }
 836        return Ok(());
 837    }
 838
 839    // Check that the commit includes the series
 840    let series_id = match tree.get_name("series") {
 841        None => { return Err(concat!("Cannot commit: initial commit must include \"series\"\n",
 842                                     "Use \"git series add series\" or \"git series commit -a\"").into()); }
 843        Some(series) => series.id()
 844    };
 845
 846    // Check that the base is still an ancestor of the series
 847    if let Some(base) = tree.get_name("base") {
 848        if base.id() != series_id && !try!(repo.graph_descendant_of(series_id, base.id())) {
 849            let (base_short_id, base_summary) = try!(commit_summarize_components(&repo, base.id()));
 850            let (series_short_id, series_summary) = try!(commit_summarize_components(&repo, series_id));
 851            return Err(format!(concat!(
 852                       "Cannot commit: base {} is not an ancestor of patch series {}\n",
 853                       "base   {} {}\n",
 854                       "series {} {}"),
 855                       base_short_id, series_short_id,
 856                       base_short_id, base_summary,
 857                       series_short_id, series_summary).into());
 858        }
 859    }
 860
 861    let msg = match m.value_of("m") {
 862        Some(s) => s.to_string(),
 863        None => {
 864            let filename = repo.path().join("SCOMMIT_EDITMSG");
 865            let mut file = try!(File::create(&filename));
 866            try!(write!(file, "{}", COMMIT_MESSAGE_COMMENT));
 867            for line in status.lines() {
 868                if line.is_empty() {
 869                    try!(writeln!(file, "#"));
 870                } else {
 871                    try!(writeln!(file, "# {}", line));
 872                }
 873            }
 874            if m.is_present("verbose") {
 875                try!(writeln!(file, "{}\n{}", SCISSOR_LINE, SCISSOR_COMMENT));
 876                try!(write_diff(&mut file, &DiffColors::plain(), &diff));
 877            }
 878            drop(file);
 879            try!(run_editor(&config, &filename));
 880            let mut file = try!(File::open(&filename));
 881            let mut msg = String::new();
 882            try!(file.read_to_string(&mut msg));
 883            if let Some(scissor_index) = msg.find(SCISSOR_LINE) {
 884                msg.truncate(scissor_index);
 885            }
 886            try!(git2::message_prettify(msg, git2::DEFAULT_COMMENT_CHAR))
 887        }
 888    };
 889    if msg.is_empty() {
 890        return Err("Aborting series commit due to empty commit message.".into());
 891    }
 892
 893    let author = try!(get_signature(&config, "AUTHOR"));
 894    let committer = try!(get_signature(&config, "COMMITTER"));
 895    let mut parents: Vec<Oid> = Vec::new();
 896    // Include all commits from tree, to keep them reachable and fetchable.
 897    for e in tree.iter() {
 898        if e.kind() == Some(ObjectType::Commit) && e.name().unwrap() != "base" {
 899            parents.push(e.id())
 900        }
 901    }
 902    let parents = try!(parents_from_ids(repo, parents));
 903    let parents_ref: Vec<&_> = shead_commit.iter().chain(parents.iter()).collect();
 904    let new_commit_oid = try!(repo.commit(Some(SHEAD_REF), &author, &committer, &msg, &tree, &parents_ref));
 905
 906    if commit_all {
 907        internals.staged = try!(repo.treebuilder(Some(&tree)));
 908        try!(internals.write(repo));
 909    }
 910
 911    let (new_commit_short_id, new_commit_summary) = try!(commit_summarize_components(&repo, new_commit_oid));
 912    try!(writeln!(out, "[{} {}] {}", series_name, new_commit_short_id, new_commit_summary));
 913
 914    Ok(())
 915}
 916
 917fn cover(repo: &Repository, m: &ArgMatches) -> Result<()> {
 918    let mut internals = try!(Internals::read(repo));
 919
 920    let (working_cover_id, working_cover_content) = match try!(internals.working.get("cover")) {
 921        None => (zero_oid(), String::new()),
 922        Some(entry) => (entry.id(), try!(std::str::from_utf8(try!(repo.find_blob(entry.id())).content())).to_string()),
 923    };
 924
 925    if m.is_present("delete") {
 926        if working_cover_id.is_zero() {
 927            return Err("No cover to delete".into());
 928        }
 929        try!(internals.working.remove("cover"));
 930        try!(internals.write(repo));
 931        println!("Deleted cover letter");
 932        return Ok(());
 933    }
 934
 935    let filename = repo.path().join("COVER_EDITMSG");
 936    let mut file = try!(File::create(&filename));
 937    if working_cover_content.is_empty() {
 938        try!(write!(file, "{}", COVER_LETTER_COMMENT));
 939    } else {
 940        try!(write!(file, "{}", working_cover_content));
 941    }
 942    drop(file);
 943    let config = try!(repo.config());
 944    try!(run_editor(&config, &filename));
 945    let mut file = try!(File::open(&filename));
 946    let mut msg = String::new();
 947    try!(file.read_to_string(&mut msg));
 948    let msg = try!(git2::message_prettify(msg, git2::DEFAULT_COMMENT_CHAR));
 949    if msg.is_empty() {
 950        return Err("Empty cover letter; not changing.\n(To delete the cover letter, use \"git series cover -d\".)".into());
 951    }
 952
 953    let new_cover_id = try!(repo.blob(msg.as_bytes()));
 954    if new_cover_id == working_cover_id {
 955        println!("Cover letter unchanged");
 956    } else {
 957        try!(internals.working.insert("cover", new_cover_id, GIT_FILEMODE_BLOB as i32));
 958        try!(internals.write(repo));
 959        println!("Updated cover letter");
 960    }
 961
 962    Ok(())
 963}
 964
 965fn cp_mv(repo: &Repository, m: &ArgMatches, mv: bool) -> Result<()> {
 966    let shead_target = if let Some(shead) = try!(notfound_to_none(repo.find_reference(SHEAD_REF))) {
 967        Some(try!(shead_series_name(&shead)))
 968    } else {
 969        None
 970    };
 971    let mut source_dest = m.values_of("source_dest").unwrap();
 972    let dest = source_dest.next_back().unwrap();
 973    let (update_shead, source) = match source_dest.next_back().map(String::from) {
 974        Some(name) => (shead_target.as_ref() == Some(&name), name),
 975        None => (true, try!(shead_target.ok_or("No current series"))),
 976    };
 977
 978    if try!(Internals::exists(&repo, dest)) {
 979        return Err(format!("The destination series \"{}\" already exists", dest).into());
 980    }
 981    if !try!(Internals::copy(&repo, &source, &dest)) {
 982        return Err(format!("The source series \"{}\" does not exist", source).into());
 983    }
 984
 985    if mv {
 986        if update_shead {
 987            let prefixed_dest = &[SERIES_PREFIX, dest].concat();
 988            try!(repo.reference_symbolic(SHEAD_REF, &prefixed_dest, true, &format!("git series mv {} {}", source, dest)));
 989        }
 990        try!(Internals::delete(&repo, &source));
 991    }
 992
 993    Ok(())
 994}
 995
 996fn date_822(t: git2::Time) -> String {
 997    let offset = chrono::offset::fixed::FixedOffset::east(t.offset_minutes()*60);
 998    let datetime = offset.timestamp(t.seconds(), 0);
 999    datetime.to_rfc2822()
1000}
1001
1002fn shortlog(commits: &mut [Commit]) -> String {
1003    let mut s = String::new();
1004    let mut author_map = std::collections::HashMap::new();
1005
1006    for mut commit in commits {
1007        let author = commit.author().name().unwrap().to_string();
1008        author_map.entry(author).or_insert(Vec::new()).push(commit.summary().unwrap().to_string());
1009    }
1010
1011    let mut authors: Vec<_> = author_map.keys().collect();
1012    authors.sort();
1013    let mut first = true;
1014    for author in authors {
1015        if first {
1016            first = false;
1017        } else {
1018            writeln!(s, "").unwrap();
1019        }
1020        let summaries = author_map.get(author).unwrap();
1021        writeln!(s, "{} ({}):", author, summaries.len()).unwrap();
1022        for summary in summaries {
1023            writeln!(s, "  {}", summary).unwrap();
1024        }
1025    }
1026
1027    s
1028}
1029
1030fn ascii_isalnum(c: char) -> bool {
1031    (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1032}
1033
1034fn sanitize_summary(summary: &str) -> String {
1035    let mut s = String::with_capacity(summary.len());
1036    let mut prev_dot = false;
1037    let mut need_space = false;
1038    for c in summary.chars() {
1039        if ascii_isalnum(c) || c == '_' || c == '.' {
1040            if need_space {
1041                s.push('-');
1042                need_space = false;
1043            }
1044            if !(prev_dot && c == '.') {
1045                s.push(c);
1046            }
1047        } else {
1048            if !s.is_empty() {
1049                need_space = true;
1050            }
1051        }
1052        prev_dot = c == '.';
1053    }
1054    let end = s.trim_right_matches(|c| c == '.' || c == '-').len();
1055    s.truncate(end);
1056    s
1057}
1058
1059#[test]
1060fn test_sanitize_summary() {
1061    let tests = vec![
1062        ("", ""),
1063        ("!!!!!", ""),
1064        ("Test", "Test"),
1065        ("Test case", "Test-case"),
1066        ("Test    case", "Test-case"),
1067        ("    Test    case    ", "Test-case"),
1068        ("...Test...case...", ".Test.case"),
1069        ("...Test...case.!!", ".Test.case"),
1070        (".!.Test.!.case.!.", ".-.Test.-.case"),
1071    ];
1072    for (summary, sanitized) in tests {
1073        assert_eq!(sanitize_summary(summary), sanitized.to_string());
1074    }
1075}
1076
1077fn split_message(message: &str) -> (&str, &str) {
1078    let mut iter = message.splitn(2, '\n');
1079    let subject = iter.next().unwrap().trim_right();
1080    let body = iter.next().map(|s| s.trim_left()).unwrap_or("");
1081    (subject, body)
1082}
1083
1084struct DiffColors {
1085    commit: Style,
1086    meta: Style,
1087    frag: Style,
1088    func: Style,
1089    context: Style,
1090    old: Style,
1091    new: Style,
1092}
1093
1094impl DiffColors {
1095    fn plain() -> Self {
1096        DiffColors {
1097            commit: Style::new(),
1098            meta: Style::new(),
1099            frag: Style::new(),
1100            func: Style::new(),
1101            context: Style::new(),
1102            old: Style::new(),
1103            new: Style::new(),
1104        }
1105    }
1106
1107    fn new(out: &Output, config: &Config) -> Result<Self> {
1108        Ok(DiffColors {
1109            commit: try!(out.get_color(&config, "diff", "commit", "yellow")),
1110            meta: try!(out.get_color(&config, "diff", "meta", "bold")),
1111            frag: try!(out.get_color(&config, "diff", "frag", "cyan")),
1112            func: try!(out.get_color(&config, "diff", "func", "normal")),
1113            context: try!(out.get_color(&config, "diff", "context", "normal")),
1114            old: try!(out.get_color(&config, "diff", "old", "red")),
1115            new: try!(out.get_color(&config, "diff", "new", "green")),
1116        })
1117    }
1118}
1119
1120fn diffstat(diff: &Diff) -> Result<String> {
1121    let stats = try!(diff.stats());
1122    let stats_buf = try!(stats.to_buf(git2::DIFF_STATS_FULL|git2::DIFF_STATS_INCLUDE_SUMMARY, 72));
1123    Ok(stats_buf.as_str().unwrap().to_string())
1124}
1125
1126fn write_diff<W: IoWrite>(f: &mut W, colors: &DiffColors, diff: &Diff) -> Result<()> {
1127    let mut err = Ok(());
1128    let normal = Style::new();
1129    try!(diff.print(git2::DiffFormat::Patch, |_, _, l| {
1130        err = || -> Result<()> {
1131            let o = l.origin();
1132            let style = match o {
1133                ' '|'=' => colors.context,
1134                '-'|'<' => colors.old,
1135                '+'|'>' => colors.new,
1136                'F' => colors.meta,
1137                'H' => colors.frag,
1138                _ => normal,
1139            };
1140            let obyte = [o as u8];
1141            let mut v = Vec::new();
1142            if o == '+' || o == '-' || o == ' ' {
1143                v.push(style.paint(&obyte[..]));
1144            }
1145            if o == 'H' {
1146                // Split frag and func
1147                let line = l.content();
1148                let at = &|&(_,&c): &(usize, &u8)| c == b'@';
1149                let not_at = &|&(_,&c): &(usize, &u8)| c != b'@';
1150                match line.iter().enumerate().skip_while(at).skip_while(not_at).skip_while(at).nth(1).unwrap_or((0,&b'\n')) {
1151                    (_,&c) if c == b'\n' => v.push(style.paint(&line[..line.len()-1])),
1152                    (pos,_) => {
1153                        v.push(style.paint(&line[..pos-1]));
1154                        v.push(normal.paint(" ".as_bytes()));
1155                        v.push(colors.func.paint(&line[pos..line.len()-1]));
1156                    },
1157                }
1158                v.push(normal.paint("\n".as_bytes()));
1159            } else {
1160                // The less pager resets ANSI colors at each newline, so emit colors separately for
1161                // each line.
1162                for (n, line) in l.content().split(|c| *c == b'\n').enumerate() {
1163                    if n != 0 {
1164                        v.push(normal.paint("\n".as_bytes()));
1165                    }
1166                    if !line.is_empty() {
1167                        v.push(style.paint(line));
1168                    }
1169                }
1170            }
1171            try!(ansi_term::ANSIByteStrings(&v).write_to(f));
1172            Ok(())
1173        }();
1174        err.is_ok()
1175    }));
1176    err
1177}
1178
1179fn mail_signature() -> String {
1180    format!("-- \ngit-series {}", crate_version!())
1181}
1182
1183fn ensure_space(s: &str) -> &'static str {
1184    if s.is_empty() || s.ends_with(' ') {
1185        ""
1186    } else {
1187        " "
1188    }
1189}
1190
1191fn ensure_nl(s: &str) -> &'static str {
1192    if !s.ends_with('\n') {
1193        "\n"
1194    } else {
1195        ""
1196    }
1197}
1198
1199fn format(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1200    let config = try!(try!(repo.config()).snapshot());
1201    let to_stdout = m.is_present("stdout");
1202    let no_from = m.is_present("no-from");
1203
1204    let shead_commit = try!(peel_to_commit(try!(try!(repo.find_reference(SHEAD_REF)).resolve())));
1205    let stree = try!(shead_commit.tree());
1206
1207    let series = try!(stree.get_name("series").ok_or("Internal error: series did not contain \"series\""));
1208    let base = try!(stree.get_name("base").ok_or("Cannot format series; no base set.\nUse \"git series base\" to set base."));
1209
1210    let mut revwalk = try!(repo.revwalk());
1211    revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1212    try!(revwalk.push(series.id()));
1213    try!(revwalk.hide(base.id()));
1214    let mut commits: Vec<Commit> = try!(revwalk.map(|c| {
1215        let id = try!(c);
1216        let commit = try!(repo.find_commit(id));
1217        if commit.parent_ids().count() > 1 {
1218            return Err(format!("Error: cannot format merge commit as patch:\n{}", try!(commit_summarize(repo, id))).into());
1219        }
1220        Ok(commit)
1221    }).collect::<Result<_>>());
1222    if commits.is_empty() {
1223        return Err("No patches to format; series and base identical.".into());
1224    }
1225
1226    let committer = try!(get_signature(&config, "COMMITTER"));
1227    let committer_name = committer.name().unwrap();
1228    let committer_email = committer.email().unwrap();
1229    let message_id_suffix = format!("{}.git-series.{}", committer.when().seconds(), committer_email);
1230
1231    let cover_entry = stree.get_name("cover");
1232    let mut in_reply_to_message_id = m.value_of("in-reply-to").map(|v| {
1233        format!("{}{}{}",
1234                if v.starts_with('<') { "" } else { "<" },
1235                v,
1236                if v.ends_with('>') { "" } else { ">" })
1237    });
1238
1239    let version = m.value_of("reroll-count");
1240    let subject_prefix = if m.is_present("rfc") {
1241        "RFC PATCH"
1242    } else {
1243        m.value_of("subject-prefix").unwrap_or("PATCH")
1244    };
1245    let subject_patch = version.map_or(
1246            subject_prefix.to_string(),
1247            |n| format!("{}{}v{}", subject_prefix, ensure_space(&subject_prefix), n));
1248    let file_prefix = version.map_or("".to_string(), |n| format!("v{}-", n));
1249
1250    let signature = mail_signature();
1251
1252    if to_stdout {
1253        try!(out.auto_pager(&config, "format-patch", true));
1254    }
1255    let diffcolors = if to_stdout {
1256        try!(DiffColors::new(out, &config))
1257    } else {
1258        DiffColors::plain()
1259    };
1260    let mut out : Box<IoWrite> = if to_stdout {
1261        Box::new(out)
1262    } else {
1263        Box::new(std::io::stdout())
1264    };
1265    let patch_file = |name: &str| -> Result<Box<IoWrite>> {
1266        let name = format!("{}{}", file_prefix, name);
1267        println!("{}", name);
1268        Ok(Box::new(try!(File::create(name))))
1269    };
1270
1271    if let Some(ref entry) = cover_entry {
1272        let cover_blob = try!(repo.find_blob(entry.id()));
1273        let content = try!(std::str::from_utf8(cover_blob.content())).to_string();
1274        let (subject, body) = split_message(&content);
1275
1276        let series_tree = try!(repo.find_commit(series.id())).tree().unwrap();
1277        let base_tree = try!(repo.find_commit(base.id())).tree().unwrap();
1278        let diff = try!(repo.diff_tree_to_tree(Some(&base_tree), Some(&series_tree), None));
1279        let stats = try!(diffstat(&diff));
1280
1281        if !to_stdout {
1282            out = try!(patch_file("0000-cover-letter.patch"));
1283        }
1284        try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", shead_commit.id()));
1285        let cover_message_id = format!("<cover.{}.{}>", shead_commit.id(), message_id_suffix);
1286        try!(writeln!(out, "Message-Id: {}", cover_message_id));
1287        if let Some(ref message_id) = in_reply_to_message_id {
1288            try!(writeln!(out, "In-Reply-To: {}", message_id));
1289            try!(writeln!(out, "References: {}", message_id));
1290        }
1291        in_reply_to_message_id = Some(cover_message_id);
1292        try!(writeln!(out, "From: {} <{}>", committer_name, committer_email));
1293        try!(writeln!(out, "Date: {}", date_822(committer.when())));
1294        try!(writeln!(out, "Subject: [{}{}0/{}] {}\n", subject_patch, ensure_space(&subject_patch), commits.len(), subject));
1295        if !body.is_empty() {
1296            try!(writeln!(out, "{}", body));
1297        }
1298        try!(writeln!(out, "{}", shortlog(&mut commits)));
1299        try!(writeln!(out, "{}", stats));
1300        try!(writeln!(out, "base-commit: {}", base.id()));
1301        try!(writeln!(out, "{}", signature));
1302    }
1303
1304    for (commit_num, commit) in commits.iter().enumerate() {
1305        let first_mail = commit_num == 0 && cover_entry.is_none();
1306        if to_stdout && !first_mail {
1307            try!(writeln!(out, ""));
1308        }
1309
1310        let message = commit.message().unwrap();
1311        let (subject, body) = split_message(message);
1312        let commit_id = commit.id();
1313        let commit_author = commit.author();
1314        let commit_author_name = commit_author.name().unwrap();
1315        let commit_author_email = commit_author.email().unwrap();
1316        let summary_sanitized = sanitize_summary(&subject);
1317        let this_message_id = format!("<{}.{}>", commit_id, message_id_suffix);
1318        let parent = try!(commit.parent(0));
1319        let diff = try!(repo.diff_tree_to_tree(Some(&parent.tree().unwrap()), Some(&commit.tree().unwrap()), None));
1320        let stats = try!(diffstat(&diff));
1321
1322        if !to_stdout {
1323            out = try!(patch_file(&format!("{:04}-{}.patch", commit_num+1, summary_sanitized)));
1324        }
1325        try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", commit_id));
1326        try!(writeln!(out, "Message-Id: {}", this_message_id));
1327        if let Some(ref message_id) = in_reply_to_message_id {
1328            try!(writeln!(out, "In-Reply-To: {}", message_id));
1329            try!(writeln!(out, "References: {}", message_id));
1330        }
1331        if first_mail {
1332            in_reply_to_message_id = Some(this_message_id);
1333        }
1334        if no_from {
1335            try!(writeln!(out, "From: {} <{}>", commit_author_name, commit_author_email));
1336        } else {
1337            try!(writeln!(out, "From: {} <{}>", committer_name, committer_email));
1338        }
1339        try!(writeln!(out, "Date: {}", date_822(commit_author.when())));
1340        let prefix = if commits.len() == 1 && cover_entry.is_none() {
1341            if subject_patch.is_empty() {
1342                "".to_string()
1343            } else {
1344                format!("[{}] ", subject_patch)
1345            }
1346        } else {
1347            format!("[{}{}{}/{}] ", subject_patch, ensure_space(&subject_patch), commit_num+1, commits.len())
1348        };
1349        try!(writeln!(out, "Subject: {}{}\n", prefix, subject));
1350
1351        if !no_from && (commit_author_name != committer_name || commit_author_email != committer_email) {
1352            try!(writeln!(out, "From: {} <{}>\n", commit_author_name, commit_author_email));
1353        }
1354        if !body.is_empty() {
1355            try!(write!(out, "{}{}", body, ensure_nl(&body)));
1356        }
1357        try!(writeln!(out, "---"));
1358        try!(writeln!(out, "{}", stats));
1359        try!(write_diff(&mut out, &diffcolors, &diff));
1360        if first_mail {
1361            try!(writeln!(out, "\nbase-commit: {}", base.id()));
1362        }
1363        try!(writeln!(out, "{}", signature));
1364    }
1365
1366    Ok(())
1367}
1368
1369fn log(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1370    let config = try!(try!(repo.config()).snapshot());
1371    try!(out.auto_pager(&config, "log", true));
1372    let diffcolors = try!(DiffColors::new(out, &config));
1373
1374    let shead_id = try!(repo.refname_to_id(SHEAD_REF));
1375    let mut hidden_ids = std::collections::HashSet::new();
1376    let mut commit_stack = Vec::new();
1377    commit_stack.push(shead_id);
1378    while let Some(oid) = commit_stack.pop() {
1379        let commit = try!(repo.find_commit(oid));
1380        let tree = try!(commit.tree());
1381        for parent_id in commit.parent_ids() {
1382            if tree.get_id(parent_id).is_some() {
1383                hidden_ids.insert(parent_id);
1384            } else {
1385                commit_stack.push(parent_id);
1386            }
1387        }
1388    }
1389
1390    let mut revwalk = try!(repo.revwalk());
1391    revwalk.set_sorting(git2::SORT_TOPOLOGICAL);
1392    try!(revwalk.push(shead_id));
1393    for id in hidden_ids {
1394        try!(revwalk.hide(id));
1395    }
1396
1397    let show_diff = m.is_present("patch");
1398
1399    let mut first = true;
1400    for oid in revwalk {
1401        if first {
1402            first = false;
1403        } else {
1404            try!(writeln!(out, ""));
1405        }
1406        let oid = try!(oid);
1407        let commit = try!(repo.find_commit(oid));
1408        let author = commit.author();
1409
1410        try!(writeln!(out, "{}", diffcolors.commit.paint(format!("commit {}", oid))));
1411        try!(writeln!(out, "Author: {} <{}>", author.name().unwrap(), author.email().unwrap()));
1412        try!(writeln!(out, "Date:   {}\n", date_822(author.when())));
1413        for line in commit.message().unwrap().lines() {
1414            try!(writeln!(out, "    {}", line));
1415        }
1416
1417        if show_diff {
1418            let tree = try!(commit.tree());
1419            let parent_ids: Vec<_> = commit.parent_ids().take_while(|parent_id| tree.get_id(*parent_id).is_none()).collect();
1420
1421            try!(writeln!(out, ""));
1422            if parent_ids.len() > 1 {
1423                try!(writeln!(out, "(Diffs of series merge commits not yet supported)"));
1424            } else {
1425                let parent_tree = if parent_ids.len() == 0 {
1426                    None
1427                } else {
1428                    Some(try!(try!(repo.find_commit(parent_ids[0])).tree()))
1429                };
1430                let diff = try!(repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None));
1431                try!(write_diff(out, &diffcolors, &diff));
1432            }
1433        }
1434    }
1435
1436    Ok(())
1437}
1438
1439fn rebase(repo: &Repository, m: &ArgMatches) -> Result<()> {
1440    match repo.state() {
1441        git2::RepositoryState::Clean => (),
1442        git2::RepositoryState::RebaseMerge if repo.path().join("rebase-merge").join("git-series").exists() => {
1443            return Err("git series rebase already in progress.\nUse \"git rebase --continue\" or \"git rebase --abort\".".into());
1444        },
1445        s => { return Err(format!("{:?} in progress; cannot rebase", s).into()); }
1446    }
1447
1448    let internals = try!(Internals::read(repo));
1449    let series = try!(try!(internals.working.get("series")).ok_or("Could not find entry \"series\" in working index"));
1450    let base = try!(try!(internals.working.get("base")).ok_or("Cannot rebase series; no base set.\nUse \"git series base\" to set base."));
1451    if series.id() == base.id() {
1452        return Err("No patches to rebase; series and base identical.".into());
1453    } else if !try!(repo.graph_descendant_of(series.id(), base.id())) {
1454        return Err(format!("Cannot rebase: current base {} not an ancestor of series {}", base.id(), series.id()).into());
1455    }
1456
1457    // Check for unstaged or uncommitted changes before attempting to rebase.
1458    let series_commit = try!(repo.find_commit(series.id()));
1459    let series_tree = try!(series_commit.tree());
1460    let mut unclean = String::new();
1461    if !diff_empty(&try!(repo.diff_tree_to_index(Some(&series_tree), None, None))) {
1462        writeln!(unclean, "Cannot rebase: you have unstaged changes.").unwrap();
1463    }
1464    if !diff_empty(&try!(repo.diff_index_to_workdir(None, None))) {
1465        if unclean.is_empty() {
1466            writeln!(unclean, "Cannot rebase: your index contains uncommitted changes.").unwrap();
1467        } else {
1468            writeln!(unclean, "Additionally, your index contains uncommitted changes.").unwrap();
1469        }
1470    }
1471    if !unclean.is_empty() {
1472        return Err(unclean.into());
1473    }
1474
1475    let mut revwalk = try!(repo.revwalk());
1476    revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1477    try!(revwalk.push(series.id()));
1478    try!(revwalk.hide(base.id()));
1479    let commits: Vec<Commit> = try!(revwalk.map(|c| {
1480        let id = try!(c);
1481        let mut commit = try!(repo.find_commit(id));
1482        if commit.parent_ids().count() > 1 {
1483            return Err(format!("Error: cannot rebase merge commit:\n{}", try!(commit_obj_summarize(&mut commit))).into());
1484        }
1485        Ok(commit)
1486    }).collect::<Result<_>>());
1487
1488    let interactive = m.is_present("interactive");
1489    let onto = match m.value_of("onto") {
1490        None => None,
1491        Some(onto) => {
1492            let obj = try!(repo.revparse_single(onto));
1493            let commit = try!(obj.peel(ObjectType::Commit));
1494            Some(commit.id())
1495        },
1496    };
1497
1498    let newbase = onto.unwrap_or(base.id());
1499    if newbase == base.id() && !interactive {
1500        println!("Nothing to do: base unchanged and not rebasing interactively");
1501        return Ok(());
1502    }
1503
1504    let (base_short, _) = try!(commit_summarize_components(&repo, base.id()));
1505    let (newbase_short, _) = try!(commit_summarize_components(&repo, newbase));
1506    let (series_short, _) = try!(commit_summarize_components(&repo, series.id()));
1507
1508    let newbase_obj = try!(repo.find_commit(newbase)).into_object();
1509
1510    let dir = try!(TempDir::new_in(repo.path(), "rebase-merge"));
1511    let final_path = repo.path().join("rebase-merge");
1512    let mut create = std::fs::OpenOptions::new();
1513    create.write(true).create_new(true);
1514
1515    try!(create.open(dir.path().join("git-series")));
1516    try!(create.open(dir.path().join("quiet")));
1517    try!(create.open(dir.path().join("interactive")));
1518
1519    let mut head_name_file = try!(create.open(dir.path().join("head-name")));
1520    try!(writeln!(head_name_file, "detached HEAD"));
1521
1522    let mut onto_file = try!(create.open(dir.path().join("onto")));
1523    try!(writeln!(onto_file, "{}", newbase));
1524
1525    let mut orig_head_file = try!(create.open(dir.path().join("orig-head")));
1526    try!(writeln!(orig_head_file, "{}", series.id()));
1527
1528    let git_rebase_todo_filename = dir.path().join("git-rebase-todo");
1529    let mut git_rebase_todo = try!(create.open(&git_rebase_todo_filename));
1530    for mut commit in commits {
1531        try!(writeln!(git_rebase_todo, "pick {}", try!(commit_obj_summarize(&mut commit))));
1532    }
1533    if let Some(onto) = onto {
1534        try!(writeln!(git_rebase_todo, "exec git series base {}", onto));
1535    }
1536    try!(writeln!(git_rebase_todo, "\n# Rebase {}..{} onto {}", base_short, series_short, newbase_short));
1537    try!(write!(git_rebase_todo, "{}", REBASE_COMMENT));
1538    drop(git_rebase_todo);
1539
1540    // Interactive editor if interactive {
1541    if interactive {
1542        let config = try!(repo.config());
1543        try!(run_editor(&config, &git_rebase_todo_filename));
1544        let mut file = try!(File::open(&git_rebase_todo_filename));
1545        let mut todo = String::new();
1546        try!(file.read_to_string(&mut todo));
1547        let todo = try!(git2::message_prettify(todo, git2::DEFAULT_COMMENT_CHAR));
1548        if todo.is_empty() {
1549            return Err("Nothing to do".into());
1550        }
1551    }
1552
1553    // Avoid races by not calling .into_path until after the rename succeeds.
1554    try!(std::fs::rename(dir.path(), final_path));
1555    dir.into_path();
1556
1557    try!(checkout_tree(repo, &newbase_obj));
1558    try!(repo.reference("HEAD", newbase, true, &format!("rebase -i (start): checkout {}", newbase)));
1559
1560    let status = try!(Command::new("git").arg("rebase").arg("--continue").status());
1561    if !status.success() {
1562        return Err(format!("git rebase --continue exited with status {}", status).into());
1563    }
1564
1565    Ok(())
1566}
1567
1568fn req(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1569    let config = try!(try!(repo.config()).snapshot());
1570    let shead = try!(repo.find_reference(SHEAD_REF));
1571    let shead_commit = try!(peel_to_commit(try!(shead.resolve())));
1572    let stree = try!(shead_commit.tree());
1573
1574    let series = try!(stree.get_name("series").ok_or("Internal error: series did not contain \"series\""));
1575    let series_id = series.id();
1576    let mut series_commit = try!(repo.find_commit(series_id));
1577    let base = try!(stree.get_name("base").ok_or("Cannot request pull; no base set.\nUse \"git series base\" to set base."));
1578    let mut base_commit = try!(repo.find_commit(base.id()));
1579
1580    let (cover_content, subject, cover_body) = if let Some(entry) = stree.get_name("cover") {
1581        let cover_blob = try!(repo.find_blob(entry.id()));
1582        let content = try!(std::str::from_utf8(cover_blob.content())).to_string();
1583        let (subject, body) = split_message(&content);
1584        (Some(content.to_string()), subject.to_string(), Some(body.to_string()))
1585    } else {
1586        (None, try!(shead_series_name(&shead)), None)
1587    };
1588
1589    let url = m.value_of("url").unwrap();
1590    let tag = m.value_of("tag").unwrap();
1591    let full_tag = format!("refs/tags/{}", tag);
1592    let full_tag_peeled = format!("{}^{{}}", full_tag);
1593    let full_head = format!("refs/heads/{}", tag);
1594    let mut remote = try!(repo.remote_anonymous(url));
1595    try!(remote.connect(git2::Direction::Fetch).map_err(|e| format!("Could not connect to remote repository {}\n{}", url, e)));
1596    let remote_heads = try!(remote.list());
1597
1598    /* Find the requested name as either a tag or head */
1599    let mut opt_remote_tag = None;
1600    let mut opt_remote_tag_peeled = None;
1601    let mut opt_remote_head = None;
1602    for h in remote_heads {
1603        if h.name() == full_tag {
1604            opt_remote_tag = Some(h.oid());
1605        } else if h.name() == full_tag_peeled {
1606            opt_remote_tag_peeled = Some(h.oid());
1607        } else if h.name() == full_head {
1608            opt_remote_head = Some(h.oid());
1609        }
1610    }
1611    let (msg, extra_body, remote_pull_name) = match (opt_remote_tag, opt_remote_tag_peeled, opt_remote_head) {
1612        (Some(remote_tag), Some(remote_tag_peeled), _) => {
1613            if remote_tag_peeled != series_id {
1614                return Err(format!("Remote tag {} does not refer to series {}", tag, series_id).into());
1615            }
1616            let local_tag = try!(repo.find_tag(remote_tag).map_err(|e|
1617                    format!("Could not find remote tag {} ({}) in local repository: {}", tag, remote_tag, e)));
1618            let mut local_tag_msg = local_tag.message().unwrap().to_string();
1619            if let Some(sig_index) = local_tag_msg.find("-----BEGIN PGP ") {
1620                local_tag_msg.truncate(sig_index);
1621            }
1622            let extra_body = match cover_content {
1623                Some(ref content) if !local_tag_msg.contains(content) => cover_body,
1624                _ => None,
1625            };
1626            (Some(local_tag_msg), extra_body, full_tag)
1627        },
1628        (Some(remote_tag), None, _) => {
1629            if remote_tag != series_id {
1630                return Err(format!("Remote unannotated tag {} does not refer to series {}", tag, series_id).into());
1631            }
1632            (cover_content, None, full_tag)
1633        }
1634        (_, _, Some(remote_head)) => {
1635            if remote_head != series_id {
1636                return Err(format!("Remote branch {} does not refer to series {}", tag, series_id).into());
1637            }
1638            (cover_content, None, full_head)
1639        },
1640        _ => {
1641            return Err(format!("Remote does not have either a tag or branch named {}", tag).into())
1642        }
1643    };
1644
1645    let commit_subject_date = |commit: &mut Commit| -> String {
1646        let date = date_822(commit.author().when());
1647        let summary = commit.summary().unwrap();
1648        format!("  {} ({})", summary, date)
1649    };
1650
1651    let mut revwalk = try!(repo.revwalk());
1652    revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1653    try!(revwalk.push(series_id));
1654    try!(revwalk.hide(base.id()));
1655    let mut commits: Vec<Commit> = try!(revwalk.map(|c| {
1656        Ok(try!(repo.find_commit(try!(c))))
1657    }).collect::<Result<_>>());
1658    if commits.is_empty() {
1659        return Err("No patches to request pull of; series and base identical.".into());
1660    }
1661
1662    let author = try!(get_signature(&config, "AUTHOR"));
1663    let author_email = author.email().unwrap();
1664    let message_id = format!("<pull.{}.{}.git-series.{}>", shead_commit.id(), author.when().seconds(), author_email);
1665
1666    let diff = try!(repo.diff_tree_to_tree(Some(&base_commit.tree().unwrap()), Some(&series_commit.tree().unwrap()), None));
1667    let stats = try!(diffstat(&diff));
1668
1669    try!(out.auto_pager(&config, "request-pull", true));
1670    let diffcolors = try!(DiffColors::new(out, &config));
1671
1672    try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", shead_commit.id()));
1673    try!(writeln!(out, "Message-Id: {}", message_id));
1674    try!(writeln!(out, "From: {} <{}>", author.name().unwrap(), author_email));
1675    try!(writeln!(out, "Date: {}", date_822(author.when())));
1676    try!(writeln!(out, "Subject: [GIT PULL] {}\n", subject));
1677    if let Some(extra_body) = extra_body {
1678        try!(writeln!(out, "{}", extra_body));
1679    }
1680    try!(writeln!(out, "The following changes since commit {}:\n", base.id()));
1681    try!(writeln!(out, "{}\n", commit_subject_date(&mut base_commit)));
1682    try!(writeln!(out, "are available in the git repository at:\n"));
1683    try!(writeln!(out, "  {} {}\n", url, remote_pull_name));
1684    try!(writeln!(out, "for you to fetch changes up to {}:\n", series.id()));
1685    try!(writeln!(out, "{}\n", commit_subject_date(&mut series_commit)));
1686    try!(writeln!(out, "----------------------------------------------------------------"));
1687    if let Some(msg) = msg {
1688        try!(writeln!(out, "{}", msg));
1689        try!(writeln!(out, "----------------------------------------------------------------"));
1690    }
1691    try!(writeln!(out, "{}", shortlog(&mut commits)));
1692    try!(writeln!(out, "{}", stats));
1693    if m.is_present("patch") {
1694        try!(write_diff(out, &diffcolors, &diff));
1695    }
1696    try!(writeln!(out, "{}", mail_signature()));
1697
1698    Ok(())
1699}
1700
1701fn main() {
1702    let m = App::new("git-series")
1703            .bin_name("git series")
1704            .about("Track patch series in git")
1705            .author("Josh Triplett <josh@joshtriplett.org>")
1706            .version(crate_version!())
1707            .global_setting(AppSettings::ColoredHelp)
1708            .global_setting(AppSettings::UnifiedHelpMessage)
1709            .global_setting(AppSettings::VersionlessSubcommands)
1710            .subcommands(vec![
1711                SubCommand::with_name("add")
1712                    .about("Add changes to the index for the next series commit")
1713                    .arg_from_usage("<change>... 'Changes to add (\"series\", \"base\", \"cover\")'"),
1714                SubCommand::with_name("base")
1715                    .about("Get or set the base commit for the patch series")
1716                    .arg(Arg::with_name("base").help("Base commit").conflicts_with("delete"))
1717                    .arg_from_usage("-d, --delete 'Clear patch series base'"),
1718                SubCommand::with_name("checkout")
1719                    .about("Resume work on a patch series; check out the current version")
1720                    .arg_from_usage("<name> 'Patch series to check out'"),
1721                SubCommand::with_name("commit")
1722                    .about("Record changes to the patch series")
1723                    .arg_from_usage("-a, --all 'Commit all changes'")
1724                    .arg_from_usage("-m [msg] 'Commit message'")
1725                    .arg_from_usage("-v, --verbose 'Show diff when preparing commit message'"),
1726                SubCommand::with_name("cover")
1727                    .about("Create or edit the cover letter for the patch series")
1728                    .arg_from_usage("-d, --delete 'Delete cover letter'"),
1729                SubCommand::with_name("cp")
1730                    .about("Copy a patch series")
1731                    .arg(Arg::with_name("source_dest").required(true).min_values(1).max_values(2).help("source (default: current series) and destination (required)")),
1732                SubCommand::with_name("delete")
1733                    .about("Delete a patch series")
1734                    .arg_from_usage("<name> 'Patch series to delete'"),
1735                SubCommand::with_name("detach")
1736                    .about("Stop working on any patch series"),
1737                SubCommand::with_name("format")
1738                    .about("Prepare patch series for email")
1739                    .arg_from_usage("--in-reply-to [Message-Id] 'Make the first mail a reply to the specified Message-Id'")
1740                    .arg_from_usage("--no-from 'Don't include in-body \"From:\" headers when formatting patches authored by others'")
1741                    .arg_from_usage("-v, --reroll-count=[N] 'Mark the patch series as PATCH vN'")
1742                    .arg(Arg::from_usage("--rfc 'Use [RFC PATCH] instead of the standard [PATCH] prefix'").conflicts_with("subject-prefix"))
1743                    .arg_from_usage("--stdout 'Write patches to stdout rather than files'")
1744                    .arg_from_usage("--subject-prefix [Subject-Prefix] 'Use [Subject-Prefix] instead of the standard [PATCH] prefix'"),
1745                SubCommand::with_name("log")
1746                    .about("Show the history of the patch series")
1747                    .arg_from_usage("-p, --patch 'Include a patch for each change committed to the series'"),
1748                SubCommand::with_name("mv")
1749                    .about("Move (rename) a patch series")
1750                    .visible_alias("rename")
1751                    .arg(Arg::with_name("source_dest").required(true).min_values(1).max_values(2).help("source (default: current series) and destination (required)")),
1752                SubCommand::with_name("rebase")
1753                    .about("Rebase the patch series")
1754                    .arg_from_usage("[onto] 'Commit to rebase onto'")
1755                    .arg_from_usage("-i, --interactive 'Interactively edit the list of commits'")
1756                    .group(ArgGroup::with_name("action").args(&["onto", "interactive"]).multiple(true).required(true)),
1757                SubCommand::with_name("req")
1758                    .about("Generate a mail requesting a pull of the patch series")
1759                    .visible_aliases(&["pull-request", "request-pull"])
1760                    .arg_from_usage("-p, --patch 'Include patch in the mail'")
1761                    .arg_from_usage("<url> 'Repository URL to request pull of'")
1762                    .arg_from_usage("<tag> 'Tag or branch name to request pull of'"),
1763                SubCommand::with_name("status")
1764                    .about("Show the status of the patch series"),
1765                SubCommand::with_name("start")
1766                    .about("Start a new patch series")
1767                    .arg_from_usage("<name> 'Patch series name'"),
1768                SubCommand::with_name("unadd")
1769                    .about("Undo \"git series add\", removing changes from the next series commit")
1770                    .arg_from_usage("<change>... 'Changes to remove (\"series\", \"base\", \"cover\")'"),
1771            ]).get_matches();
1772
1773    let mut out = Output::new();
1774
1775    let err = || -> Result<()> {
1776        let repo = try!(Repository::discover("."));
1777        match m.subcommand() {
1778            ("", _) => series(&mut out, &repo),
1779            ("add", Some(ref sm)) => add(&repo, &sm),
1780            ("base", Some(ref sm)) => base(&repo, &sm),
1781            ("checkout", Some(ref sm)) => checkout(&repo, &sm),
1782            ("commit", Some(ref sm)) => commit_status(&mut out, &repo, &sm, false),
1783            ("cover", Some(ref sm)) => cover(&repo, &sm),
1784            ("cp", Some(ref sm)) => cp_mv(&repo, &sm, false),
1785            ("delete", Some(ref sm)) => delete(&repo, &sm),
1786            ("detach", _) => detach(&repo),
1787            ("format", Some(ref sm)) => format(&mut out, &repo, &sm),
1788            ("log", Some(ref sm)) => log(&mut out, &repo, &sm),
1789            ("mv", Some(ref sm)) => cp_mv(&repo, &sm, true),
1790            ("rebase", Some(ref sm)) => rebase(&repo, &sm),
1791            ("req", Some(ref sm)) => req(&mut out, &repo, &sm),
1792            ("start", Some(ref sm)) => start(&repo, &sm),
1793            ("status", Some(ref sm)) => commit_status(&mut out, &repo, &sm, true),
1794            ("unadd", Some(ref sm)) => unadd(&repo, &sm),
1795            _ => unreachable!()
1796        }
1797    }();
1798
1799    if let Err(e) = err {
1800        let msg = e.to_string();
1801        out.write_err(&format!("{}{}", msg, ensure_nl(&msg)));
1802        drop(out);
1803        std::process::exit(1);
1804    }
1805}