upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/git.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git.rs')
-rw-r--r--src/git.rs967
1 files changed, 954 insertions, 13 deletions
diff --git a/src/git.rs b/src/git.rs
index 281f00c..f4f73a5 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -6,6 +6,8 @@ use anyhow::{bail, Context, Result};
6use git2::{Oid, Revwalk}; 6use git2::{Oid, Revwalk};
7use nostr::prelude::{sha1::Hash as Sha1Hash, Hash}; 7use nostr::prelude::{sha1::Hash as Sha1Hash, Hash};
8 8
9use crate::sub_commands::prs::list::tag_value;
10
9pub struct Repo { 11pub struct Repo {
10 git_repo: git2::Repository, 12 git_repo: git2::Repository,
11} 13}
@@ -36,14 +38,26 @@ pub trait RepoActions {
36 fn does_commit_exist(&self, commit: &str) -> Result<bool>; 38 fn does_commit_exist(&self, commit: &str) -> Result<bool>;
37 fn get_head_commit(&self) -> Result<Sha1Hash>; 39 fn get_head_commit(&self) -> Result<Sha1Hash>;
38 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>; 40 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>;
41 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String>;
42 /// returns vector ["name", "email", "unixtime"]
43 /// eg ["joe bloggs", "joe@pm.me", "12176,-300"]
44 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
45 /// returns vector ["name", "email", "unixtime"]
46 /// eg ["joe bloggs", "joe@pm.me", "12176,-300"]
47 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
39 fn get_commits_ahead_behind( 48 fn get_commits_ahead_behind(
40 &self, 49 &self,
41 base_commit: &Sha1Hash, 50 base_commit: &Sha1Hash,
42 latest_commit: &Sha1Hash, 51 latest_commit: &Sha1Hash,
43 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>; 52 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>;
44 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String>; 53 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String>;
45 fn checkout(&self, ref_name: &str) -> Result<()>; 54 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash>;
46 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>; 55 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>;
56 fn apply_patch_chain(
57 &self,
58 branch_name: &str,
59 patch_and_ancestors: Vec<nostr::Event>,
60 ) -> Result<Vec<nostr::Event>>;
47} 61}
48 62
49impl RepoActions for Repo { 63impl RepoActions for Repo {
@@ -122,7 +136,7 @@ impl RepoActions for Repo {
122 } 136 }
123 137
124 fn does_commit_exist(&self, commit: &str) -> Result<bool> { 138 fn does_commit_exist(&self, commit: &str) -> Result<bool> {
125 if let Ok(c) = self.git_repo.find_commit(Oid::from_str(commit)?) { 139 if self.git_repo.find_commit(Oid::from_str(commit)?).is_ok() {
126 Ok(true) 140 Ok(true)
127 } else { 141 } else {
128 Ok(false) 142 Ok(false)
@@ -148,6 +162,34 @@ impl RepoActions for Repo {
148 Ok(oid_to_sha1(&parent_oid)) 162 Ok(oid_to_sha1(&parent_oid))
149 } 163 }
150 164
165 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String> {
166 Ok(self
167 .git_repo
168 .find_commit(sha1_to_oid(commit)?)
169 .context(format!("could not find commit {commit}"))?
170 .message_raw()
171 .context("commit message has unusual characters in (not valid utf-8)")?
172 .to_string())
173 }
174
175 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
176 let commit = self
177 .git_repo
178 .find_commit(sha1_to_oid(commit)?)
179 .context(format!("could not find commit {commit}"))?;
180 let sig = commit.author();
181 Ok(git_sig_to_tag_vec(&sig))
182 }
183
184 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
185 let commit = self
186 .git_repo
187 .find_commit(sha1_to_oid(commit)?)
188 .context(format!("could not find commit {commit}"))?;
189 let sig = commit.committer();
190 Ok(git_sig_to_tag_vec(&sig))
191 }
192
151 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String> { 193 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String> {
152 let c = self 194 let c = self
153 .git_repo 195 .git_repo
@@ -225,7 +267,7 @@ impl RepoActions for Repo {
225 Ok((ahead, behind)) 267 Ok((ahead, behind))
226 } 268 }
227 269
228 fn checkout(&self, ref_name: &str) -> Result<()> { 270 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash> {
229 let (object, reference) = self.git_repo.revparse_ext(ref_name)?; 271 let (object, reference) = self.git_repo.revparse_ext(ref_name)?;
230 272
231 self.git_repo.checkout_tree(&object, None)?; 273 self.git_repo.checkout_tree(&object, None)?;
@@ -236,7 +278,9 @@ impl RepoActions for Repo {
236 // this is a commit, not a reference 278 // this is a commit, not a reference
237 None => self.git_repo.set_head_detached(object.id()), 279 None => self.git_repo.set_head_detached(object.id()),
238 }?; 280 }?;
239 Ok(()) 281 let oid = self.git_repo.head()?.peel_to_commit()?.id();
282
283 Ok(oid_to_sha1(&oid))
240 } 284 }
241 285
242 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> { 286 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> {
@@ -244,11 +288,66 @@ impl RepoActions for Repo {
244 .branch( 288 .branch(
245 branch_name, 289 branch_name,
246 &self.git_repo.find_commit(Oid::from_str(commit)?)?, 290 &self.git_repo.find_commit(Oid::from_str(commit)?)?,
247 false, 291 true,
248 ) 292 )
249 .context("branch could not be created")?; 293 .context("branch could not be created")?;
250 Ok(()) 294 Ok(())
251 } 295 }
296 /* returns patches applied */
297 fn apply_patch_chain(
298 &self,
299 branch_name: &str,
300 patch_and_ancestors: Vec<nostr::Event>,
301 ) -> Result<Vec<nostr::Event>> {
302 // filter out existing ancestors
303 let mut patches_to_apply: Vec<nostr::Event> = patch_and_ancestors
304 .into_iter()
305 .filter(|e| {
306 !self
307 .does_commit_exist(&tag_value(e, "commit").unwrap())
308 .unwrap()
309 })
310 .collect();
311
312 let parent_commit_id = tag_value(
313 if let Ok(last_patch) = patches_to_apply.last().context("no patches") {
314 last_patch
315 } else {
316 self.checkout(branch_name).context("latest commit in pr doesnt connect with an existing commit. Try a git pull first.")?;
317 return Ok(vec![]);
318 },
319 "parent-commit",
320 )?;
321
322 // check patches can be applied
323 if !self.does_commit_exist(&parent_commit_id)? {
324 bail!("cannot find parent commit ({parent_commit_id}). run git pull and try again.")
325 }
326
327 // check for rebase or changes
328 if let Ok(current_tip) = self.get_tip_of_local_branch(branch_name) {
329 if !current_tip.to_string().eq(&parent_commit_id) {
330 // TODO: either changes have been made on the local branch or
331 // the latest commit in the pr has rebased onto a newer commit
332 // that you havn't pulled yet ask user whether
333 // they want to proceed
334 }
335 }
336
337 // checkout branch
338 if !self.get_checked_out_branch_name()?.eq(&branch_name) {
339 self.create_branch_at_commit(branch_name, &parent_commit_id)?;
340 }
341 self.checkout(branch_name)?;
342
343 // apply commits
344 patches_to_apply.reverse();
345
346 for patch in &patches_to_apply {
347 apply_patch(self, patch)?;
348 }
349 Ok(patches_to_apply)
350 }
252} 351}
253 352
254fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 353fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
@@ -285,8 +384,142 @@ fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> {
285 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid") 384 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid")
286} 385}
287 386
387fn git_sig_to_tag_vec(sig: &git2::Signature) -> Vec<String> {
388 vec![
389 sig.name().unwrap_or("").to_string(),
390 sig.email().unwrap_or("").to_string(),
391 format!("{},{}", sig.when().seconds(), sig.when().offset_minutes()),
392 ]
393}
394
395fn apply_patch(git_repo: &Repo, patch: &nostr::Event) -> Result<()> {
396 // check parent commit matches head
397 if !git_repo
398 .get_head_commit()?
399 .to_string()
400 .eq(&tag_value(patch, "parent-commit")?)
401 {
402 bail!(
403 "patch parent ({}) doesnt match current head ({})",
404 tag_value(patch, "parent-commit")?,
405 git_repo.get_head_commit()?
406 );
407 }
408
409 let diff_from_patch = git2::Diff::from_buffer(patch.content.as_bytes()).unwrap();
410
411 let mut apply_opts = git2::ApplyOptions::new();
412 apply_opts.check(false);
413
414 git_repo.git_repo.apply(
415 &diff_from_patch,
416 git2::ApplyLocation::WorkDir,
417 Some(&mut apply_opts),
418 )?;
419 // stage and commit
420 let prev_oid = git_repo.git_repo.head().unwrap().peel_to_commit()?;
421
422 let mut index = git_repo.git_repo.index()?;
423 index.add_all(["."], git2::IndexAddOption::DEFAULT, None)?;
424 index.write()?;
425
426 git_repo.git_repo.commit(
427 Some("HEAD"),
428 &extract_sig_from_patch_tags(&patch.tags, "author")?,
429 &extract_sig_from_patch_tags(&patch.tags, "committer")?,
430 tag_value(patch, "description")?.as_str(),
431 &git_repo.git_repo.find_tree(index.write_tree()?)?,
432 &[&prev_oid],
433 )?;
434 // end of stage and commit
435 // check commit applied
436 if git_repo
437 .get_head_commit()?
438 .to_string()
439 .eq(&tag_value(patch, "parent-commit")?)
440 {
441 bail!("applying patch failed");
442 }
443
444 let mut revwalk = git_repo.git_repo.revwalk().context("revwalk error")?;
445 revwalk.push_head().context("revwalk.push_head")?;
446
447 for (i, oid) in revwalk.enumerate() {
448 if i == 0 {
449 let old_commit = git_repo
450 .git_repo
451 .find_commit(oid.context("cannot get oid in revwalk")?)
452 .context("cannot find newly added commit oid")?;
453 // create commit using amend which relects the original commit id
454 let updated_commit_oid = old_commit
455 .amend(
456 None,
457 Some(&old_commit.author()),
458 Some(&old_commit.committer()),
459 None,
460 None,
461 None,
462 )
463 .context("cannot ammend commit to produce new oid")?;
464 // replace the commit with the wrong oid with the newly created one with the
465 // correct oid
466 git_repo
467 .git_repo
468 .head()
469 .context("cannot get head of git_repo")?
470 .set_target(updated_commit_oid, "ref commit with fix committer details")
471 .context("cannot update branch with fixed commit")?;
472
473 if !updated_commit_oid
474 .to_string()
475 .eq(&tag_value(patch, "commit")?)
476 {
477 bail!(
478 "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})",
479 updated_commit_oid.to_string(),
480 tag_value(patch, "commit")?,
481 )
482 }
483 }
484 }
485 Ok(())
486}
487
488fn extract_sig_from_patch_tags<'a>(
489 tags: &'a [nostr::Tag],
490 tag_name: &str,
491) -> Result<git2::Signature<'a>> {
492 let v = tags
493 .iter()
494 .find(|t| t.as_vec()[0].eq(tag_name))
495 .context(format!("tag '{tag_name}' not present in patch"))?
496 .as_vec();
497 if v.len() != 4 {
498 bail!("tag '{tag_name}' is incorrectly formatted")
499 }
500 let git_time: Vec<&str> = v[3].split(',').collect();
501 if git_time.len() != 2 {
502 bail!("tag '{tag_name}' time is incorrectly formatted")
503 }
504 git2::Signature::new(
505 v[1].as_str(),
506 v[2].as_str(),
507 &git2::Time::new(
508 git_time[0]
509 .parse()
510 .context("tag time is incorrectly formatted")?,
511 git_time[1]
512 .parse()
513 .context("tag time offset is incorrectly formatted")?,
514 ),
515 )
516 .context("failed to create git signature")
517}
518
288#[cfg(test)] 519#[cfg(test)]
289mod tests { 520mod tests {
521 use std::fs;
522
290 use test_utils::git::GitTestRepo; 523 use test_utils::git::GitTestRepo;
291 524
292 use super::*; 525 use super::*;
@@ -308,16 +541,177 @@ mod tests {
308 Ok(()) 541 Ok(())
309 } 542 }
310 543
544 mod get_commit_message {
545 use super::*;
546 fn run(message: &str) -> Result<()> {
547 let test_repo = GitTestRepo::default();
548 test_repo.populate()?;
549 std::fs::write(test_repo.dir.join("t100.md"), "some content")?;
550 let oid = test_repo.stage_and_commit(message)?;
551
552 let git_repo = Repo::from_path(&test_repo.dir)?;
553
554 assert_eq!(message, git_repo.get_commit_message(&oid_to_sha1(&oid))?,);
555 Ok(())
556 }
557 #[test]
558 fn one_liner() -> Result<()> {
559 run("add t100.md")
560 }
561
562 #[test]
563 fn multiline() -> Result<()> {
564 run("add t100.md\r\nanother line\r\nthird line")
565 }
566
567 #[test]
568 fn trailing_newlines() -> Result<()> {
569 run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n")
570 }
571
572 #[test]
573 fn unicode_characters() -> Result<()> {
574 run("add t100.md ❤️")
575 }
576 }
577
578 mod get_commit_author {
579 use super::*;
580
581 static NAME: &str = "carole";
582 static EMAIL: &str = "carole@pm.me";
583
584 fn prep(time: &git2::Time) -> Result<Vec<String>> {
585 let test_repo = GitTestRepo::default();
586 test_repo.populate()?;
587 fs::write(test_repo.dir.join("x1.md"), "some content")?;
588 let oid = test_repo.stage_and_commit_custom_signature(
589 "add x1.md",
590 Some(&git2::Signature::new(NAME, EMAIL, time)?),
591 None,
592 )?;
593
594 let git_repo = Repo::from_path(&test_repo.dir)?;
595 git_repo.get_commit_author(&oid_to_sha1(&oid))
596 }
597
598 #[test]
599 fn name() -> Result<()> {
600 let res = prep(&git2::Time::new(5000, 0))?;
601 assert_eq!(NAME, res[0]);
602 Ok(())
603 }
604
605 #[test]
606 fn email() -> Result<()> {
607 let res = prep(&git2::Time::new(5000, 0))?;
608 assert_eq!(EMAIL, res[1]);
609 Ok(())
610 }
611
612 mod time {
613 use super::*;
614
615 #[test]
616 fn no_offset() -> Result<()> {
617 let res = prep(&git2::Time::new(5000, 0))?;
618 assert_eq!("5000,0", res[2]);
619 Ok(())
620 }
621 #[test]
622 fn positive_offset() -> Result<()> {
623 let res = prep(&git2::Time::new(5000, 300))?;
624 assert_eq!("5000,300", res[2]);
625 Ok(())
626 }
627 #[test]
628 fn negative_offset() -> Result<()> {
629 let res = prep(&git2::Time::new(5000, -300))?;
630 assert_eq!("5000,-300", res[2]);
631 Ok(())
632 }
633 }
634
635 mod extract_sig_from_patch_tags {
636 use super::*;
637
638 fn test(time: git2::Time) -> Result<()> {
639 assert_eq!(
640 extract_sig_from_patch_tags(
641 &[nostr::Tag::Generic(
642 nostr::TagKind::Custom("author".to_string()),
643 prep(&time)?,
644 )],
645 "author",
646 )?
647 .to_string(),
648 git2::Signature::new(NAME, EMAIL, &time)?.to_string(),
649 );
650 Ok(())
651 }
652
653 #[test]
654 fn no_offset() -> Result<()> {
655 test(git2::Time::new(5000, 0))
656 }
657
658 #[test]
659 fn positive_offset() -> Result<()> {
660 test(git2::Time::new(5000, 300))
661 }
662
663 #[test]
664 fn negative_offset() -> Result<()> {
665 test(git2::Time::new(5000, -300))
666 }
667 }
668 }
669
670 mod get_commit_comitter {
671 use super::*;
672
673 static NAME: &str = "carole";
674 static EMAIL: &str = "carole@pm.me";
675
676 fn prep(time: &git2::Time) -> Result<Vec<String>> {
677 let test_repo = GitTestRepo::default();
678 test_repo.populate()?;
679 fs::write(test_repo.dir.join("x1.md"), "some content")?;
680 let oid = test_repo.stage_and_commit_custom_signature(
681 "add x1.md",
682 None,
683 Some(&git2::Signature::new(NAME, EMAIL, time)?),
684 )?;
685
686 let git_repo = Repo::from_path(&test_repo.dir)?;
687 git_repo.get_commit_comitter(&oid_to_sha1(&oid))
688 }
689
690 #[test]
691 fn name() -> Result<()> {
692 let res = prep(&git2::Time::new(5000, 0))?;
693 assert_eq!(NAME, res[0]);
694 Ok(())
695 }
696
697 #[test]
698 fn email() -> Result<()> {
699 let res = prep(&git2::Time::new(5000, 0))?;
700 assert_eq!(EMAIL, res[1]);
701 Ok(())
702 }
703 }
704
311 mod does_commit_exist { 705 mod does_commit_exist {
312 use super::*; 706 use super::*;
313 707
314 #[test] 708 #[test]
315 fn existing_commits_results_in_true() -> Result<()> { 709 fn existing_commits_results_in_true() -> Result<()> {
316 let test_repo = GitTestRepo::default(); 710 let test_repo = GitTestRepo::default();
317 let oid = test_repo.populate()?; 711 test_repo.populate()?;
318 let git_repo = Repo::from_path(&test_repo.dir)?; 712 let git_repo = Repo::from_path(&test_repo.dir)?;
319 713
320 assert!(git_repo.does_commit_exist(&"431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?); 714 assert!(git_repo.does_commit_exist("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?);
321 Ok(()) 715 Ok(())
322 } 716 }
323 717
@@ -325,10 +719,10 @@ mod tests {
325 fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false() 719 fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false()
326 -> Result<()> { 720 -> Result<()> {
327 let test_repo = GitTestRepo::default(); 721 let test_repo = GitTestRepo::default();
328 let oid = test_repo.populate()?; 722 test_repo.populate()?;
329 let git_repo = Repo::from_path(&test_repo.dir)?; 723 let git_repo = Repo::from_path(&test_repo.dir)?;
330 724
331 assert!(!git_repo.does_commit_exist(&"000004edc0d2fa118d63faa3c2db9c73d630a5ae")?); 725 assert!(!git_repo.does_commit_exist("000004edc0d2fa118d63faa3c2db9c73d630a5ae")?);
332 Ok(()) 726 Ok(())
333 } 727 }
334 728
@@ -336,10 +730,10 @@ mod tests {
336 fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error() 730 fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error()
337 -> Result<()> { 731 -> Result<()> {
338 let test_repo = GitTestRepo::default(); 732 let test_repo = GitTestRepo::default();
339 let oid = test_repo.populate()?; 733 test_repo.populate()?;
340 let git_repo = Repo::from_path(&test_repo.dir)?; 734 let git_repo = Repo::from_path(&test_repo.dir)?;
341 735
342 assert!(!git_repo.does_commit_exist(&"00").is_err()); 736 assert!(git_repo.does_commit_exist("00").is_ok());
343 Ok(()) 737 Ok(())
344 } 738 }
345 } 739 }
@@ -633,7 +1027,7 @@ mod tests {
633 let branch_name = "test-name-1"; 1027 let branch_name = "test-name-1";
634 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; 1028 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
635 1029
636 assert!(test_repo.checkout(&branch_name).is_ok()); 1030 assert!(test_repo.checkout(branch_name).is_ok());
637 Ok(()) 1031 Ok(())
638 } 1032 }
639 1033
@@ -654,8 +1048,555 @@ mod tests {
654 let branch_name = "test-name-1"; 1048 let branch_name = "test-name-1";
655 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; 1049 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
656 1050
657 assert_eq!(test_repo.checkout(&branch_name)?, ahead_1_oid); 1051 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid);
1052 Ok(())
1053 }
1054
1055 mod when_branch_already_exists {
1056 use super::*;
1057
1058 #[test]
1059 fn when_new_tip_specified_it_is_updated() -> Result<()> {
1060 let test_repo = GitTestRepo::default();
1061 test_repo.populate()?;
1062 // create feature branch and add 2 commits
1063 test_repo.create_branch("feature")?;
1064 test_repo.checkout("feature")?;
1065 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1066 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1067 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1068 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1069
1070 let git_repo = Repo::from_path(&test_repo.dir)?;
1071
1072 let branch_name = "test-name-1";
1073 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1074
1075 git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?;
1076 assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid);
1077 Ok(())
1078 }
1079
1080 #[test]
1081 fn when_same_tip_is_specified_it_doesnt_error() -> Result<()> {
1082 let test_repo = GitTestRepo::default();
1083 test_repo.populate()?;
1084 // create feature branch and add 2 commits
1085 test_repo.create_branch("feature")?;
1086 test_repo.checkout("feature")?;
1087 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1088 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1089 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1090 test_repo.stage_and_commit("add t4.md")?;
1091
1092 let git_repo = Repo::from_path(&test_repo.dir)?;
1093
1094 let branch_name = "test-name-1";
1095 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1096
1097 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1098 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid);
1099 Ok(())
1100 }
1101 }
1102 }
1103
1104 mod apply_patch {
1105 use test_utils::TEST_KEY_1_KEYS;
1106
1107 use super::*;
1108 use crate::sub_commands::prs::create::generate_patch_event;
1109
1110 fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> {
1111 let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id();
1112 let git_repo = Repo::from_path(&test_repo.dir)?;
1113 generate_patch_event(
1114 &git_repo,
1115 &git_repo.get_root_commit("main")?,
1116 &oid_to_sha1(&original_oid),
1117 nostr::EventId::all_zeros(),
1118 &TEST_KEY_1_KEYS,
1119 )
1120 }
1121 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> {
1122 let test_repo = GitTestRepo::default();
1123 test_repo.populate()?;
1124 let git_repo = Repo::from_path(&test_repo.dir)?;
1125 println!("{:?}", &patch_event);
1126 apply_patch(&git_repo, &patch_event)?;
1127 let commit_id = tag_value(&patch_event, "commit")?;
1128 // does commit with id exist?
1129 assert!(git_repo.does_commit_exist(&commit_id)?);
1130 // is commit head
1131 assert_eq!(
1132 test_repo
1133 .git_repo
1134 .head()?
1135 .peel_to_commit()?
1136 .id()
1137 .to_string(),
1138 commit_id,
1139 );
1140 // applied to current checked branch (head hasn't moved to specific commit)
1141 assert_eq!(
1142 test_repo
1143 .git_repo
1144 .head()?
1145 .shorthand()
1146 .context("an object without a shorthand is checked out")?
1147 .to_string(),
1148 "main",
1149 );
1150
658 Ok(()) 1151 Ok(())
659 } 1152 }
1153
1154 mod patch_created_as_commit_with_matching_id {
1155 use test_utils::git::joe_signature;
1156
1157 use super::*;
1158
1159 #[test]
1160 fn simple_signature_author_committer_same_as_git_user_0_unixtime_no_pgp_signature()
1161 -> Result<()> {
1162 let source_repo = GitTestRepo::default();
1163 source_repo.populate()?;
1164 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1165 source_repo.stage_and_commit("add x1.md")?;
1166
1167 test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?)
1168 }
1169
1170 #[test]
1171 fn signature_with_specific_author_time() -> Result<()> {
1172 let source_repo = GitTestRepo::default();
1173 source_repo.populate()?;
1174 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1175 source_repo.stage_and_commit_custom_signature(
1176 "add x1.md",
1177 Some(&git2::Signature::new(
1178 joe_signature().name().unwrap(),
1179 joe_signature().email().unwrap(),
1180 &git2::Time::new(5000, 0),
1181 )?),
1182 None,
1183 )?;
1184
1185 test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?)
1186 }
1187
1188 #[test]
1189 fn author_name_and_email_not_current_git_user() -> Result<()> {
1190 let source_repo = GitTestRepo::default();
1191 source_repo.populate()?;
1192 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1193 source_repo.stage_and_commit_custom_signature(
1194 "add x1.md",
1195 Some(&git2::Signature::new(
1196 "carole",
1197 "carole@pm.me",
1198 &git2::Time::new(0, 0),
1199 )?),
1200 None,
1201 )?;
1202
1203 test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?)
1204 }
1205
1206 #[test]
1207 fn comiiter_name_and_email_not_current_git_user_or_author() -> Result<()> {
1208 let source_repo = GitTestRepo::default();
1209 source_repo.populate()?;
1210 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1211 source_repo.stage_and_commit_custom_signature(
1212 "add x1.md",
1213 Some(&git2::Signature::new(
1214 "carole",
1215 "carole@pm.me",
1216 &git2::Time::new(0, 0),
1217 )?),
1218 Some(&git2::Signature::new(
1219 "bob",
1220 "bob@pm.me",
1221 &git2::Time::new(0, 0),
1222 )?),
1223 )?;
1224
1225 test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?)
1226 }
1227
1228 // TODO: pgp signature
1229
1230 #[test]
1231 fn unique_author_and_commiter_details() -> Result<()> {
1232 let source_repo = GitTestRepo::default();
1233 source_repo.populate()?;
1234 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1235 source_repo.stage_and_commit_custom_signature(
1236 "add x1.md",
1237 Some(&git2::Signature::new(
1238 "carole",
1239 "carole@pm.me",
1240 &git2::Time::new(5000, 0),
1241 )?),
1242 Some(&git2::Signature::new(
1243 "bob",
1244 "bob@pm.me",
1245 &git2::Time::new(1000, 0),
1246 )?),
1247 )?;
1248
1249 test_patch_applies_to_repository(generate_patch_from_head_commit(&source_repo)?)
1250 }
1251 }
1252 }
1253
1254 mod apply_patch_chain {
1255 use test_utils::TEST_KEY_1_KEYS;
1256
1257 use super::*;
1258 use crate::sub_commands::prs::create::generate_pr_and_patch_events;
1259
1260 static BRANCH_NAME: &str = "add-example-feature";
1261 // returns original_repo, pr_event, patch_events
1262 fn generate_test_repo_and_events() -> Result<(GitTestRepo, nostr::Event, Vec<nostr::Event>)>
1263 {
1264 let original_repo = GitTestRepo::default();
1265 let oid3 = original_repo.populate_with_test_branch()?;
1266 let oid2 = original_repo.git_repo.find_commit(oid3)?.parent_id(0)?;
1267 let oid1 = original_repo.git_repo.find_commit(oid2)?.parent_id(0)?;
1268 // TODO: generate pr and patch events
1269 let git_repo = Repo::from_path(&original_repo.dir)?;
1270
1271 let mut events = generate_pr_and_patch_events(
1272 "title",
1273 "description",
1274 BRANCH_NAME,
1275 &git_repo,
1276 &vec![oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)],
1277 &TEST_KEY_1_KEYS,
1278 )?;
1279
1280 events.reverse();
1281
1282 Ok((original_repo, events.pop().unwrap(), events))
1283 }
1284
1285 mod when_branch_and_commits_dont_exist {
1286 use super::*;
1287
1288 mod when_branch_root_is_tip_of_main {
1289 use super::*;
1290
1291 #[test]
1292 fn branch_gets_created_with_name_specified_in_pr() -> Result<()> {
1293 let (_, _, patch_events) = generate_test_repo_and_events()?;
1294 let test_repo = GitTestRepo::default();
1295 test_repo.populate()?;
1296 let git_repo = Repo::from_path(&test_repo.dir)?;
1297 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1298 assert!(
1299 git_repo
1300 .get_local_branch_names()?
1301 .contains(&BRANCH_NAME.to_string())
1302 );
1303 Ok(())
1304 }
1305
1306 #[test]
1307 fn branch_checked_out() -> Result<()> {
1308 let (_, _, patch_events) = generate_test_repo_and_events()?;
1309 let test_repo = GitTestRepo::default();
1310 test_repo.populate()?;
1311 let git_repo = Repo::from_path(&test_repo.dir)?;
1312 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1313 assert_eq!(
1314 git_repo.get_checked_out_branch_name()?,
1315 BRANCH_NAME.to_string(),
1316 );
1317 Ok(())
1318 }
1319
1320 #[test]
1321 fn patches_get_created_as_commits() -> Result<()> {
1322 let (original_repo, _, patch_events) = generate_test_repo_and_events()?;
1323 let test_repo = GitTestRepo::default();
1324 test_repo.populate()?;
1325 let git_repo = Repo::from_path(&test_repo.dir)?;
1326 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1327 assert_eq!(
1328 test_repo.git_repo.head()?.peel_to_commit()?.id(),
1329 original_repo.git_repo.head()?.peel_to_commit()?.id(),
1330 );
1331 Ok(())
1332 }
1333
1334 #[test]
1335 fn branch_tip_is_most_recent_patch() -> Result<()> {
1336 let (original_repo, _, patch_events) = generate_test_repo_and_events()?;
1337 let test_repo = GitTestRepo::default();
1338 test_repo.populate()?;
1339 let git_repo = Repo::from_path(&test_repo.dir)?;
1340 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1341 assert_eq!(
1342 git_repo.get_tip_of_local_branch(BRANCH_NAME)?,
1343 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
1344 );
1345 Ok(())
1346 }
1347
1348 #[test]
1349 fn previously_checked_out_branch_tip_does_not_change() -> Result<()> {
1350 let (_, _, patch_events) = generate_test_repo_and_events()?;
1351 let test_repo = GitTestRepo::default();
1352 test_repo.populate()?;
1353 let existing_branch = test_repo.get_checked_out_branch_name()?;
1354 let git_repo = Repo::from_path(&test_repo.dir)?;
1355 let previous_tip_of_existing_branch =
1356 git_repo.get_tip_of_local_branch(existing_branch.as_str())?;
1357 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1358 assert_eq!(
1359 previous_tip_of_existing_branch,
1360 git_repo.get_tip_of_local_branch(existing_branch.as_str())?,
1361 );
1362 Ok(())
1363 }
1364
1365 #[test]
1366 fn returns_all_patches_applied() -> Result<()> {
1367 let (_, _, patch_events) = generate_test_repo_and_events()?;
1368 let test_repo = GitTestRepo::default();
1369 test_repo.populate()?;
1370 let git_repo = Repo::from_path(&test_repo.dir)?;
1371 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1372 assert_eq!(res.len(), 3);
1373 Ok(())
1374 }
1375 }
1376
1377 mod when_branch_root_is_tip_behind_main {
1378 use super::*;
1379
1380 #[test]
1381 fn branch_gets_created_with_name_specified_in_pr() -> Result<()> {
1382 let (_, _, patch_events) = generate_test_repo_and_events()?;
1383 let test_repo = GitTestRepo::default();
1384 test_repo.populate()?;
1385 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
1386 test_repo.stage_and_commit("add m3.md")?;
1387 let git_repo = Repo::from_path(&test_repo.dir)?;
1388 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1389 assert!(
1390 git_repo
1391 .get_local_branch_names()?
1392 .contains(&BRANCH_NAME.to_string())
1393 );
1394 Ok(())
1395 }
1396
1397 #[test]
1398 fn branch_checked_out() -> Result<()> {
1399 let (_, _, patch_events) = generate_test_repo_and_events()?;
1400 let test_repo = GitTestRepo::default();
1401 test_repo.populate()?;
1402 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
1403 test_repo.stage_and_commit("add m3.md")?;
1404 let git_repo = Repo::from_path(&test_repo.dir)?;
1405 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1406 assert_eq!(
1407 git_repo.get_checked_out_branch_name()?,
1408 BRANCH_NAME.to_string(),
1409 );
1410 Ok(())
1411 }
1412
1413 #[test]
1414 fn branch_tip_is_most_recent_patch() -> Result<()> {
1415 let (original_repo, _, patch_events) = generate_test_repo_and_events()?;
1416 let test_repo = GitTestRepo::default();
1417 test_repo.populate()?;
1418 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
1419 test_repo.stage_and_commit("add m3.md")?;
1420 let git_repo = Repo::from_path(&test_repo.dir)?;
1421 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1422 assert_eq!(
1423 git_repo.get_tip_of_local_branch(BRANCH_NAME)?,
1424 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
1425 );
1426 Ok(())
1427 }
1428
1429 #[test]
1430 fn previously_checked_out_branch_tip_does_not_change() -> Result<()> {
1431 let (_, _, patch_events) = generate_test_repo_and_events()?;
1432 let test_repo = GitTestRepo::default();
1433 test_repo.populate()?;
1434 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
1435 test_repo.stage_and_commit("add m3.md")?;
1436 let existing_branch = test_repo.get_checked_out_branch_name()?;
1437 let git_repo = Repo::from_path(&test_repo.dir)?;
1438 let previous_tip_of_existing_branch =
1439 git_repo.get_tip_of_local_branch(existing_branch.as_str())?;
1440 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1441 assert_eq!(
1442 previous_tip_of_existing_branch,
1443 git_repo.get_tip_of_local_branch(existing_branch.as_str())?,
1444 );
1445 Ok(())
1446 }
1447
1448 #[test]
1449 fn returns_all_patches_applied() -> Result<()> {
1450 let (_, _, patch_events) = generate_test_repo_and_events()?;
1451 let test_repo = GitTestRepo::default();
1452 test_repo.populate()?;
1453 let git_repo = Repo::from_path(&test_repo.dir)?;
1454 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1455 assert_eq!(res.len(), 3);
1456 Ok(())
1457 }
1458 }
1459
1460 // TODO when_pr_root_is_tip_ahead_of_main_and_doesnt_exist
1461 }
1462
1463 mod when_branch_and_first_commits_exists {
1464 use super::*;
1465
1466 mod when_branch_already_checked_out {
1467 use super::*;
1468
1469 #[test]
1470 fn branch_tip_is_most_recent_patch() -> Result<()> {
1471 let (original_repo, _, mut patch_events) = generate_test_repo_and_events()?;
1472 let test_repo = GitTestRepo::default();
1473 test_repo.populate()?;
1474 let git_repo = Repo::from_path(&test_repo.dir)?;
1475 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
1476 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1477
1478 assert_eq!(
1479 git_repo.get_tip_of_local_branch(BRANCH_NAME)?,
1480 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
1481 );
1482 Ok(())
1483 }
1484
1485 #[test]
1486 fn returns_all_patches_applied() -> Result<()> {
1487 let (_, _, mut patch_events) = generate_test_repo_and_events()?;
1488 let test_repo = GitTestRepo::default();
1489 test_repo.populate()?;
1490 let git_repo = Repo::from_path(&test_repo.dir)?;
1491 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
1492 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1493 assert_eq!(res.len(), 2);
1494 Ok(())
1495 }
1496 }
1497 mod when_branch_not_checked_out {
1498 use super::*;
1499
1500 #[test]
1501 fn branch_tip_is_most_recent_patch() -> Result<()> {
1502 let (original_repo, _, mut patch_events) = generate_test_repo_and_events()?;
1503 let test_repo = GitTestRepo::default();
1504 test_repo.populate()?;
1505 let git_repo = Repo::from_path(&test_repo.dir)?;
1506 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
1507 git_repo.checkout("main")?;
1508 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1509
1510 assert_eq!(
1511 git_repo.get_tip_of_local_branch(BRANCH_NAME)?,
1512 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
1513 );
1514 Ok(())
1515 }
1516
1517 #[test]
1518 fn branch_checked_out() -> Result<()> {
1519 let (_, _, mut patch_events) = generate_test_repo_and_events()?;
1520 let test_repo = GitTestRepo::default();
1521 test_repo.populate()?;
1522 let git_repo = Repo::from_path(&test_repo.dir)?;
1523 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
1524 git_repo.checkout("main")?;
1525 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1526
1527 assert_eq!(
1528 git_repo.get_checked_out_branch_name()?,
1529 BRANCH_NAME.to_string(),
1530 );
1531 Ok(())
1532 }
1533
1534 #[test]
1535 fn returns_all_patches_applied() -> Result<()> {
1536 let (_, _, mut patch_events) = generate_test_repo_and_events()?;
1537 let test_repo = GitTestRepo::default();
1538 test_repo.populate()?;
1539 let git_repo = Repo::from_path(&test_repo.dir)?;
1540 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
1541 git_repo.checkout("main")?;
1542 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1543 assert_eq!(res.len(), 2);
1544 Ok(())
1545 }
1546 }
1547 // TODO when branch ahead (rebased or user commits)
1548 }
1549 mod when_branch_exists_and_is_up_to_date {
1550 use super::*;
1551
1552 mod when_branch_already_checked_out {
1553 use super::*;
1554
1555 #[test]
1556 fn returns_all_patches_applied_0() -> Result<()> {
1557 let (_, _, patch_events) = generate_test_repo_and_events()?;
1558 let test_repo = GitTestRepo::default();
1559 test_repo.populate()?;
1560 let git_repo = Repo::from_path(&test_repo.dir)?;
1561 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
1562 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1563 assert_eq!(res.len(), 0);
1564 Ok(())
1565 }
1566 }
1567 mod when_branch_not_checked_out {
1568 use super::*;
1569
1570 #[test]
1571 fn branch_checked_out() -> Result<()> {
1572 let (_, _, patch_events) = generate_test_repo_and_events()?;
1573 let test_repo = GitTestRepo::default();
1574 test_repo.populate()?;
1575 let git_repo = Repo::from_path(&test_repo.dir)?;
1576 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
1577 git_repo.checkout("main")?;
1578 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1579
1580 assert_eq!(
1581 git_repo.get_checked_out_branch_name()?,
1582 BRANCH_NAME.to_string(),
1583 );
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn returns_all_patches_applied_0() -> Result<()> {
1589 let (_, _, patch_events) = generate_test_repo_and_events()?;
1590 let test_repo = GitTestRepo::default();
1591 test_repo.populate()?;
1592 let git_repo = Repo::from_path(&test_repo.dir)?;
1593 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
1594 git_repo.checkout("main")?;
1595 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
1596 assert_eq!(res.len(), 0);
1597 Ok(())
1598 }
1599 }
1600 }
660 } 1601 }
661} 1602}