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