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 maybe_get_ref = |prefix: &str| -> Result<TreeBuilder<'repo>> {
165 match try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", prefix, series_name)))) {
166 Some(id) => {
167 let c = try!(repo.find_commit(id));
168 let t = try!(c.tree());
169 Ok(try!(repo.treebuilder(Some(&t))))
170 }
171 None => Ok(try!(repo.treebuilder(None))),
172 }
173 };
174 Ok(Internals {
175 staged: try!(maybe_get_ref(STAGED_PREFIX)),
176 working: try!(maybe_get_ref(WORKING_PREFIX)),
177 })
178 }
179
180 fn exists(repo: &'repo Repository, series_name: &str) -> Result<bool> {
181 for prefix in [STAGED_PREFIX, WORKING_PREFIX].iter() {
182 let prefixed_name = format!("{}{}", prefix, series_name);
183 if try!(notfound_to_none(repo.refname_to_id(&prefixed_name))).is_some() {
184 return Ok(true);
185 }
186 }
187 Ok(false)
188 }
189
190 // Returns true if it had anything to delete.
191 fn delete(repo: &'repo Repository, series_name: &str) -> Result<bool> {
192 let mut deleted_any = false;
193 for prefix in [STAGED_PREFIX, WORKING_PREFIX].iter() {
194 let prefixed_name = format!("{}{}", prefix, series_name);
195 if let Some(mut r) = try!(notfound_to_none(repo.find_reference(&prefixed_name))) {
196 try!(r.delete());
197 deleted_any = true;
198 }
199 }
200 Ok(deleted_any)
201 }
202
203 fn update_series(&mut self, repo: &'repo Repository) -> Result<()> {
204 let head_id = try!(repo.refname_to_id("HEAD"));
205 try!(self.working.insert("series", head_id, GIT_FILEMODE_COMMIT as i32));
206 Ok(())
207 }
208
209 fn write(&self, repo: &'repo Repository) -> Result<()> {
210 let config = try!(repo.config());
211 let author = try!(get_signature(&config, "AUTHOR"));
212 let committer = try!(get_signature(&config, "COMMITTER"));
213
214 let shead = try!(repo.find_reference(SHEAD_REF));
215 let series_name = try!(shead_series_name(&shead));
216 let maybe_commit = |prefix: &str, tb: &TreeBuilder| -> Result<()> {
217 let tree_id = try!(tb.write());
218 let refname = format!("{}{}", prefix, series_name);
219 let old_commit_id = try!(notfound_to_none(repo.refname_to_id(&refname)));
220 if let Some(id) = old_commit_id {
221 let c = try!(repo.find_commit(id));
222 if c.tree_id() == tree_id {
223 return Ok(());
224 }
225 }
226 let tree = try!(repo.find_tree(tree_id));
227 let mut parents = Vec::new();
228 // Include all commits from tree, to keep them reachable and fetchable. Include base,
229 // because series might not have it as an ancestor; we don't enforce that until commit.
230 for e in tree.iter() {
231 if e.kind() == Some(ObjectType::Commit) {
232 parents.push(e.id());
233 }
234 }
235 let parents = try!(parents_from_ids(repo, parents));
236 let parents_ref: Vec<&_> = parents.iter().collect();
237 let commit_id = try!(repo.commit(None, &author, &committer, &refname, &tree, &parents_ref));
238 try!(repo.reference_ensure_log(&refname));
239 try!(reference_matching_opt(repo, &refname, commit_id, true, old_commit_id, &format!("commit: {}", refname)));
240 Ok(())
241 };
242 try!(maybe_commit(STAGED_PREFIX, &self.staged));
243 try!(maybe_commit(WORKING_PREFIX, &self.working));
244 Ok(())
245 }
246}
247
248fn diff_empty(diff: &Diff) -> bool {
249 diff.deltas().len() == 0
250}
251
252fn add(repo: &Repository, m: &ArgMatches) -> Result<()> {
253 let mut internals = try!(Internals::read(repo));
254 for file in m.values_of_os("change").unwrap() {
255 match try!(internals.working.get(file)) {
256 Some(entry) => { try!(internals.staged.insert(file, entry.id(), entry.filemode())); }
257 None => {
258 if try!(internals.staged.get(file)).is_some() {
259 try!(internals.staged.remove(file));
260 }
261 }
262 }
263 }
264 internals.write(repo)
265}
266
267fn unadd(repo: &Repository, m: &ArgMatches) -> Result<()> {
268 let shead = try!(repo.find_reference(SHEAD_REF));
269 let started = {
270 let shead_target = try!(shead.symbolic_target().ok_or("SHEAD not a symbolic reference"));
271 try!(notfound_to_none(repo.find_reference(shead_target))).is_some()
272 };
273
274 let mut internals = try!(Internals::read(repo));
275 if started {
276 let shead_commit = try!(peel_to_commit(shead));
277 let shead_tree = try!(shead_commit.tree());
278
279 for file in m.values_of("change").unwrap() {
280 match shead_tree.get_name(file) {
281 Some(entry) => {
282 try!(internals.staged.insert(file, entry.id(), entry.filemode()));
283 }
284 None => { try!(internals.staged.remove(file)); }
285 }
286 }
287 } else {
288 for file in m.values_of("change").unwrap() {
289 try!(internals.staged.remove(file))
290 }
291 }
292 internals.write(repo)
293}
294
295fn shead_series_name(shead: &Reference) -> Result<String> {
296 let shead_target = try!(shead.symbolic_target().ok_or("SHEAD not a symbolic reference"));
297 if !shead_target.starts_with(SERIES_PREFIX) {
298 return Err(format!("SHEAD does not start with {}", SERIES_PREFIX).into());
299 }
300 Ok(shead_target[SERIES_PREFIX.len()..].to_string())
301}
302
303fn series(out: &mut Output, repo: &Repository) -> Result<()> {
304 let mut refs = Vec::new();
305 for prefix in [SERIES_PREFIX, STAGED_PREFIX, WORKING_PREFIX].iter() {
306 let l = prefix.len();
307 for r in try!(repo.references_glob(&[prefix, "*"].concat())).names() {
308 refs.push(try!(r)[l..].to_string());
309 }
310 }
311 let shead_target = if let Some(shead) = try!(notfound_to_none(repo.find_reference(SHEAD_REF))) {
312 Some(try!(shead_series_name(&shead)).to_string())
313 } else {
314 None
315 };
316 refs.extend(shead_target.clone().into_iter());
317 refs.sort();
318 refs.dedup();
319
320 let config = try!(try!(repo.config()).snapshot());
321 try!(out.auto_pager(&config, "branch", false));
322 let color_current = try!(out.get_color(&config, "branch", "current", "green"));
323 let color_plain = try!(out.get_color(&config, "branch", "plain", "normal"));
324 for name in refs.iter() {
325 let (star, color) = if Some(name) == shead_target.as_ref() {
326 ('*', color_current)
327 } else {
328 (' ', color_plain)
329 };
330 let new = if try!(notfound_to_none(repo.refname_to_id(&format!("{}{}", SERIES_PREFIX, name)))).is_none() {
331 " (new, no commits yet)"
332 } else {
333 ""
334 };
335 try!(writeln!(out, "{} {}{}", star, color.paint(name as &str), new));
336 }
337 if refs.is_empty() {
338 try!(writeln!(out, "No series; use \"git series start <name>\" to start"));
339 }
340 Ok(())
341}
342
343fn start(repo: &Repository, m: &ArgMatches) -> Result<()> {
344 let name = m.value_of("name").unwrap();
345 let prefixed_name = &[SERIES_PREFIX, name].concat();
346 let branch_exists = try!(notfound_to_none(repo.refname_to_id(&prefixed_name))).is_some()
347 || try!(Internals::exists(repo, name));
348 if branch_exists {
349 return Err(format!("Series {} already exists.\nUse checkout to resume working on an existing patch series.", name).into());
350 }
351 try!(repo.reference_symbolic(SHEAD_REF, &prefixed_name, true, &format!("git series start {}", name)));
352
353 let internals = try!(Internals::read(repo));
354 try!(internals.write(repo));
355 Ok(())
356}
357
358fn checkout_tree(repo: &Repository, treeish: &Object) -> Result<()> {
359 let mut conflicts = Vec::new();
360 let mut dirty = Vec::new();
361 let result = {
362 let mut opts = git2::build::CheckoutBuilder::new();
363 opts.safe();
364 opts.notify_on(git2::CHECKOUT_NOTIFICATION_CONFLICT | git2::CHECKOUT_NOTIFICATION_DIRTY);
365 opts.notify(|t, path, _, _, _| {
366 let path = path.unwrap().to_owned();
367 if t == git2::CHECKOUT_NOTIFICATION_CONFLICT {
368 conflicts.push(path);
369 } else if t == git2::CHECKOUT_NOTIFICATION_DIRTY {
370 dirty.push(path);
371 }
372 true
373 });
374 if isatty::stdout_isatty() {
375 opts.progress(|_, completed, total| {
376 let total = total.to_string();
377 print!("\rChecking out files: {1:0$}/{2}", total.len(), completed, total);
378 });
379 }
380 repo.checkout_tree(treeish, Some(&mut opts))
381 };
382 match result {
383 Err(ref e) if e.code() == git2::ErrorCode::Conflict => {
384 let mut msg = String::new();
385 writeln!(msg, "error: Your changes to the following files would be overwritten by checkout:").unwrap();
386 for path in conflicts {
387 writeln!(msg, " {}", path.to_string_lossy()).unwrap();
388 }
389 writeln!(msg, "Please, commit your changes or stash them before you switch series.").unwrap();
390 return Err(msg.into());
391 }
392 _ => try!(result),
393 }
394 println!("");
395 if !dirty.is_empty() {
396 let mut stderr = std::io::stderr();
397 writeln!(stderr, "Files with changes unaffected by checkout:").unwrap();
398 for path in dirty {
399 writeln!(stderr, " {}", path.to_string_lossy()).unwrap();
400 }
401 }
402 Ok(())
403}
404
405fn checkout(repo: &Repository, m: &ArgMatches) -> Result<()> {
406 match repo.state() {
407 git2::RepositoryState::Clean => (),
408 s => { return Err(format!("{:?} in progress; cannot checkout patch series", s).into()); }
409 }
410 let name = m.value_of("name").unwrap();
411 let prefixed_name = &[SERIES_PREFIX, name].concat();
412 // Make sure the ref exists
413 let branch_exists = try!(notfound_to_none(repo.refname_to_id(&prefixed_name))).is_some()
414 || try!(Internals::exists(repo, name));
415 if !branch_exists {
416 return Err(format!("Series {} does not exist.\nUse \"git series start <name>\" to start a new patch series.", name).into());
417 }
418
419 let internals = try!(Internals::read_series(repo, name));
420 let new_head_id = try!(try!(internals.working.get("series")).ok_or(format!("Could not find \"series\" in working version of \"{}\"", name))).id();
421 let new_head = try!(repo.find_commit(new_head_id)).into_object();
422
423 try!(checkout_tree(repo, &new_head));
424
425 let head = try!(repo.head());
426 let head_commit = try!(peel_to_commit(head));
427 let head_id = head_commit.as_object().id();
428 println!("Previous HEAD position was {}", try!(commit_summarize(&repo, head_id)));
429
430 try!(repo.reference_symbolic(SHEAD_REF, &prefixed_name, true, &format!("git series checkout {}", name)));
431
432 // git status parses this reflog string; the prefix must remain "checkout: moving from ".
433 try!(repo.reference("HEAD", new_head_id, true, &format!("checkout: moving from {} to {} (git series checkout {})", head_id, new_head_id, name)));
434 println!("HEAD is now detached at {}", try!(commit_summarize(&repo, new_head_id)));
435
436 Ok(())
437}
438
439fn base(repo: &Repository, m: &ArgMatches) -> Result<()> {
440 let mut internals = try!(Internals::read(repo));
441
442 let current_base_id = match try!(internals.working.get("base")) {
443 Some(entry) => entry.id(),
444 _ => zero_oid(),
445 };
446
447 if !m.is_present("delete") && !m.is_present("base") {
448 if current_base_id.is_zero() {
449 return Err("Patch series has no base set".into());
450 } else {
451 println!("{}", current_base_id);
452 return Ok(());
453 }
454 }
455
456 let new_base_id = if m.is_present("delete") {
457 zero_oid()
458 } else {
459 let base = m.value_of("base").unwrap();
460 let base_object = try!(repo.revparse_single(base));
461 let base_id = base_object.id();
462 let s_working_series = try!(try!(internals.working.get("series")).ok_or("Could not find entry \"series\" in working vesion of current series"));
463 if base_id != s_working_series.id() && !try!(repo.graph_descendant_of(s_working_series.id(), base_id)) {
464 return Err(format!("Cannot set base to {}: not an ancestor of the patch series {}", base, s_working_series.id()).into());
465 }
466 base_id
467 };
468
469 if current_base_id == new_base_id {
470 return Err("Base unchanged".into());
471 }
472
473 if !current_base_id.is_zero() {
474 println!("Previous base was {}", try!(commit_summarize(&repo, current_base_id)));
475 }
476
477 if new_base_id.is_zero() {
478 try!(internals.working.remove("base"));
479 try!(internals.write(repo));
480 println!("Cleared patch series base");
481 } else {
482 try!(internals.working.insert("base", new_base_id, GIT_FILEMODE_COMMIT as i32));
483 try!(internals.write(repo));
484 println!("Set patch series base to {}", try!(commit_summarize(&repo, new_base_id)));
485 }
486
487 Ok(())
488}
489
490fn detach(repo: &Repository) -> Result<()> {
491 match repo.find_reference(SHEAD_REF) {
492 Ok(mut r) => try!(r.delete()),
493 Err(_) => { return Err("No current patch series to detach from.".into()); }
494 }
495 Ok(())
496}
497
498fn delete(repo: &Repository, m: &ArgMatches) -> Result<()> {
499 let name = m.value_of("name").unwrap();
500 if let Ok(shead) = repo.find_reference(SHEAD_REF) {
501 let shead_target = try!(shead_series_name(&shead));
502 if shead_target == name {
503 return Err(format!("Cannot delete the current series \"{}\"; detach first.", name).into());
504 }
505 }
506 let prefixed_name = &[SERIES_PREFIX, name].concat();
507 let deleted_ref = if let Some(mut r) = try!(notfound_to_none(repo.find_reference(prefixed_name))) {
508 try!(r.delete());
509 true
510 } else {
511 false
512 };
513 let deleted_internals = try!(Internals::delete(repo, name));
514 if !deleted_ref && !deleted_internals {
515 return Err(format!("Nothing to delete: series \"{}\" does not exist.", name).into());
516 }
517 Ok(())
518}
519
520fn get_editor(config: &Config) -> Result<OsString> {
521 if let Some(e) = env::var_os("GIT_EDITOR") {
522 return Ok(e);
523 }
524 if let Ok(e) = config.get_path("core.editor") {
525 return Ok(e.into());
526 }
527 let terminal_is_dumb = match env::var_os("TERM") {
528 None => true,
529 Some(t) => t.as_os_str() == "dumb",
530 };
531 if !terminal_is_dumb {
532 if let Some(e) = env::var_os("VISUAL") {
533 return Ok(e);
534 }
535 }
536 if let Some(e) = env::var_os("EDITOR") {
537 return Ok(e);
538 }
539 if terminal_is_dumb {
540 return Err("TERM unset or \"dumb\" but EDITOR unset".into());
541 }
542 return Ok("vi".into());
543}
544
545// Get the pager to use; with for_cmd set, get the pager for use by the
546// specified git command. If get_pager returns None, don't use a pager.
547fn get_pager(config: &Config, for_cmd: &str, default: bool) -> Option<OsString> {
548 if !isatty::stdout_isatty() {
549 return None;
550 }
551 // pager.cmd can contain a boolean (if false, force no pager) or a
552 // command-specific pager; only treat it as a command if it doesn't parse
553 // as a boolean.
554 let maybe_pager = config.get_path(&format!("pager.{}", for_cmd)).ok();
555 let (cmd_want_pager, cmd_pager) = maybe_pager.map_or((default, None), |p|
556 if let Ok(b) = Config::parse_bool(&p) {
557 (b, None)
558 } else {
559 (true, Some(p))
560 }
561 );
562 if !cmd_want_pager {
563 return None;
564 }
565 let pager =
566 if let Some(e) = env::var_os("GIT_PAGER") {
567 Some(e)
568 } else if let Some(p) = cmd_pager {
569 Some(p.into())
570 } else if let Ok(e) = config.get_path("core.pager") {
571 Some(e.into())
572 } else if let Some(e) = env::var_os("PAGER") {
573 Some(e)
574 } else {
575 Some("less".into())
576 };
577 pager.and_then(|p| if p.is_empty() || p == OsString::from("cat") { None } else { Some(p) })
578}
579
580/// Construct a Command, using the shell if the command contains shell metachars
581fn cmd_maybe_shell<S: AsRef<OsStr>>(program: S, args: bool) -> Command {
582 if program.as_ref().to_string_lossy().contains(|c| SHELL_METACHARS.contains(c)) {
583 let mut cmd = Command::new("sh");
584 cmd.arg("-c");
585 if args {
586 let mut program_with_args = program.as_ref().to_os_string();
587 program_with_args.push(" \"$@\"");
588 cmd.arg(program_with_args).arg(program);
589 } else {
590 cmd.arg(program);
591 }
592 cmd
593 } else {
594 Command::new(program)
595 }
596}
597
598fn run_editor<S: AsRef<OsStr>>(config: &Config, filename: S) -> Result<()> {
599 let editor = try!(get_editor(&config));
600 let editor_status = try!(cmd_maybe_shell(editor, true).arg(&filename).status());
601 if !editor_status.success() {
602 return Err(format!("Editor exited with status {}", editor_status).into());
603 }
604 Ok(())
605}
606
607struct Output {
608 pager: Option<std::process::Child>,
609 include_stderr: bool,
610}
611
612impl Output {
613 fn new() -> Self {
614 Output { pager: None, include_stderr: false }
615 }
616
617 fn auto_pager(&mut self, config: &Config, for_cmd: &str, default: bool) -> Result<()> {
618 if let Some(pager) = get_pager(config, for_cmd, default) {
619 let mut cmd = cmd_maybe_shell(pager, false);
620 cmd.stdin(std::process::Stdio::piped());
621 if env::var_os("LESS").is_none() {
622 cmd.env("LESS", "FRX");
623 }
624 if env::var_os("LV").is_none() {
625 cmd.env("LV", "-c");
626 }
627 let child = try!(cmd.spawn());
628 self.pager = Some(child);
629 self.include_stderr = isatty::stderr_isatty();
630 }
631 Ok(())
632 }
633
634 // Get a color to write text with, taking git configuration into account.
635 //
636 // config: the configuration to determine the color from.
637 // command: the git command to act like.
638 // slot: the color "slot" of that git command to act like.
639 // default: the color to use if not configured.
640 fn get_color(&self, config: &Config, command: &str, slot: &str, default: &str) -> Result<Style> {
641 if !cfg!(unix) {
642 return Ok(Style::new());
643 }
644 let color_ui = try!(notfound_to_none(config.get_str("color.ui"))).unwrap_or("auto");
645 let color_cmd = try!(notfound_to_none(config.get_str(&format!("color.{}", command)))).unwrap_or(color_ui);
646 if color_cmd == "never" || Config::parse_bool(color_cmd) == Ok(false) {
647 return Ok(Style::new());
648 }
649 if self.pager.is_some() {
650 let color_pager = try!(notfound_to_none(config.get_bool(&format!("color.pager")))).unwrap_or(true);
651 if !color_pager {
652 return Ok(Style::new());
653 }
654 } else if !isatty::stdout_isatty() {
655 return Ok(Style::new());
656 }
657 let cfg = format!("color.{}.{}", command, slot);
658 let color = try!(notfound_to_none(config.get_str(&cfg))).unwrap_or(default);
659 colorparse::parse(color).map_err(|e| format!("Error parsing {}: {}", cfg, e).into())
660 }
661
662 fn write_err(&mut self, msg: &str) {
663 if self.include_stderr {
664 if write!(self, "{}", msg).is_err() {
665 write!(std::io::stderr(), "{}", msg).unwrap();
666 }
667 } else {
668 write!(std::io::stderr(), "{}", msg).unwrap();
669 }
670 }
671}
672
673impl Drop for Output {
674 fn drop(&mut self) {
675 if let Some(ref mut child) = self.pager {
676 let status = child.wait().unwrap();
677 if !status.success() {
678 writeln!(std::io::stderr(), "Pager exited with status {}", status).unwrap();
679 }
680 }
681 }
682}
683
684impl IoWrite for Output {
685 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
686 match self.pager {
687 Some(ref mut child) => child.stdin.as_mut().unwrap().write(buf),
688 None => std::io::stdout().write(buf),
689 }
690 }
691
692 fn flush(&mut self) -> std::io::Result<()> {
693 match self.pager {
694 Some(ref mut child) => child.stdin.as_mut().unwrap().flush(),
695 None => std::io::stdout().flush(),
696 }
697 }
698}
699
700fn get_signature(config: &Config, which: &str) -> Result<git2::Signature<'static>> {
701 let name_var = ["GIT_", which, "_NAME"].concat();
702 let email_var = ["GIT_", which, "_EMAIL"].concat();
703 let which_lc = which.to_lowercase();
704 let name = try!(env::var(&name_var).or_else(
705 |_| config.get_string("user.name").or_else(
706 |_| Err(format!("Could not determine {} name: checked ${} and user.name in git config", which_lc, name_var)))));
707 let email = try!(env::var(&email_var).or_else(
708 |_| config.get_string("user.email").or_else(
709 |_| env::var("EMAIL").or_else(
710 |_| Err(format!("Could not determine {} email: checked ${}, user.email in git config, and $EMAIL", which_lc, email_var))))));
711 Ok(try!(git2::Signature::now(&name, &email)))
712}
713
714fn commit_status(out: &mut Output, repo: &Repository, m: &ArgMatches, do_status: bool) -> Result<()> {
715 let config = try!(try!(repo.config()).snapshot());
716 let shead = match repo.find_reference(SHEAD_REF) {
717 Err(ref e) if e.code() == git2::ErrorCode::NotFound => { println!("No series; use \"git series start <name>\" to start"); return Ok(()); }
718 result => try!(result),
719 };
720 let series_name = try!(shead_series_name(&shead));
721
722 if do_status {
723 try!(out.auto_pager(&config, "status", false));
724 }
725 let get_color = |out: &Output, color: &str, default: &str| {
726 if do_status {
727 out.get_color(&config, "status", color, default)
728 } else {
729 Ok(Style::new())
730 }
731 };
732 let color_normal = Style::new();
733 let color_header = try!(get_color(out, "header", "normal"));
734 let color_updated = try!(get_color(out, "updated", "green"));
735 let color_changed = try!(get_color(out, "changed", "red"));
736
737 let write_status = |status: &mut Vec<ansi_term::ANSIString>, diff: &Diff, heading: &str, color: &Style, show_hints: bool, hints: &[&str]| -> Result<bool> {
738 let mut changes = false;
739
740 try!(diff.foreach(&mut |delta, _| {
741 if !changes {
742 changes = true;
743 status.push(color_header.paint(format!("{}\n", heading.to_string())));
744 if show_hints {
745 for hint in hints {
746 status.push(color_header.paint(format!(" ({})\n", hint)));
747 }
748 }
749 status.push(color_normal.paint("\n"));
750 }
751 status.push(color_normal.paint(" "));
752 status.push(color.paint(format!("{:?}: {}\n", delta.status(), delta.old_file().path().unwrap().to_str().unwrap())));
753 true
754 }, None, None, None));
755
756 if changes {
757 status.push(color_normal.paint("\n"));
758 }
759
760 Ok(changes)
761 };
762
763 let mut status = Vec::new();
764 status.push(color_header.paint(format!("On series {}\n", series_name)));
765
766 let mut internals = try!(Internals::read(repo));
767 let working_tree = try!(repo.find_tree(try!(internals.working.write())));
768 let staged_tree = try!(repo.find_tree(try!(internals.staged.write())));
769
770 let shead_commit = match shead.resolve() {
771 Ok(r) => Some(try!(peel_to_commit(r))),
772 Err(ref e) if e.code() == git2::ErrorCode::NotFound => {
773 status.push(color_header.paint("\nInitial series commit\n"));
774 None
775 }
776 Err(e) => try!(Err(e)),
777 };
778 let shead_tree = match shead_commit {
779 Some(ref c) => Some(try!(c.tree())),
780 None => None,
781 };
782
783 let commit_all = m.is_present("all");
784
785 let (changes, tree, diff) = if commit_all {
786 let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&working_tree), None));
787 let changes = try!(write_status(&mut status, &diff, "Changes to be committed:", &color_normal, false, &[]));
788 if !changes {
789 status.push(color_normal.paint("nothing to commit; series unchanged\n"));
790 }
791 (changes, working_tree, diff)
792 } else {
793 let diff = try!(repo.diff_tree_to_tree(shead_tree.as_ref(), Some(&staged_tree), None));
794 let changes_to_be_committed = try!(write_status(&mut status, &diff,
795 "Changes to be committed:", &color_updated, do_status,
796 &["use \"git series commit\" to commit",
797 "use \"git series unadd <file>...\" to undo add"]));
798
799 let diff_not_staged = try!(repo.diff_tree_to_tree(Some(&staged_tree), Some(&working_tree), None));
800 let changes_not_staged = try!(write_status(&mut status, &diff_not_staged,
801 "Changes not staged for commit:", &color_changed, do_status,
802 &["use \"git series add <file>...\" to update what will be committed"]));
803
804 if !changes_to_be_committed {
805 if changes_not_staged {
806 status.push(color_normal.paint("no changes added to commit (use \"git series add\" or \"git series commit -a\")\n"));
807 } else {
808 status.push(color_normal.paint("nothing to commit; series unchanged\n"));
809 }
810 }
811
812 (changes_to_be_committed, staged_tree, diff)
813 };
814
815 let status = ansi_term::ANSIStrings(&status).to_string();
816 if do_status || !changes {
817 if do_status {
818 try!(write!(out, "{}", status));
819 } else {
820 return Err(status.into());
821 }
822 return Ok(());
823 }
824
825 // Check that the commit includes the series
826 let series_id = match tree.get_name("series") {
827 None => { return Err(concat!("Cannot commit: initial commit must include \"series\"\n",
828 "Use \"git series add series\" or \"git series commit -a\"").into()); }
829 Some(series) => series.id()
830 };
831
832 // Check that the base is still an ancestor of the series
833 if let Some(base) = tree.get_name("base") {
834 if base.id() != series_id && !try!(repo.graph_descendant_of(series_id, base.id())) {
835 let (base_short_id, base_summary) = try!(commit_summarize_components(&repo, base.id()));
836 let (series_short_id, series_summary) = try!(commit_summarize_components(&repo, series_id));
837 return Err(format!(concat!(
838 "Cannot commit: base {} is not an ancestor of patch series {}\n",
839 "base {} {}\n",
840 "series {} {}"),
841 base_short_id, series_short_id,
842 base_short_id, base_summary,
843 series_short_id, series_summary).into());
844 }
845 }
846
847 let msg = match m.value_of("m") {
848 Some(s) => s.to_string(),
849 None => {
850 let filename = repo.path().join("SCOMMIT_EDITMSG");
851 let mut file = try!(File::create(&filename));
852 try!(write!(file, "{}", COMMIT_MESSAGE_COMMENT));
853 for line in status.lines() {
854 if line.is_empty() {
855 try!(writeln!(file, "#"));
856 } else {
857 try!(writeln!(file, "# {}", line));
858 }
859 }
860 if m.is_present("verbose") {
861 try!(writeln!(file, "{}\n{}", SCISSOR_LINE, SCISSOR_COMMENT));
862 try!(write_diff(&mut file, &diff));
863 }
864 drop(file);
865 try!(run_editor(&config, &filename));
866 let mut file = try!(File::open(&filename));
867 let mut msg = String::new();
868 try!(file.read_to_string(&mut msg));
869 if let Some(scissor_index) = msg.find(SCISSOR_LINE) {
870 msg.truncate(scissor_index);
871 }
872 try!(git2::message_prettify(msg, git2::DEFAULT_COMMENT_CHAR))
873 }
874 };
875 if msg.is_empty() {
876 return Err("Aborting series commit due to empty commit message.".into());
877 }
878
879 let author = try!(get_signature(&config, "AUTHOR"));
880 let committer = try!(get_signature(&config, "COMMITTER"));
881 let mut parents: Vec<Oid> = Vec::new();
882 // Include all commits from tree, to keep them reachable and fetchable.
883 for e in tree.iter() {
884 if e.kind() == Some(ObjectType::Commit) && e.name().unwrap() != "base" {
885 parents.push(e.id())
886 }
887 }
888 let parents = try!(parents_from_ids(repo, parents));
889 let parents_ref: Vec<&_> = shead_commit.iter().chain(parents.iter()).collect();
890 let new_commit_oid = try!(repo.commit(Some(SHEAD_REF), &author, &committer, &msg, &tree, &parents_ref));
891
892 if commit_all {
893 internals.staged = try!(repo.treebuilder(Some(&tree)));
894 try!(internals.write(repo));
895 }
896
897 let (new_commit_short_id, new_commit_summary) = try!(commit_summarize_components(&repo, new_commit_oid));
898 try!(writeln!(out, "[{} {}] {}", series_name, new_commit_short_id, new_commit_summary));
899
900 Ok(())
901}
902
903fn cover(repo: &Repository, m: &ArgMatches) -> Result<()> {
904 let mut internals = try!(Internals::read(repo));
905
906 let (working_cover_id, working_cover_content) = match try!(internals.working.get("cover")) {
907 None => (zero_oid(), String::new()),
908 Some(entry) => (entry.id(), try!(std::str::from_utf8(try!(repo.find_blob(entry.id())).content())).to_string()),
909 };
910
911 if m.is_present("delete") {
912 if working_cover_id.is_zero() {
913 return Err("No cover to delete".into());
914 }
915 try!(internals.working.remove("cover"));
916 try!(internals.write(repo));
917 println!("Deleted cover letter");
918 return Ok(());
919 }
920
921 let filename = repo.path().join("COVER_EDITMSG");
922 let mut file = try!(File::create(&filename));
923 if working_cover_content.is_empty() {
924 try!(write!(file, "{}", COVER_LETTER_COMMENT));
925 } else {
926 try!(write!(file, "{}", working_cover_content));
927 }
928 drop(file);
929 let config = try!(repo.config());
930 try!(run_editor(&config, &filename));
931 let mut file = try!(File::open(&filename));
932 let mut msg = String::new();
933 try!(file.read_to_string(&mut msg));
934 let msg = try!(git2::message_prettify(msg, git2::DEFAULT_COMMENT_CHAR));
935 if msg.is_empty() {
936 return Err("Empty cover letter; not changing.\n(To delete the cover letter, use \"git series -d\".)".into());
937 }
938
939 let new_cover_id = try!(repo.blob(msg.as_bytes()));
940 if new_cover_id == working_cover_id {
941 println!("Cover letter unchanged");
942 } else {
943 try!(internals.working.insert("cover", new_cover_id, GIT_FILEMODE_BLOB as i32));
944 try!(internals.write(repo));
945 println!("Updated cover letter");
946 }
947
948 Ok(())
949}
950
951fn date_822(t: git2::Time) -> String {
952 let offset = chrono::offset::fixed::FixedOffset::east(t.offset_minutes()*60);
953 let datetime = offset.timestamp(t.seconds(), 0);
954 datetime.to_rfc2822()
955}
956
957fn shortlog(commits: &mut [Commit]) -> String {
958 let mut s = String::new();
959 let mut author_map = std::collections::HashMap::new();
960
961 for mut commit in commits {
962 let author = commit.author().name().unwrap().to_string();
963 author_map.entry(author).or_insert(Vec::new()).push(commit.summary().unwrap().to_string());
964 }
965
966 let mut authors: Vec<_> = author_map.keys().collect();
967 authors.sort();
968 let mut first = true;
969 for author in authors {
970 if first {
971 first = false;
972 } else {
973 writeln!(s, "").unwrap();
974 }
975 let summaries = author_map.get(author).unwrap();
976 writeln!(s, "{} ({}):", author, summaries.len()).unwrap();
977 for summary in summaries {
978 writeln!(s, " {}", summary).unwrap();
979 }
980 }
981
982 s
983}
984
985fn ascii_isalnum(c: char) -> bool {
986 (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
987}
988
989fn sanitize_summary(summary: &str) -> String {
990 let mut s = String::with_capacity(summary.len());
991 let mut prev_dot = false;
992 let mut need_space = false;
993 for c in summary.chars() {
994 if ascii_isalnum(c) || c == '_' || c == '.' {
995 if need_space {
996 s.push('-');
997 need_space = false;
998 }
999 if !(prev_dot && c == '.') {
1000 s.push(c);
1001 }
1002 } else {
1003 if !s.is_empty() {
1004 need_space = true;
1005 }
1006 }
1007 prev_dot = c == '.';
1008 }
1009 let end = s.trim_right_matches(|c| c == '.' || c == '-').len();
1010 s.truncate(end);
1011 s
1012}
1013
1014#[test]
1015fn test_sanitize_summary() {
1016 let tests = vec![
1017 ("", ""),
1018 ("!!!!!", ""),
1019 ("Test", "Test"),
1020 ("Test case", "Test-case"),
1021 ("Test case", "Test-case"),
1022 (" Test case ", "Test-case"),
1023 ("...Test...case...", ".Test.case"),
1024 ("...Test...case.!!", ".Test.case"),
1025 (".!.Test.!.case.!.", ".-.Test.-.case"),
1026 ];
1027 for (summary, sanitized) in tests {
1028 assert_eq!(sanitize_summary(summary), sanitized.to_string());
1029 }
1030}
1031
1032fn split_message(message: &str) -> (&str, &str) {
1033 let mut iter = message.splitn(2, '\n');
1034 let subject = iter.next().unwrap().trim_right();
1035 let body = iter.next().map(|s| s.trim_left()).unwrap_or("");
1036 (subject, body)
1037}
1038
1039fn diffstat(diff: &Diff) -> Result<String> {
1040 let stats = try!(diff.stats());
1041 let stats_buf = try!(stats.to_buf(git2::DIFF_STATS_FULL|git2::DIFF_STATS_INCLUDE_SUMMARY, 72));
1042 Ok(stats_buf.as_str().unwrap().to_string())
1043}
1044
1045fn write_diff<W: IoWrite>(f: &mut W, diff: &Diff) -> Result<()> {
1046 let mut err = Ok(());
1047 try!(diff.print(git2::DiffFormat::Patch, |_, _, l| {
1048 err = || -> Result<()> {
1049 let o = l.origin();
1050 if o == '+' || o == '-' || o == ' ' {
1051 try!(f.write_all(&[o as u8]));
1052 }
1053 try!(f.write_all(l.content()));
1054 Ok(())
1055 }();
1056 err.is_ok()
1057 }));
1058 err
1059}
1060
1061fn mail_signature() -> String {
1062 format!("-- \ngit-series {}", crate_version!())
1063}
1064
1065fn ensure_nl(s: &str) -> &'static str {
1066 if !s.ends_with('\n') {
1067 "\n"
1068 } else {
1069 ""
1070 }
1071}
1072
1073fn format(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1074 let config = try!(repo.config());
1075 let to_stdout = m.is_present("stdout");
1076
1077 let shead_commit = try!(peel_to_commit(try!(try!(repo.find_reference(SHEAD_REF)).resolve())));
1078 let stree = try!(shead_commit.tree());
1079
1080 let series = try!(stree.get_name("series").ok_or("Internal error: series did not contain \"series\""));
1081 let base = try!(stree.get_name("base").ok_or("Cannot format series; no base set.\nUse \"git series base\" to set base."));
1082
1083 let mut revwalk = try!(repo.revwalk());
1084 revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1085 try!(revwalk.push(series.id()));
1086 try!(revwalk.hide(base.id()));
1087 let mut commits: Vec<Commit> = try!(revwalk.map(|c| {
1088 let id = try!(c);
1089 let commit = try!(repo.find_commit(id));
1090 if commit.parent_ids().count() > 1 {
1091 return Err(format!("Error: cannot format merge commit as patch:\n{}", try!(commit_summarize(repo, id))).into());
1092 }
1093 Ok(commit)
1094 }).collect::<Result<_>>());
1095 if commits.is_empty() {
1096 return Err("No patches to format; series and base identical.".into());
1097 }
1098
1099 let author = try!(get_signature(&config, "AUTHOR"));
1100 let author_name = author.name().unwrap();
1101 let author_email = author.email().unwrap();
1102 let message_id_suffix = format!("{}.git-series.{}", author.when().seconds(), author_email);
1103
1104 let cover_entry = stree.get_name("cover");
1105 let mut in_reply_to_message_id = None;
1106
1107 let signature = mail_signature();
1108
1109 let mut out : Box<IoWrite> = if to_stdout {
1110 try!(out.auto_pager(&config, "format-patch", true));
1111 Box::new(out)
1112 } else {
1113 Box::new(std::io::stdout())
1114 };
1115 let patch_file = |name: &str| -> Result<Box<IoWrite>> {
1116 println!("{}", name);
1117 Ok(Box::new(try!(File::create(name))))
1118 };
1119
1120 if let Some(ref entry) = cover_entry {
1121 let cover_blob = try!(repo.find_blob(entry.id()));
1122 let content = try!(std::str::from_utf8(cover_blob.content())).to_string();
1123 let (subject, body) = split_message(&content);
1124
1125 let series_tree = try!(repo.find_commit(series.id())).tree().unwrap();
1126 let base_tree = try!(repo.find_commit(base.id())).tree().unwrap();
1127 let diff = try!(repo.diff_tree_to_tree(Some(&base_tree), Some(&series_tree), None));
1128 let stats = try!(diffstat(&diff));
1129
1130 if !to_stdout {
1131 out = try!(patch_file("0000-cover-letter.patch"));
1132 }
1133 try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", shead_commit.id()));
1134 let cover_message_id = format!("<cover.{}.{}>", shead_commit.id(), message_id_suffix);
1135 try!(writeln!(out, "Message-Id: {}", cover_message_id));
1136 in_reply_to_message_id = Some(cover_message_id);
1137 try!(writeln!(out, "From: {} <{}>", author_name, author_email));
1138 try!(writeln!(out, "Date: {}", date_822(author.when())));
1139 try!(writeln!(out, "Subject: [PATCH 0/{}] {}\n", commits.len(), subject));
1140 if !body.is_empty() {
1141 try!(writeln!(out, "{}", body));
1142 }
1143 try!(writeln!(out, "{}", shortlog(&mut commits)));
1144 try!(writeln!(out, "{}", stats));
1145 try!(writeln!(out, "{}", signature));
1146 }
1147
1148 for (commit_num, commit) in commits.iter().enumerate() {
1149 if to_stdout && (commit_num > 0 || cover_entry.is_some()) {
1150 try!(writeln!(out, ""));
1151 }
1152
1153 let message = commit.message().unwrap();
1154 let (subject, body) = split_message(message);
1155 let commit_id = commit.id();
1156 let commit_author = commit.author();
1157 let summary_sanitized = sanitize_summary(&subject);
1158 let this_message_id = format!("<{}.{}>", commit_id, message_id_suffix);
1159 let parent = try!(commit.parent(0));
1160 let diff = try!(repo.diff_tree_to_tree(Some(&parent.tree().unwrap()), Some(&commit.tree().unwrap()), None));
1161 let stats = try!(diffstat(&diff));
1162
1163 if !to_stdout {
1164 out = try!(patch_file(&format!("{:04}-{}.patch", commit_num+1, summary_sanitized)));
1165 }
1166 try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", commit_id));
1167 try!(writeln!(out, "Message-Id: {}", this_message_id));
1168 if let Some(ref message_id) = in_reply_to_message_id {
1169 try!(writeln!(out, "In-Reply-To: {}", message_id));
1170 try!(writeln!(out, "References: {}", message_id));
1171 }
1172 if commit_num == 0 && cover_entry.is_none() {
1173 in_reply_to_message_id = Some(this_message_id);
1174 }
1175 try!(writeln!(out, "From: {} <{}>", author_name, author_email));
1176 try!(writeln!(out, "Date: {}", date_822(commit_author.when())));
1177 try!(writeln!(out, "Subject: [PATCH {}/{}] {}\n", commit_num+1, commits.len(), subject));
1178 if !body.is_empty() {
1179 try!(write!(out, "{}{}", body, ensure_nl(&body)));
1180 }
1181 try!(writeln!(out, "---"));
1182 try!(writeln!(out, "{}", stats));
1183 try!(write_diff(&mut out, &diff));
1184 try!(writeln!(out, "{}", signature));
1185 }
1186
1187 Ok(())
1188}
1189
1190fn log(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1191 let config = try!(try!(repo.config()).snapshot());
1192 try!(out.auto_pager(&config, "log", true));
1193 let color_commit = try!(out.get_color(&config, "diff", "commit", "yellow"));
1194
1195 let mut revwalk = try!(repo.revwalk());
1196 revwalk.simplify_first_parent();
1197 try!(revwalk.push_ref(SHEAD_REF));
1198
1199 let show_diff = m.is_present("patch");
1200
1201 for oid in revwalk {
1202 let oid = try!(oid);
1203 let commit = try!(repo.find_commit(oid));
1204 let tree = try!(commit.tree());
1205 let author = commit.author();
1206
1207 let first_parent_id = try!(commit.parent_id(0).map_err(|e| format!("Malformed series commit {}: {}", oid, e)));
1208 let first_series_commit = tree.iter().find(|entry| entry.id() == first_parent_id).is_some();
1209
1210 try!(writeln!(out, "{}", color_commit.paint(format!("commit {}", oid))));
1211 try!(writeln!(out, "Author: {} <{}>", author.name().unwrap(), author.email().unwrap()));
1212 try!(writeln!(out, "Date: {}\n", date_822(author.when())));
1213 for line in commit.message().unwrap().lines() {
1214 try!(writeln!(out, " {}", line));
1215 }
1216 if show_diff {
1217 try!(writeln!(out, ""));
1218 let parent_tree = if first_series_commit {
1219 None
1220 } else {
1221 Some(try!(try!(repo.find_commit(first_parent_id)).tree()))
1222 };
1223 let diff = try!(repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&tree), None));
1224 try!(write_diff(out, &diff));
1225 }
1226
1227 if first_series_commit {
1228 break;
1229 } else {
1230 try!(writeln!(out, ""));
1231 }
1232 }
1233
1234 Ok(())
1235}
1236
1237fn rebase(repo: &Repository, m: &ArgMatches) -> Result<()> {
1238 match repo.state() {
1239 git2::RepositoryState::Clean => (),
1240 git2::RepositoryState::RebaseMerge if repo.path().join("rebase-merge").join("git-series").exists() => {
1241 return Err("git series rebase already in progress.\nUse \"git rebase --continue\" or \"git rebase --abort\".".into());
1242 },
1243 s => { return Err(format!("{:?} in progress; cannot rebase", s).into()); }
1244 }
1245
1246 let internals = try!(Internals::read(repo));
1247 let series = try!(try!(internals.working.get("series")).ok_or("Could not find entry \"series\" in working index"));
1248 let base = try!(try!(internals.working.get("base")).ok_or("Cannot rebase series; no base set.\nUse \"git series base\" to set base."));
1249 if series.id() == base.id() {
1250 return Err("No patches to rebase; series and base identical.".into());
1251 } else if !try!(repo.graph_descendant_of(series.id(), base.id())) {
1252 return Err(format!("Cannot rebase: current base {} not an ancestor of series {}", base.id(), series.id()).into());
1253 }
1254
1255 // Check for unstaged or uncommitted changes before attempting to rebase.
1256 let series_commit = try!(repo.find_commit(series.id()));
1257 let series_tree = try!(series_commit.tree());
1258 let mut unclean = String::new();
1259 if !diff_empty(&try!(repo.diff_tree_to_index(Some(&series_tree), None, None))) {
1260 writeln!(unclean, "Cannot rebase: you have unstaged changes.").unwrap();
1261 }
1262 if !diff_empty(&try!(repo.diff_index_to_workdir(None, None))) {
1263 if unclean.is_empty() {
1264 writeln!(unclean, "Cannot rebase: your index contains uncommitted changes.").unwrap();
1265 } else {
1266 writeln!(unclean, "Additionally, your index contains uncommitted changes.").unwrap();
1267 }
1268 }
1269 if !unclean.is_empty() {
1270 return Err(unclean.into());
1271 }
1272
1273 let mut revwalk = try!(repo.revwalk());
1274 revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1275 try!(revwalk.push(series.id()));
1276 try!(revwalk.hide(base.id()));
1277 let commits: Vec<Commit> = try!(revwalk.map(|c| {
1278 let id = try!(c);
1279 let mut commit = try!(repo.find_commit(id));
1280 if commit.parent_ids().count() > 1 {
1281 return Err(format!("Error: cannot rebase merge commit:\n{}", try!(commit_obj_summarize(&mut commit))).into());
1282 }
1283 Ok(commit)
1284 }).collect::<Result<_>>());
1285
1286 let interactive = m.is_present("interactive");
1287 let onto = match m.value_of("onto") {
1288 None => None,
1289 Some(onto) => {
1290 let obj = try!(repo.revparse_single(onto));
1291 Some(obj.id())
1292 },
1293 };
1294
1295 let newbase = onto.unwrap_or(base.id());
1296 let (base_short, _) = try!(commit_summarize_components(&repo, base.id()));
1297 let (newbase_short, _) = try!(commit_summarize_components(&repo, newbase));
1298 let (series_short, _) = try!(commit_summarize_components(&repo, series.id()));
1299
1300 let newbase_obj = try!(repo.find_commit(newbase)).into_object();
1301
1302 let dir = try!(TempDir::new_in(repo.path(), "rebase-merge"));
1303 let final_path = repo.path().join("rebase-merge");
1304 let mut create = std::fs::OpenOptions::new();
1305 create.write(true).create_new(true);
1306
1307 try!(create.open(dir.path().join("git-series")));
1308 try!(create.open(dir.path().join("quiet")));
1309 try!(create.open(dir.path().join("interactive")));
1310
1311 let mut head_name_file = try!(create.open(dir.path().join("head-name")));
1312 try!(writeln!(head_name_file, "detached HEAD"));
1313
1314 let mut onto_file = try!(create.open(dir.path().join("onto")));
1315 try!(writeln!(onto_file, "{}", newbase));
1316
1317 let mut orig_head_file = try!(create.open(dir.path().join("orig-head")));
1318 try!(writeln!(orig_head_file, "{}", series.id()));
1319
1320 let git_rebase_todo_filename = dir.path().join("git-rebase-todo");
1321 let mut git_rebase_todo = try!(create.open(&git_rebase_todo_filename));
1322 for mut commit in commits {
1323 try!(writeln!(git_rebase_todo, "pick {}", try!(commit_obj_summarize(&mut commit))));
1324 }
1325 if let Some(onto) = onto {
1326 try!(writeln!(git_rebase_todo, "exec git series base {}", onto));
1327 }
1328 try!(writeln!(git_rebase_todo, "\n# Rebase {}..{} onto {}", base_short, series_short, newbase_short));
1329 try!(write!(git_rebase_todo, "{}", REBASE_COMMENT));
1330 drop(git_rebase_todo);
1331
1332 // Interactive editor if interactive {
1333 if interactive {
1334 let config = try!(repo.config());
1335 try!(run_editor(&config, &git_rebase_todo_filename));
1336 let mut file = try!(File::open(&git_rebase_todo_filename));
1337 let mut todo = String::new();
1338 try!(file.read_to_string(&mut todo));
1339 let todo = try!(git2::message_prettify(todo, git2::DEFAULT_COMMENT_CHAR));
1340 if todo.is_empty() {
1341 return Err("Nothing to do".into());
1342 }
1343 }
1344
1345 // Avoid races by not calling .into_path until after the rename succeeds.
1346 try!(std::fs::rename(dir.path(), final_path));
1347 dir.into_path();
1348
1349 try!(checkout_tree(repo, &newbase_obj));
1350 try!(repo.reference("HEAD", newbase, true, &format!("rebase -i (start): checkout {}", newbase)));
1351
1352 let status = try!(Command::new("git").arg("rebase").arg("--continue").status());
1353 if !status.success() {
1354 return Err(format!("git rebase --continue exited with status {}", status).into());
1355 }
1356
1357 Ok(())
1358}
1359
1360fn req(out: &mut Output, repo: &Repository, m: &ArgMatches) -> Result<()> {
1361 let config = try!(repo.config());
1362 let shead = try!(repo.find_reference(SHEAD_REF));
1363 let shead_commit = try!(peel_to_commit(try!(shead.resolve())));
1364 let stree = try!(shead_commit.tree());
1365
1366 let series = try!(stree.get_name("series").ok_or("Internal error: series did not contain \"series\""));
1367 let series_id = series.id();
1368 let mut series_commit = try!(repo.find_commit(series_id));
1369 let base = try!(stree.get_name("base").ok_or("Cannot request pull; no base set.\nUse \"git series base\" to set base."));
1370 let mut base_commit = try!(repo.find_commit(base.id()));
1371
1372 let (cover_content, subject, cover_body) = if let Some(entry) = stree.get_name("cover") {
1373 let cover_blob = try!(repo.find_blob(entry.id()));
1374 let content = try!(std::str::from_utf8(cover_blob.content())).to_string();
1375 let (subject, body) = split_message(&content);
1376 (Some(content.to_string()), subject.to_string(), Some(body.to_string()))
1377 } else {
1378 (None, try!(shead_series_name(&shead)), None)
1379 };
1380
1381 let url = m.value_of("url").unwrap();
1382 let tag = m.value_of("tag").unwrap();
1383 let full_tag = format!("refs/tags/{}", tag);
1384 let full_tag_peeled = format!("{}^{{}}", full_tag);
1385 let full_head = format!("refs/heads/{}", tag);
1386 let mut remote = try!(repo.remote_anonymous(url));
1387 try!(remote.connect(git2::Direction::Fetch).map_err(|e| format!("Could not connect to remote repository {}\n{}", url, e)));
1388 let remote_heads = try!(remote.list());
1389
1390 /* Find the requested name as either a tag or head */
1391 let mut opt_remote_tag = None;
1392 let mut opt_remote_tag_peeled = None;
1393 let mut opt_remote_head = None;
1394 for h in remote_heads {
1395 if h.name() == full_tag {
1396 opt_remote_tag = Some(h.oid());
1397 } else if h.name() == full_tag_peeled {
1398 opt_remote_tag_peeled = Some(h.oid());
1399 } else if h.name() == full_head {
1400 opt_remote_head = Some(h.oid());
1401 }
1402 }
1403 let (msg, extra_body, remote_pull_name) = match (opt_remote_tag, opt_remote_tag_peeled, opt_remote_head) {
1404 (Some(remote_tag), Some(remote_tag_peeled), _) => {
1405 if remote_tag_peeled != series_id {
1406 return Err(format!("Remote tag {} does not refer to series {}", tag, series_id).into());
1407 }
1408 let local_tag = try!(repo.find_tag(remote_tag).map_err(|e|
1409 format!("Could not find remote tag {} ({}) in local repository: {}", tag, remote_tag, e)));
1410 let mut local_tag_msg = local_tag.message().unwrap().to_string();
1411 if let Some(sig_index) = local_tag_msg.find("-----BEGIN PGP ") {
1412 local_tag_msg.truncate(sig_index);
1413 }
1414 let extra_body = match cover_content {
1415 Some(ref content) if !local_tag_msg.contains(content) => cover_body,
1416 _ => None,
1417 };
1418 (Some(local_tag_msg), extra_body, full_tag)
1419 },
1420 (Some(remote_tag), None, _) => {
1421 if remote_tag != series_id {
1422 return Err(format!("Remote unannotated tag {} does not refer to series {}", tag, series_id).into());
1423 }
1424 (cover_content, None, full_tag)
1425 }
1426 (_, _, Some(remote_head)) => {
1427 if remote_head != series_id {
1428 return Err(format!("Remote branch {} does not refer to series {}", tag, series_id).into());
1429 }
1430 (cover_content, None, full_head)
1431 },
1432 _ => {
1433 return Err(format!("Remote does not have either a tag or branch named {}", tag).into())
1434 }
1435 };
1436
1437 let commit_subject_date = |commit: &mut Commit| -> String {
1438 let date = date_822(commit.author().when());
1439 let summary = commit.summary().unwrap();
1440 format!(" {} ({})", summary, date)
1441 };
1442
1443 let mut revwalk = try!(repo.revwalk());
1444 revwalk.set_sorting(git2::SORT_TOPOLOGICAL|git2::SORT_REVERSE);
1445 try!(revwalk.push(series_id));
1446 try!(revwalk.hide(base.id()));
1447 let mut commits: Vec<Commit> = try!(revwalk.map(|c| {
1448 Ok(try!(repo.find_commit(try!(c))))
1449 }).collect::<Result<_>>());
1450 if commits.is_empty() {
1451 return Err("No patches to request pull of; series and base identical.".into());
1452 }
1453
1454 let author = try!(get_signature(&config, "AUTHOR"));
1455 let author_email = author.email().unwrap();
1456 let message_id = format!("<pull.{}.{}.git-series.{}>", shead_commit.id(), author.when().seconds(), author_email);
1457
1458 let diff = try!(repo.diff_tree_to_tree(Some(&base_commit.tree().unwrap()), Some(&series_commit.tree().unwrap()), None));
1459 let stats = try!(diffstat(&diff));
1460
1461 try!(out.auto_pager(&config, "request-pull", true));
1462 try!(writeln!(out, "From {} Mon Sep 17 00:00:00 2001", shead_commit.id()));
1463 try!(writeln!(out, "Message-Id: {}", message_id));
1464 try!(writeln!(out, "From: {} <{}>", author.name().unwrap(), author_email));
1465 try!(writeln!(out, "Date: {}", date_822(author.when())));
1466 try!(writeln!(out, "Subject: [GIT PULL] {}\n", subject));
1467 if let Some(extra_body) = extra_body {
1468 try!(writeln!(out, "{}", extra_body));
1469 }
1470 try!(writeln!(out, "The following changes since commit {}:\n", base.id()));
1471 try!(writeln!(out, "{}\n", commit_subject_date(&mut base_commit)));
1472 try!(writeln!(out, "are available in the git repository at:\n"));
1473 try!(writeln!(out, " {} {}\n", url, remote_pull_name));
1474 try!(writeln!(out, "for you to fetch changes up to {}:\n", series.id()));
1475 try!(writeln!(out, "{}\n", commit_subject_date(&mut series_commit)));
1476 try!(writeln!(out, "----------------------------------------------------------------"));
1477 if let Some(msg) = msg {
1478 try!(writeln!(out, "{}", msg));
1479 try!(writeln!(out, "----------------------------------------------------------------"));
1480 }
1481 try!(writeln!(out, "{}", shortlog(&mut commits)));
1482 try!(writeln!(out, "{}", stats));
1483 if m.is_present("patch") {
1484 try!(write_diff(out, &diff));
1485 }
1486 try!(writeln!(out, "{}", mail_signature()));
1487
1488 Ok(())
1489}
1490
1491fn main() {
1492 let m = App::new("git-series")
1493 .bin_name("git series")
1494 .about("Track patch series in git")
1495 .author("Josh Triplett <josh@joshtriplett.org>")
1496 .version(crate_version!())
1497 .global_setting(AppSettings::ColoredHelp)
1498 .global_setting(AppSettings::VersionlessSubcommands)
1499 .subcommands(vec![
1500 SubCommand::with_name("add")
1501 .about("Add changes to the index for the next series commit")
1502 .arg_from_usage("<change>... 'Changes to add (\"series\", \"base\", \"cover\")'"),
1503 SubCommand::with_name("base")
1504 .about("Get or set the base commit for the patch series")
1505 .arg(Arg::with_name("base").help("Base commit").conflicts_with("delete"))
1506 .arg_from_usage("-d, --delete 'Clear patch series base'"),
1507 SubCommand::with_name("checkout")
1508 .about("Resume work on a patch series; check out the current version")
1509 .arg_from_usage("<name> 'Patch series to check out'"),
1510 SubCommand::with_name("commit")
1511 .about("Record changes to the patch series")
1512 .arg_from_usage("-a, --all 'Commit all changes'")
1513 .arg_from_usage("-m [msg] 'Commit message'")
1514 .arg_from_usage("-v, --verbose 'Show diff when preparing commit message'"),
1515 SubCommand::with_name("cover")
1516 .about("Create or edit the cover letter for the patch series")
1517 .arg_from_usage("-d, --delete 'Delete cover letter'"),
1518 SubCommand::with_name("delete")
1519 .about("Delete a patch series")
1520 .arg_from_usage("<name> 'Patch series to delete'"),
1521 SubCommand::with_name("detach")
1522 .about("Stop working on any patch series"),
1523 SubCommand::with_name("format")
1524 .arg_from_usage("--stdout 'Write patches to stdout rather than files.")
1525 .about("Prepare patch series for email"),
1526 SubCommand::with_name("log")
1527 .about("Show the history of the patch series")
1528 .arg_from_usage("-p, --patch 'Include a patch for each change committed to the series'"),
1529 SubCommand::with_name("rebase")
1530 .about("Rebase the patch series")
1531 .arg_from_usage("[onto] 'Commit to rebase onto'")
1532 .arg_from_usage("-i, --interactive 'Interactively edit the list of commits'")
1533 .group(ArgGroup::with_name("action").args(&["onto", "interactive"]).multiple(true).required(true)),
1534 SubCommand::with_name("req")
1535 .about("Generate a mail requesting a pull of the patch series")
1536 .visible_aliases(&["pull-request", "request-pull"])
1537 .arg_from_usage("-p, --patch 'Include patch in the mail'")
1538 .arg_from_usage("<url> 'Repository URL to request pull of'")
1539 .arg_from_usage("<tag> 'Tag or branch name to request pull of'"),
1540 SubCommand::with_name("status")
1541 .about("Show the status of the patch series"),
1542 SubCommand::with_name("start")
1543 .about("Start a new patch series")
1544 .arg_from_usage("<name> 'Patch series name'"),
1545 SubCommand::with_name("unadd")
1546 .about("Undo \"git series add\", removing changes from the next series commit")
1547 .arg_from_usage("<change>... 'Changes to remove (\"series\", \"base\", \"cover\")'"),
1548 ]).get_matches();
1549
1550 let mut out = Output::new();
1551
1552 let err = || -> Result<()> {
1553 let repo = try!(Repository::discover("."));
1554 match m.subcommand() {
1555 ("", _) => series(&mut out, &repo),
1556 ("add", Some(ref sm)) => add(&repo, &sm),
1557 ("base", Some(ref sm)) => base(&repo, &sm),
1558 ("checkout", Some(ref sm)) => checkout(&repo, &sm),
1559 ("commit", Some(ref sm)) => commit_status(&mut out, &repo, &sm, false),
1560 ("cover", Some(ref sm)) => cover(&repo, &sm),
1561 ("delete", Some(ref sm)) => delete(&repo, &sm),
1562 ("detach", _) => detach(&repo),
1563 ("format", Some(ref sm)) => format(&mut out, &repo, &sm),
1564 ("log", Some(ref sm)) => log(&mut out, &repo, &sm),
1565 ("rebase", Some(ref sm)) => rebase(&repo, &sm),
1566 ("req", Some(ref sm)) => req(&mut out, &repo, &sm),
1567 ("start", Some(ref sm)) => start(&repo, &sm),
1568 ("status", Some(ref sm)) => commit_status(&mut out, &repo, &sm, true),
1569 ("unadd", Some(ref sm)) => unadd(&repo, &sm),
1570 _ => unreachable!()
1571 }
1572 }();
1573
1574 if let Err(e) = err {
1575 let msg = e.to_string();
1576 out.write_err(&format!("{}{}", msg, ensure_nl(&msg)));
1577 drop(out);
1578 std::process::exit(1);
1579 }
1580}