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