summaryrefslogtreecommitdiff
path: root/src/model/repo.rs
blob: 557efb46223831a28679ed72081fd7e0ad3d135e (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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Definition of an iris [repo][Repository].

use crate::{error, Result};

use log::{debug, trace};
use std::{path::PathBuf, sync::OnceLock};
use url::Url;

/// A handle to a locally stored git repository.
pub struct Repository {
	/// The remote url that the git repository is located at.
	pub url: Url,
	/// The path on disk that the this repository is/will be located at.
	pub path: PathBuf,
	/// The git2 lib handle to the repo
	inner: OnceLock<git2::Repository>,
}

impl Repository {
	/// Creates a new handle to a local git repository.
	pub const fn new(url: Url, path: PathBuf) -> Self {
		Self {
			url,
			path,
			inner: OnceLock::new(),
		}
	}

	/// Gets the handle to the local repository
	fn repo(&self) -> Result<&git2::Repository> {
		// we only need to initalize once
		if let Some(repo) = self.inner.get() {
			return Ok(repo);
		}

		trace!("loading repo");

		// repository not loaded, try loading it
		let repo = if self.path.exists() {
			// repo exists on file system, just open it
			git2::Repository::open(&self.path)?
		} else {
			// repo needs to be cloned
			git2::Repository::clone(self.url.as_str(), &self.path)?
		};

		// save and return repo
		Ok(self.inner.get_or_init(|| repo))
	}

	/// Gets the current remote in the git repository
	fn remote(&self) -> Result<git2::Remote> {
		let repo = self.repo()?;
		let remote_name = "origin";
		let mut remote = repo.find_remote(remote_name)?;

		// make sure that out remote is connected before
		// we do anything with it. otherwise things will
		// fail!
		if !remote.connected() {
			trace!("connecting to remote");
			remote.connect(git2::Direction::Fetch)?;
		}

		Ok(remote)
	}

	/// Fetches the latest commits in the provided branch.
	pub fn fetch(&self, branch: &str) -> Result<()> {
		let mut remote = self.remote()?;
		trace!("fetching '{branch}");
		remote.fetch(&[branch], None, None)?;
		Ok(())
	}

	/// Returns the default branch of the git repositroy.
	pub fn default_branch(&self) -> Result<String> {
		let remote = self.remote()?;
		let buf = remote.default_branch()?;
		let name = buf
			.as_str()
			.and_then(|s| s.strip_prefix("refs/heads/"))
			.map(String::from)
			.ok_or_else(|| error::error!("cannot get default branch"))?;
		debug!("default branch is '{name}'");
		Ok(name)
	}

	/// Returns the latest fetched commit in `branch`.
	pub fn latest_commit(&self, branch: &str) -> Result<String> {
		let repo = self.repo()?;
		let refr_name = format!("refs/remotes/origin/{branch}");
		let refr = repo.find_reference(&refr_name)?;
		let commit = refr.peel_to_commit()?;
		let oid = commit.id().to_string();
		Ok(oid)
	}

	/// Checks out the local repository to the given commit and detaches
	/// HEAD to that commit.
	pub fn checkout(&self, commit: &str) -> Result<()> {
		let repo = self.repo()?;
		trace!("checkout to '{commit}'");
		let oid = git2::Oid::from_str(commit)?;
		let commit = repo.find_commit(oid)?;
		repo.reset(commit.as_object(), git2::ResetType::Hard, None)?;
		Ok(())
	}
}

impl std::fmt::Debug for Repository {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		write!(f, "_")
	}
}

impl PartialEq for Repository {
    fn eq(&self, other: &Self) -> bool {
        self.url.eq(&other.url) && self.path.eq(&other.path)
    }
}

impl Eq for Repository {}