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