main.rs

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