1use crate::{
2 BranchHeads, GitCommitMeta, GitCredentials, GitRepo, GitRepoCloneRequest, GitRepoInfo,
3};
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use color_eyre::eyre::{eyre, Context, ContextCompat, Result};
9use git2::{Branch, BranchType, Commit, Cred, Oid, Repository};
10use mktemp::Temp;
11use tracing::debug;
12
13impl GitRepoInfo {
14 pub fn to_repo(&self) -> GitRepo {
15 self.into()
16 }
17
18 pub fn to_clone(&self) -> GitRepoCloneRequest {
19 self.into()
20 }
21
22 pub fn get_remote_name(&self, r: &git2::Repository) -> Result<String> {
25 let local_branch = r.head().and_then(|h| h.resolve())?;
26 let local_branch = local_branch.name();
27
28 if let Some(refname) = local_branch {
29 let upstream_remote = r.branch_upstream_remote(refname)?;
30
31 if let Some(name) = upstream_remote.as_str() {
32 Ok(name.to_string())
33 } else {
34 Err(eyre!("Upstream remote name not valid utf-8"))
35 }
36 } else {
37 Err(eyre!("Local branch name not valid utf-8"))
38 }
39 }
40
41 pub fn get_remote_branch_head_refs(
46 &self,
47 branch_filter: Option<Vec<String>>,
48 ) -> Result<BranchHeads> {
49 let temp_dir = if let Ok(temp_dir) = Temp::new_dir() {
51 temp_dir
52 } else {
53 return Err(eyre!("Unable to create temp directory"));
54 };
55
56 let repo = if let Some(p) = self.path.clone() {
59 GitRepo::to_repository_from_path(p)?
60 } else {
61 let clone: GitRepoCloneRequest = self.into();
64 clone
65 .git_clone_shallow(temp_dir.as_path())?
66 .to_repository()?
67 };
68
69 let cb = self.build_git2_remotecallback();
70
71 let remote_name = if let Ok(name) = self.get_remote_name(&repo) {
72 name
73 } else {
74 return Err(eyre!("Could not read remote name from git2::Repository"));
75 };
76
77 let mut remote = if let Ok(r) = repo.find_remote(&remote_name) {
78 r
79 } else if let Ok(anon_remote) = repo.remote_anonymous(&remote_name) {
80 anon_remote
81 } else {
82 return Err(eyre!(
83 "Could not create anonymous remote from: {:?}",
84 &remote_name
85 ));
86 };
87
88 let connection =
91 if let Ok(conn) = remote.connect_auth(git2::Direction::Fetch, Some(cb?), None) {
92 conn
93 } else {
94 return Err(eyre!("Unable to connect to git repo"));
95 };
96
97 let git_branch_ref_prefix = "refs/heads/";
98 let mut ref_map: HashMap<String, GitCommitMeta> = HashMap::new();
99
100 for git_ref in connection
101 .list()?
102 .iter()
103 .filter(|head| head.name().starts_with(git_branch_ref_prefix))
104 {
105 let branch_name = git_ref
106 .name()
107 .to_string()
108 .rsplit(git_branch_ref_prefix)
109 .collect::<Vec<&str>>()[0]
110 .to_string();
111
112 if let Some(ref branches) = branch_filter {
113 if branches.contains(&branch_name.to_string()) {
114 continue;
115 }
116 }
117
118 let commit = repo.find_commit(git_ref.oid())?;
120
121 let head_commit = GitCommitMeta::new(commit.id().as_bytes())
122 .with_timestamp(commit.time().seconds())
123 .with_message(commit.message().map(|m| m.to_string()));
124
125 ref_map.insert(branch_name, head_commit);
126 }
127
128 Ok(ref_map)
129 }
130
131 pub fn is_commit_in_branch<'repo>(
133 r: &'repo Repository,
134 commit: &Commit,
135 branch: &Branch,
136 ) -> Result<bool> {
137 let branch_head = branch.get().peel_to_commit();
138
139 if branch_head.is_err() {
140 return Ok(false);
141 }
142
143 let branch_head = branch_head.wrap_err("Unable to extract branch HEAD commit")?;
144 if branch_head.id() == commit.id() {
145 return Ok(true);
146 }
147
148 let check_commit_in_branch = r.graph_descendant_of(branch_head.id(), commit.id());
151 if check_commit_in_branch.is_err() {
154 return Ok(false);
155 }
156
157 check_commit_in_branch.wrap_err("Unable to determine if commit exists within branch")
158 }
159
160 pub fn get_git2_branch<'repo>(
163 r: &'repo Repository,
164 local_branch: &Option<String>,
165 ) -> Result<Option<Branch<'repo>>> {
166 match local_branch {
167 Some(branch) => {
168 if let Ok(git2_branch) = r.find_branch(branch, BranchType::Local) {
170 debug!("Returning given branch: {:?}", &git2_branch.name());
171 Ok(Some(git2_branch))
172 } else {
173 Ok(None)
175 }
176 }
177 None => {
178 let head = r.head();
180
181 let local_branch = Branch::wrap(head?);
183
184 debug!("Returning HEAD branch: {:?}", local_branch.name()?);
185
186 let maybe_local_branch_name = if let Ok(Some(name)) = local_branch.name() {
187 Some(name)
188 } else {
189 None
191 };
192
193 if let Some(local_branch_name) = maybe_local_branch_name {
194 match r.find_branch(local_branch_name, BranchType::Local) {
195 Ok(b) => Ok(Some(b)),
196 Err(_e) => Ok(None),
197 }
198 } else {
199 Ok(None)
200 }
201 }
202 }
203 }
204
205 pub fn remote_url_from_repository(r: &Repository) -> Result<Option<String>> {
209 let remote_name = GitRepoInfo::remote_name_from_repository(r)?;
211
212 if let Some(remote) = remote_name {
213 let remote_url: String = if let Some(url) = r.find_remote(&remote)?.url() {
214 url.chars().collect()
215 } else {
216 return Err(eyre!("Unable to extract repo url from remote"));
217 };
218
219 Ok(Some(remote_url))
220 } else {
221 Ok(None)
222 }
223 }
224
225 fn remote_name_from_repository(r: &Repository) -> Result<Option<String>> {
227 let local_branch = r.head().and_then(|h| h.resolve())?;
228 let local_branch_name = if let Some(name) = local_branch.name() {
229 name
230 } else {
231 return Err(eyre!("Local branch name is not valid utf-8"));
232 };
233
234 let upstream_remote_name_buf =
235 if let Ok(remote) = r.branch_upstream_remote(local_branch_name) {
236 Some(remote)
237 } else {
238 None
240 };
241
242 if let Some(remote) = upstream_remote_name_buf {
243 let remote_name = if let Some(name) = remote.as_str() {
244 Some(name.to_string())
245 } else {
246 return Err(eyre!("Remote name not valid utf-8"));
247 };
248
249 debug!("Remote name: {:?}", &remote_name);
250
251 Ok(remote_name)
252 } else {
253 Ok(None)
254 }
255 }
256
257 pub fn git_remote_from_path(path: &Path) -> Result<Option<String>> {
259 let r = GitRepo::to_repository_from_path(path)?;
260 GitRepoInfo::remote_url_from_repository(&r)
261 }
262
263 pub fn git_remote_from_repo(local_repo: &Repository) -> Result<Option<String>> {
265 GitRepoInfo::remote_url_from_repository(local_repo)
266 }
267
268 pub fn list_files_changed_between<S: AsRef<str>>(
270 &self,
271 commit1: S,
272 commit2: S,
273 ) -> Result<Option<Vec<PathBuf>>> {
274 let repo = self.to_repo();
275
276 let commit1 = self.expand_partial_commit_id(commit1.as_ref())?;
277 let commit2 = self.expand_partial_commit_id(commit2.as_ref())?;
278
279 let repo = repo.to_repository()?;
280
281 let oid1 = Oid::from_str(&commit1)?;
282 let oid2 = Oid::from_str(&commit2)?;
283
284 let git2_commit1 = repo.find_commit(oid1)?.tree()?;
285 let git2_commit2 = repo.find_commit(oid2)?.tree()?;
286
287 let diff = repo.diff_tree_to_tree(Some(&git2_commit1), Some(&git2_commit2), None)?;
288
289 let mut paths = Vec::new();
290
291 diff.print(git2::DiffFormat::NameOnly, |delta, _hunk, _line| {
292 let delta_path = if let Some(p) = delta.new_file().path() {
293 p
294 } else {
295 return false;
296 };
297
298 paths.push(delta_path.to_path_buf());
299 true
300 })
301 .wrap_err("File path not found in new commit to compare")?;
302
303 if !paths.is_empty() {
304 return Ok(Some(paths));
305 }
306
307 Ok(None)
308 }
309
310 pub fn list_files_changed_at<S: AsRef<str>>(&self, commit: S) -> Result<Option<Vec<PathBuf>>> {
312 let repo = self.to_repo();
313
314 let commit = self.expand_partial_commit_id(commit.as_ref())?;
315
316 let git2_repo = repo.to_repository()?;
317
318 let oid = Oid::from_str(&commit)?;
319 let git2_commit = git2_repo.find_commit(oid)?;
320
321 let mut changed_files = Vec::new();
322
323 for parent in git2_commit.parents() {
324 let parent_commit_id = hex::encode(parent.id().as_bytes());
325
326 if let Some(path_vec) = self.list_files_changed_between(&parent_commit_id, &commit)? {
327 for p in path_vec {
328 changed_files.push(p);
329 }
330 }
331 }
332
333 if !changed_files.is_empty() {
334 Ok(Some(changed_files))
335 } else {
336 Ok(None)
337 }
338 }
339
340 pub fn expand_partial_commit_id<S: AsRef<str>>(&self, partial_commit_id: S) -> Result<String> {
342 let repo: GitRepo = self.to_repo();
343
344 if partial_commit_id.as_ref().len() == 40 {
347 return Ok(partial_commit_id.as_ref().to_string());
348 }
349
350 if repo.to_repository()?.is_shallow() {
352 return Err(eyre!(
353 "No support for partial commit id expand on shallow clones"
354 ));
355 }
356
357 let repo = repo.to_repository()?;
358
359 let extended_commit = hex::encode(
360 repo.revparse_single(partial_commit_id.as_ref())?
361 .peel_to_commit()?
362 .id()
363 .as_bytes(),
364 );
365
366 Ok(extended_commit)
367 }
368
369 pub fn has_path_changed<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
373 let repo = self.to_repo();
374 let git2_repo = repo.to_repository().wrap_err("Could not open repo")?;
375
376 let head = git2_repo
379 .head()
380 .wrap_err("Could not get HEAD ref")?
381 .peel_to_commit()
382 .wrap_err("Could not convert to commit")?;
383 let head_commit_id = hex::encode(head.id().as_bytes());
384 for commit in head.parents() {
385 let parent_commit_id = hex::encode(commit.id().as_bytes());
386
387 if self.has_path_changed_between(&path, &head_commit_id, &parent_commit_id)? {
388 return Ok(true);
389 }
390 }
391
392 Ok(false)
393 }
394
395 pub fn has_path_changed_between<P: AsRef<Path>, S: AsRef<str>>(
399 &self,
400 path: P,
401 commit1: S,
402 commit2: S,
403 ) -> Result<bool> {
404 let commit1 = self
405 .expand_partial_commit_id(commit1.as_ref())
406 .wrap_err("Could not expand partial commit id for commit1")?;
407 let commit2 = self
408 .expand_partial_commit_id(commit2.as_ref())
409 .wrap_err("Could not expand partial commit id for commit2")?;
410
411 let changed_files = self
412 .list_files_changed_between(&commit1, &commit2)
413 .wrap_err("Error retrieving commit changes")?;
414
415 if let Some(files) = changed_files {
416 for f in files.iter() {
417 if f.to_str()
418 .wrap_err("Couldn't convert pathbuf to str")?
419 .starts_with(
420 &path
421 .as_ref()
422 .to_path_buf()
423 .to_str()
424 .wrap_err("Couldn't convert pathbuf to str")?,
425 )
426 {
427 return Ok(true);
428 }
429 }
430 }
431
432 Ok(false)
433 }
434
435 pub fn new_commits_exist(&self) -> Result<bool> {
437 let repo = if let Ok(gitrepo) = GitRepo::new(self.url.to_string()) {
439 let branch = if let Some(branch) = self.branch.clone() {
440 branch
441 } else {
442 return Err(eyre!("No branch set"));
443 };
444
445 gitrepo
446 .with_branch(Some(branch))
447 .with_credentials(self.credentials.clone())
448 } else {
449 return Err(eyre!("Could not crete new GitUrl"));
450 };
451
452 let tempdir = if let Ok(dir) = Temp::new_dir() {
453 dir
454 } else {
455 return Err(eyre!("Could not create temporary dir"));
456 };
457
458 let clone: GitRepoCloneRequest = repo.into();
460 let repo = if let Ok(gitrepo) = clone.git_clone_shallow(tempdir) {
461 gitrepo
462 } else {
463 return Err(eyre!("Could not shallow clone dir"));
464 };
465
466 Ok(self.head != repo.head)
468 }
469
470 pub fn build_git2_remotecallback(&self) -> Result<git2::RemoteCallbacks> {
473 if let Some(cred) = self.credentials.clone() {
474 debug!("Before building callback: {:?}", &cred);
475
476 match cred {
477 GitCredentials::SshKey {
478 username,
479 public_key,
480 private_key,
481 passphrase,
482 } => {
483 let mut cb = git2::RemoteCallbacks::new();
484
485 cb.credentials(
486 move |_, _, _| match (public_key.clone(), passphrase.clone()) {
487 (None, None) => {
488 let key = if let Ok(key) =
489 Cred::ssh_key(&username, None, private_key.as_path(), None)
490 {
491 key
492 } else {
493 return Err(git2::Error::from_str(
494 "Could not create credentials object for ssh key",
495 ));
496 };
497 Ok(key)
498 }
499 (None, Some(pp)) => {
500 let key = if let Ok(key) = Cred::ssh_key(
501 &username,
502 None,
503 private_key.as_path(),
504 Some(pp.as_ref()),
505 ) {
506 key
507 } else {
508 return Err(git2::Error::from_str(
509 "Could not create credentials object for ssh key",
510 ));
511 };
512 Ok(key)
513 }
514 (Some(pk), None) => {
515 let key = if let Ok(key) = Cred::ssh_key(
516 &username,
517 Some(pk.as_path()),
518 private_key.as_path(),
519 None,
520 ) {
521 key
522 } else {
523 return Err(git2::Error::from_str(
524 "Could not create credentials object for ssh key",
525 ));
526 };
527 Ok(key)
528 }
529 (Some(pk), Some(pp)) => {
530 let key = if let Ok(key) = Cred::ssh_key(
531 &username,
532 Some(pk.as_path()),
533 private_key.as_path(),
534 Some(pp.as_ref()),
535 ) {
536 key
537 } else {
538 return Err(git2::Error::from_str(
539 "Could not create credentials object for ssh key",
540 ));
541 };
542 Ok(key)
543 }
544 },
545 );
546
547 Ok(cb)
548 }
549 GitCredentials::UserPassPlaintext { username, password } => {
550 let mut cb = git2::RemoteCallbacks::new();
551 cb.credentials(move |_, _, _| {
552 Cred::userpass_plaintext(username.as_str(), password.as_str())
553 });
554
555 Ok(cb)
556 }
557 }
558 } else {
559 Ok(git2::RemoteCallbacks::new())
561 }
562 }
563}