autoschematic_core/
git_util.rs1use std::{
2 fs,
3 path::{Path, PathBuf},
4};
5
6use anyhow::{Context, bail};
7use git2::{
8 Cred, FetchOptions, IndexAddOption, PushOptions, RemoteCallbacks, Repository, Status, StatusOptions,
9 build::{CheckoutBuilder, RepoBuilder},
10};
11use secrecy::{ExposeSecret, SecretBox};
12
13pub async fn clone_repo(
14 owner: &str,
15 repo: &str,
16 path: &Path,
17 head_ref: &str,
18 token: &SecretBox<str>,
19) -> Result<Repository, anyhow::Error> {
20 let owner_path = path.join(owner);
21 fs::create_dir_all(&owner_path)?;
22
23 let repo_path = path.join(owner).join(repo);
24
25 let repo_url = format!("https://github.com/{owner}/{repo}.git");
26
27 let mut callbacks = RemoteCallbacks::new();
28 callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
29 Cred::userpass_plaintext("x-access-token", token.expose_secret())
33 });
34
35 let mut fetch_opts = FetchOptions::new();
36 fetch_opts.remote_callbacks(callbacks);
37 fetch_opts.depth(1);
38 let _checkout_opts = CheckoutBuilder::new();
39
40 let repository = match Repository::open(&repo_path) {
41 Ok(repository) => repository,
42 Err(_) => RepoBuilder::new()
43 .fetch_options(fetch_opts)
44 .branch(head_ref)
45 .clone(&repo_url, &repo_path)
46 .context("RepoBuilder::new()")?,
47 };
48
49 repository.remote_set_url("origin", &repo_url)?;
78
79 Ok(repository)
86}
87
88pub async fn checkout_new_branch(repo_path: &Path, branch_name: &str) -> Result<Repository, anyhow::Error> {
89 let Ok(repository) = Repository::open(repo_path) else {
90 bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
91 };
92
93 {
94 let branch = repository.branch(branch_name, &repository.head()?.peel_to_commit()?, false)?;
95
96 repository.checkout_tree(&branch.get().peel(git2::ObjectType::Tree)?, None)?;
98 repository.set_head(&format!("refs/heads/{branch_name}"))?;
99 }
100
101 Ok(repository)
102}
103
104pub async fn checkout_branch(repo_path: &Path, branch_name: &str) -> anyhow::Result<()> {
105 let Ok(repository) = Repository::open(repo_path) else {
106 bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
107 };
108
109 let (object, reference) = repository.revparse_ext(branch_name)?;
110
111 repository.checkout_tree(&object, None)?;
112
113 match reference {
114 Some(gref) => {
116 repository.set_head(gref.name().unwrap())?;
117 }
118 None => {
120 repository.set_head_detached(object.id())?;
121 }
122 }
123
124 Ok(())
125}
126
127pub fn git_add(repo_path: &Path, path: &Path) -> anyhow::Result<()> {
128 let Ok(repository) = Repository::open(repo_path) else {
134 bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
135 };
136
137 let mut index = repository.index()?;
138 index.add_all([path], IndexAddOption::default(), None)?;
139 index.write()?;
140 Ok(())
141}
142
143pub fn git_commit(repo_path: &Path, username: &str, email: &str, message: &str) -> anyhow::Result<()> {
144 let Ok(repository) = Repository::open(repo_path) else {
145 bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
146 };
147
148 let mut index = repository.index()?;
149 let oid = index.write_tree()?;
150 let parent_commit = repository.head()?.peel_to_commit()?;
151 let tree = repository.find_tree(oid)?;
152 let sig = git2::Signature::now(username, email)?;
153 repository.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent_commit])?;
154
155 Ok(())
156}
157
158pub fn git_commit_and_push(repo_path: &Path, head_ref: &str, token: &SecretBox<str>, message: &str) -> anyhow::Result<()> {
159 let Ok(repository) = Repository::open(repo_path) else {
160 bail!("No repository at {}", &repo_path.to_str().unwrap_or_default())
161 };
162
163 let mut index = repository.index()?;
164 let oid = index.write_tree()?;
165 let parent_commit = repository.head()?.peel_to_commit()?;
166 let tree = repository.find_tree(oid)?;
167 let sig = git2::Signature::now("autoschematic", "apply@autoschematic.sh")?;
168 repository.commit(Some("HEAD"), &sig, &sig, message, &tree, &[&parent_commit])?;
169
170 let mut remote = repository.find_remote("origin")?;
171
172 let refspec = format!("refs/heads/{head_ref}:refs/heads/{head_ref}");
173
174 let mut callbacks = RemoteCallbacks::new();
175 callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
176 Cred::userpass_plaintext("x-access-token", token.expose_secret())
180 });
181
182 let mut push_options = PushOptions::new();
183 push_options.remote_callbacks(callbacks);
184 remote.push::<&str>(&[&refspec], Some(&mut push_options))?;
185 Ok(())
186}
187
188pub fn pull_with_rebase(repo_path: &Path, branch_name: &str, token: &SecretBox<str>) -> Result<(), anyhow::Error> {
189 let Ok(repository) = Repository::open(repo_path) else {
190 bail!("No repository at {}", repo_path.to_str().unwrap_or_default())
191 };
192
193 let mut callbacks = RemoteCallbacks::new();
194 callbacks.credentials(move |_url, _username_from_url, _allowed_types| {
195 Cred::userpass_plaintext("x-access-token", token.expose_secret())
199 });
200
201 let mut fetch_opts = FetchOptions::new();
202 fetch_opts.remote_callbacks(callbacks);
203 let mut remote = repository.find_remote("origin")?;
206
207 remote.fetch(&[branch_name], Some(&mut fetch_opts), None)?;
208
209 let fetch_head = repository.find_reference("FETCH_HEAD")?;
210
211 let fetch_ref = repository.reference_to_annotated_commit(&fetch_head)?;
212
213 let branch_ref_name = format!("refs/heads/{branch_name}");
214 let mut branch_ref = repository.find_reference(&branch_ref_name)?;
215
216 let msg = format!("Fast-Forward: Setting {} to id: {}", branch_ref_name, fetch_ref.id());
217
218 branch_ref.set_target(fetch_ref.id(), &msg)?;
219
220 repository.set_head(&branch_ref_name)?;
221
222 repository.checkout_head(Some(
223 git2::build::CheckoutBuilder::default()
224 .force(),
228 ))?;
229
230 Ok(())
231}
232
233pub fn get_head_sha(repo_path: &Path) -> anyhow::Result<String> {
234 if let Ok(repository) = Repository::open(repo_path) {
235 let head = repository.head()?;
236 Ok(head.peel_to_commit()?.id().to_string())
237 } else {
238 bail!("No repository at {}", repo_path.to_str().unwrap_or_default())
239 }
240}
241
242pub fn get_staged_files() -> Result<Vec<PathBuf>, git2::Error> {
243 let repo = Repository::discover(".")?;
245
246 let mut status_opts = StatusOptions::new();
248 status_opts.show(git2::StatusShow::Index);
249 status_opts.include_untracked(false).renames_head_to_index(true);
250
251 let statuses = repo.statuses(Some(&mut status_opts))?;
253
254 let mut staged = Vec::new();
256 for entry in statuses.iter() {
257 let s = entry.status();
258
259 let is_staged = s.intersects(
260 Status::INDEX_NEW
261 | Status::INDEX_MODIFIED
262 | Status::INDEX_DELETED
263 | Status::INDEX_RENAMED
264 | Status::INDEX_TYPECHANGE,
265 );
266
267 if is_staged && let Some(path) = entry.path() {
268 staged.push(PathBuf::from(path));
269 }
270
271 if s.intersects(Status::INDEX_RENAMED)
272 && let Some(new_path) = entry.head_to_index().and_then(|d| d.new_file().path())
273 {
274 staged.push(PathBuf::from(new_path));
275 }
276 }
277 Ok(staged)
278}