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