main.rs

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