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