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