1use bstr::ByteSlice;
8use itertools::Itertools;
9
10pub fn head_id(repo: &git2::Repository) -> Option<git2::Oid> {
12 repo.head().ok()?.resolve().ok()?.target()
13}
14
15pub fn head_branch(repo: &git2::Repository) -> Option<String> {
17 repo.head()
18 .ok()?
19 .resolve()
20 .ok()?
21 .shorthand()
22 .map(String::from)
23 .ok()
24}
25
26pub fn is_dirty(repo: &git2::Repository) -> bool {
28 if repo.state() != git2::RepositoryState::Clean {
29 log::trace!("Repository status is unclean: {:?}", repo.state());
30 return true;
31 }
32
33 let status = repo
34 .statuses(Some(git2::StatusOptions::new().include_ignored(false)))
35 .unwrap();
36 if status.is_empty() {
37 false
38 } else {
39 log::trace!(
40 "Repository is dirty: {}",
41 status
42 .iter()
43 .filter_map(|s| s.path().map(|s| s.to_owned()).ok())
44 .join(", ")
45 );
46 true
47 }
48}
49
50pub fn cherry_pick(
52 repo: &git2::Repository,
53 head_id: git2::Oid,
54 cherry_id: git2::Oid,
55 sign: Option<&dyn Sign>,
56) -> Result<git2::Oid, git2::Error> {
57 let cherry_commit = repo.find_commit(cherry_id)?;
58 let base_id = match cherry_commit.parent_count() {
59 0 => cherry_id,
60 1 => cherry_commit.parent_id(0)?,
61 _ => cherry_commit
62 .parent_ids()
63 .find(|id| *id == head_id)
64 .map(Ok)
65 .unwrap_or_else(|| cherry_commit.parent_id(0))?,
66 };
67 if base_id == head_id {
68 return Ok(cherry_id);
70 }
71
72 let base_ann_commit = repo.find_annotated_commit(base_id)?;
73 let head_ann_commit = repo.find_annotated_commit(head_id)?;
74 let cherry_ann_commit = repo.find_annotated_commit(cherry_id)?;
75 let mut rebase = repo.rebase(
76 Some(&cherry_ann_commit),
77 Some(&base_ann_commit),
78 Some(&head_ann_commit),
79 Some(git2::RebaseOptions::new().inmemory(true)),
80 )?;
81
82 let mut tip_id = head_id;
83 while let Some(op) = rebase.next() {
84 op.inspect_err(|_err| {
85 let _ = rebase.abort();
86 })?;
87 let inmemory_index = rebase.inmemory_index().unwrap();
88 if inmemory_index.has_conflicts() {
89 let conflicts = inmemory_index
90 .conflicts()?
91 .map(|conflict| {
92 let conflict = conflict.unwrap();
93 let our_path = conflict
94 .our
95 .as_ref()
96 .map(|c| crate::bytes::bytes2path(&c.path))
97 .or_else(|| {
98 conflict
99 .their
100 .as_ref()
101 .map(|c| crate::bytes::bytes2path(&c.path))
102 })
103 .or_else(|| {
104 conflict
105 .ancestor
106 .as_ref()
107 .map(|c| crate::bytes::bytes2path(&c.path))
108 })
109 .unwrap_or_else(|| std::path::Path::new("<unknown>"));
110 format!("{}", our_path.display())
111 })
112 .join("\n ");
113 return Err(git2::Error::new(
114 git2::ErrorCode::Unmerged,
115 git2::ErrorClass::Index,
116 format!("cherry-pick conflicts:\n {conflicts}\n"),
117 ));
118 }
119
120 let mut sig = commit_signature(repo)?;
121 if let (Ok(name), Ok(email)) = (sig.name(), sig.email()) {
122 sig = git2::Signature::new(name, email, &cherry_commit.time())?.to_owned();
124 }
125 let commit_id = rebase.commit(None, &sig, None).inspect_err(|_err| {
126 let _ = rebase.abort();
127 });
128 let commit_id = match commit_id {
129 Ok(commit_id) => Ok(commit_id),
130 Err(err) => {
131 if err.class() == git2::ErrorClass::Rebase && err.code() == git2::ErrorCode::Applied
132 {
133 log::trace!("Skipping {cherry_id}, already applied to {head_id}");
134 return Ok(tip_id);
135 }
136 Err(err)
137 }
138 }?;
139
140 let rebased_commit = repo.find_commit(commit_id).expect("commit succeeded");
141 let tree = rebased_commit.tree()?;
142 let parent_commit = repo.find_commit(head_id).expect("it worked earlier");
143 let signed_id = commit(
144 repo,
145 &rebased_commit.author(),
146 &rebased_commit.committer(),
147 rebased_commit.message().unwrap(),
148 &tree,
149 &[&parent_commit],
150 sign,
151 )?;
152
153 tip_id = signed_id;
154 }
155 rebase.finish(None)?;
156 Ok(tip_id)
157}
158
159pub fn squash(
163 repo: &git2::Repository,
164 head_id: git2::Oid,
165 into_id: git2::Oid,
166 sign: Option<&dyn Sign>,
167) -> Result<git2::Oid, git2::Error> {
168 let head_commit = repo.find_commit(head_id)?;
170 let head_tree = repo.find_tree(head_commit.tree_id())?;
171
172 let base_commit = if 0 < head_commit.parent_count() {
173 head_commit.parent(0)?
174 } else {
175 head_commit.clone()
176 };
177 let base_tree = repo.find_tree(base_commit.tree_id())?;
178
179 let into_commit = repo.find_commit(into_id)?;
180 let into_tree = repo.find_tree(into_commit.tree_id())?;
181
182 let onto_commit;
183 let onto_commits;
184 let onto_commits: &[&git2::Commit<'_>] = if 0 < into_commit.parent_count() {
185 onto_commit = into_commit.parent(0)?;
186 onto_commits = [&onto_commit];
187 &onto_commits
188 } else {
189 &[]
190 };
191
192 let mut result_index = repo.merge_trees(&base_tree, &into_tree, &head_tree, None)?;
193 if result_index.has_conflicts() {
194 let conflicts = result_index
195 .conflicts()?
196 .map(|conflict| {
197 let conflict = conflict.unwrap();
198 let our_path = conflict
199 .our
200 .as_ref()
201 .map(|c| crate::bytes::bytes2path(&c.path))
202 .or_else(|| {
203 conflict
204 .their
205 .as_ref()
206 .map(|c| crate::bytes::bytes2path(&c.path))
207 })
208 .or_else(|| {
209 conflict
210 .ancestor
211 .as_ref()
212 .map(|c| crate::bytes::bytes2path(&c.path))
213 })
214 .unwrap_or_else(|| std::path::Path::new("<unknown>"));
215 format!("{}", our_path.display())
216 })
217 .join("\n ");
218 return Err(git2::Error::new(
219 git2::ErrorCode::Unmerged,
220 git2::ErrorClass::Index,
221 format!("squash conflicts:\n {conflicts}\n"),
222 ));
223 }
224 let result_id = result_index.write_tree_to(repo)?;
225 let result_tree = repo.find_tree(result_id)?;
226 let new_id = commit(
227 repo,
228 &into_commit.author(),
229 &into_commit.committer(),
230 into_commit.message().unwrap(),
231 &result_tree,
232 onto_commits,
233 sign,
234 )?;
235 Ok(new_id)
236}
237
238pub fn reword(
240 repo: &git2::Repository,
241 head_id: git2::Oid,
242 msg: &str,
243 sign: Option<&dyn Sign>,
244) -> Result<git2::Oid, git2::Error> {
245 let old_commit = repo.find_commit(head_id)?;
246 let parents = old_commit.parents().collect::<Vec<_>>();
247 let parents = parents.iter().collect::<Vec<_>>();
248 let tree = repo.find_tree(old_commit.tree_id())?;
249 let new_id = commit(
250 repo,
251 &old_commit.author(),
252 &old_commit.committer(),
253 msg,
254 &tree,
255 &parents,
256 sign,
257 )?;
258 Ok(new_id)
259}
260
261pub fn commit(
263 repo: &git2::Repository,
264 author: &git2::Signature<'_>,
265 committer: &git2::Signature<'_>,
266 message: &str,
267 tree: &git2::Tree<'_>,
268 parents: &[&git2::Commit<'_>],
269 sign: Option<&dyn Sign>,
270) -> Result<git2::Oid, git2::Error> {
271 if let Some(sign) = sign {
272 let content = repo.commit_create_buffer(author, committer, message, tree, parents)?;
273 let content = std::str::from_utf8(&content).unwrap();
274 let signed = sign.sign(content)?;
275 repo.commit_signed(content, &signed, None)
276 } else {
277 repo.commit(None, author, committer, message, tree, parents)
278 }
279}
280
281pub trait Sign {
285 fn sign(&self, buffer: &str) -> Result<String, git2::Error>;
286}
287
288pub struct UserSign(UserSignInner);
289
290enum UserSignInner {
291 Gpg(GpgSign),
292 Ssh(SshSign),
293}
294
295impl UserSign {
296 pub fn from_config(
297 repo: &git2::Repository,
298 config: &git2::Config,
299 ) -> Result<Self, git2::Error> {
300 let format = config
301 .get_string("gpg.format")
302 .unwrap_or_else(|_| "openpgp".to_owned());
303 match format.as_str() {
304 "openpgp" => {
305 let program = config
306 .get_string("gpg.openpgp.program")
307 .or_else(|_| config.get_string("gpg.program"))
308 .unwrap_or_else(|_| "gpg".to_owned());
309
310 let signing_key = config.get_string("user.signingkey").or_else(
311 |_| -> Result<_, git2::Error> {
312 let sig = commit_signature(repo)?;
313 Ok(sig.to_string())
314 },
315 )?;
316
317 Ok(UserSign(UserSignInner::Gpg(GpgSign::new(
318 program,
319 signing_key,
320 ))))
321 }
322 "x509" => {
323 let program = config
324 .get_string("gpg.x509.program")
325 .unwrap_or_else(|_| "gpgsm".to_owned());
326
327 let signing_key = config.get_string("user.signingkey").or_else(
328 |_| -> Result<_, git2::Error> {
329 let sig = commit_signature(repo)?;
330 Ok(sig.to_string())
331 },
332 )?;
333
334 Ok(UserSign(UserSignInner::Gpg(GpgSign::new(
335 program,
336 signing_key,
337 ))))
338 }
339 "ssh" => {
340 let program = config
341 .get_string("gpg.ssh.program")
342 .unwrap_or_else(|_| "ssh-keygen".to_owned());
343
344 let signing_key = config
345 .get_string("user.signingkey")
346 .map(Ok)
347 .unwrap_or_else(|_| -> Result<_, git2::Error> {
348 get_default_ssh_signing_key(config)?.map(Ok).unwrap_or_else(
349 || -> Result<_, git2::Error> {
350 let sig = commit_signature(repo)?;
351 Ok(sig.to_string())
352 },
353 )
354 })?;
355
356 Ok(UserSign(UserSignInner::Ssh(SshSign::new(
357 program,
358 signing_key,
359 ))))
360 }
361 _ => Err(git2::Error::new(
362 git2::ErrorCode::Invalid,
363 git2::ErrorClass::Config,
364 format!("invalid valid for gpg.format: {format}"),
365 )),
366 }
367 }
368}
369
370impl Sign for UserSign {
371 fn sign(&self, buffer: &str) -> Result<String, git2::Error> {
372 match &self.0 {
373 UserSignInner::Gpg(s) => s.sign(buffer),
374 UserSignInner::Ssh(s) => s.sign(buffer),
375 }
376 }
377}
378
379pub struct GpgSign {
380 program: String,
381 signing_key: String,
382}
383
384impl GpgSign {
385 pub fn new(program: String, signing_key: String) -> Self {
386 Self {
387 program,
388 signing_key,
389 }
390 }
391}
392
393impl Sign for GpgSign {
394 fn sign(&self, buffer: &str) -> Result<String, git2::Error> {
395 let output = pipe_command(
396 std::process::Command::new(&self.program)
397 .arg("--status-fd=2")
398 .arg("-bsau")
399 .arg(&self.signing_key),
400 Some(buffer),
401 )
402 .map_err(|e| {
403 git2::Error::new(
404 git2::ErrorCode::GenericError,
405 git2::ErrorClass::Os,
406 format!("{} failed to sign the data: {}", self.program, e),
407 )
408 })?;
409 if !output.status.success() {
410 return Err(git2::Error::new(
411 git2::ErrorCode::GenericError,
412 git2::ErrorClass::Os,
413 format!("{} failed to sign the data", self.program),
414 ));
415 }
416 if output.stderr.find(b"\n[GNUPG:] SIG_CREATED ").is_none() {
417 return Err(git2::Error::new(
418 git2::ErrorCode::GenericError,
419 git2::ErrorClass::Os,
420 format!("{} failed to sign the data", self.program),
421 ));
422 }
423
424 let sig = std::str::from_utf8(&output.stdout).map_err(|e| {
425 git2::Error::new(
426 git2::ErrorCode::GenericError,
427 git2::ErrorClass::Os,
428 format!("{} failed to sign the data: {}", self.program, e),
429 )
430 })?;
431
432 let normalized = remove_cr_after(sig);
434
435 Ok(normalized)
436 }
437}
438
439pub struct SshSign {
440 program: String,
441 signing_key: String,
442}
443
444impl SshSign {
445 pub fn new(program: String, signing_key: String) -> Self {
446 Self {
447 program,
448 signing_key,
449 }
450 }
451}
452
453impl Sign for SshSign {
454 fn sign(&self, buffer: &str) -> Result<String, git2::Error> {
455 let mut literal_key_file = None;
456 let ssh_signing_key_file = if let Some(literal_key) = literal_key(&self.signing_key) {
457 let temp = tempfile::NamedTempFile::new().map_err(|e| {
458 git2::Error::new(
459 git2::ErrorCode::GenericError,
460 git2::ErrorClass::Os,
461 format!("failed writing ssh signing key: {e}"),
462 )
463 })?;
464
465 std::fs::write(temp.path(), literal_key).map_err(|e| {
466 git2::Error::new(
467 git2::ErrorCode::GenericError,
468 git2::ErrorClass::Os,
469 format!("failed writing ssh signing key: {e}"),
470 )
471 })?;
472 let path = temp.path().to_owned();
473 literal_key_file = Some(temp);
474 path
475 } else {
476 fn expanduser(path: &str) -> std::path::PathBuf {
477 std::path::PathBuf::from(path)
479 }
480
481 expanduser(&self.signing_key)
483 };
484
485 let buffer_file = tempfile::NamedTempFile::new().map_err(|e| {
486 git2::Error::new(
487 git2::ErrorCode::GenericError,
488 git2::ErrorClass::Os,
489 format!("failed writing buffer: {e}"),
490 )
491 })?;
492 std::fs::write(buffer_file.path(), buffer).map_err(|e| {
493 git2::Error::new(
494 git2::ErrorCode::GenericError,
495 git2::ErrorClass::Os,
496 format!("failed writing buffer: {e}"),
497 )
498 })?;
499
500 let output = pipe_command(
501 std::process::Command::new(&self.program)
502 .arg("-Y")
503 .arg("sign")
504 .arg("-n")
505 .arg("git")
506 .arg("-f")
507 .arg(&ssh_signing_key_file)
508 .arg(buffer_file.path()),
509 Some(buffer),
510 )
511 .map_err(|e| {
512 git2::Error::new(
513 git2::ErrorCode::GenericError,
514 git2::ErrorClass::Os,
515 format!("{} failed to sign the data: {}", self.program, e),
516 )
517 })?;
518 if !output.status.success() {
519 if output.stderr.find("usage:").is_some() {
520 return Err(git2::Error::new(
521 git2::ErrorCode::GenericError,
522 git2::ErrorClass::Os,
523 "ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)",
524 ));
525 } else {
526 return Err(git2::Error::new(
527 git2::ErrorCode::GenericError,
528 git2::ErrorClass::Os,
529 format!(
530 "{} failed to sign the data: {}",
531 self.program,
532 String::from_utf8_lossy(&output.stderr)
533 ),
534 ));
535 }
536 }
537
538 let mut ssh_signature_filename = buffer_file.path().as_os_str().to_owned();
539 ssh_signature_filename.push(".sig");
540 let ssh_signature_filename = std::path::PathBuf::from(ssh_signature_filename);
541 let sig = std::fs::read_to_string(&ssh_signature_filename).map_err(|e| {
542 git2::Error::new(
543 git2::ErrorCode::GenericError,
544 git2::ErrorClass::Os,
545 format!(
546 "failed reading ssh signing data buffer from {}: {}",
547 ssh_signature_filename.display(),
548 e
549 ),
550 )
551 })?;
552 let normalized = remove_cr_after(&sig);
554
555 buffer_file.close().map_err(|e| {
556 git2::Error::new(
557 git2::ErrorCode::GenericError,
558 git2::ErrorClass::Os,
559 format!("failed writing buffer: {e}"),
560 )
561 })?;
562 if let Some(literal_key_file) = literal_key_file {
563 literal_key_file.close().map_err(|e| {
564 git2::Error::new(
565 git2::ErrorCode::GenericError,
566 git2::ErrorClass::Os,
567 format!("failed writing ssh signing key: {e}"),
568 )
569 })?;
570 }
571
572 Ok(normalized)
573 }
574}
575
576fn pipe_command(
577 cmd: &mut std::process::Command,
578 stdin: Option<&str>,
579) -> Result<std::process::Output, std::io::Error> {
580 use std::io::Write;
581
582 let mut child = cmd
583 .stdin(if stdin.is_some() {
584 std::process::Stdio::piped()
585 } else {
586 std::process::Stdio::null()
587 })
588 .stdout(std::process::Stdio::piped())
589 .stderr(std::process::Stdio::piped())
590 .spawn()?;
591 if let Some(stdin) = stdin {
592 let mut stdin_sync = child.stdin.take().expect("stdin is piped");
593 write!(stdin_sync, "{stdin}")?;
594 }
595 child.wait_with_output()
596}
597
598fn remove_cr_after(sig: &str) -> String {
599 let mut normalized = String::new();
600 for line in sig.lines() {
601 normalized.push_str(line);
602 normalized.push('\n');
603 }
604 normalized
605}
606
607fn literal_key(signing_key: &str) -> Option<&str> {
608 if let Some(literal) = signing_key.strip_prefix("key::") {
609 Some(literal)
610 } else if signing_key.starts_with("ssh-") {
611 Some(signing_key)
612 } else {
613 None
614 }
615}
616
617fn get_default_ssh_signing_key(config: &git2::Config) -> Result<Option<String>, git2::Error> {
619 let ssh_default_key_command = config
620 .get_string("gpg.ssh.defaultKeyCommand")
621 .map_err(|_| {
622 git2::Error::new(
623 git2::ErrorCode::Invalid,
624 git2::ErrorClass::Config,
625 "either user.signingkey or gpg.ssh.defaultKeyCommand needs to be configured",
626 )
627 })?;
628 let ssh_default_key_args = shlex::split(&ssh_default_key_command).ok_or_else(|| {
629 git2::Error::new(
630 git2::ErrorCode::Invalid,
631 git2::ErrorClass::Config,
632 format!("malformed gpg.ssh.defaultKeyCommand: {ssh_default_key_command}"),
633 )
634 })?;
635 if ssh_default_key_args.is_empty() {
636 return Err(git2::Error::new(
637 git2::ErrorCode::Invalid,
638 git2::ErrorClass::Config,
639 format!("malformed gpg.ssh.defaultKeyCommand: {ssh_default_key_command}"),
640 ));
641 }
642
643 let Ok(output) = pipe_command(
644 std::process::Command::new(&ssh_default_key_args[0]).args(&ssh_default_key_args[1..]),
645 None,
646 ) else {
647 return Ok(None);
648 };
649
650 let Ok(keys) = std::str::from_utf8(&output.stdout) else {
651 return Ok(None);
652 };
653 let Some((default_key, _)) = keys.split_once('\n') else {
654 return Ok(None);
655 };
656 if literal_key(default_key).is_none() {
659 return Ok(None);
660 }
661
662 Ok(Some(default_key.to_owned()))
663}
664
665#[doc(hidden)]
666#[deprecated(
667 since = "0.4.3",
668 note = "Replaced with `commit_signature`, `author_signature`"
669)]
670pub fn signature(repo: &git2::Repository) -> Result<git2::Signature<'_>, git2::Error> {
671 commit_signature(repo)
672}
673
674pub fn commit_signature(repo: &git2::Repository) -> Result<git2::Signature<'_>, git2::Error> {
676 let config = repo.config()?;
677 let name = read_signature_field(&config, "GIT_COMMITTER_NAME", "committer.name", "user.name")?;
678 let email = read_signature_field(
679 &config,
680 "GIT_COMMITTER_EMAIL",
681 "committer.email",
682 "user.email",
683 )?;
684
685 git2::Signature::now(&name, &email)
686}
687
688pub fn author_signature(repo: &git2::Repository) -> Result<git2::Signature<'_>, git2::Error> {
690 let config = repo.config()?;
691 let name = read_signature_field(&config, "GIT_AUTHOR_NAME", "author.name", "user.name")?;
692 let email = read_signature_field(&config, "GIT_AUTHOR_EMAIL", "author.email", "user.email")?;
693
694 git2::Signature::now(&name, &email)
695}
696
697fn read_signature_field(
698 config: &git2::Config,
699 env_var: &str,
700 specialized_key: &str,
701 general_key: &str,
702) -> Result<String, git2::Error> {
703 std::env::var_os(env_var)
704 .map(|os| {
705 os.into_string().map_err(|os| {
706 git2::Error::new(
707 git2::ErrorCode::Unmerged,
708 git2::ErrorClass::Invalid,
709 format!("`{}` is not valid UTF-8: {}", env_var, os.to_string_lossy()),
710 )
711 })
712 })
713 .or_else(|| config.get_string(specialized_key).ok().map(Ok))
714 .unwrap_or_else(|| config.get_string(general_key))
715}