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.rs2566
1 files changed, 0 insertions, 2566 deletions
diff --git a/src/git.rs b/src/git.rs
deleted file mode 100644
index 5919667..0000000
--- a/src/git.rs
+++ /dev/null
@@ -1,2566 +0,0 @@
1use std::{
2 collections::HashSet,
3 env::current_dir,
4 path::{Path, PathBuf},
5};
6
7use anyhow::{bail, Context, Result};
8use git2::{DiffOptions, Oid, Revwalk};
9use nostr::nips::nip01::Coordinate;
10use nostr_sdk::{
11 hashes::{sha1::Hash as Sha1Hash, Hash},
12 PublicKey, Url,
13};
14
15use crate::sub_commands::list::{get_commit_id_from_patch, tag_value};
16
17pub struct Repo {
18 pub git_repo: git2::Repository,
19}
20
21impl Repo {
22 pub fn discover() -> Result<Self> {
23 Ok(Self {
24 git_repo: git2::Repository::discover(current_dir()?)?,
25 })
26 }
27 pub fn from_path(path: &PathBuf) -> Result<Self> {
28 Ok(Self {
29 git_repo: git2::Repository::open(path)?,
30 })
31 }
32}
33
34// pub type CommitId = [u8; 7];
35// pub type Sha1 = [u8; 20];
36
37pub trait RepoActions {
38 fn get_path(&self) -> Result<&Path>;
39 fn get_origin_url(&self) -> Result<String>;
40 fn get_remote_branch_names(&self) -> Result<Vec<String>>;
41 fn get_local_branch_names(&self) -> Result<Vec<String>>;
42 fn get_origin_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
43 fn get_local_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
44 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
45 fn get_checked_out_branch_name(&self) -> Result<String>;
46 fn get_tip_of_branch(&self, branch_name: &str) -> Result<Sha1Hash>;
47 fn get_commit_or_tip_of_reference(&self, reference: &str) -> Result<Sha1Hash>;
48 fn get_root_commit(&self) -> Result<Sha1Hash>;
49 fn does_commit_exist(&self, commit: &str) -> Result<bool>;
50 fn get_head_commit(&self) -> Result<Sha1Hash>;
51 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>;
52 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String>;
53 fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result<String>;
54 #[allow(clippy::doc_link_with_quotes)]
55 /// returns vector ["name", "email", "unixtime", "offset"]
56 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"]
57 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
58 #[allow(clippy::doc_link_with_quotes)]
59 /// returns vector ["name", "email", "unixtime", "offset"]
60 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"]
61 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
62 fn get_commits_ahead_behind(
63 &self,
64 base_commit: &Sha1Hash,
65 latest_commit: &Sha1Hash,
66 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>;
67 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
68 // including (un)staged changes and (un)tracked files
69 fn has_outstanding_changes(&self) -> Result<bool>;
70 fn make_patch_from_commit(
71 &self,
72 commit: &Sha1Hash,
73 series_count: &Option<(u64, u64)>,
74 ) -> Result<String>;
75 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String>;
76 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash>;
77 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>;
78 fn apply_patch_chain(
79 &self,
80 branch_name: &str,
81 patch_and_ancestors: Vec<nostr::Event>,
82 ) -> Result<Vec<nostr::Event>>;
83 fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result<Oid>;
84 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>;
85 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>;
86 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>;
87 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>;
88 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>;
89}
90
91impl RepoActions for Repo {
92 fn get_path(&self) -> Result<&Path> {
93 self.git_repo
94 .path()
95 .parent()
96 .context("cannot find repositiory path as .git has no parent")
97 }
98
99 fn get_origin_url(&self) -> Result<String> {
100 Ok(self
101 .git_repo
102 .find_remote("origin")
103 .context("cannot find origin")?
104 .url()
105 .context("cannot find origin url")?
106 .to_string())
107 }
108
109 fn get_origin_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> {
110 let main_branch_name = {
111 let remote_branches = self
112 .get_remote_branch_names()
113 .context("cannot find any local branches")?;
114 if remote_branches.contains(&"origin/main".to_string()) {
115 "origin/main"
116 } else if remote_branches.contains(&"origin/master".to_string()) {
117 "origin/master"
118 } else {
119 bail!("no main or master branch locally in this git repository to initiate from",)
120 }
121 };
122
123 let tip = self
124 .get_tip_of_branch(main_branch_name)
125 .context(format!(
126 "branch {main_branch_name} was listed as a remote branch but cannot get its tip commit id",
127 ))?;
128
129 Ok((main_branch_name, tip))
130 }
131
132 fn get_local_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> {
133 let main_branch_name = {
134 let local_branches = self
135 .get_local_branch_names()
136 .context("cannot find any local branches")?;
137 if local_branches.contains(&"main".to_string()) {
138 "main"
139 } else if local_branches.contains(&"master".to_string()) {
140 "master"
141 } else {
142 bail!("no main or master branch locally in this git repository to initiate from",)
143 }
144 };
145
146 let tip = self
147 .get_tip_of_branch(main_branch_name)
148 .context(format!(
149 "branch {main_branch_name} was listed as a local branch but cannot get its tip commit id",
150 ))?;
151
152 Ok((main_branch_name, tip))
153 }
154
155 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> {
156 if let Ok(main_tuple) = self
157 .get_origin_main_or_master_branch()
158 .context("the default branches (main or master) do not exist")
159 {
160 Ok(main_tuple)
161 } else {
162 self.get_local_main_or_master_branch()
163 .context("the default branches (main or master) do not exist")
164 }
165 }
166
167 fn get_local_branch_names(&self) -> Result<Vec<String>> {
168 let local_branches = self
169 .git_repo
170 .branches(Some(git2::BranchType::Local))
171 .context("getting GitRepo branches should not error even for a blank repository")?;
172
173 let mut branch_names = vec![];
174
175 for iter in local_branches {
176 let branch = iter?.0;
177 if let Some(name) = branch.name()? {
178 branch_names.push(name.to_string());
179 }
180 }
181 Ok(branch_names)
182 }
183
184 fn get_remote_branch_names(&self) -> Result<Vec<String>> {
185 let remote_branches = self
186 .git_repo
187 .branches(Some(git2::BranchType::Remote))
188 .context("getting GitRepo branches should not error even for a blank repository")?;
189
190 let mut branch_names = vec![];
191
192 for iter in remote_branches {
193 let branch = iter?.0;
194 if let Some(name) = branch.name()? {
195 branch_names.push(name.to_string());
196 }
197 }
198 Ok(branch_names)
199 }
200
201 fn get_checked_out_branch_name(&self) -> Result<String> {
202 Ok(self
203 .git_repo
204 .head()?
205 .shorthand()
206 .context("an object without a shorthand is checked out")?
207 .to_string())
208 }
209
210 fn get_tip_of_branch(&self, branch_name: &str) -> Result<Sha1Hash> {
211 let branch = if let Ok(branch) = self
212 .git_repo
213 .find_branch(branch_name, git2::BranchType::Local)
214 .context(format!("cannot find local branch {branch_name}"))
215 {
216 branch
217 } else {
218 self.git_repo
219 .find_branch(branch_name, git2::BranchType::Remote)
220 .context(format!("cannot find local or remote branch {branch_name}"))?
221 };
222 Ok(oid_to_sha1(&branch.into_reference().peel_to_commit()?.id()))
223 }
224
225 fn get_commit_or_tip_of_reference(&self, sha1_or_reference: &str) -> Result<Sha1Hash> {
226 let oid = {
227 if let Ok(oid) = Oid::from_str(sha1_or_reference) {
228 self.git_repo.find_commit(oid)?;
229 oid
230 } else {
231 self.git_repo
232 .find_reference(sha1_or_reference)?
233 .peel_to_commit()?
234 .id()
235 }
236 };
237 Ok(oid_to_sha1(&oid))
238 }
239
240 fn get_root_commit(&self) -> Result<Sha1Hash> {
241 let mut revwalk = self
242 .git_repo
243 .revwalk()
244 .context("revwalk should be created from git repo")?;
245 revwalk
246 .push(sha1_to_oid(&self.get_head_commit()?)?)
247 .context("revwalk should accept tip oid")?;
248 Ok(oid_to_sha1(
249 &revwalk
250 .last()
251 .context("revwalk from tip should be at least contain the tip oid")?
252 .context("revwalk iter from branch tip should not result in an error")?,
253 ))
254 }
255
256 fn does_commit_exist(&self, commit: &str) -> Result<bool> {
257 if self.git_repo.find_commit(Oid::from_str(commit)?).is_ok() {
258 Ok(true)
259 } else {
260 Ok(false)
261 }
262 }
263
264 fn get_head_commit(&self) -> Result<Sha1Hash> {
265 let head = self
266 .git_repo
267 .head()
268 .context("failed to get git repo head")?;
269 let oid = head.peel_to_commit()?.id();
270 Ok(oid_to_sha1(&oid))
271 }
272
273 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash> {
274 let parent_oid = self
275 .git_repo
276 .find_commit(sha1_to_oid(commit)?)
277 .context(format!("could not find commit {commit}"))?
278 .parent_id(0)
279 .context(format!("could not find parent of commit {commit}"))?;
280 Ok(oid_to_sha1(&parent_oid))
281 }
282
283 fn get_commit_message(&self, commit: &Sha1Hash) -> Result<String> {
284 Ok(self
285 .git_repo
286 .find_commit(sha1_to_oid(commit)?)
287 .context(format!("could not find commit {commit}"))?
288 .message_raw()
289 .context("commit message has unusual characters in (not valid utf-8)")?
290 .to_string())
291 }
292
293 fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result<String> {
294 Ok(self
295 .git_repo
296 .find_commit(sha1_to_oid(commit)?)
297 .context(format!("could not find commit {commit}"))?
298 .message_raw()
299 .context("commit message has unusual characters in (not valid utf-8)")?
300 .split('\r')
301 .collect::<Vec<&str>>()[0]
302 .split('\n')
303 .collect::<Vec<&str>>()[0]
304 .to_string()
305 .trim()
306 .to_string())
307 }
308
309 fn get_commit_author(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
310 let commit = self
311 .git_repo
312 .find_commit(sha1_to_oid(commit)?)
313 .context(format!("could not find commit {commit}"))?;
314 let sig = commit.author();
315 Ok(git_sig_to_tag_vec(&sig))
316 }
317
318 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
319 let commit = self
320 .git_repo
321 .find_commit(sha1_to_oid(commit)?)
322 .context(format!("could not find commit {commit}"))?;
323 let sig = commit.committer();
324 Ok(git_sig_to_tag_vec(&sig))
325 }
326
327 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
328 Ok(self
329 .git_repo
330 .references()?
331 .filter(|r| {
332 if let Ok(r) = r {
333 if let Ok(ref_tip) = r.peel_to_commit() {
334 ref_tip.id().to_string().eq(&commit.to_string())
335 } else {
336 false
337 }
338 } else {
339 false
340 }
341 })
342 .map(|r| r.unwrap().shorthand().unwrap().to_string())
343 .collect::<Vec<String>>())
344 }
345
346 fn make_patch_from_commit(
347 &self,
348 commit: &Sha1Hash,
349 series_count: &Option<(u64, u64)>,
350 ) -> Result<String> {
351 let c = self
352 .git_repo
353 .find_commit(Oid::from_bytes(commit.as_byte_array()).context(format!(
354 "failed to convert commit_id format for {}",
355 &commit
356 ))?)
357 .context(format!("failed to find commit {}", &commit))?;
358 let mut options = git2::EmailCreateOptions::default();
359 if let Some((n, total)) = series_count {
360 options.subject_prefix(format!("PATCH {n}/{total}"));
361 }
362 let patch = git2::Email::from_commit(&c, &mut options)
363 .context(format!("failed to create patch from commit {}", &commit))?;
364
365 Ok(std::str::from_utf8(patch.as_slice())
366 .context("patch content could not be converted to a utf8 string")?
367 .to_owned())
368 }
369
370 fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result<String> {
371 let oid = Oid::from_bytes(commit.as_byte_array()).context(format!(
372 "failed to convert commit_id format for {}",
373 &commit
374 ))?;
375
376 let (sign, _data) = self
377 .git_repo
378 .extract_signature(&oid, None)
379 .context("failed to extract signature - perhaps there is no signature?")?;
380
381 Ok(std::str::from_utf8(&sign)
382 .context("commit signature cannot be converted to a utf8 string")?
383 .to_owned())
384 }
385
386 // including (un)staged changes and (un)tracked files
387 fn has_outstanding_changes(&self) -> Result<bool> {
388 let diff = self.git_repo.diff_tree_to_workdir_with_index(
389 Some(&self.git_repo.head()?.peel_to_tree()?),
390 Some(DiffOptions::new().include_untracked(true)),
391 )?;
392
393 Ok(diff.deltas().len().gt(&0))
394 }
395
396 fn get_commits_ahead_behind(
397 &self,
398 base_commit: &Sha1Hash,
399 latest_commit: &Sha1Hash,
400 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)> {
401 let mut ahead: Vec<Sha1Hash> = vec![];
402 let mut behind: Vec<Sha1Hash> = vec![];
403
404 let get_revwalk = |commit: &Sha1Hash| -> Result<Revwalk> {
405 let mut revwalk = self
406 .git_repo
407 .revwalk()
408 .context("revwalk should be created from git repo")?;
409 revwalk
410 .push(sha1_to_oid(commit)?)
411 .context("revwalk should accept commit oid")?;
412 Ok(revwalk)
413 };
414
415 // scan through the base commit ancestory until a common ancestor is found
416 let most_recent_shared_commit = match get_revwalk(base_commit)
417 .context("failed to get revwalk for base_commit")?
418 .find(|base_res| {
419 let base_oid = base_res.as_ref().unwrap();
420
421 if get_revwalk(latest_commit)
422 .unwrap()
423 .any(|latest_res| base_oid.eq(latest_res.as_ref().unwrap()))
424 {
425 true
426 } else {
427 // add commits not found in latest ancestory to 'behind' vector
428 behind.push(oid_to_sha1(base_oid));
429 false
430 }
431 }) {
432 None => {
433 bail!(format!(
434 "{} is not an ancestor of {}",
435 latest_commit, base_commit
436 ));
437 }
438 Some(res) => res.context("revwalk failed to reveal commit")?,
439 };
440
441 // scan through the latest commits until shared commit is reached
442 get_revwalk(latest_commit)
443 .context("failed to get revwalk for latest_commit")?
444 .any(|latest_res| {
445 let latest_oid = latest_res.as_ref().unwrap();
446 if latest_oid.eq(&most_recent_shared_commit) {
447 true
448 } else {
449 // add commits not found in base to 'ahead' vector
450 ahead.push(oid_to_sha1(latest_oid));
451 false
452 }
453 });
454 Ok((ahead, behind))
455 }
456
457 fn checkout(&self, ref_name: &str) -> Result<Sha1Hash> {
458 let (object, reference) = self.git_repo.revparse_ext(ref_name)?;
459
460 self.git_repo.checkout_tree(&object, None)?;
461
462 match reference {
463 // gref is an actual reference like branches or tags
464 Some(gref) => self.git_repo.set_head(gref.name().unwrap()),
465 // this is a commit, not a reference
466 None => self.git_repo.set_head_detached(object.id()),
467 }?;
468 let oid = self.git_repo.head()?.peel_to_commit()?.id();
469
470 Ok(oid_to_sha1(&oid))
471 }
472
473 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> {
474 let branch_checkedout = self.get_checked_out_branch_name()?.eq(branch_name);
475 if branch_checkedout {
476 let (name, _) = self.get_main_or_master_branch()?;
477 self.checkout(name)?;
478 }
479
480 self.git_repo
481 .branch(
482 branch_name,
483 &self.git_repo.find_commit(Oid::from_str(commit)?)?,
484 true,
485 )
486 .context("branch could not be created")?;
487
488 if branch_checkedout {
489 self.checkout(branch_name)?;
490 }
491 Ok(())
492 }
493 /* returns patches applied */
494 fn apply_patch_chain(
495 &self,
496 branch_name: &str,
497 patch_and_ancestors: Vec<nostr::Event>,
498 ) -> Result<Vec<nostr::Event>> {
499 let branch_tip_result = self.get_tip_of_branch(branch_name);
500
501 // filter out existing ancestors in branch
502 let mut patches_to_apply: Vec<nostr::Event> = patch_and_ancestors
503 .into_iter()
504 .filter(|e| {
505 let commit_id = get_commit_id_from_patch(e).unwrap();
506 if let Ok(branch_tip) = branch_tip_result {
507 !branch_tip.to_string().eq(&commit_id)
508 && !self
509 .ancestor_of(&branch_tip, &str_to_sha1(&commit_id).unwrap())
510 .unwrap()
511 } else {
512 true
513 }
514 })
515 .collect();
516
517 let parent_commit_id = tag_value(
518 if let Ok(last_patch) = patches_to_apply.last().context("no patches") {
519 last_patch
520 } else {
521 self.checkout(branch_name)
522 .context("no patches and so cannot create a proposal branch")?;
523 return Ok(vec![]);
524 },
525 "parent-commit",
526 )?;
527
528 // check patches can be applied
529 if !self.does_commit_exist(&parent_commit_id)? {
530 bail!("cannot find parent commit ({parent_commit_id}). run git pull and try again.")
531 }
532
533 // checkout branch
534 self.create_branch_at_commit(branch_name, &parent_commit_id)?;
535 self.checkout(branch_name)?;
536
537 // apply commits
538 patches_to_apply.reverse();
539
540 for patch in &patches_to_apply {
541 let commit_id = get_commit_id_from_patch(patch)?;
542 // only create new commits - otherwise make them the tip
543 if !self.does_commit_exist(&commit_id)? {
544 self.create_commit_from_patch(patch)?;
545 }
546 self.create_branch_at_commit(branch_name, &commit_id)?;
547 self.checkout(branch_name)?;
548 }
549 Ok(patches_to_apply)
550 }
551 fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result<Oid> {
552 let commit_id = get_commit_id_from_patch(patch)?;
553 if self.does_commit_exist(&commit_id)? {
554 return Ok(Oid::from_str(&commit_id)?);
555 }
556 let parent_commit_id = tag_value(patch, "parent-commit")?;
557
558 let parent_commit = self
559 .git_repo
560 .find_commit(Oid::from_str(&parent_commit_id)?)
561 .context("parrent commit doesnt exist")?;
562 let parent_tree = parent_commit.tree()?;
563
564 // let mut apply_opts = git2::ApplyOptions::new();
565 // apply_opts.check(false);
566 let mut existing_index = self.git_repo.index()?;
567 let mut index = self.git_repo.apply_to_tree(
568 &parent_tree,
569 &git2::Diff::from_buffer(patch.content.as_bytes())?,
570 // Some(&mut apply_opts),
571 None,
572 )?;
573 let tree = self
574 .git_repo
575 .find_tree(index.write_tree_to(&self.git_repo)?)?;
576
577 let pgp_sig = if let Ok(pgp_sig) = tag_value(patch, "commit-pgp-sig") {
578 if pgp_sig.is_empty() {
579 None
580 } else {
581 Some(pgp_sig)
582 }
583 } else {
584 None
585 };
586
587 let commit_buff = self.git_repo.commit_create_buffer(
588 &extract_sig_from_patch_tags(&patch.tags, "author")?,
589 &extract_sig_from_patch_tags(&patch.tags, "committer")?,
590 tag_value(patch, "description")?.as_str(),
591 &tree,
592 &[&parent_commit],
593 )?;
594
595 let mut applied_oid = self
596 .git_repo
597 .commit_signed(
598 commit_buff.as_str().unwrap(),
599 pgp_sig.unwrap_or(String::new()).as_str(),
600 None,
601 )
602 .context("failed to create signed commit")?;
603
604 // I beleive this was added to address a bug where commit author / committer
605 // were identical when in a scenario when they should be different but I dont
606 // think we have a test case for it. surely we should be using the
607 // extract_sig_from_patch_tags outputs to address this?
608 if !applied_oid.to_string().eq(&commit_id) {
609 let commit = self.git_repo.find_commit(applied_oid)?;
610 applied_oid = commit
611 .amend(
612 None,
613 Some(&commit.author()),
614 Some(&commit.committer()),
615 None,
616 None,
617 None,
618 )
619 .context("cannot amend commit to produce new oid")?;
620 }
621 if !applied_oid.to_string().eq(&commit_id) {
622 bail!(
623 "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})",
624 applied_oid.to_string(),
625 get_commit_id_from_patch(patch)?,
626 );
627 }
628 self.git_repo.set_index(&mut existing_index)?;
629 Ok(applied_oid)
630 }
631 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>> {
632 let revspec = self
633 .git_repo
634 .revparse(starting_commits)
635 .context("specified value not in a valid format")?;
636 if revspec.mode().is_no_single() {
637 let (ahead, _) = self
638 .get_commits_ahead_behind(
639 &oid_to_sha1(
640 &revspec
641 .from()
642 .context("cannot get starting commit from specified value")?
643 .id(),
644 ),
645 &self
646 .get_head_commit()
647 .context("cannot get head commit with gitlib2")?,
648 )
649 .context("specified commit is not an ancestor of current head")?;
650 Ok(ahead)
651 } else if revspec.mode().is_range() {
652 let (ahead, _) = self
653 .get_commits_ahead_behind(
654 &oid_to_sha1(
655 &revspec
656 .from()
657 .context("cannot get starting commit of range from specified value")?
658 .id(),
659 ),
660 &oid_to_sha1(
661 &revspec
662 .to()
663 .context("cannot get end of range commit from specified value")?
664 .id(),
665 ),
666 )
667 .context("specified commit is not an ancestor of current head")?;
668 Ok(ahead)
669 } else {
670 bail!("specified value not in a supported format")
671 }
672 }
673
674 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool> {
675 if let Ok(res) = self
676 .git_repo
677 .graph_descendant_of(sha1_to_oid(decendant)?, sha1_to_oid(ancestor)?)
678 .context("could not run graph_descendant_of in gitlib2")
679 {
680 Ok(res)
681 } else {
682 Ok(false)
683 }
684 }
685
686 /// setting global to None will suppliment local config with global items
687 /// not in local
688 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>> {
689 let just_global = if let Some(just_global) = global {
690 just_global
691 } else {
692 false
693 };
694 match if just_global {
695 self.git_repo
696 .config()
697 .context("cannot open git config")?
698 .open_global()
699 .context("cannot open global git config")?
700 } else {
701 self.git_repo.config().context("cannot open git config")?
702 }
703 .get_entry(item)
704 {
705 Ok(item) => {
706 if let Some(global) = global {
707 if item.level().eq(&git2::ConfigLevel::Local) {
708 if global {
709 bail!("only local repository login available")
710 }
711 } else if !global {
712 bail!("only global repository login available")
713 }
714 }
715 Ok(Some(
716 item.value()
717 .context("cannot find git config item")?
718 .to_string(),
719 ))
720 }
721 Err(_) => Ok(None),
722 }
723 }
724
725 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()> {
726 if global {
727 self.git_repo
728 .config()
729 .context("cannot open git config")?
730 .open_global()
731 .context("cannot open global git config")?
732 } else {
733 self.git_repo.config().context("cannot open git config")?
734 }
735 .set_str(item, value)
736 .context(format!(
737 "cannot set {} git config item {}",
738 if global { "global" } else { "local" },
739 item
740 ))?;
741 Ok(())
742 }
743
744 /// returns false if item doesn't exist
745 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool> {
746 if self.get_git_config_item(item, Some(global))?.is_none() {
747 Ok(false)
748 } else {
749 if global {
750 self.git_repo
751 .config()
752 .context("cannot open git config")?
753 .open_global()
754 .context("cannot open global git config")?
755 } else {
756 self.git_repo.config().context("cannot open git config")?
757 }
758 .remove(item)
759 .context("cannot remove existing git config item")?;
760 Ok(true)
761 }
762 }
763}
764
765fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
766 let b = oid.as_bytes();
767 [
768 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13],
769 b[14], b[15], b[16], b[17], b[18], b[19],
770 ]
771}
772
773// fn oid_to_shorthand_string(oid: Oid) -> Result<String> {
774// let binding = oid.to_string();
775// let b = binding.as_bytes();
776// String::from_utf8(vec![b[0], b[1], b[2], b[3], b[4], b[5], b[6]])
777// .context("oid should always start with 7 u8 btyes of utf8")
778// }
779
780// fn oid_to_sha1_string(oid: Oid) -> Result<String> {
781// let b = oid.as_bytes();
782// String::from_utf8(vec![
783// b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10],
784// b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19],
785// ])
786// .context("oid should contain 20 u8 btyes of utf8")
787// }
788
789// git2 Oid object to Sha1Hash
790pub fn oid_to_sha1(oid: &Oid) -> Sha1Hash {
791 Sha1Hash::from_byte_array(oid_to_u8_20_bytes(oid))
792}
793
794/// `Sha1Hash` to git2 `Oid` object
795pub fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> {
796 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid")
797}
798
799pub fn str_to_sha1(s: &str) -> Result<Sha1Hash> {
800 Ok(oid_to_sha1(
801 &Oid::from_str(s).context("string is not a sha1 hash")?,
802 ))
803}
804
805fn git_sig_to_tag_vec(sig: &git2::Signature) -> Vec<String> {
806 vec![
807 sig.name().unwrap_or("").to_string(),
808 sig.email().unwrap_or("").to_string(),
809 format!("{}", sig.when().seconds()),
810 format!("{}", sig.when().offset_minutes()),
811 ]
812}
813
814fn extract_sig_from_patch_tags<'a>(
815 tags: &'a [nostr::Tag],
816 tag_name: &str,
817) -> Result<git2::Signature<'a>> {
818 let v = tags
819 .iter()
820 .find(|t| t.as_vec()[0].eq(tag_name))
821 .context(format!("tag '{tag_name}' not present in patch"))?
822 .as_vec();
823 if v.len() != 5 {
824 bail!("tag '{tag_name}' is incorrectly formatted")
825 }
826 git2::Signature::new(
827 v[1].as_str(),
828 v[2].as_str(),
829 &git2::Time::new(
830 v[3].parse().context("tag time is incorrectly formatted")?,
831 v[4].parse()
832 .context("tag time offset is incorrectly formatted")?,
833 ),
834 )
835 .context("failed to create git signature")
836}
837
838#[derive(Debug, PartialEq)]
839pub enum ServerProtocol {
840 Ssh,
841 Https,
842 Http,
843 Git,
844}
845
846#[derive(Debug, PartialEq)]
847pub struct NostrUrlDecoded {
848 pub coordinates: HashSet<Coordinate>,
849 pub protocol: Option<ServerProtocol>,
850 pub user: Option<String>,
851}
852
853static INCORRECT_NOSTR_URL_FORMAT_ERROR: &str = "incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo";
854
855impl NostrUrlDecoded {
856 pub fn from_str(url: &str) -> Result<Self> {
857 let mut coordinates = HashSet::new();
858 let mut protocol = None;
859 let mut user = None;
860 let mut relays = vec![];
861
862 if !url.starts_with("nostr://") {
863 bail!("nostr git url must start with nostr://");
864 }
865 // process get url parameters if present
866 for (name, value) in Url::parse(url)?.query_pairs() {
867 if name.contains("relay") {
868 let mut decoded = urlencoding::decode(&value)
869 .context("could not parse relays in nostr git url")?
870 .to_string();
871 if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") {
872 decoded = format!("wss://{decoded}");
873 }
874 let url =
875 Url::parse(&decoded).context("could not parse relays in nostr git url")?;
876 relays.push(url.to_string());
877 } else if name == "protocol" {
878 protocol = match value.as_ref() {
879 "ssh" => Some(ServerProtocol::Ssh),
880 "https" => Some(ServerProtocol::Https),
881 "http" => Some(ServerProtocol::Http),
882 "git" => Some(ServerProtocol::Git),
883 _ => None,
884 };
885 } else if name == "user" {
886 user = Some(value.to_string());
887 }
888 }
889
890 let mut parts: Vec<&str> = url[8..]
891 .split('?')
892 .next()
893 .unwrap_or("")
894 .split('/')
895 .collect();
896
897 // extract optional protocol
898 if protocol.is_none() {
899 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
900 let protocol_str = if let Some(at_index) = part.find('@') {
901 user = Some(part[..at_index].to_string());
902 &part[at_index + 1..]
903 } else {
904 part
905 };
906 protocol = match protocol_str {
907 "ssh" => Some(ServerProtocol::Ssh),
908 "https" => Some(ServerProtocol::Https),
909 "http" => Some(ServerProtocol::Http),
910 "git" => Some(ServerProtocol::Git),
911 _ => protocol,
912 };
913 if protocol.is_some() {
914 parts.remove(0);
915 }
916 }
917 // extract naddr npub/<optional-relays>/identifer
918 let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;
919 // naddr used
920 if let Ok(coordinate) = Coordinate::parse(part) {
921 if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) {
922 coordinates.insert(coordinate);
923 } else {
924 bail!("naddr doesnt point to a git repository announcement");
925 }
926 // npub/<optional-relays>/identifer used
927 } else if let Ok(public_key) = PublicKey::parse(part) {
928 parts.remove(0);
929 let identifier = parts
930 .pop()
931 .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")?
932 .to_string();
933 for relay in parts {
934 let mut decoded = urlencoding::decode(relay)
935 .context("could not parse relays in nostr git url")?
936 .to_string();
937 if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") {
938 decoded = format!("wss://{decoded}");
939 }
940 let url =
941 Url::parse(&decoded).context("could not parse relays in nostr git url")?;
942 relays.push(url.to_string());
943 }
944 coordinates.insert(Coordinate {
945 identifier,
946 public_key,
947 kind: nostr_sdk::Kind::GitRepoAnnouncement,
948 relays,
949 });
950 } else {
951 bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR);
952 }
953
954 Ok(Self {
955 coordinates,
956 protocol,
957 user,
958 })
959 }
960}
961
962/** produce error when using local repo or custom protocols */
963pub fn convert_clone_url_to_https(url: &str) -> Result<String> {
964 // Strip credentials if present
965 let stripped_url = strip_credentials(url);
966
967 // Check if the URL is already in HTTPS format
968 if stripped_url.starts_with("https://") {
969 return Ok(stripped_url);
970 }
971 // Convert http:// to https://
972 else if stripped_url.starts_with("http://") {
973 return Ok(stripped_url.replace("http://", "https://"));
974 }
975 // Check if the URL starts with SSH
976 else if stripped_url.starts_with("ssh://") {
977 // Convert SSH to HTTPS
978 let parts: Vec<&str> = stripped_url
979 .trim_start_matches("ssh://")
980 .split('/')
981 .collect();
982 if parts.len() >= 2 {
983 // Construct the HTTPS URL
984 return Ok(format!("https://{}/{}", parts[0], parts[1..].join("/")));
985 }
986 bail!("Invalid SSH URL format: {}", url);
987 }
988 // Convert ftp:// to https://
989 else if stripped_url.starts_with("ftp://") {
990 return Ok(stripped_url.replace("ftp://", "https://"));
991 }
992 // Convert git:// to https://
993 else if stripped_url.starts_with("git://") {
994 return Ok(stripped_url.replace("git://", "https://"));
995 }
996
997 // If the URL is neither HTTPS, SSH, nor git@, return an error
998 bail!("Unsupported URL protocol: {}", url);
999}
1000
1001// Function to strip username and password from the URL
1002fn strip_credentials(url: &str) -> String {
1003 if let Some(pos) = url.find("://") {
1004 let (protocol, rest) = url.split_at(pos + 3); // Split at "://"
1005 let rest_parts: Vec<&str> = rest.split('@').collect();
1006 if rest_parts.len() > 1 {
1007 // If there are credentials, return the URL without them
1008 return format!("{}{}", protocol, rest_parts[1]);
1009 }
1010 } else if let Some(at_pos) = url.find('@') {
1011 // Handle user@host:path format
1012 let (_, rest) = url.split_at(at_pos);
1013 // This is a git@ syntax
1014 let host_and_repo = &rest[1..]; // Skip the ':'
1015 return format!("ssh://{}", host_and_repo.replace(':', "/"));
1016 }
1017 url.to_string() // Return the original URL if no credentials are found
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use std::fs;
1023
1024 use test_utils::{generate_repo_ref_event, git::GitTestRepo};
1025
1026 use super::*;
1027
1028 mod git_config_item_local {
1029 use super::*;
1030
1031 #[test]
1032 fn save_git_config_item_returns_ok() -> Result<()> {
1033 let test_repo = GitTestRepo::default();
1034 let git_repo = Repo::from_path(&test_repo.dir)?;
1035 git_repo.save_git_config_item("test.item", "testvalue", false)?;
1036 Ok(())
1037 }
1038
1039 #[test]
1040 fn get_git_config_item_returns_item_just_saved() -> Result<()> {
1041 let test_repo = GitTestRepo::default();
1042 let git_repo = Repo::from_path(&test_repo.dir)?;
1043 git_repo.save_git_config_item("test.item", "testvalue", false)?;
1044 assert_eq!(
1045 git_repo
1046 .get_git_config_item("test.item", Some(false))?
1047 .unwrap(),
1048 "testvalue",
1049 );
1050 Ok(())
1051 }
1052
1053 #[test]
1054 fn get_git_config_item_returns_none_if_not_present() -> Result<()> {
1055 let test_repo = GitTestRepo::default();
1056 let git_repo = Repo::from_path(&test_repo.dir)?;
1057 assert_eq!(
1058 git_repo.get_git_config_item("test.item", Some(false))?,
1059 None
1060 );
1061 Ok(())
1062 }
1063
1064 #[test]
1065 fn get_git_config_item_empty_string_returns_empty_string_instead_of_none() -> Result<()> {
1066 let test_repo = GitTestRepo::default();
1067 let git_repo = Repo::from_path(&test_repo.dir)?;
1068 git_repo.save_git_config_item("test.item", "", false)?;
1069 assert_eq!(
1070 git_repo.get_git_config_item("test.item", Some(false))?,
1071 Some("".to_string()),
1072 );
1073 Ok(())
1074 }
1075
1076 #[test]
1077 fn remove_local_git_config_item() -> Result<()> {
1078 let test_repo = GitTestRepo::default();
1079 let git_repo = Repo::from_path(&test_repo.dir)?;
1080 git_repo.save_git_config_item("test.item", "testvalue", false)?;
1081 assert!(git_repo.remove_git_config_item("test.item", false)?);
1082 assert_eq!(
1083 git_repo.get_git_config_item("test.item", Some(false))?,
1084 None,
1085 );
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn remove_git_config_item_returns_false_if_item_wasnt_set() -> Result<()> {
1091 let test_repo = GitTestRepo::default();
1092 let git_repo = Repo::from_path(&test_repo.dir)?;
1093 assert!(!(git_repo.remove_git_config_item("test.item", false)?));
1094 Ok(())
1095 }
1096 }
1097
1098 #[test]
1099 fn get_commit_parent() -> Result<()> {
1100 let test_repo = GitTestRepo::default();
1101 let parent_oid = test_repo.populate()?;
1102 std::fs::write(test_repo.dir.join("t100.md"), "some content")?;
1103 let child_oid = test_repo.stage_and_commit("add t100.md")?;
1104
1105 let git_repo = Repo::from_path(&test_repo.dir)?;
1106
1107 assert_eq!(
1108 // Sha1Hash::from_byte_array("bla".to_string().as_bytes()),
1109 oid_to_sha1(&parent_oid),
1110 git_repo.get_commit_parent(&oid_to_sha1(&child_oid))?,
1111 );
1112 Ok(())
1113 }
1114
1115 mod get_commit_message {
1116 use super::*;
1117 fn run(message: &str) -> Result<()> {
1118 let test_repo = GitTestRepo::default();
1119 test_repo.populate()?;
1120 std::fs::write(test_repo.dir.join("t100.md"), "some content")?;
1121 let oid = test_repo.stage_and_commit(message)?;
1122
1123 let git_repo = Repo::from_path(&test_repo.dir)?;
1124
1125 assert_eq!(message, git_repo.get_commit_message(&oid_to_sha1(&oid))?,);
1126 Ok(())
1127 }
1128 #[test]
1129 fn one_liner() -> Result<()> {
1130 run("add t100.md")
1131 }
1132
1133 #[test]
1134 fn multiline() -> Result<()> {
1135 run("add t100.md\r\nanother line\r\nthird line")
1136 }
1137
1138 #[test]
1139 fn trailing_newlines() -> Result<()> {
1140 run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n")
1141 }
1142
1143 #[test]
1144 fn unicode_characters() -> Result<()> {
1145 run("add t100.md ❤️")
1146 }
1147 }
1148
1149 mod get_commit_message_summary {
1150 use super::*;
1151 fn run(message: &str, summary: &str) -> Result<()> {
1152 let test_repo = GitTestRepo::default();
1153 test_repo.populate()?;
1154 std::fs::write(test_repo.dir.join("t100.md"), "some content")?;
1155 let oid = test_repo.stage_and_commit(message)?;
1156
1157 let git_repo = Repo::from_path(&test_repo.dir)?;
1158
1159 assert_eq!(
1160 summary,
1161 git_repo.get_commit_message_summary(&oid_to_sha1(&oid))?,
1162 );
1163 Ok(())
1164 }
1165 #[test]
1166 fn one_liner() -> Result<()> {
1167 run("add t100.md", "add t100.md")
1168 }
1169
1170 #[test]
1171 fn multiline() -> Result<()> {
1172 run("add t100.md\r\nanother line\r\nthird line", "add t100.md")
1173 }
1174
1175 #[test]
1176 fn trailing_newlines() -> Result<()> {
1177 run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n", "add t100.md")
1178 }
1179
1180 #[test]
1181 fn unicode_characters() -> Result<()> {
1182 run("add t100.md ❤️", "add t100.md ❤️")
1183 }
1184 }
1185
1186 mod get_commit_author {
1187 use super::*;
1188
1189 static NAME: &str = "carole";
1190 static EMAIL: &str = "carole@pm.me";
1191
1192 fn prep(time: &git2::Time) -> Result<Vec<String>> {
1193 let test_repo = GitTestRepo::default();
1194 test_repo.populate()?;
1195 fs::write(test_repo.dir.join("x1.md"), "some content")?;
1196 let oid = test_repo.stage_and_commit_custom_signature(
1197 "add x1.md",
1198 Some(&git2::Signature::new(NAME, EMAIL, time)?),
1199 None,
1200 )?;
1201
1202 let git_repo = Repo::from_path(&test_repo.dir)?;
1203 git_repo.get_commit_author(&oid_to_sha1(&oid))
1204 }
1205
1206 #[test]
1207 fn name() -> Result<()> {
1208 let res = prep(&git2::Time::new(5000, 0))?;
1209 assert_eq!(NAME, res[0]);
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn email() -> Result<()> {
1215 let res = prep(&git2::Time::new(5000, 0))?;
1216 assert_eq!(EMAIL, res[1]);
1217 Ok(())
1218 }
1219
1220 mod time {
1221 use super::*;
1222
1223 #[test]
1224 fn no_offset() -> Result<()> {
1225 let res = prep(&git2::Time::new(5000, 0))?;
1226 assert_eq!("5000", res[2]);
1227 assert_eq!("0", res[3]);
1228 Ok(())
1229 }
1230 #[test]
1231 fn positive_offset() -> Result<()> {
1232 let res = prep(&git2::Time::new(5000, 300))?;
1233 assert_eq!("5000", res[2]);
1234 assert_eq!("300", res[3]);
1235 Ok(())
1236 }
1237 #[test]
1238 fn negative_offset() -> Result<()> {
1239 let res = prep(&git2::Time::new(5000, -300))?;
1240 assert_eq!("5000", res[2]);
1241 assert_eq!("-300", res[3]);
1242 Ok(())
1243 }
1244 }
1245
1246 mod extract_sig_from_patch_tags {
1247 use super::*;
1248
1249 fn test(time: git2::Time) -> Result<()> {
1250 assert_eq!(
1251 extract_sig_from_patch_tags(
1252 &[nostr::Tag::custom(
1253 nostr::TagKind::Custom("author".to_string().into()),
1254 prep(&time)?,
1255 )],
1256 "author",
1257 )?
1258 .to_string(),
1259 git2::Signature::new(NAME, EMAIL, &time)?.to_string(),
1260 );
1261 Ok(())
1262 }
1263
1264 #[test]
1265 fn no_offset() -> Result<()> {
1266 test(git2::Time::new(5000, 0))
1267 }
1268
1269 #[test]
1270 fn positive_offset() -> Result<()> {
1271 test(git2::Time::new(5000, 300))
1272 }
1273
1274 #[test]
1275 fn negative_offset() -> Result<()> {
1276 test(git2::Time::new(5000, -300))
1277 }
1278 }
1279 }
1280
1281 mod get_commit_comitter {
1282 use super::*;
1283
1284 static NAME: &str = "carole";
1285 static EMAIL: &str = "carole@pm.me";
1286
1287 fn prep(time: &git2::Time) -> Result<Vec<String>> {
1288 let test_repo = GitTestRepo::default();
1289 test_repo.populate()?;
1290 fs::write(test_repo.dir.join("x1.md"), "some content")?;
1291 let oid = test_repo.stage_and_commit_custom_signature(
1292 "add x1.md",
1293 None,
1294 Some(&git2::Signature::new(NAME, EMAIL, time)?),
1295 )?;
1296
1297 let git_repo = Repo::from_path(&test_repo.dir)?;
1298 git_repo.get_commit_comitter(&oid_to_sha1(&oid))
1299 }
1300
1301 #[test]
1302 fn name() -> Result<()> {
1303 let res = prep(&git2::Time::new(5000, 0))?;
1304 assert_eq!(NAME, res[0]);
1305 Ok(())
1306 }
1307
1308 #[test]
1309 fn email() -> Result<()> {
1310 let res = prep(&git2::Time::new(5000, 0))?;
1311 assert_eq!(EMAIL, res[1]);
1312 Ok(())
1313 }
1314 }
1315
1316 mod does_commit_exist {
1317 use super::*;
1318
1319 #[test]
1320 fn existing_commits_results_in_true() -> Result<()> {
1321 let test_repo = GitTestRepo::default();
1322 test_repo.populate()?;
1323 let git_repo = Repo::from_path(&test_repo.dir)?;
1324
1325 assert!(git_repo.does_commit_exist("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?);
1326 Ok(())
1327 }
1328
1329 #[test]
1330 fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false()
1331 -> Result<()> {
1332 let test_repo = GitTestRepo::default();
1333 test_repo.populate()?;
1334 let git_repo = Repo::from_path(&test_repo.dir)?;
1335
1336 assert!(!git_repo.does_commit_exist("000004edc0d2fa118d63faa3c2db9c73d630a5ae")?);
1337 Ok(())
1338 }
1339
1340 #[test]
1341 fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error()
1342 -> Result<()> {
1343 let test_repo = GitTestRepo::default();
1344 test_repo.populate()?;
1345 let git_repo = Repo::from_path(&test_repo.dir)?;
1346
1347 assert!(git_repo.does_commit_exist("00").is_ok());
1348 Ok(())
1349 }
1350 }
1351
1352 mod make_patch_from_commit {
1353 use super::*;
1354 #[test]
1355 fn simple_patch_matches_string() -> Result<()> {
1356 let test_repo = GitTestRepo::default();
1357 let oid = test_repo.populate()?;
1358
1359 let git_repo = Repo::from_path(&test_repo.dir)?;
1360
1361 assert_eq!(
1362 "\
1363 From 431b84edc0d2fa118d63faa3c2db9c73d630a5ae Mon Sep 17 00:00:00 2001\n\
1364 From: Joe Bloggs <joe.bloggs@pm.me>\n\
1365 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\
1366 Subject: [PATCH] add t2.md\n\
1367 \n\
1368 ---\n \
1369 t2.md | 1 +\n \
1370 1 file changed, 1 insertion(+)\n \
1371 create mode 100644 t2.md\n\
1372 \n\
1373 diff --git a/t2.md b/t2.md\n\
1374 new file mode 100644\n\
1375 index 0000000..a66525d\n\
1376 --- /dev/null\n\
1377 +++ b/t2.md\n\
1378 @@ -0,0 +1 @@\n\
1379 +some content1\n\\ \
1380 No newline at end of file\n\
1381 --\n\
1382 libgit2 1.7.2\n\
1383 \n\
1384 ",
1385 git_repo.make_patch_from_commit(&oid_to_sha1(&oid), &None)?,
1386 );
1387 Ok(())
1388 }
1389
1390 #[test]
1391 fn series_count() -> Result<()> {
1392 let test_repo = GitTestRepo::default();
1393 let oid = test_repo.populate()?;
1394
1395 let git_repo = Repo::from_path(&test_repo.dir)?;
1396
1397 assert_eq!(
1398 "\
1399 From 431b84edc0d2fa118d63faa3c2db9c73d630a5ae Mon Sep 17 00:00:00 2001\n\
1400 From: Joe Bloggs <joe.bloggs@pm.me>\n\
1401 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\
1402 Subject: [PATCH 3/5] add t2.md\n\
1403 \n\
1404 ---\n \
1405 t2.md | 1 +\n \
1406 1 file changed, 1 insertion(+)\n \
1407 create mode 100644 t2.md\n\
1408 \n\
1409 diff --git a/t2.md b/t2.md\n\
1410 new file mode 100644\n\
1411 index 0000000..a66525d\n\
1412 --- /dev/null\n\
1413 +++ b/t2.md\n\
1414 @@ -0,0 +1 @@\n\
1415 +some content1\n\\ \
1416 No newline at end of file\n\
1417 --\n\
1418 libgit2 1.7.2\n\
1419 \n\
1420 ",
1421 git_repo.make_patch_from_commit(&oid_to_sha1(&oid), &Some((3, 5)))?,
1422 );
1423 Ok(())
1424 }
1425 }
1426
1427 mod get_main_or_master_branch {
1428
1429 use super::*;
1430
1431 #[test]
1432 fn return_origin_main_if_exists() -> Result<()> {
1433 let test_origin_repo = GitTestRepo::new("main")?;
1434 let main_origin_oid = test_origin_repo.populate()?;
1435
1436 let test_repo = GitTestRepo::new("main")?;
1437 test_repo.populate()?;
1438 test_repo.add_remote("origin", test_origin_repo.dir.to_str().unwrap())?;
1439 test_repo
1440 .git_repo
1441 .find_remote("origin")?
1442 .fetch(&["main"], None, None)?;
1443
1444 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1445 test_repo.stage_and_commit("add t3.md")?;
1446
1447 let git_repo = Repo::from_path(&test_repo.dir)?;
1448 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
1449 assert_eq!(name, "origin/main");
1450 assert_eq!(commit_hash, oid_to_sha1(&main_origin_oid));
1451 Ok(())
1452 }
1453
1454 mod returns_main {
1455 use super::*;
1456 #[test]
1457 fn when_it_exists() -> Result<()> {
1458 let test_repo = GitTestRepo::new("main")?;
1459 let main_oid = test_repo.populate()?;
1460 let git_repo = Repo::from_path(&test_repo.dir)?;
1461 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
1462 assert_eq!(name, "main");
1463 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn when_it_exists_and_other_branch_checkedout() -> Result<()> {
1469 let test_repo = GitTestRepo::new("main")?;
1470 let main_oid = test_repo.populate()?;
1471 test_repo.create_branch("feature")?;
1472 test_repo.checkout("feature")?;
1473 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1474 let feature_oid = test_repo.stage_and_commit("add t3.md")?;
1475
1476 let git_repo = Repo::from_path(&test_repo.dir)?;
1477 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
1478 assert_eq!(name, "main");
1479 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
1480 assert_ne!(commit_hash, oid_to_sha1(&feature_oid));
1481 Ok(())
1482 }
1483
1484 #[test]
1485 fn when_exists_even_if_master_is_checkedout() -> Result<()> {
1486 let test_repo = GitTestRepo::new("main")?;
1487 let main_oid = test_repo.populate()?;
1488 test_repo.create_branch("master")?;
1489 test_repo.checkout("master")?;
1490 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1491 let master_oid = test_repo.stage_and_commit("add t3.md")?;
1492
1493 let git_repo = Repo::from_path(&test_repo.dir)?;
1494 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
1495 assert_eq!(name, "main");
1496 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
1497 assert_ne!(commit_hash, oid_to_sha1(&master_oid));
1498 Ok(())
1499 }
1500 }
1501
1502 #[test]
1503 fn returns_master_if_exists_and_main_doesnt() -> Result<()> {
1504 let test_repo = GitTestRepo::new("master")?;
1505 let master_oid = test_repo.populate()?;
1506 test_repo.create_branch("feature")?;
1507 test_repo.checkout("feature")?;
1508 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1509 let feature_oid = test_repo.stage_and_commit("add t3.md")?;
1510
1511 let git_repo = Repo::from_path(&test_repo.dir)?;
1512 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
1513 assert_eq!(name, "master");
1514 assert_eq!(commit_hash, oid_to_sha1(&master_oid));
1515 assert_ne!(commit_hash, oid_to_sha1(&feature_oid));
1516 Ok(())
1517 }
1518 #[test]
1519 fn returns_error_if_no_main_or_master() -> Result<()> {
1520 let test_repo = GitTestRepo::new("feature")?;
1521 test_repo.populate()?;
1522 let git_repo = Repo::from_path(&test_repo.dir)?;
1523 assert!(git_repo.get_main_or_master_branch().is_err());
1524 Ok(())
1525 }
1526 }
1527
1528 mod get_origin_url {
1529 use super::*;
1530
1531 #[test]
1532 fn returns_origin_url() -> Result<()> {
1533 let test_repo = GitTestRepo::default();
1534 test_repo.add_remote("origin", "https://localhost:1000")?;
1535 let git_repo = Repo::from_path(&test_repo.dir)?;
1536 assert_eq!(git_repo.get_origin_url()?, "https://localhost:1000");
1537 Ok(())
1538 }
1539 }
1540 mod get_checked_out_branch_name {
1541 use super::*;
1542
1543 #[test]
1544 fn returns_checked_out_branch_name() -> Result<()> {
1545 let test_repo = GitTestRepo::default();
1546 let _ = test_repo.populate()?;
1547 // create feature branch
1548 test_repo.create_branch("example-feature")?;
1549 test_repo.checkout("example-feature")?;
1550
1551 let git_repo = Repo::from_path(&test_repo.dir)?;
1552
1553 assert_eq!(
1554 git_repo.get_checked_out_branch_name()?,
1555 "example-feature".to_string()
1556 );
1557 Ok(())
1558 }
1559 }
1560
1561 mod get_commits_ahead_behind {
1562 use super::*;
1563 mod returns_main {
1564 use super::*;
1565
1566 #[test]
1567 fn when_on_same_commit_return_empty() -> Result<()> {
1568 let test_repo = GitTestRepo::default();
1569 let oid = test_repo.populate()?;
1570 // create feature branch
1571 test_repo.create_branch("feature")?;
1572 test_repo.checkout("feature")?;
1573
1574 let git_repo = Repo::from_path(&test_repo.dir)?;
1575
1576 let (ahead, behind) =
1577 git_repo.get_commits_ahead_behind(&oid_to_sha1(&oid), &oid_to_sha1(&oid))?;
1578 assert_eq!(ahead, vec![]);
1579 assert_eq!(behind, vec![]);
1580 Ok(())
1581 }
1582
1583 #[test]
1584 fn when_2_commit_behind() -> Result<()> {
1585 let test_repo = GitTestRepo::default();
1586 test_repo.populate()?;
1587 // create feature branch
1588 test_repo.create_branch("feature")?;
1589 let feature_oid = test_repo.checkout("feature")?;
1590 // checkout main and add 2 commits
1591 test_repo.checkout("main")?;
1592 std::fs::write(test_repo.dir.join("t5.md"), "some content")?;
1593 let behind_1_oid = test_repo.stage_and_commit("add t5.md")?;
1594 std::fs::write(test_repo.dir.join("t6.md"), "some content")?;
1595 let behind_2_oid = test_repo.stage_and_commit("add t6.md")?;
1596
1597 let git_repo = Repo::from_path(&test_repo.dir)?;
1598
1599 let (ahead, behind) = git_repo.get_commits_ahead_behind(
1600 &oid_to_sha1(&behind_2_oid),
1601 &oid_to_sha1(&feature_oid),
1602 )?;
1603 assert_eq!(ahead, vec![]);
1604 assert_eq!(
1605 behind,
1606 vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid),],
1607 );
1608 Ok(())
1609 }
1610
1611 #[test]
1612 fn when_2_commit_ahead() -> Result<()> {
1613 let test_repo = GitTestRepo::default();
1614 let main_oid = test_repo.populate()?;
1615 // create feature branch and add 2 commits
1616 test_repo.create_branch("feature")?;
1617 test_repo.checkout("feature")?;
1618 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1619 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1620 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1621 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1622
1623 let git_repo = Repo::from_path(&test_repo.dir)?;
1624
1625 let (ahead, behind) = git_repo.get_commits_ahead_behind(
1626 &oid_to_sha1(&main_oid),
1627 &oid_to_sha1(&ahead_2_oid),
1628 )?;
1629 assert_eq!(
1630 ahead,
1631 vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid),],
1632 );
1633 assert_eq!(behind, vec![]);
1634 Ok(())
1635 }
1636
1637 #[test]
1638 fn when_2_commit_ahead_and_2_commits_behind() -> Result<()> {
1639 let test_repo = GitTestRepo::default();
1640 test_repo.populate()?;
1641 // create feature branch and add 2 commits
1642 test_repo.create_branch("feature")?;
1643 test_repo.checkout("feature")?;
1644 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1645 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1646 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1647 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1648 // checkout main and add 2 commits
1649 test_repo.checkout("main")?;
1650 std::fs::write(test_repo.dir.join("t5.md"), "some content")?;
1651 let behind_1_oid = test_repo.stage_and_commit("add t5.md")?;
1652 std::fs::write(test_repo.dir.join("t6.md"), "some content")?;
1653 let behind_2_oid = test_repo.stage_and_commit("add t6.md")?;
1654
1655 let git_repo = Repo::from_path(&test_repo.dir)?;
1656
1657 let (ahead, behind) = git_repo.get_commits_ahead_behind(
1658 &oid_to_sha1(&behind_2_oid),
1659 &oid_to_sha1(&ahead_2_oid),
1660 )?;
1661 assert_eq!(
1662 ahead,
1663 vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid)],
1664 );
1665 assert_eq!(
1666 behind,
1667 vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid)],
1668 );
1669 Ok(())
1670 }
1671 }
1672 }
1673
1674 mod create_branch_at_commit {
1675 use super::*;
1676 #[test]
1677 fn doesnt_error() -> Result<()> {
1678 let test_repo = GitTestRepo::default();
1679 test_repo.populate()?;
1680 // create feature branch and add 2 commits
1681 test_repo.create_branch("feature")?;
1682 test_repo.checkout("feature")?;
1683 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1684 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1685 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1686 test_repo.stage_and_commit("add t4.md")?;
1687
1688 let git_repo = Repo::from_path(&test_repo.dir)?;
1689
1690 let branch_name = "test-name-1";
1691 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1692
1693 Ok(())
1694 }
1695
1696 #[test]
1697 fn branch_gets_created() -> Result<()> {
1698 let test_repo = GitTestRepo::default();
1699 test_repo.populate()?;
1700 // create feature branch and add 2 commits
1701 test_repo.create_branch("feature")?;
1702 test_repo.checkout("feature")?;
1703 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1704 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1705 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1706 test_repo.stage_and_commit("add t4.md")?;
1707
1708 let git_repo = Repo::from_path(&test_repo.dir)?;
1709
1710 let branch_name = "test-name-1";
1711 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1712
1713 assert!(test_repo.checkout(branch_name).is_ok());
1714 Ok(())
1715 }
1716
1717 #[test]
1718 fn branch_created_with_correct_commit() -> Result<()> {
1719 let test_repo = GitTestRepo::default();
1720 test_repo.populate()?;
1721 // create feature branch and add 2 commits
1722 test_repo.create_branch("feature")?;
1723 test_repo.checkout("feature")?;
1724 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1725 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1726 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1727 test_repo.stage_and_commit("add t4.md")?;
1728
1729 let git_repo = Repo::from_path(&test_repo.dir)?;
1730
1731 let branch_name = "test-name-1";
1732 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1733
1734 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid);
1735 Ok(())
1736 }
1737
1738 mod when_branch_already_exists {
1739 use super::*;
1740
1741 #[test]
1742 fn when_new_tip_specified_it_is_updated() -> Result<()> {
1743 let test_repo = GitTestRepo::default();
1744 test_repo.populate()?;
1745 // create feature branch and add 2 commits
1746 test_repo.create_branch("feature")?;
1747 test_repo.checkout("feature")?;
1748 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1749 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1750 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1751 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1752
1753 let git_repo = Repo::from_path(&test_repo.dir)?;
1754
1755 let branch_name = "test-name-1";
1756 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1757
1758 git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?;
1759 assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid);
1760 Ok(())
1761 }
1762
1763 #[test]
1764 fn when_same_tip_is_specified_it_doesnt_error() -> Result<()> {
1765 let test_repo = GitTestRepo::default();
1766 test_repo.populate()?;
1767 // create feature branch and add 2 commits
1768 test_repo.create_branch("feature")?;
1769 test_repo.checkout("feature")?;
1770 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1771 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1772 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1773 test_repo.stage_and_commit("add t4.md")?;
1774
1775 let git_repo = Repo::from_path(&test_repo.dir)?;
1776
1777 let branch_name = "test-name-1";
1778 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1779
1780 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1781 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid);
1782 Ok(())
1783 }
1784
1785 #[test]
1786 fn when_branch_is_checkedout_new_tip_specified_it_is_updated() -> Result<()> {
1787 let test_repo = GitTestRepo::default();
1788 test_repo.populate()?;
1789 // create feature branch and add 2 commits
1790 test_repo.create_branch("feature")?;
1791 test_repo.checkout("feature")?;
1792 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1793 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1794 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1795 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1796
1797 let git_repo = Repo::from_path(&test_repo.dir)?;
1798
1799 let branch_name = "test-name-1";
1800 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1801 test_repo.checkout(branch_name)?;
1802 git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?;
1803 test_repo.checkout("main")?;
1804
1805 assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid);
1806 Ok(())
1807 }
1808 }
1809 }
1810
1811 mod create_commit_from_patch {
1812
1813 use test_utils::TEST_KEY_1_SIGNER;
1814
1815 use super::*;
1816 use crate::{repo_ref::RepoRef, sub_commands::send::generate_patch_event};
1817
1818 async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result<nostr::Event> {
1819 let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id();
1820 let git_repo = Repo::from_path(&test_repo.dir)?;
1821 generate_patch_event(
1822 &git_repo,
1823 &git_repo.get_root_commit()?,
1824 &oid_to_sha1(&original_oid),
1825 Some(nostr::EventId::all_zeros()),
1826 &TEST_KEY_1_SIGNER,
1827 &RepoRef::try_from(generate_repo_ref_event()).unwrap(),
1828 None,
1829 None,
1830 None,
1831 &None,
1832 &[],
1833 )
1834 .await
1835 }
1836 fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> {
1837 let test_repo = GitTestRepo::default();
1838 test_repo.populate()?;
1839 let git_repo = Repo::from_path(&test_repo.dir)?;
1840 println!("{:?}", &patch_event);
1841 git_repo.create_commit_from_patch(&patch_event)?;
1842 let commit_id = tag_value(&patch_event, "commit")?;
1843 // does commit with id exist?
1844 assert!(git_repo.does_commit_exist(&commit_id)?);
1845 Ok(())
1846 }
1847
1848 mod patch_created_as_commit_with_matching_id {
1849 use test_utils::git::joe_signature;
1850
1851 use super::*;
1852
1853 #[tokio::test]
1854 async fn simple_signature_author_committer_same_as_git_user_0_unixtime_no_pgp_signature()
1855 -> Result<()> {
1856 let source_repo = GitTestRepo::default();
1857 source_repo.populate()?;
1858 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1859 source_repo.stage_and_commit("add x1.md")?;
1860
1861 test_patch_applies_to_repository(
1862 generate_patch_from_head_commit(&source_repo).await?,
1863 )
1864 }
1865
1866 #[tokio::test]
1867 async fn signature_with_specific_author_time() -> Result<()> {
1868 let source_repo = GitTestRepo::default();
1869 source_repo.populate()?;
1870 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1871 source_repo.stage_and_commit_custom_signature(
1872 "add x1.md",
1873 Some(&git2::Signature::new(
1874 joe_signature().name().unwrap(),
1875 joe_signature().email().unwrap(),
1876 &git2::Time::new(5000, 0),
1877 )?),
1878 None,
1879 )?;
1880
1881 test_patch_applies_to_repository(
1882 generate_patch_from_head_commit(&source_repo).await?,
1883 )
1884 }
1885
1886 #[tokio::test]
1887 async fn author_name_and_email_not_current_git_user() -> Result<()> {
1888 let source_repo = GitTestRepo::default();
1889 source_repo.populate()?;
1890 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1891 source_repo.stage_and_commit_custom_signature(
1892 "add x1.md",
1893 Some(&git2::Signature::new(
1894 "carole",
1895 "carole@pm.me",
1896 &git2::Time::new(0, 0),
1897 )?),
1898 None,
1899 )?;
1900
1901 test_patch_applies_to_repository(
1902 generate_patch_from_head_commit(&source_repo).await?,
1903 )
1904 }
1905
1906 #[tokio::test]
1907 async fn comiiter_name_and_email_not_current_git_user_or_author() -> Result<()> {
1908 let source_repo = GitTestRepo::default();
1909 source_repo.populate()?;
1910 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1911 source_repo.stage_and_commit_custom_signature(
1912 "add x1.md",
1913 Some(&git2::Signature::new(
1914 "carole",
1915 "carole@pm.me",
1916 &git2::Time::new(0, 0),
1917 )?),
1918 Some(&git2::Signature::new(
1919 "bob",
1920 "bob@pm.me",
1921 &git2::Time::new(0, 0),
1922 )?),
1923 )?;
1924
1925 test_patch_applies_to_repository(
1926 generate_patch_from_head_commit(&source_repo).await?,
1927 )
1928 }
1929
1930 // TODO: pgp signature
1931
1932 #[tokio::test]
1933 async fn unique_author_and_commiter_details() -> Result<()> {
1934 let source_repo = GitTestRepo::default();
1935 source_repo.populate()?;
1936 fs::write(source_repo.dir.join("x1.md"), "some content")?;
1937 source_repo.stage_and_commit_custom_signature(
1938 "add x1.md",
1939 Some(&git2::Signature::new(
1940 "carole",
1941 "carole@pm.me",
1942 &git2::Time::new(5000, 0),
1943 )?),
1944 Some(&git2::Signature::new(
1945 "bob",
1946 "bob@pm.me",
1947 &git2::Time::new(1000, 0),
1948 )?),
1949 )?;
1950
1951 test_patch_applies_to_repository(
1952 generate_patch_from_head_commit(&source_repo).await?,
1953 )
1954 }
1955 }
1956 }
1957
1958 mod apply_patch_chain {
1959 use test_utils::TEST_KEY_1_SIGNER;
1960
1961 use super::*;
1962 use crate::{
1963 repo_ref::RepoRef, sub_commands::send::generate_cover_letter_and_patch_events,
1964 };
1965
1966 static BRANCH_NAME: &str = "add-example-feature";
1967 // returns original_repo, cover_letter_event, patch_events
1968 async fn generate_test_repo_and_events()
1969 -> Result<(GitTestRepo, nostr::Event, Vec<nostr::Event>)> {
1970 let original_repo = GitTestRepo::default();
1971 let oid3 = original_repo.populate_with_test_branch()?;
1972 let oid2 = original_repo.git_repo.find_commit(oid3)?.parent_id(0)?;
1973 let oid1 = original_repo.git_repo.find_commit(oid2)?.parent_id(0)?;
1974 // TODO: generate cover_letter and patch events
1975 let git_repo = Repo::from_path(&original_repo.dir)?;
1976
1977 let mut events = generate_cover_letter_and_patch_events(
1978 Some(("test".to_string(), "test".to_string())),
1979 &git_repo,
1980 &[oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)],
1981 &TEST_KEY_1_SIGNER,
1982 &RepoRef::try_from(generate_repo_ref_event()).unwrap(),
1983 &None,
1984 &[],
1985 )
1986 .await?;
1987
1988 events.reverse();
1989
1990 Ok((original_repo, events.pop().unwrap(), events))
1991 }
1992
1993 mod when_branch_and_commits_dont_exist {
1994 use super::*;
1995
1996 mod when_branch_root_is_tip_of_main {
1997 use super::*;
1998
1999 #[tokio::test]
2000 async fn branch_gets_created_with_name_specified_in_proposal() -> Result<()> {
2001 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2002 let test_repo = GitTestRepo::default();
2003 test_repo.populate()?;
2004 let git_repo = Repo::from_path(&test_repo.dir)?;
2005 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2006 assert!(
2007 git_repo
2008 .get_local_branch_names()?
2009 .contains(&BRANCH_NAME.to_string())
2010 );
2011 Ok(())
2012 }
2013
2014 #[tokio::test]
2015 async fn branch_checked_out() -> Result<()> {
2016 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2017 let test_repo = GitTestRepo::default();
2018 test_repo.populate()?;
2019 let git_repo = Repo::from_path(&test_repo.dir)?;
2020 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2021 assert_eq!(
2022 git_repo.get_checked_out_branch_name()?,
2023 BRANCH_NAME.to_string(),
2024 );
2025 Ok(())
2026 }
2027
2028 #[tokio::test]
2029 async fn patches_get_created_as_commits() -> Result<()> {
2030 let (original_repo, _, patch_events) = generate_test_repo_and_events().await?;
2031 let test_repo = GitTestRepo::default();
2032 test_repo.populate()?;
2033 let git_repo = Repo::from_path(&test_repo.dir)?;
2034 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2035 assert_eq!(
2036 test_repo.git_repo.head()?.peel_to_commit()?.id(),
2037 original_repo.git_repo.head()?.peel_to_commit()?.id(),
2038 );
2039 Ok(())
2040 }
2041
2042 #[tokio::test]
2043 async fn branch_tip_is_most_recent_patch() -> Result<()> {
2044 let (original_repo, _, patch_events) = generate_test_repo_and_events().await?;
2045 let test_repo = GitTestRepo::default();
2046 test_repo.populate()?;
2047 let git_repo = Repo::from_path(&test_repo.dir)?;
2048 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2049 assert_eq!(
2050 git_repo.get_tip_of_branch(BRANCH_NAME)?,
2051 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
2052 );
2053 Ok(())
2054 }
2055
2056 #[tokio::test]
2057 async fn previously_checked_out_branch_tip_does_not_change() -> Result<()> {
2058 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2059 let test_repo = GitTestRepo::default();
2060 test_repo.populate()?;
2061 let existing_branch = test_repo.get_checked_out_branch_name()?;
2062 let git_repo = Repo::from_path(&test_repo.dir)?;
2063 let previous_tip_of_existing_branch =
2064 git_repo.get_tip_of_branch(existing_branch.as_str())?;
2065 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2066 assert_eq!(
2067 previous_tip_of_existing_branch,
2068 git_repo.get_tip_of_branch(existing_branch.as_str())?,
2069 );
2070 Ok(())
2071 }
2072
2073 #[tokio::test]
2074 async fn returns_all_patches_applied() -> Result<()> {
2075 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2076 let test_repo = GitTestRepo::default();
2077 test_repo.populate()?;
2078 let git_repo = Repo::from_path(&test_repo.dir)?;
2079 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2080 assert_eq!(res.len(), 3);
2081 Ok(())
2082 }
2083 }
2084
2085 mod when_branch_root_is_tip_behind_main {
2086 use super::*;
2087
2088 #[tokio::test]
2089 async fn branch_gets_created_with_name_specified_in_proposal() -> Result<()> {
2090 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2091 let test_repo = GitTestRepo::default();
2092 test_repo.populate()?;
2093 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
2094 test_repo.stage_and_commit("add m3.md")?;
2095 let git_repo = Repo::from_path(&test_repo.dir)?;
2096 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2097 assert!(
2098 git_repo
2099 .get_local_branch_names()?
2100 .contains(&BRANCH_NAME.to_string())
2101 );
2102 Ok(())
2103 }
2104
2105 #[tokio::test]
2106 async fn branch_checked_out() -> Result<()> {
2107 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2108 let test_repo = GitTestRepo::default();
2109 test_repo.populate()?;
2110 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
2111 test_repo.stage_and_commit("add m3.md")?;
2112 let git_repo = Repo::from_path(&test_repo.dir)?;
2113 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2114 assert_eq!(
2115 git_repo.get_checked_out_branch_name()?,
2116 BRANCH_NAME.to_string(),
2117 );
2118 Ok(())
2119 }
2120
2121 #[tokio::test]
2122 async fn branch_tip_is_most_recent_patch() -> Result<()> {
2123 let (original_repo, _, patch_events) = generate_test_repo_and_events().await?;
2124 let test_repo = GitTestRepo::default();
2125 test_repo.populate()?;
2126 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
2127 test_repo.stage_and_commit("add m3.md")?;
2128 let git_repo = Repo::from_path(&test_repo.dir)?;
2129 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2130 assert_eq!(
2131 git_repo.get_tip_of_branch(BRANCH_NAME)?,
2132 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
2133 );
2134 Ok(())
2135 }
2136
2137 #[tokio::test]
2138 async fn previously_checked_out_branch_tip_does_not_change() -> Result<()> {
2139 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2140 let test_repo = GitTestRepo::default();
2141 test_repo.populate()?;
2142 std::fs::write(test_repo.dir.join("m3.md"), "some content")?;
2143 test_repo.stage_and_commit("add m3.md")?;
2144 let existing_branch = test_repo.get_checked_out_branch_name()?;
2145 let git_repo = Repo::from_path(&test_repo.dir)?;
2146 let previous_tip_of_existing_branch =
2147 git_repo.get_tip_of_branch(existing_branch.as_str())?;
2148 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2149 assert_eq!(
2150 previous_tip_of_existing_branch,
2151 git_repo.get_tip_of_branch(existing_branch.as_str())?,
2152 );
2153 Ok(())
2154 }
2155
2156 #[tokio::test]
2157 async fn returns_all_patches_applied() -> Result<()> {
2158 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2159 let test_repo = GitTestRepo::default();
2160 test_repo.populate()?;
2161 let git_repo = Repo::from_path(&test_repo.dir)?;
2162 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2163 assert_eq!(res.len(), 3);
2164 Ok(())
2165 }
2166 }
2167
2168 // TODO when_proposal_root_is_tip_ahead_of_main_and_doesnt_exist
2169 }
2170
2171 mod when_branch_and_first_commits_exists {
2172 use super::*;
2173
2174 mod when_branch_already_checked_out {
2175 use super::*;
2176
2177 #[tokio::test]
2178 async fn branch_tip_is_most_recent_patch() -> Result<()> {
2179 let (original_repo, _, mut patch_events) =
2180 generate_test_repo_and_events().await?;
2181 let test_repo = GitTestRepo::default();
2182 test_repo.populate()?;
2183 let git_repo = Repo::from_path(&test_repo.dir)?;
2184 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
2185 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2186
2187 assert_eq!(
2188 git_repo.get_tip_of_branch(BRANCH_NAME)?,
2189 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
2190 );
2191 Ok(())
2192 }
2193
2194 #[tokio::test]
2195 async fn returns_all_patches_applied() -> Result<()> {
2196 let (_, _, mut patch_events) = generate_test_repo_and_events().await?;
2197 let test_repo = GitTestRepo::default();
2198 test_repo.populate()?;
2199 let git_repo = Repo::from_path(&test_repo.dir)?;
2200 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
2201 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2202 assert_eq!(res.len(), 2);
2203 Ok(())
2204 }
2205 }
2206 mod when_branch_not_checked_out {
2207 use super::*;
2208
2209 #[tokio::test]
2210 async fn branch_tip_is_most_recent_patch() -> Result<()> {
2211 let (original_repo, _, mut patch_events) =
2212 generate_test_repo_and_events().await?;
2213 let test_repo = GitTestRepo::default();
2214 test_repo.populate()?;
2215 let git_repo = Repo::from_path(&test_repo.dir)?;
2216 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
2217 git_repo.checkout("main")?;
2218 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2219
2220 assert_eq!(
2221 git_repo.get_tip_of_branch(BRANCH_NAME)?,
2222 oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),),
2223 );
2224 Ok(())
2225 }
2226
2227 #[tokio::test]
2228 async fn branch_checked_out() -> Result<()> {
2229 let (_, _, mut patch_events) = generate_test_repo_and_events().await?;
2230 let test_repo = GitTestRepo::default();
2231 test_repo.populate()?;
2232 let git_repo = Repo::from_path(&test_repo.dir)?;
2233 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
2234 git_repo.checkout("main")?;
2235 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2236
2237 assert_eq!(
2238 git_repo.get_checked_out_branch_name()?,
2239 BRANCH_NAME.to_string(),
2240 );
2241 Ok(())
2242 }
2243
2244 #[tokio::test]
2245 async fn returns_all_patches_applied() -> Result<()> {
2246 let (_, _, mut patch_events) = generate_test_repo_and_events().await?;
2247 let test_repo = GitTestRepo::default();
2248 test_repo.populate()?;
2249 let git_repo = Repo::from_path(&test_repo.dir)?;
2250 git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?;
2251 git_repo.checkout("main")?;
2252 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2253 assert_eq!(res.len(), 2);
2254 Ok(())
2255 }
2256 }
2257 // TODO when branch ahead (rebased or user commits)
2258 }
2259 mod when_branch_exists_and_is_up_to_date {
2260 use super::*;
2261
2262 mod when_branch_already_checked_out {
2263 use super::*;
2264
2265 #[tokio::test]
2266 async fn returns_all_patches_applied_0() -> Result<()> {
2267 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2268 let test_repo = GitTestRepo::default();
2269 test_repo.populate()?;
2270 let git_repo = Repo::from_path(&test_repo.dir)?;
2271 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
2272 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2273 assert_eq!(res.len(), 0);
2274 Ok(())
2275 }
2276 }
2277 mod when_branch_not_checked_out {
2278 use super::*;
2279
2280 #[tokio::test]
2281 async fn branch_checked_out() -> Result<()> {
2282 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2283 let test_repo = GitTestRepo::default();
2284 test_repo.populate()?;
2285 let git_repo = Repo::from_path(&test_repo.dir)?;
2286 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
2287 git_repo.checkout("main")?;
2288 git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2289
2290 assert_eq!(
2291 git_repo.get_checked_out_branch_name()?,
2292 BRANCH_NAME.to_string(),
2293 );
2294 Ok(())
2295 }
2296
2297 #[tokio::test]
2298 async fn returns_all_patches_applied_0() -> Result<()> {
2299 let (_, _, patch_events) = generate_test_repo_and_events().await?;
2300 let test_repo = GitTestRepo::default();
2301 test_repo.populate()?;
2302 let git_repo = Repo::from_path(&test_repo.dir)?;
2303 git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?;
2304 git_repo.checkout("main")?;
2305 let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?;
2306 assert_eq!(res.len(), 0);
2307 Ok(())
2308 }
2309 }
2310 }
2311 }
2312 mod parse_starting_commits {
2313 use super::*;
2314
2315 mod head_1_returns_latest_commit {
2316 use super::*;
2317
2318 #[test]
2319 fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> {
2320 let test_repo = GitTestRepo::default();
2321 let git_repo = Repo::from_path(&test_repo.dir)?;
2322 test_repo.populate_with_test_branch()?;
2323 test_repo.checkout("main")?;
2324
2325 assert_eq!(
2326 git_repo.parse_starting_commits("HEAD~1")?,
2327 vec![str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?],
2328 );
2329 Ok(())
2330 }
2331
2332 #[test]
2333 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
2334 let test_repo = GitTestRepo::default();
2335 let git_repo = Repo::from_path(&test_repo.dir)?;
2336 test_repo.populate_with_test_branch()?;
2337
2338 assert_eq!(
2339 git_repo.parse_starting_commits("HEAD~1")?,
2340 vec![str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?],
2341 );
2342 Ok(())
2343 }
2344 }
2345 mod head_2_returns_latest_2_commits_youngest_first {
2346 use super::*;
2347
2348 #[test]
2349 fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> {
2350 let test_repo = GitTestRepo::default();
2351 let git_repo = Repo::from_path(&test_repo.dir)?;
2352 test_repo.populate_with_test_branch()?;
2353 test_repo.checkout("main")?;
2354
2355 assert_eq!(
2356 git_repo.parse_starting_commits("HEAD~2")?,
2357 vec![
2358 str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
2359 str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?,
2360 ],
2361 );
2362 Ok(())
2363 }
2364 }
2365 mod head_3_returns_latest_3_commits_youngest_first {
2366 use super::*;
2367
2368 #[test]
2369 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
2370 let test_repo = GitTestRepo::default();
2371 let git_repo = Repo::from_path(&test_repo.dir)?;
2372 test_repo.populate_with_test_branch()?;
2373
2374 assert_eq!(
2375 git_repo.parse_starting_commits("HEAD~3")?,
2376 vec![
2377 str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?,
2378 str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
2379 str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
2380 ],
2381 );
2382 Ok(())
2383 }
2384 }
2385 mod range_of_3_commits_not_in_branch_history_returns_3_commits_youngest_first {
2386 use super::*;
2387
2388 #[test]
2389 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
2390 let test_repo = GitTestRepo::default();
2391 let git_repo = Repo::from_path(&test_repo.dir)?;
2392 test_repo.populate_with_test_branch()?;
2393 test_repo.checkout("main")?;
2394
2395 assert_eq!(
2396 git_repo.parse_starting_commits("af474d8..a23e6b0")?,
2397 vec![
2398 str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
2399 str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
2400 str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
2401 ],
2402 );
2403 Ok(())
2404 }
2405 }
2406 }
2407 mod ancestor_of {
2408 use super::*;
2409
2410 #[test]
2411 fn deep_ancestor_returns_true() -> Result<()> {
2412 let test_repo = GitTestRepo::default();
2413 let from_main_in_feature_history = test_repo.populate()?;
2414
2415 // create feature branch and add 2 commits
2416 test_repo.create_branch("feature")?;
2417
2418 test_repo.checkout("feature")?;
2419 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
2420 test_repo.stage_and_commit("add t3.md")?;
2421 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
2422 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
2423
2424 let git_repo = Repo::from_path(&test_repo.dir)?;
2425
2426 assert!(git_repo.ancestor_of(
2427 &oid_to_sha1(&ahead_2_oid),
2428 &oid_to_sha1(&from_main_in_feature_history)
2429 )?);
2430 Ok(())
2431 }
2432
2433 #[test]
2434 fn commit_parent_returns_true() -> Result<()> {
2435 let test_repo = GitTestRepo::default();
2436 test_repo.populate()?;
2437
2438 // create feature branch and add 2 commits
2439 test_repo.create_branch("feature")?;
2440
2441 test_repo.checkout("feature")?;
2442 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
2443 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
2444 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
2445 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
2446
2447 let git_repo = Repo::from_path(&test_repo.dir)?;
2448
2449 assert!(git_repo.ancestor_of(&oid_to_sha1(&ahead_2_oid), &oid_to_sha1(&ahead_1_oid))?);
2450 Ok(())
2451 }
2452
2453 #[test]
2454 fn same_commit_returns_false() -> Result<()> {
2455 let test_repo = GitTestRepo::default();
2456 test_repo.populate()?;
2457
2458 // create feature branch and add 2 commits
2459 test_repo.create_branch("feature")?;
2460
2461 test_repo.checkout("feature")?;
2462 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
2463 test_repo.stage_and_commit("add t3.md")?;
2464 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
2465 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
2466
2467 let git_repo = Repo::from_path(&test_repo.dir)?;
2468
2469 assert!(!git_repo.ancestor_of(&oid_to_sha1(&ahead_2_oid), &oid_to_sha1(&ahead_2_oid))?);
2470 Ok(())
2471 }
2472
2473 #[test]
2474 fn commit_not_in_history_returns_false() -> Result<()> {
2475 let test_repo = GitTestRepo::default();
2476 test_repo.populate()?;
2477
2478 // create feature branch and add 2 commits
2479 test_repo.create_branch("feature")?;
2480
2481 // create commit not in feature history
2482 std::fs::write(test_repo.dir.join("notfeature.md"), "some content")?;
2483 let on_main_after_feature = test_repo.stage_and_commit("add notfeature.md")?;
2484
2485 test_repo.checkout("feature")?;
2486 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
2487 test_repo.stage_and_commit("add t3.md")?;
2488 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
2489 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
2490
2491 let git_repo = Repo::from_path(&test_repo.dir)?;
2492
2493 assert!(!git_repo.ancestor_of(
2494 &oid_to_sha1(&ahead_2_oid),
2495 &oid_to_sha1(&on_main_after_feature)
2496 )?);
2497 Ok(())
2498 }
2499 }
2500 mod convert_clone_url_to_https {
2501 use super::*;
2502
2503 #[test]
2504 fn test_https_url() {
2505 let url = "https://github.com/user/repo.git";
2506 let result = convert_clone_url_to_https(url).unwrap();
2507 assert_eq!(result, "https://github.com/user/repo.git");
2508 }
2509
2510 #[test]
2511 fn test_http_url() {
2512 let url = "http://github.com/user/repo.git";
2513 let result = convert_clone_url_to_https(url).unwrap();
2514 assert_eq!(result, "https://github.com/user/repo.git");
2515 }
2516
2517 #[test]
2518 fn test_http_url_with_credentials() {
2519 let url = "http://username:password@github.com/user/repo.git";
2520 let result = convert_clone_url_to_https(url).unwrap();
2521 assert_eq!(result, "https://github.com/user/repo.git");
2522 }
2523
2524 #[test]
2525 fn test_git_at_url() {
2526 let url = "git@github.com:user/repo.git";
2527 let result = convert_clone_url_to_https(url).unwrap();
2528 assert_eq!(result, "https://github.com/user/repo.git");
2529 }
2530
2531 #[test]
2532 fn test_user_at_url() {
2533 let url = "user1@github.com:user/repo.git";
2534 let result = convert_clone_url_to_https(url).unwrap();
2535 assert_eq!(result, "https://github.com/user/repo.git");
2536 }
2537
2538 #[test]
2539 fn test_ssh_url() {
2540 let url = "ssh://github.com/user/repo.git";
2541 let result = convert_clone_url_to_https(url).unwrap();
2542 assert_eq!(result, "https://github.com/user/repo.git");
2543 }
2544
2545 #[test]
2546 fn test_ftp_url() {
2547 let url = "ftp://example.com/repo.git";
2548 let result = convert_clone_url_to_https(url).unwrap();
2549 assert_eq!(result, "https://example.com/repo.git");
2550 }
2551
2552 #[test]
2553 fn test_git_protocol_url() {
2554 let url = "git://example.com/repo.git";
2555 let result = convert_clone_url_to_https(url).unwrap();
2556 assert_eq!(result, "https://example.com/repo.git");
2557 }
2558
2559 #[test]
2560 fn test_invalid_url() {
2561 let url = "unsupported://example.com/repo.git";
2562 let result = convert_clone_url_to_https(url);
2563 assert!(result.is_err());
2564 }
2565 }
2566}