From 949c6459aa7683453a7160423b689ceadb08954b Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 4 Sep 2024 08:04:48 +0100 Subject: refactor: organise into lib and bin structure the make the code more readable this commit just moves the files, the next commit should fix the imports --- src/git.rs | 2566 ------------------------------------------------------------ 1 file changed, 2566 deletions(-) delete mode 100644 src/git.rs (limited to 'src/git.rs') 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 @@ -use std::{ - collections::HashSet, - env::current_dir, - path::{Path, PathBuf}, -}; - -use anyhow::{bail, Context, Result}; -use git2::{DiffOptions, Oid, Revwalk}; -use nostr::nips::nip01::Coordinate; -use nostr_sdk::{ - hashes::{sha1::Hash as Sha1Hash, Hash}, - PublicKey, Url, -}; - -use crate::sub_commands::list::{get_commit_id_from_patch, tag_value}; - -pub struct Repo { - pub git_repo: git2::Repository, -} - -impl Repo { - pub fn discover() -> Result { - Ok(Self { - git_repo: git2::Repository::discover(current_dir()?)?, - }) - } - pub fn from_path(path: &PathBuf) -> Result { - Ok(Self { - git_repo: git2::Repository::open(path)?, - }) - } -} - -// pub type CommitId = [u8; 7]; -// pub type Sha1 = [u8; 20]; - -pub trait RepoActions { - fn get_path(&self) -> Result<&Path>; - fn get_origin_url(&self) -> Result; - fn get_remote_branch_names(&self) -> Result>; - fn get_local_branch_names(&self) -> Result>; - fn get_origin_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>; - fn get_local_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>; - fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>; - fn get_checked_out_branch_name(&self) -> Result; - fn get_tip_of_branch(&self, branch_name: &str) -> Result; - fn get_commit_or_tip_of_reference(&self, reference: &str) -> Result; - fn get_root_commit(&self) -> Result; - fn does_commit_exist(&self, commit: &str) -> Result; - fn get_head_commit(&self) -> Result; - fn get_commit_parent(&self, commit: &Sha1Hash) -> Result; - fn get_commit_message(&self, commit: &Sha1Hash) -> Result; - fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result; - #[allow(clippy::doc_link_with_quotes)] - /// returns vector ["name", "email", "unixtime", "offset"] - /// eg ["joe bloggs", "joe@pm.me", "12176","-300"] - fn get_commit_author(&self, commit: &Sha1Hash) -> Result>; - #[allow(clippy::doc_link_with_quotes)] - /// returns vector ["name", "email", "unixtime", "offset"] - /// eg ["joe bloggs", "joe@pm.me", "12176","-300"] - fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result>; - fn get_commits_ahead_behind( - &self, - base_commit: &Sha1Hash, - latest_commit: &Sha1Hash, - ) -> Result<(Vec, Vec)>; - fn get_refs(&self, commit: &Sha1Hash) -> Result>; - // including (un)staged changes and (un)tracked files - fn has_outstanding_changes(&self) -> Result; - fn make_patch_from_commit( - &self, - commit: &Sha1Hash, - series_count: &Option<(u64, u64)>, - ) -> Result; - fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result; - fn checkout(&self, ref_name: &str) -> Result; - fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()>; - fn apply_patch_chain( - &self, - branch_name: &str, - patch_and_ancestors: Vec, - ) -> Result>; - fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result; - fn parse_starting_commits(&self, starting_commits: &str) -> Result>; - fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result; - fn get_git_config_item(&self, item: &str, global: Option) -> Result>; - fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; - fn remove_git_config_item(&self, item: &str, global: bool) -> Result; -} - -impl RepoActions for Repo { - fn get_path(&self) -> Result<&Path> { - self.git_repo - .path() - .parent() - .context("cannot find repositiory path as .git has no parent") - } - - fn get_origin_url(&self) -> Result { - Ok(self - .git_repo - .find_remote("origin") - .context("cannot find origin")? - .url() - .context("cannot find origin url")? - .to_string()) - } - - fn get_origin_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> { - let main_branch_name = { - let remote_branches = self - .get_remote_branch_names() - .context("cannot find any local branches")?; - if remote_branches.contains(&"origin/main".to_string()) { - "origin/main" - } else if remote_branches.contains(&"origin/master".to_string()) { - "origin/master" - } else { - bail!("no main or master branch locally in this git repository to initiate from",) - } - }; - - let tip = self - .get_tip_of_branch(main_branch_name) - .context(format!( - "branch {main_branch_name} was listed as a remote branch but cannot get its tip commit id", - ))?; - - Ok((main_branch_name, tip)) - } - - fn get_local_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> { - let main_branch_name = { - let local_branches = self - .get_local_branch_names() - .context("cannot find any local branches")?; - if local_branches.contains(&"main".to_string()) { - "main" - } else if local_branches.contains(&"master".to_string()) { - "master" - } else { - bail!("no main or master branch locally in this git repository to initiate from",) - } - }; - - let tip = self - .get_tip_of_branch(main_branch_name) - .context(format!( - "branch {main_branch_name} was listed as a local branch but cannot get its tip commit id", - ))?; - - Ok((main_branch_name, tip)) - } - - fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> { - if let Ok(main_tuple) = self - .get_origin_main_or_master_branch() - .context("the default branches (main or master) do not exist") - { - Ok(main_tuple) - } else { - self.get_local_main_or_master_branch() - .context("the default branches (main or master) do not exist") - } - } - - fn get_local_branch_names(&self) -> Result> { - let local_branches = self - .git_repo - .branches(Some(git2::BranchType::Local)) - .context("getting GitRepo branches should not error even for a blank repository")?; - - let mut branch_names = vec![]; - - for iter in local_branches { - let branch = iter?.0; - if let Some(name) = branch.name()? { - branch_names.push(name.to_string()); - } - } - Ok(branch_names) - } - - fn get_remote_branch_names(&self) -> Result> { - let remote_branches = self - .git_repo - .branches(Some(git2::BranchType::Remote)) - .context("getting GitRepo branches should not error even for a blank repository")?; - - let mut branch_names = vec![]; - - for iter in remote_branches { - let branch = iter?.0; - if let Some(name) = branch.name()? { - branch_names.push(name.to_string()); - } - } - Ok(branch_names) - } - - fn get_checked_out_branch_name(&self) -> Result { - Ok(self - .git_repo - .head()? - .shorthand() - .context("an object without a shorthand is checked out")? - .to_string()) - } - - fn get_tip_of_branch(&self, branch_name: &str) -> Result { - let branch = if let Ok(branch) = self - .git_repo - .find_branch(branch_name, git2::BranchType::Local) - .context(format!("cannot find local branch {branch_name}")) - { - branch - } else { - self.git_repo - .find_branch(branch_name, git2::BranchType::Remote) - .context(format!("cannot find local or remote branch {branch_name}"))? - }; - Ok(oid_to_sha1(&branch.into_reference().peel_to_commit()?.id())) - } - - fn get_commit_or_tip_of_reference(&self, sha1_or_reference: &str) -> Result { - let oid = { - if let Ok(oid) = Oid::from_str(sha1_or_reference) { - self.git_repo.find_commit(oid)?; - oid - } else { - self.git_repo - .find_reference(sha1_or_reference)? - .peel_to_commit()? - .id() - } - }; - Ok(oid_to_sha1(&oid)) - } - - fn get_root_commit(&self) -> Result { - let mut revwalk = self - .git_repo - .revwalk() - .context("revwalk should be created from git repo")?; - revwalk - .push(sha1_to_oid(&self.get_head_commit()?)?) - .context("revwalk should accept tip oid")?; - Ok(oid_to_sha1( - &revwalk - .last() - .context("revwalk from tip should be at least contain the tip oid")? - .context("revwalk iter from branch tip should not result in an error")?, - )) - } - - fn does_commit_exist(&self, commit: &str) -> Result { - if self.git_repo.find_commit(Oid::from_str(commit)?).is_ok() { - Ok(true) - } else { - Ok(false) - } - } - - fn get_head_commit(&self) -> Result { - let head = self - .git_repo - .head() - .context("failed to get git repo head")?; - let oid = head.peel_to_commit()?.id(); - Ok(oid_to_sha1(&oid)) - } - - fn get_commit_parent(&self, commit: &Sha1Hash) -> Result { - let parent_oid = self - .git_repo - .find_commit(sha1_to_oid(commit)?) - .context(format!("could not find commit {commit}"))? - .parent_id(0) - .context(format!("could not find parent of commit {commit}"))?; - Ok(oid_to_sha1(&parent_oid)) - } - - fn get_commit_message(&self, commit: &Sha1Hash) -> Result { - Ok(self - .git_repo - .find_commit(sha1_to_oid(commit)?) - .context(format!("could not find commit {commit}"))? - .message_raw() - .context("commit message has unusual characters in (not valid utf-8)")? - .to_string()) - } - - fn get_commit_message_summary(&self, commit: &Sha1Hash) -> Result { - Ok(self - .git_repo - .find_commit(sha1_to_oid(commit)?) - .context(format!("could not find commit {commit}"))? - .message_raw() - .context("commit message has unusual characters in (not valid utf-8)")? - .split('\r') - .collect::>()[0] - .split('\n') - .collect::>()[0] - .to_string() - .trim() - .to_string()) - } - - fn get_commit_author(&self, commit: &Sha1Hash) -> Result> { - let commit = self - .git_repo - .find_commit(sha1_to_oid(commit)?) - .context(format!("could not find commit {commit}"))?; - let sig = commit.author(); - Ok(git_sig_to_tag_vec(&sig)) - } - - fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result> { - let commit = self - .git_repo - .find_commit(sha1_to_oid(commit)?) - .context(format!("could not find commit {commit}"))?; - let sig = commit.committer(); - Ok(git_sig_to_tag_vec(&sig)) - } - - fn get_refs(&self, commit: &Sha1Hash) -> Result> { - Ok(self - .git_repo - .references()? - .filter(|r| { - if let Ok(r) = r { - if let Ok(ref_tip) = r.peel_to_commit() { - ref_tip.id().to_string().eq(&commit.to_string()) - } else { - false - } - } else { - false - } - }) - .map(|r| r.unwrap().shorthand().unwrap().to_string()) - .collect::>()) - } - - fn make_patch_from_commit( - &self, - commit: &Sha1Hash, - series_count: &Option<(u64, u64)>, - ) -> Result { - let c = self - .git_repo - .find_commit(Oid::from_bytes(commit.as_byte_array()).context(format!( - "failed to convert commit_id format for {}", - &commit - ))?) - .context(format!("failed to find commit {}", &commit))?; - let mut options = git2::EmailCreateOptions::default(); - if let Some((n, total)) = series_count { - options.subject_prefix(format!("PATCH {n}/{total}")); - } - let patch = git2::Email::from_commit(&c, &mut options) - .context(format!("failed to create patch from commit {}", &commit))?; - - Ok(std::str::from_utf8(patch.as_slice()) - .context("patch content could not be converted to a utf8 string")? - .to_owned()) - } - - fn extract_commit_pgp_signature(&self, commit: &Sha1Hash) -> Result { - let oid = Oid::from_bytes(commit.as_byte_array()).context(format!( - "failed to convert commit_id format for {}", - &commit - ))?; - - let (sign, _data) = self - .git_repo - .extract_signature(&oid, None) - .context("failed to extract signature - perhaps there is no signature?")?; - - Ok(std::str::from_utf8(&sign) - .context("commit signature cannot be converted to a utf8 string")? - .to_owned()) - } - - // including (un)staged changes and (un)tracked files - fn has_outstanding_changes(&self) -> Result { - let diff = self.git_repo.diff_tree_to_workdir_with_index( - Some(&self.git_repo.head()?.peel_to_tree()?), - Some(DiffOptions::new().include_untracked(true)), - )?; - - Ok(diff.deltas().len().gt(&0)) - } - - fn get_commits_ahead_behind( - &self, - base_commit: &Sha1Hash, - latest_commit: &Sha1Hash, - ) -> Result<(Vec, Vec)> { - let mut ahead: Vec = vec![]; - let mut behind: Vec = vec![]; - - let get_revwalk = |commit: &Sha1Hash| -> Result { - let mut revwalk = self - .git_repo - .revwalk() - .context("revwalk should be created from git repo")?; - revwalk - .push(sha1_to_oid(commit)?) - .context("revwalk should accept commit oid")?; - Ok(revwalk) - }; - - // scan through the base commit ancestory until a common ancestor is found - let most_recent_shared_commit = match get_revwalk(base_commit) - .context("failed to get revwalk for base_commit")? - .find(|base_res| { - let base_oid = base_res.as_ref().unwrap(); - - if get_revwalk(latest_commit) - .unwrap() - .any(|latest_res| base_oid.eq(latest_res.as_ref().unwrap())) - { - true - } else { - // add commits not found in latest ancestory to 'behind' vector - behind.push(oid_to_sha1(base_oid)); - false - } - }) { - None => { - bail!(format!( - "{} is not an ancestor of {}", - latest_commit, base_commit - )); - } - Some(res) => res.context("revwalk failed to reveal commit")?, - }; - - // scan through the latest commits until shared commit is reached - get_revwalk(latest_commit) - .context("failed to get revwalk for latest_commit")? - .any(|latest_res| { - let latest_oid = latest_res.as_ref().unwrap(); - if latest_oid.eq(&most_recent_shared_commit) { - true - } else { - // add commits not found in base to 'ahead' vector - ahead.push(oid_to_sha1(latest_oid)); - false - } - }); - Ok((ahead, behind)) - } - - fn checkout(&self, ref_name: &str) -> Result { - let (object, reference) = self.git_repo.revparse_ext(ref_name)?; - - self.git_repo.checkout_tree(&object, None)?; - - match reference { - // gref is an actual reference like branches or tags - Some(gref) => self.git_repo.set_head(gref.name().unwrap()), - // this is a commit, not a reference - None => self.git_repo.set_head_detached(object.id()), - }?; - let oid = self.git_repo.head()?.peel_to_commit()?.id(); - - Ok(oid_to_sha1(&oid)) - } - - fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> { - let branch_checkedout = self.get_checked_out_branch_name()?.eq(branch_name); - if branch_checkedout { - let (name, _) = self.get_main_or_master_branch()?; - self.checkout(name)?; - } - - self.git_repo - .branch( - branch_name, - &self.git_repo.find_commit(Oid::from_str(commit)?)?, - true, - ) - .context("branch could not be created")?; - - if branch_checkedout { - self.checkout(branch_name)?; - } - Ok(()) - } - /* returns patches applied */ - fn apply_patch_chain( - &self, - branch_name: &str, - patch_and_ancestors: Vec, - ) -> Result> { - let branch_tip_result = self.get_tip_of_branch(branch_name); - - // filter out existing ancestors in branch - let mut patches_to_apply: Vec = patch_and_ancestors - .into_iter() - .filter(|e| { - let commit_id = get_commit_id_from_patch(e).unwrap(); - if let Ok(branch_tip) = branch_tip_result { - !branch_tip.to_string().eq(&commit_id) - && !self - .ancestor_of(&branch_tip, &str_to_sha1(&commit_id).unwrap()) - .unwrap() - } else { - true - } - }) - .collect(); - - let parent_commit_id = tag_value( - if let Ok(last_patch) = patches_to_apply.last().context("no patches") { - last_patch - } else { - self.checkout(branch_name) - .context("no patches and so cannot create a proposal branch")?; - return Ok(vec![]); - }, - "parent-commit", - )?; - - // check patches can be applied - if !self.does_commit_exist(&parent_commit_id)? { - bail!("cannot find parent commit ({parent_commit_id}). run git pull and try again.") - } - - // checkout branch - self.create_branch_at_commit(branch_name, &parent_commit_id)?; - self.checkout(branch_name)?; - - // apply commits - patches_to_apply.reverse(); - - for patch in &patches_to_apply { - let commit_id = get_commit_id_from_patch(patch)?; - // only create new commits - otherwise make them the tip - if !self.does_commit_exist(&commit_id)? { - self.create_commit_from_patch(patch)?; - } - self.create_branch_at_commit(branch_name, &commit_id)?; - self.checkout(branch_name)?; - } - Ok(patches_to_apply) - } - fn create_commit_from_patch(&self, patch: &nostr::Event) -> Result { - let commit_id = get_commit_id_from_patch(patch)?; - if self.does_commit_exist(&commit_id)? { - return Ok(Oid::from_str(&commit_id)?); - } - let parent_commit_id = tag_value(patch, "parent-commit")?; - - let parent_commit = self - .git_repo - .find_commit(Oid::from_str(&parent_commit_id)?) - .context("parrent commit doesnt exist")?; - let parent_tree = parent_commit.tree()?; - - // let mut apply_opts = git2::ApplyOptions::new(); - // apply_opts.check(false); - let mut existing_index = self.git_repo.index()?; - let mut index = self.git_repo.apply_to_tree( - &parent_tree, - &git2::Diff::from_buffer(patch.content.as_bytes())?, - // Some(&mut apply_opts), - None, - )?; - let tree = self - .git_repo - .find_tree(index.write_tree_to(&self.git_repo)?)?; - - let pgp_sig = if let Ok(pgp_sig) = tag_value(patch, "commit-pgp-sig") { - if pgp_sig.is_empty() { - None - } else { - Some(pgp_sig) - } - } else { - None - }; - - let commit_buff = self.git_repo.commit_create_buffer( - &extract_sig_from_patch_tags(&patch.tags, "author")?, - &extract_sig_from_patch_tags(&patch.tags, "committer")?, - tag_value(patch, "description")?.as_str(), - &tree, - &[&parent_commit], - )?; - - let mut applied_oid = self - .git_repo - .commit_signed( - commit_buff.as_str().unwrap(), - pgp_sig.unwrap_or(String::new()).as_str(), - None, - ) - .context("failed to create signed commit")?; - - // I beleive this was added to address a bug where commit author / committer - // were identical when in a scenario when they should be different but I dont - // think we have a test case for it. surely we should be using the - // extract_sig_from_patch_tags outputs to address this? - if !applied_oid.to_string().eq(&commit_id) { - let commit = self.git_repo.find_commit(applied_oid)?; - applied_oid = commit - .amend( - None, - Some(&commit.author()), - Some(&commit.committer()), - None, - None, - None, - ) - .context("cannot amend commit to produce new oid")?; - } - if !applied_oid.to_string().eq(&commit_id) { - bail!( - "when applied the patch commit id ({}) doesn't match the one specified in the event tag ({})", - applied_oid.to_string(), - get_commit_id_from_patch(patch)?, - ); - } - self.git_repo.set_index(&mut existing_index)?; - Ok(applied_oid) - } - fn parse_starting_commits(&self, starting_commits: &str) -> Result> { - let revspec = self - .git_repo - .revparse(starting_commits) - .context("specified value not in a valid format")?; - if revspec.mode().is_no_single() { - let (ahead, _) = self - .get_commits_ahead_behind( - &oid_to_sha1( - &revspec - .from() - .context("cannot get starting commit from specified value")? - .id(), - ), - &self - .get_head_commit() - .context("cannot get head commit with gitlib2")?, - ) - .context("specified commit is not an ancestor of current head")?; - Ok(ahead) - } else if revspec.mode().is_range() { - let (ahead, _) = self - .get_commits_ahead_behind( - &oid_to_sha1( - &revspec - .from() - .context("cannot get starting commit of range from specified value")? - .id(), - ), - &oid_to_sha1( - &revspec - .to() - .context("cannot get end of range commit from specified value")? - .id(), - ), - ) - .context("specified commit is not an ancestor of current head")?; - Ok(ahead) - } else { - bail!("specified value not in a supported format") - } - } - - fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result { - if let Ok(res) = self - .git_repo - .graph_descendant_of(sha1_to_oid(decendant)?, sha1_to_oid(ancestor)?) - .context("could not run graph_descendant_of in gitlib2") - { - Ok(res) - } else { - Ok(false) - } - } - - /// setting global to None will suppliment local config with global items - /// not in local - fn get_git_config_item(&self, item: &str, global: Option) -> Result> { - let just_global = if let Some(just_global) = global { - just_global - } else { - false - }; - match if just_global { - self.git_repo - .config() - .context("cannot open git config")? - .open_global() - .context("cannot open global git config")? - } else { - self.git_repo.config().context("cannot open git config")? - } - .get_entry(item) - { - Ok(item) => { - if let Some(global) = global { - if item.level().eq(&git2::ConfigLevel::Local) { - if global { - bail!("only local repository login available") - } - } else if !global { - bail!("only global repository login available") - } - } - Ok(Some( - item.value() - .context("cannot find git config item")? - .to_string(), - )) - } - Err(_) => Ok(None), - } - } - - fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()> { - if global { - self.git_repo - .config() - .context("cannot open git config")? - .open_global() - .context("cannot open global git config")? - } else { - self.git_repo.config().context("cannot open git config")? - } - .set_str(item, value) - .context(format!( - "cannot set {} git config item {}", - if global { "global" } else { "local" }, - item - ))?; - Ok(()) - } - - /// returns false if item doesn't exist - fn remove_git_config_item(&self, item: &str, global: bool) -> Result { - if self.get_git_config_item(item, Some(global))?.is_none() { - Ok(false) - } else { - if global { - self.git_repo - .config() - .context("cannot open git config")? - .open_global() - .context("cannot open global git config")? - } else { - self.git_repo.config().context("cannot open git config")? - } - .remove(item) - .context("cannot remove existing git config item")?; - Ok(true) - } - } -} - -fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { - let b = oid.as_bytes(); - [ - 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], - b[14], b[15], b[16], b[17], b[18], b[19], - ] -} - -// fn oid_to_shorthand_string(oid: Oid) -> Result { -// let binding = oid.to_string(); -// let b = binding.as_bytes(); -// String::from_utf8(vec![b[0], b[1], b[2], b[3], b[4], b[5], b[6]]) -// .context("oid should always start with 7 u8 btyes of utf8") -// } - -// fn oid_to_sha1_string(oid: Oid) -> Result { -// let b = oid.as_bytes(); -// String::from_utf8(vec![ -// 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], b[14], b[15], b[16], b[17], b[18], b[19], -// ]) -// .context("oid should contain 20 u8 btyes of utf8") -// } - -// git2 Oid object to Sha1Hash -pub fn oid_to_sha1(oid: &Oid) -> Sha1Hash { - Sha1Hash::from_byte_array(oid_to_u8_20_bytes(oid)) -} - -/// `Sha1Hash` to git2 `Oid` object -pub fn sha1_to_oid(hash: &Sha1Hash) -> Result { - Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid") -} - -pub fn str_to_sha1(s: &str) -> Result { - Ok(oid_to_sha1( - &Oid::from_str(s).context("string is not a sha1 hash")?, - )) -} - -fn git_sig_to_tag_vec(sig: &git2::Signature) -> Vec { - vec![ - sig.name().unwrap_or("").to_string(), - sig.email().unwrap_or("").to_string(), - format!("{}", sig.when().seconds()), - format!("{}", sig.when().offset_minutes()), - ] -} - -fn extract_sig_from_patch_tags<'a>( - tags: &'a [nostr::Tag], - tag_name: &str, -) -> Result> { - let v = tags - .iter() - .find(|t| t.as_vec()[0].eq(tag_name)) - .context(format!("tag '{tag_name}' not present in patch"))? - .as_vec(); - if v.len() != 5 { - bail!("tag '{tag_name}' is incorrectly formatted") - } - git2::Signature::new( - v[1].as_str(), - v[2].as_str(), - &git2::Time::new( - v[3].parse().context("tag time is incorrectly formatted")?, - v[4].parse() - .context("tag time offset is incorrectly formatted")?, - ), - ) - .context("failed to create git signature") -} - -#[derive(Debug, PartialEq)] -pub enum ServerProtocol { - Ssh, - Https, - Http, - Git, -} - -#[derive(Debug, PartialEq)] -pub struct NostrUrlDecoded { - pub coordinates: HashSet, - pub protocol: Option, - pub user: Option, -} - -static 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"; - -impl NostrUrlDecoded { - pub fn from_str(url: &str) -> Result { - let mut coordinates = HashSet::new(); - let mut protocol = None; - let mut user = None; - let mut relays = vec![]; - - if !url.starts_with("nostr://") { - bail!("nostr git url must start with nostr://"); - } - // process get url parameters if present - for (name, value) in Url::parse(url)?.query_pairs() { - if name.contains("relay") { - let mut decoded = urlencoding::decode(&value) - .context("could not parse relays in nostr git url")? - .to_string(); - if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { - decoded = format!("wss://{decoded}"); - } - let url = - Url::parse(&decoded).context("could not parse relays in nostr git url")?; - relays.push(url.to_string()); - } else if name == "protocol" { - protocol = match value.as_ref() { - "ssh" => Some(ServerProtocol::Ssh), - "https" => Some(ServerProtocol::Https), - "http" => Some(ServerProtocol::Http), - "git" => Some(ServerProtocol::Git), - _ => None, - }; - } else if name == "user" { - user = Some(value.to_string()); - } - } - - let mut parts: Vec<&str> = url[8..] - .split('?') - .next() - .unwrap_or("") - .split('/') - .collect(); - - // extract optional protocol - if protocol.is_none() { - let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; - let protocol_str = if let Some(at_index) = part.find('@') { - user = Some(part[..at_index].to_string()); - &part[at_index + 1..] - } else { - part - }; - protocol = match protocol_str { - "ssh" => Some(ServerProtocol::Ssh), - "https" => Some(ServerProtocol::Https), - "http" => Some(ServerProtocol::Http), - "git" => Some(ServerProtocol::Git), - _ => protocol, - }; - if protocol.is_some() { - parts.remove(0); - } - } - // extract naddr npub//identifer - let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?; - // naddr used - if let Ok(coordinate) = Coordinate::parse(part) { - if coordinate.kind.eq(&nostr_sdk::Kind::GitRepoAnnouncement) { - coordinates.insert(coordinate); - } else { - bail!("naddr doesnt point to a git repository announcement"); - } - // npub//identifer used - } else if let Ok(public_key) = PublicKey::parse(part) { - parts.remove(0); - let identifier = parts - .pop() - .context("nostr url must have an identifier eg. nostr://npub123/repo-identifier")? - .to_string(); - for relay in parts { - let mut decoded = urlencoding::decode(relay) - .context("could not parse relays in nostr git url")? - .to_string(); - if !decoded.starts_with("ws://") && !decoded.starts_with("wss://") { - decoded = format!("wss://{decoded}"); - } - let url = - Url::parse(&decoded).context("could not parse relays in nostr git url")?; - relays.push(url.to_string()); - } - coordinates.insert(Coordinate { - identifier, - public_key, - kind: nostr_sdk::Kind::GitRepoAnnouncement, - relays, - }); - } else { - bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR); - } - - Ok(Self { - coordinates, - protocol, - user, - }) - } -} - -/** produce error when using local repo or custom protocols */ -pub fn convert_clone_url_to_https(url: &str) -> Result { - // Strip credentials if present - let stripped_url = strip_credentials(url); - - // Check if the URL is already in HTTPS format - if stripped_url.starts_with("https://") { - return Ok(stripped_url); - } - // Convert http:// to https:// - else if stripped_url.starts_with("http://") { - return Ok(stripped_url.replace("http://", "https://")); - } - // Check if the URL starts with SSH - else if stripped_url.starts_with("ssh://") { - // Convert SSH to HTTPS - let parts: Vec<&str> = stripped_url - .trim_start_matches("ssh://") - .split('/') - .collect(); - if parts.len() >= 2 { - // Construct the HTTPS URL - return Ok(format!("https://{}/{}", parts[0], parts[1..].join("/"))); - } - bail!("Invalid SSH URL format: {}", url); - } - // Convert ftp:// to https:// - else if stripped_url.starts_with("ftp://") { - return Ok(stripped_url.replace("ftp://", "https://")); - } - // Convert git:// to https:// - else if stripped_url.starts_with("git://") { - return Ok(stripped_url.replace("git://", "https://")); - } - - // If the URL is neither HTTPS, SSH, nor git@, return an error - bail!("Unsupported URL protocol: {}", url); -} - -// Function to strip username and password from the URL -fn strip_credentials(url: &str) -> String { - if let Some(pos) = url.find("://") { - let (protocol, rest) = url.split_at(pos + 3); // Split at "://" - let rest_parts: Vec<&str> = rest.split('@').collect(); - if rest_parts.len() > 1 { - // If there are credentials, return the URL without them - return format!("{}{}", protocol, rest_parts[1]); - } - } else if let Some(at_pos) = url.find('@') { - // Handle user@host:path format - let (_, rest) = url.split_at(at_pos); - // This is a git@ syntax - let host_and_repo = &rest[1..]; // Skip the ':' - return format!("ssh://{}", host_and_repo.replace(':', "/")); - } - url.to_string() // Return the original URL if no credentials are found -} - -#[cfg(test)] -mod tests { - use std::fs; - - use test_utils::{generate_repo_ref_event, git::GitTestRepo}; - - use super::*; - - mod git_config_item_local { - use super::*; - - #[test] - fn save_git_config_item_returns_ok() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.save_git_config_item("test.item", "testvalue", false)?; - Ok(()) - } - - #[test] - fn get_git_config_item_returns_item_just_saved() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.save_git_config_item("test.item", "testvalue", false)?; - assert_eq!( - git_repo - .get_git_config_item("test.item", Some(false))? - .unwrap(), - "testvalue", - ); - Ok(()) - } - - #[test] - fn get_git_config_item_returns_none_if_not_present() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - assert_eq!( - git_repo.get_git_config_item("test.item", Some(false))?, - None - ); - Ok(()) - } - - #[test] - fn get_git_config_item_empty_string_returns_empty_string_instead_of_none() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.save_git_config_item("test.item", "", false)?; - assert_eq!( - git_repo.get_git_config_item("test.item", Some(false))?, - Some("".to_string()), - ); - Ok(()) - } - - #[test] - fn remove_local_git_config_item() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.save_git_config_item("test.item", "testvalue", false)?; - assert!(git_repo.remove_git_config_item("test.item", false)?); - assert_eq!( - git_repo.get_git_config_item("test.item", Some(false))?, - None, - ); - Ok(()) - } - - #[test] - fn remove_git_config_item_returns_false_if_item_wasnt_set() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - assert!(!(git_repo.remove_git_config_item("test.item", false)?)); - Ok(()) - } - } - - #[test] - fn get_commit_parent() -> Result<()> { - let test_repo = GitTestRepo::default(); - let parent_oid = test_repo.populate()?; - std::fs::write(test_repo.dir.join("t100.md"), "some content")?; - let child_oid = test_repo.stage_and_commit("add t100.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!( - // Sha1Hash::from_byte_array("bla".to_string().as_bytes()), - oid_to_sha1(&parent_oid), - git_repo.get_commit_parent(&oid_to_sha1(&child_oid))?, - ); - Ok(()) - } - - mod get_commit_message { - use super::*; - fn run(message: &str) -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("t100.md"), "some content")?; - let oid = test_repo.stage_and_commit(message)?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!(message, git_repo.get_commit_message(&oid_to_sha1(&oid))?,); - Ok(()) - } - #[test] - fn one_liner() -> Result<()> { - run("add t100.md") - } - - #[test] - fn multiline() -> Result<()> { - run("add t100.md\r\nanother line\r\nthird line") - } - - #[test] - fn trailing_newlines() -> Result<()> { - run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n") - } - - #[test] - fn unicode_characters() -> Result<()> { - run("add t100.md ❤️") - } - } - - mod get_commit_message_summary { - use super::*; - fn run(message: &str, summary: &str) -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("t100.md"), "some content")?; - let oid = test_repo.stage_and_commit(message)?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!( - summary, - git_repo.get_commit_message_summary(&oid_to_sha1(&oid))?, - ); - Ok(()) - } - #[test] - fn one_liner() -> Result<()> { - run("add t100.md", "add t100.md") - } - - #[test] - fn multiline() -> Result<()> { - run("add t100.md\r\nanother line\r\nthird line", "add t100.md") - } - - #[test] - fn trailing_newlines() -> Result<()> { - run("add t100.md\r\n\r\n\r\n\r\n\r\n\r\n", "add t100.md") - } - - #[test] - fn unicode_characters() -> Result<()> { - run("add t100.md ❤️", "add t100.md ❤️") - } - } - - mod get_commit_author { - use super::*; - - static NAME: &str = "carole"; - static EMAIL: &str = "carole@pm.me"; - - fn prep(time: &git2::Time) -> Result> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - fs::write(test_repo.dir.join("x1.md"), "some content")?; - let oid = test_repo.stage_and_commit_custom_signature( - "add x1.md", - Some(&git2::Signature::new(NAME, EMAIL, time)?), - None, - )?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.get_commit_author(&oid_to_sha1(&oid)) - } - - #[test] - fn name() -> Result<()> { - let res = prep(&git2::Time::new(5000, 0))?; - assert_eq!(NAME, res[0]); - Ok(()) - } - - #[test] - fn email() -> Result<()> { - let res = prep(&git2::Time::new(5000, 0))?; - assert_eq!(EMAIL, res[1]); - Ok(()) - } - - mod time { - use super::*; - - #[test] - fn no_offset() -> Result<()> { - let res = prep(&git2::Time::new(5000, 0))?; - assert_eq!("5000", res[2]); - assert_eq!("0", res[3]); - Ok(()) - } - #[test] - fn positive_offset() -> Result<()> { - let res = prep(&git2::Time::new(5000, 300))?; - assert_eq!("5000", res[2]); - assert_eq!("300", res[3]); - Ok(()) - } - #[test] - fn negative_offset() -> Result<()> { - let res = prep(&git2::Time::new(5000, -300))?; - assert_eq!("5000", res[2]); - assert_eq!("-300", res[3]); - Ok(()) - } - } - - mod extract_sig_from_patch_tags { - use super::*; - - fn test(time: git2::Time) -> Result<()> { - assert_eq!( - extract_sig_from_patch_tags( - &[nostr::Tag::custom( - nostr::TagKind::Custom("author".to_string().into()), - prep(&time)?, - )], - "author", - )? - .to_string(), - git2::Signature::new(NAME, EMAIL, &time)?.to_string(), - ); - Ok(()) - } - - #[test] - fn no_offset() -> Result<()> { - test(git2::Time::new(5000, 0)) - } - - #[test] - fn positive_offset() -> Result<()> { - test(git2::Time::new(5000, 300)) - } - - #[test] - fn negative_offset() -> Result<()> { - test(git2::Time::new(5000, -300)) - } - } - } - - mod get_commit_comitter { - use super::*; - - static NAME: &str = "carole"; - static EMAIL: &str = "carole@pm.me"; - - fn prep(time: &git2::Time) -> Result> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - fs::write(test_repo.dir.join("x1.md"), "some content")?; - let oid = test_repo.stage_and_commit_custom_signature( - "add x1.md", - None, - Some(&git2::Signature::new(NAME, EMAIL, time)?), - )?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.get_commit_comitter(&oid_to_sha1(&oid)) - } - - #[test] - fn name() -> Result<()> { - let res = prep(&git2::Time::new(5000, 0))?; - assert_eq!(NAME, res[0]); - Ok(()) - } - - #[test] - fn email() -> Result<()> { - let res = prep(&git2::Time::new(5000, 0))?; - assert_eq!(EMAIL, res[1]); - Ok(()) - } - } - - mod does_commit_exist { - use super::*; - - #[test] - fn existing_commits_results_in_true() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(git_repo.does_commit_exist("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?); - Ok(()) - } - - #[test] - fn correctly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_false() - -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(!git_repo.does_commit_exist("000004edc0d2fa118d63faa3c2db9c73d630a5ae")?); - Ok(()) - } - - #[test] - fn incorrectly_formatted_hash_that_doesnt_correspond_to_an_existing_commit_results_in_error() - -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(git_repo.does_commit_exist("00").is_ok()); - Ok(()) - } - } - - mod make_patch_from_commit { - use super::*; - #[test] - fn simple_patch_matches_string() -> Result<()> { - let test_repo = GitTestRepo::default(); - let oid = test_repo.populate()?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!( - "\ - From 431b84edc0d2fa118d63faa3c2db9c73d630a5ae Mon Sep 17 00:00:00 2001\n\ - From: Joe Bloggs \n\ - Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ - Subject: [PATCH] add t2.md\n\ - \n\ - ---\n \ - t2.md | 1 +\n \ - 1 file changed, 1 insertion(+)\n \ - create mode 100644 t2.md\n\ - \n\ - diff --git a/t2.md b/t2.md\n\ - new file mode 100644\n\ - index 0000000..a66525d\n\ - --- /dev/null\n\ - +++ b/t2.md\n\ - @@ -0,0 +1 @@\n\ - +some content1\n\\ \ - No newline at end of file\n\ - --\n\ - libgit2 1.7.2\n\ - \n\ - ", - git_repo.make_patch_from_commit(&oid_to_sha1(&oid), &None)?, - ); - Ok(()) - } - - #[test] - fn series_count() -> Result<()> { - let test_repo = GitTestRepo::default(); - let oid = test_repo.populate()?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!( - "\ - From 431b84edc0d2fa118d63faa3c2db9c73d630a5ae Mon Sep 17 00:00:00 2001\n\ - From: Joe Bloggs \n\ - Date: Thu, 1 Jan 1970 00:00:00 +0000\n\ - Subject: [PATCH 3/5] add t2.md\n\ - \n\ - ---\n \ - t2.md | 1 +\n \ - 1 file changed, 1 insertion(+)\n \ - create mode 100644 t2.md\n\ - \n\ - diff --git a/t2.md b/t2.md\n\ - new file mode 100644\n\ - index 0000000..a66525d\n\ - --- /dev/null\n\ - +++ b/t2.md\n\ - @@ -0,0 +1 @@\n\ - +some content1\n\\ \ - No newline at end of file\n\ - --\n\ - libgit2 1.7.2\n\ - \n\ - ", - git_repo.make_patch_from_commit(&oid_to_sha1(&oid), &Some((3, 5)))?, - ); - Ok(()) - } - } - - mod get_main_or_master_branch { - - use super::*; - - #[test] - fn return_origin_main_if_exists() -> Result<()> { - let test_origin_repo = GitTestRepo::new("main")?; - let main_origin_oid = test_origin_repo.populate()?; - - let test_repo = GitTestRepo::new("main")?; - test_repo.populate()?; - test_repo.add_remote("origin", test_origin_repo.dir.to_str().unwrap())?; - test_repo - .git_repo - .find_remote("origin")? - .fetch(&["main"], None, None)?; - - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - test_repo.stage_and_commit("add t3.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - let (name, commit_hash) = git_repo.get_main_or_master_branch()?; - assert_eq!(name, "origin/main"); - assert_eq!(commit_hash, oid_to_sha1(&main_origin_oid)); - Ok(()) - } - - mod returns_main { - use super::*; - #[test] - fn when_it_exists() -> Result<()> { - let test_repo = GitTestRepo::new("main")?; - let main_oid = test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - let (name, commit_hash) = git_repo.get_main_or_master_branch()?; - assert_eq!(name, "main"); - assert_eq!(commit_hash, oid_to_sha1(&main_oid)); - Ok(()) - } - - #[test] - fn when_it_exists_and_other_branch_checkedout() -> Result<()> { - let test_repo = GitTestRepo::new("main")?; - let main_oid = test_repo.populate()?; - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let feature_oid = test_repo.stage_and_commit("add t3.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - let (name, commit_hash) = git_repo.get_main_or_master_branch()?; - assert_eq!(name, "main"); - assert_eq!(commit_hash, oid_to_sha1(&main_oid)); - assert_ne!(commit_hash, oid_to_sha1(&feature_oid)); - Ok(()) - } - - #[test] - fn when_exists_even_if_master_is_checkedout() -> Result<()> { - let test_repo = GitTestRepo::new("main")?; - let main_oid = test_repo.populate()?; - test_repo.create_branch("master")?; - test_repo.checkout("master")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let master_oid = test_repo.stage_and_commit("add t3.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - let (name, commit_hash) = git_repo.get_main_or_master_branch()?; - assert_eq!(name, "main"); - assert_eq!(commit_hash, oid_to_sha1(&main_oid)); - assert_ne!(commit_hash, oid_to_sha1(&master_oid)); - Ok(()) - } - } - - #[test] - fn returns_master_if_exists_and_main_doesnt() -> Result<()> { - let test_repo = GitTestRepo::new("master")?; - let master_oid = test_repo.populate()?; - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let feature_oid = test_repo.stage_and_commit("add t3.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - let (name, commit_hash) = git_repo.get_main_or_master_branch()?; - assert_eq!(name, "master"); - assert_eq!(commit_hash, oid_to_sha1(&master_oid)); - assert_ne!(commit_hash, oid_to_sha1(&feature_oid)); - Ok(()) - } - #[test] - fn returns_error_if_no_main_or_master() -> Result<()> { - let test_repo = GitTestRepo::new("feature")?; - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - assert!(git_repo.get_main_or_master_branch().is_err()); - Ok(()) - } - } - - mod get_origin_url { - use super::*; - - #[test] - fn returns_origin_url() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.add_remote("origin", "https://localhost:1000")?; - let git_repo = Repo::from_path(&test_repo.dir)?; - assert_eq!(git_repo.get_origin_url()?, "https://localhost:1000"); - Ok(()) - } - } - mod get_checked_out_branch_name { - use super::*; - - #[test] - fn returns_checked_out_branch_name() -> Result<()> { - let test_repo = GitTestRepo::default(); - let _ = test_repo.populate()?; - // create feature branch - test_repo.create_branch("example-feature")?; - test_repo.checkout("example-feature")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert_eq!( - git_repo.get_checked_out_branch_name()?, - "example-feature".to_string() - ); - Ok(()) - } - } - - mod get_commits_ahead_behind { - use super::*; - mod returns_main { - use super::*; - - #[test] - fn when_on_same_commit_return_empty() -> Result<()> { - let test_repo = GitTestRepo::default(); - let oid = test_repo.populate()?; - // create feature branch - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let (ahead, behind) = - git_repo.get_commits_ahead_behind(&oid_to_sha1(&oid), &oid_to_sha1(&oid))?; - assert_eq!(ahead, vec![]); - assert_eq!(behind, vec![]); - Ok(()) - } - - #[test] - fn when_2_commit_behind() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch - test_repo.create_branch("feature")?; - let feature_oid = test_repo.checkout("feature")?; - // checkout main and add 2 commits - test_repo.checkout("main")?; - std::fs::write(test_repo.dir.join("t5.md"), "some content")?; - let behind_1_oid = test_repo.stage_and_commit("add t5.md")?; - std::fs::write(test_repo.dir.join("t6.md"), "some content")?; - let behind_2_oid = test_repo.stage_and_commit("add t6.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let (ahead, behind) = git_repo.get_commits_ahead_behind( - &oid_to_sha1(&behind_2_oid), - &oid_to_sha1(&feature_oid), - )?; - assert_eq!(ahead, vec![]); - assert_eq!( - behind, - vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid),], - ); - Ok(()) - } - - #[test] - fn when_2_commit_ahead() -> Result<()> { - let test_repo = GitTestRepo::default(); - let main_oid = test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let (ahead, behind) = git_repo.get_commits_ahead_behind( - &oid_to_sha1(&main_oid), - &oid_to_sha1(&ahead_2_oid), - )?; - assert_eq!( - ahead, - vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid),], - ); - assert_eq!(behind, vec![]); - Ok(()) - } - - #[test] - fn when_2_commit_ahead_and_2_commits_behind() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - // checkout main and add 2 commits - test_repo.checkout("main")?; - std::fs::write(test_repo.dir.join("t5.md"), "some content")?; - let behind_1_oid = test_repo.stage_and_commit("add t5.md")?; - std::fs::write(test_repo.dir.join("t6.md"), "some content")?; - let behind_2_oid = test_repo.stage_and_commit("add t6.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let (ahead, behind) = git_repo.get_commits_ahead_behind( - &oid_to_sha1(&behind_2_oid), - &oid_to_sha1(&ahead_2_oid), - )?; - assert_eq!( - ahead, - vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid)], - ); - assert_eq!( - behind, - vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid)], - ); - Ok(()) - } - } - } - - mod create_branch_at_commit { - use super::*; - #[test] - fn doesnt_error() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - - Ok(()) - } - - #[test] - fn branch_gets_created() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - - assert!(test_repo.checkout(branch_name).is_ok()); - Ok(()) - } - - #[test] - fn branch_created_with_correct_commit() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - - assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid); - Ok(()) - } - - mod when_branch_already_exists { - use super::*; - - #[test] - fn when_new_tip_specified_it_is_updated() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - - git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?; - assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid); - Ok(()) - } - - #[test] - fn when_same_tip_is_specified_it_doesnt_error() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid); - Ok(()) - } - - #[test] - fn when_branch_is_checkedout_new_tip_specified_it_is_updated() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - let branch_name = "test-name-1"; - git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?; - test_repo.checkout(branch_name)?; - git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?; - test_repo.checkout("main")?; - - assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid); - Ok(()) - } - } - } - - mod create_commit_from_patch { - - use test_utils::TEST_KEY_1_SIGNER; - - use super::*; - use crate::{repo_ref::RepoRef, sub_commands::send::generate_patch_event}; - - async fn generate_patch_from_head_commit(test_repo: &GitTestRepo) -> Result { - let original_oid = test_repo.git_repo.head()?.peel_to_commit()?.id(); - let git_repo = Repo::from_path(&test_repo.dir)?; - generate_patch_event( - &git_repo, - &git_repo.get_root_commit()?, - &oid_to_sha1(&original_oid), - Some(nostr::EventId::all_zeros()), - &TEST_KEY_1_SIGNER, - &RepoRef::try_from(generate_repo_ref_event()).unwrap(), - None, - None, - None, - &None, - &[], - ) - .await - } - fn test_patch_applies_to_repository(patch_event: nostr::Event) -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - println!("{:?}", &patch_event); - git_repo.create_commit_from_patch(&patch_event)?; - let commit_id = tag_value(&patch_event, "commit")?; - // does commit with id exist? - assert!(git_repo.does_commit_exist(&commit_id)?); - Ok(()) - } - - mod patch_created_as_commit_with_matching_id { - use test_utils::git::joe_signature; - - use super::*; - - #[tokio::test] - async fn simple_signature_author_committer_same_as_git_user_0_unixtime_no_pgp_signature() - -> Result<()> { - let source_repo = GitTestRepo::default(); - source_repo.populate()?; - fs::write(source_repo.dir.join("x1.md"), "some content")?; - source_repo.stage_and_commit("add x1.md")?; - - test_patch_applies_to_repository( - generate_patch_from_head_commit(&source_repo).await?, - ) - } - - #[tokio::test] - async fn signature_with_specific_author_time() -> Result<()> { - let source_repo = GitTestRepo::default(); - source_repo.populate()?; - fs::write(source_repo.dir.join("x1.md"), "some content")?; - source_repo.stage_and_commit_custom_signature( - "add x1.md", - Some(&git2::Signature::new( - joe_signature().name().unwrap(), - joe_signature().email().unwrap(), - &git2::Time::new(5000, 0), - )?), - None, - )?; - - test_patch_applies_to_repository( - generate_patch_from_head_commit(&source_repo).await?, - ) - } - - #[tokio::test] - async fn author_name_and_email_not_current_git_user() -> Result<()> { - let source_repo = GitTestRepo::default(); - source_repo.populate()?; - fs::write(source_repo.dir.join("x1.md"), "some content")?; - source_repo.stage_and_commit_custom_signature( - "add x1.md", - Some(&git2::Signature::new( - "carole", - "carole@pm.me", - &git2::Time::new(0, 0), - )?), - None, - )?; - - test_patch_applies_to_repository( - generate_patch_from_head_commit(&source_repo).await?, - ) - } - - #[tokio::test] - async fn comiiter_name_and_email_not_current_git_user_or_author() -> Result<()> { - let source_repo = GitTestRepo::default(); - source_repo.populate()?; - fs::write(source_repo.dir.join("x1.md"), "some content")?; - source_repo.stage_and_commit_custom_signature( - "add x1.md", - Some(&git2::Signature::new( - "carole", - "carole@pm.me", - &git2::Time::new(0, 0), - )?), - Some(&git2::Signature::new( - "bob", - "bob@pm.me", - &git2::Time::new(0, 0), - )?), - )?; - - test_patch_applies_to_repository( - generate_patch_from_head_commit(&source_repo).await?, - ) - } - - // TODO: pgp signature - - #[tokio::test] - async fn unique_author_and_commiter_details() -> Result<()> { - let source_repo = GitTestRepo::default(); - source_repo.populate()?; - fs::write(source_repo.dir.join("x1.md"), "some content")?; - source_repo.stage_and_commit_custom_signature( - "add x1.md", - Some(&git2::Signature::new( - "carole", - "carole@pm.me", - &git2::Time::new(5000, 0), - )?), - Some(&git2::Signature::new( - "bob", - "bob@pm.me", - &git2::Time::new(1000, 0), - )?), - )?; - - test_patch_applies_to_repository( - generate_patch_from_head_commit(&source_repo).await?, - ) - } - } - } - - mod apply_patch_chain { - use test_utils::TEST_KEY_1_SIGNER; - - use super::*; - use crate::{ - repo_ref::RepoRef, sub_commands::send::generate_cover_letter_and_patch_events, - }; - - static BRANCH_NAME: &str = "add-example-feature"; - // returns original_repo, cover_letter_event, patch_events - async fn generate_test_repo_and_events() - -> Result<(GitTestRepo, nostr::Event, Vec)> { - let original_repo = GitTestRepo::default(); - let oid3 = original_repo.populate_with_test_branch()?; - let oid2 = original_repo.git_repo.find_commit(oid3)?.parent_id(0)?; - let oid1 = original_repo.git_repo.find_commit(oid2)?.parent_id(0)?; - // TODO: generate cover_letter and patch events - let git_repo = Repo::from_path(&original_repo.dir)?; - - let mut events = generate_cover_letter_and_patch_events( - Some(("test".to_string(), "test".to_string())), - &git_repo, - &[oid_to_sha1(&oid1), oid_to_sha1(&oid2), oid_to_sha1(&oid3)], - &TEST_KEY_1_SIGNER, - &RepoRef::try_from(generate_repo_ref_event()).unwrap(), - &None, - &[], - ) - .await?; - - events.reverse(); - - Ok((original_repo, events.pop().unwrap(), events)) - } - - mod when_branch_and_commits_dont_exist { - use super::*; - - mod when_branch_root_is_tip_of_main { - use super::*; - - #[tokio::test] - async fn branch_gets_created_with_name_specified_in_proposal() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert!( - git_repo - .get_local_branch_names()? - .contains(&BRANCH_NAME.to_string()) - ); - Ok(()) - } - - #[tokio::test] - async fn branch_checked_out() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - git_repo.get_checked_out_branch_name()?, - BRANCH_NAME.to_string(), - ); - Ok(()) - } - - #[tokio::test] - async fn patches_get_created_as_commits() -> Result<()> { - let (original_repo, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - test_repo.git_repo.head()?.peel_to_commit()?.id(), - original_repo.git_repo.head()?.peel_to_commit()?.id(), - ); - Ok(()) - } - - #[tokio::test] - async fn branch_tip_is_most_recent_patch() -> Result<()> { - let (original_repo, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - git_repo.get_tip_of_branch(BRANCH_NAME)?, - oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), - ); - Ok(()) - } - - #[tokio::test] - async fn previously_checked_out_branch_tip_does_not_change() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let existing_branch = test_repo.get_checked_out_branch_name()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - let previous_tip_of_existing_branch = - git_repo.get_tip_of_branch(existing_branch.as_str())?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - previous_tip_of_existing_branch, - git_repo.get_tip_of_branch(existing_branch.as_str())?, - ); - Ok(()) - } - - #[tokio::test] - async fn returns_all_patches_applied() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 3); - Ok(()) - } - } - - mod when_branch_root_is_tip_behind_main { - use super::*; - - #[tokio::test] - async fn branch_gets_created_with_name_specified_in_proposal() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("m3.md"), "some content")?; - test_repo.stage_and_commit("add m3.md")?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert!( - git_repo - .get_local_branch_names()? - .contains(&BRANCH_NAME.to_string()) - ); - Ok(()) - } - - #[tokio::test] - async fn branch_checked_out() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("m3.md"), "some content")?; - test_repo.stage_and_commit("add m3.md")?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - git_repo.get_checked_out_branch_name()?, - BRANCH_NAME.to_string(), - ); - Ok(()) - } - - #[tokio::test] - async fn branch_tip_is_most_recent_patch() -> Result<()> { - let (original_repo, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("m3.md"), "some content")?; - test_repo.stage_and_commit("add m3.md")?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - git_repo.get_tip_of_branch(BRANCH_NAME)?, - oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), - ); - Ok(()) - } - - #[tokio::test] - async fn previously_checked_out_branch_tip_does_not_change() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - std::fs::write(test_repo.dir.join("m3.md"), "some content")?; - test_repo.stage_and_commit("add m3.md")?; - let existing_branch = test_repo.get_checked_out_branch_name()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - let previous_tip_of_existing_branch = - git_repo.get_tip_of_branch(existing_branch.as_str())?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!( - previous_tip_of_existing_branch, - git_repo.get_tip_of_branch(existing_branch.as_str())?, - ); - Ok(()) - } - - #[tokio::test] - async fn returns_all_patches_applied() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 3); - Ok(()) - } - } - - // TODO when_proposal_root_is_tip_ahead_of_main_and_doesnt_exist - } - - mod when_branch_and_first_commits_exists { - use super::*; - - mod when_branch_already_checked_out { - use super::*; - - #[tokio::test] - async fn branch_tip_is_most_recent_patch() -> Result<()> { - let (original_repo, _, mut patch_events) = - generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - - assert_eq!( - git_repo.get_tip_of_branch(BRANCH_NAME)?, - oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), - ); - Ok(()) - } - - #[tokio::test] - async fn returns_all_patches_applied() -> Result<()> { - let (_, _, mut patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 2); - Ok(()) - } - } - mod when_branch_not_checked_out { - use super::*; - - #[tokio::test] - async fn branch_tip_is_most_recent_patch() -> Result<()> { - let (original_repo, _, mut patch_events) = - generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; - git_repo.checkout("main")?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - - assert_eq!( - git_repo.get_tip_of_branch(BRANCH_NAME)?, - oid_to_sha1(&original_repo.git_repo.head()?.peel_to_commit()?.id(),), - ); - Ok(()) - } - - #[tokio::test] - async fn branch_checked_out() -> Result<()> { - let (_, _, mut patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; - git_repo.checkout("main")?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - - assert_eq!( - git_repo.get_checked_out_branch_name()?, - BRANCH_NAME.to_string(), - ); - Ok(()) - } - - #[tokio::test] - async fn returns_all_patches_applied() -> Result<()> { - let (_, _, mut patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, vec![patch_events.pop().unwrap()])?; - git_repo.checkout("main")?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 2); - Ok(()) - } - } - // TODO when branch ahead (rebased or user commits) - } - mod when_branch_exists_and_is_up_to_date { - use super::*; - - mod when_branch_already_checked_out { - use super::*; - - #[tokio::test] - async fn returns_all_patches_applied_0() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 0); - Ok(()) - } - } - mod when_branch_not_checked_out { - use super::*; - - #[tokio::test] - async fn branch_checked_out() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; - git_repo.checkout("main")?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - - assert_eq!( - git_repo.get_checked_out_branch_name()?, - BRANCH_NAME.to_string(), - ); - Ok(()) - } - - #[tokio::test] - async fn returns_all_patches_applied_0() -> Result<()> { - let (_, _, patch_events) = generate_test_repo_and_events().await?; - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - let git_repo = Repo::from_path(&test_repo.dir)?; - git_repo.apply_patch_chain(BRANCH_NAME, patch_events.clone())?; - git_repo.checkout("main")?; - let res = git_repo.apply_patch_chain(BRANCH_NAME, patch_events)?; - assert_eq!(res.len(), 0); - Ok(()) - } - } - } - } - mod parse_starting_commits { - use super::*; - - mod head_1_returns_latest_commit { - use super::*; - - #[test] - fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - test_repo.populate_with_test_branch()?; - test_repo.checkout("main")?; - - assert_eq!( - git_repo.parse_starting_commits("HEAD~1")?, - vec![str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?], - ); - Ok(()) - } - - #[test] - fn when_checked_out_branch_ahead_of_main() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - test_repo.populate_with_test_branch()?; - - assert_eq!( - git_repo.parse_starting_commits("HEAD~1")?, - vec![str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?], - ); - Ok(()) - } - } - mod head_2_returns_latest_2_commits_youngest_first { - use super::*; - - #[test] - fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - test_repo.populate_with_test_branch()?; - test_repo.checkout("main")?; - - assert_eq!( - git_repo.parse_starting_commits("HEAD~2")?, - vec![ - str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?, - str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?, - ], - ); - Ok(()) - } - } - mod head_3_returns_latest_3_commits_youngest_first { - use super::*; - - #[test] - fn when_checked_out_branch_ahead_of_main() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - test_repo.populate_with_test_branch()?; - - assert_eq!( - git_repo.parse_starting_commits("HEAD~3")?, - vec![ - str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?, - str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?, - str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?, - ], - ); - Ok(()) - } - } - mod range_of_3_commits_not_in_branch_history_returns_3_commits_youngest_first { - use super::*; - - #[test] - fn when_checked_out_branch_ahead_of_main() -> Result<()> { - let test_repo = GitTestRepo::default(); - let git_repo = Repo::from_path(&test_repo.dir)?; - test_repo.populate_with_test_branch()?; - test_repo.checkout("main")?; - - assert_eq!( - git_repo.parse_starting_commits("af474d8..a23e6b0")?, - vec![ - str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?, - str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?, - str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?, - ], - ); - Ok(()) - } - } - } - mod ancestor_of { - use super::*; - - #[test] - fn deep_ancestor_returns_true() -> Result<()> { - let test_repo = GitTestRepo::default(); - let from_main_in_feature_history = test_repo.populate()?; - - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(git_repo.ancestor_of( - &oid_to_sha1(&ahead_2_oid), - &oid_to_sha1(&from_main_in_feature_history) - )?); - Ok(()) - } - - #[test] - fn commit_parent_returns_true() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(git_repo.ancestor_of(&oid_to_sha1(&ahead_2_oid), &oid_to_sha1(&ahead_1_oid))?); - Ok(()) - } - - #[test] - fn same_commit_returns_false() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(!git_repo.ancestor_of(&oid_to_sha1(&ahead_2_oid), &oid_to_sha1(&ahead_2_oid))?); - Ok(()) - } - - #[test] - fn commit_not_in_history_returns_false() -> Result<()> { - let test_repo = GitTestRepo::default(); - test_repo.populate()?; - - // create feature branch and add 2 commits - test_repo.create_branch("feature")?; - - // create commit not in feature history - std::fs::write(test_repo.dir.join("notfeature.md"), "some content")?; - let on_main_after_feature = test_repo.stage_and_commit("add notfeature.md")?; - - test_repo.checkout("feature")?; - std::fs::write(test_repo.dir.join("t3.md"), "some content")?; - test_repo.stage_and_commit("add t3.md")?; - std::fs::write(test_repo.dir.join("t4.md"), "some content")?; - let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?; - - let git_repo = Repo::from_path(&test_repo.dir)?; - - assert!(!git_repo.ancestor_of( - &oid_to_sha1(&ahead_2_oid), - &oid_to_sha1(&on_main_after_feature) - )?); - Ok(()) - } - } - mod convert_clone_url_to_https { - use super::*; - - #[test] - fn test_https_url() { - let url = "https://github.com/user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_http_url() { - let url = "http://github.com/user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_http_url_with_credentials() { - let url = "http://username:password@github.com/user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_git_at_url() { - let url = "git@github.com:user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_user_at_url() { - let url = "user1@github.com:user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_ssh_url() { - let url = "ssh://github.com/user/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://github.com/user/repo.git"); - } - - #[test] - fn test_ftp_url() { - let url = "ftp://example.com/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://example.com/repo.git"); - } - - #[test] - fn test_git_protocol_url() { - let url = "git://example.com/repo.git"; - let result = convert_clone_url_to_https(url).unwrap(); - assert_eq!(result, "https://example.com/repo.git"); - } - - #[test] - fn test_invalid_url() { - let url = "unsupported://example.com/repo.git"; - let result = convert_clone_url_to_https(url); - assert!(result.is_err()); - } - } -} -- cgit v1.2.3