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