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