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