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