main.rs

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