summaryrefslogtreecommitdiff
path: root/crates/git-tracker/src/tracker.rs
blob: e6a3f541352d407cfe852538843b7d74776f44d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
use std::path::Path;

use git2::{Branch, BranchType, Commit, ErrorCode, Oid, Reference, Repository};

/// Helper struct for tracking Git objects
pub struct Tracker {
	repository: Repository,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
	#[error("libgit2 error")]
	Git(#[from] git2::Error),
	#[error("Repository path not found at `{0}`")]
	RepositoryPathNotFound(String),
}

impl Tracker {
	/// Create a new [`Tracker`] using the repository at [`path`]
	///
	/// # Errors
	///
	/// Will return [`Err`] if the repository can not be opened
	pub fn from_path(path: &str) -> Result<Self, Error> {
		let repository_path = Path::new(path);
		if repository_path.exists() {
			let repository = Repository::open(repository_path)?;
			Ok(Self { repository })
		} else {
			Err(Error::RepositoryPathNotFound(path.to_string()))
		}
	}

	/// Finds a branch of name [`name`]
	///
	/// # Errors
	///
	/// Will return [`Err`] if the branch cannot be found locally
	pub fn branch_by_name(&self, name: &str) -> Result<Branch, Error> {
		Ok(self.repository.find_branch(name, BranchType::Remote)?)
	}

	/// Finds a commit with a SHA match [`sha`]
	///
	/// # Errors
	///
	/// Will return [`Err`] if [`sha`] cannot be converted an [`Oid`] or
	/// a commit matching it cannot be found
	pub fn commit_by_sha(&self, sha: &str) -> Result<Commit, Error> {
		let oid = Oid::from_str(sha)?;
		let commit = self.repository.find_commit(oid)?;

		Ok(commit)
	}

	/// Check if [`Reference`] [`ref`] contains [`Commit`] [`commit`]
	///
	/// # Errors
	///
	/// Will return [`Err`] if the reference cannot be resolved to a commit or the descendants
	/// of the reference cannot be resolved
	pub fn ref_contains_commit(
		&self,
		reference: &Reference,
		commit: &Commit,
	) -> Result<bool, Error> {
		let head = reference.peel_to_commit()?;

		// NOTE: we have to check this as `Repository::graph_descendant_of()` (like the name says)
		// only finds *descendants* of it's parent commit, and will not tell us if the parent commit
		// *is* the child commit. i have no idea why i didn't think of this, but that's why this
		// comment is here now
		let is_head = head.id() == commit.id();

		let has_commit = self
			.repository
			.graph_descendant_of(head.id(), commit.id())?;

		Ok(is_head || has_commit)
	}

	/// Check if a [`Branch`] named [`branch_name`] has a commit with the SHA [`commit_sha`]
	///
	/// # Errors
	///
	/// Will return [`Err`] if the commit SHA cannot be resolved to an object id, the branch name cannot
	/// be resolved to a branch, or the descendants of the resolved branch cannot be resolved
	pub fn branch_contains_sha(&self, branch_name: &str, commit_sha: &str) -> Result<bool, Error> {
		let commit = match self.commit_by_sha(commit_sha) {
			Ok(commit) => commit,
			Err(why) => {
				// NOTE: we assume commits not found are just not in the branch *yet*, not an error
				// this is because github decides to report merge commit shas for unmerged PRs...yeah
				if let Error::Git(git_error) = &why {
					if git_error.code() == ErrorCode::NotFound {
						return Ok(false);
					}
				}

				return Err(why);
			}
		};

		let branch = self.branch_by_name(branch_name)?;
		let has_pr = self.ref_contains_commit(&branch.into_reference(), &commit)?;

		Ok(has_pr)
	}
}