codeberg_cli/types/
git.rs1use std::path::PathBuf;
2use std::str::FromStr;
3
4use anyhow::Context;
5use git2::{Remote, Repository};
6
7#[derive(Debug, Clone)]
8pub struct OwnerRepo {
9 pub owner: String,
10 pub repo: String,
11}
12
13impl FromStr for OwnerRepo {
14 type Err = anyhow::Error;
15
16 fn from_str(s: &str) -> Result<Self, Self::Err> {
17 let (owner, repo) = s.split_once('/').ok_or_else(|| {
18 anyhow::anyhow!("Please provide the repository in the format OWNER/REPO.")
19 })?;
20 Ok(OwnerRepo {
21 owner: owner.to_owned(),
22 repo: repo.to_owned(),
23 })
24 }
25}
26
27pub struct Git {
28 pub repo: Option<Repository>,
29}
30
31impl Default for Git {
32 fn default() -> Self {
33 Self {
34 repo: Repository::discover("./.").ok(),
35 }
36 }
37}
38
39impl Git {
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn origin(&self) -> Option<Remote<'_>> {
46 self.repo.as_ref().and_then(|repo| {
47 let remotes_list = repo.remotes().ok()?;
48 remotes_list
49 .into_iter()
50 .flatten()
51 .find_map(|remote| repo.find_remote(remote).ok())
52 })
53 }
54
55 pub fn remotes(&self) -> anyhow::Result<Vec<Remote<'_>>> {
56 let repo = self
57 .repo
58 .as_ref()
59 .context("No repository found in the current path even though one is needed!")?;
60 let remotes = repo
61 .remotes()?
62 .into_iter()
63 .filter_map(|remote| {
64 let remote = remote?;
65 let remote = repo.find_remote(remote).ok()?;
66 Some(remote)
67 })
68 .collect::<Vec<_>>();
69 Ok(remotes)
70 }
71
72 pub fn owner_repo(&self) -> anyhow::Result<OwnerRepo> {
73 let remotes = self
74 .remotes()?
75 .into_iter()
76 .filter_map(|remote| {
77 let mut git_url = remote.url().map(PathBuf::from)?;
78 let repo = git_url
83 .file_name()?
84 .to_str()?
85 .trim_end_matches(".git")
86 .to_owned();
87 git_url.pop();
88
89 let https_owner = git_url
90 .file_name()?
91 .to_str()
92 .filter(|owner| !owner.contains(":"))
93 .map(|owner| owner.to_owned());
94 let ssh_owner = git_url
95 .to_str()?
96 .split_once(":")
97 .map(|(_junk, owner)| owner.to_owned());
98 let remote_name = remote.name()?.to_owned();
99 Some((remote_name, https_owner.or(ssh_owner)?, repo))
100 })
101 .inspect(|x| tracing::debug!("{x:?}"))
102 .collect::<Vec<_>>();
103
104 remotes
105 .iter()
106 .find_map(|(name, owner, repo)| (*name == "origin").then_some((owner, repo)))
107 .or_else(|| remotes.first().map(|(_, owner, repo)| (owner, repo)))
108 .map(|(owner, repo)| OwnerRepo {
109 owner: owner.clone(),
110 repo: repo.clone(),
111 })
112 .context("Couldn't find owner and repo")
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_owner_repo_valid_parsing() {
122 let result = OwnerRepo::from_str("owner/repo").unwrap();
123 assert_eq!(result.owner, "owner");
124 assert_eq!(result.repo, "repo");
125 }
126
127 #[test]
128 fn test_owner_repo_with_hyphens() {
129 let result = OwnerRepo::from_str("my-owner/my-repo").unwrap();
130 assert_eq!(result.owner, "my-owner");
131 assert_eq!(result.repo, "my-repo");
132 }
133
134 #[test]
135 fn test_owner_repo_with_underscores() {
136 let result = OwnerRepo::from_str("my_owner/my_repo").unwrap();
137 assert_eq!(result.owner, "my_owner");
138 assert_eq!(result.repo, "my_repo");
139 }
140
141 #[test]
142 fn test_owner_repo_invalid_no_slash() {
143 let result = OwnerRepo::from_str("invalid");
144 assert!(result.is_err());
145 assert_eq!(
146 result.unwrap_err().to_string(),
147 "Please provide the repository in the format OWNER/REPO."
148 );
149 }
150
151 #[test]
152 fn test_owner_repo_invalid_multiple_slashes() {
153 let result = OwnerRepo::from_str("owner/repo/extra").unwrap();
154 assert_eq!(result.owner, "owner");
156 assert_eq!(result.repo, "repo/extra");
157 }
158
159 #[test]
160 fn test_owner_repo_empty_owner() {
161 let result = OwnerRepo::from_str("/repo").unwrap();
162 assert_eq!(result.owner, "");
163 assert_eq!(result.repo, "repo");
164 }
165
166 #[test]
167 fn test_owner_repo_empty_repo() {
168 let result = OwnerRepo::from_str("owner/").unwrap();
169 assert_eq!(result.owner, "owner");
170 assert_eq!(result.repo, "");
171 }
172}