1use std::fmt::Debug;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::{GitCommitMeta, GitCredentials, GitRepo, GitRepoCloneRequest, GitRepoInfo};
6use git_url_parse::GitUrl;
7
8use git2::{Branch, Commit, Repository};
9
10use color_eyre::eyre::{eyre, Result};
11use tracing::debug;
12
13impl GitRepo {
14 pub fn open(path: PathBuf, branch: Option<String>, commit_id: Option<String>) -> Result<Self> {
19 let local_repo = Self::to_repository_from_path(path.clone())?;
21 let remote_url = GitRepoInfo::git_remote_from_repo(&local_repo)?;
22
23 let working_branch_name =
25 if let Ok(Some(git2_branch)) = GitRepoInfo::get_git2_branch(&local_repo, &branch) {
26 git2_branch.name()?.map(str::to_string)
27 } else {
28 None
30 };
31
32 if let Some(_c) = &commit_id {
34 if local_repo.is_shallow() {
35 return Err(eyre!("Can't open by commit on shallow clones"));
36 }
37 }
38
39 let commit = Self::get_git2_commit(&local_repo, &working_branch_name, &commit_id)?;
41
42 if let Some(url) = remote_url {
43 Ok(Self::new(url)?
44 .with_path(path)?
45 .with_branch(working_branch_name)
46 .with_git2_commit(commit))
47 } else {
48 let file_path = path.as_os_str().to_str().unwrap_or_default();
50 Ok(Self::new(file_path)?
51 .with_path(path)?
52 .with_branch(working_branch_name)
53 .with_git2_commit(commit))
54 }
55 }
56
57 pub fn with_path(mut self, path: PathBuf) -> Result<Self> {
59 self.path = if let Ok(p) = fs::canonicalize(path) {
61 Some(p)
62 } else {
63 return Err(eyre!("Directory was not found"));
64 };
65 Ok(self)
66 }
67
68 pub fn with_branch(mut self, branch: Option<String>) -> Self {
70 if let Some(b) = branch {
71 self.branch = Some(b);
72 }
73 self
74 }
75
76 pub fn with_commit(mut self, commit_id: Option<String>) -> Result<Self> {
78 self = if let Some(path) = self.path {
79 if let Ok(repo) = Self::open(path, self.branch, commit_id) {
80 repo
81 } else {
82 return Err(eyre!("Unable to open GitRepo with commit id"));
83 }
84 } else {
85 return Err(eyre!("No path to GitRepo set"));
86 };
87 Ok(self)
88 }
89
90 pub fn with_git2_commit(mut self, commit: Option<Commit>) -> Self {
92 match commit {
93 Some(c) => {
94 let commit_msg = c.message().unwrap_or_default().to_string();
95
96 let commit = GitCommitMeta::new(c.id())
97 .with_message(Some(commit_msg))
98 .with_timestamp(c.time().seconds());
99
100 self.head = Some(commit);
101 self
102 }
103 None => {
104 self.head = None;
105 self
106 }
107 }
108 }
109
110 pub fn with_credentials(mut self, creds: Option<GitCredentials>) -> Self {
113 self.credentials = creds;
114 self
115 }
116
117 pub fn new<S: AsRef<str>>(url: S) -> Result<Self> {
121 let url = if let Ok(url) = GitUrl::parse(url.as_ref()) {
122 url
123 } else {
124 return Err(eyre!("url failed to parse as GitUrl"));
125 };
126
127 Ok(Self {
128 url,
129 credentials: None,
130 head: None,
131 branch: None,
132 path: None,
133 })
134 }
135
136 pub fn to_clone(&self) -> GitRepoCloneRequest {
137 self.into()
138 }
139
140 pub fn to_info(&self) -> GitRepoInfo {
141 self.into()
142 }
143
144 pub fn to_repository(&self) -> Result<Repository> {
146 if let Some(path) = self.path.as_ref() {
147 Ok(Self::to_repository_from_path(path.as_os_str())?)
148 } else {
149 Err(eyre!("No path set to open"))
150 }
151 }
152
153 pub fn to_repository_from_path<P: AsRef<Path> + Debug>(path: P) -> Result<Repository> {
155 if let Ok(repo) = Repository::open(path.as_ref().as_os_str()) {
156 Ok(repo)
157 } else {
158 Err(eyre!("Failed to open repo at {path:#?}"))
159 }
160 }
161
162 fn get_git2_commit<'repo>(
165 r: &'repo Repository,
166 branch: &Option<String>,
167 commit_id: &Option<String>,
168 ) -> Result<Option<Commit<'repo>>> {
169 if let (None, None) = (branch, commit_id) {
171 if let Ok(commit) = r.head()?.peel_to_commit() {
175 return Ok(Some(commit));
176 } else {
177 return Err(eyre!(
178 "Unable to retrieve HEAD commit object from remote branch"
179 ));
180 }
181 }
182
183 match commit_id {
184 Some(id) => {
185 debug!("Commit provided. Using {}", id);
186 let commit = r.find_commit(git2::Oid::from_str(id)?)?;
187
188 Ok(Some(commit))
197 }
198
199 None => {
201 debug!("No commit provided. Attempting to use HEAD commit from remote branch");
202
203 if branch.is_some() {
204 if let Ok(Some(git2_branch)) = GitRepoInfo::get_git2_branch(r, branch) {
205 match git2_branch.upstream() {
206 Ok(upstream_branch) => {
207 let working_ref = upstream_branch.into_reference();
208
209 let commit = if let Ok(commit) = working_ref.peel_to_commit() {
210 commit
211 } else {
212 return Err(eyre!(
213 "Unable to retrieve HEAD commit object from remote branch"
214 ));
215 };
216
217 let _ = GitRepoInfo::is_commit_in_branch(
218 r,
219 &commit,
220 &Branch::wrap(working_ref),
221 );
222
223 Ok(Some(commit))
224 }
225 Err(_e) => {
227 debug!(
228 "No remote branch found. Using HEAD commit from local branch"
229 );
230 let working_ref = git2_branch.into_reference();
231
232 let commit = if let Ok(commit) = working_ref.peel_to_commit() {
233 commit
234 } else {
235 return Err(eyre!(
236 "Unable to retrieve HEAD commit object from remote branch"
237 ));
238 };
239
240 let _ = GitRepoInfo::is_commit_in_branch(
241 r,
242 &commit,
243 &Branch::wrap(working_ref),
244 );
245
246 Ok(Some(commit))
247 }
248 }
249 } else {
250 Ok(None)
252 }
253 } else {
254 unreachable!("We should have returned Err() early if both commit and branch not provided. We need one.")
255 }
256 }
257 }
258 }
259
260 pub fn is_shallow(&self) -> Result<bool> {
262 let repo = self.to_repository()?;
263 Ok(repo.is_shallow())
264 }
265}