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