1use std::io;
8use std::path::Path;
9
10use anyhow::{bail, format_err, Context};
11use git2::build::RepoBuilder;
12use git2::{
13 Cred, CredentialType, FetchOptions, RemoteCallbacks, Repository, RepositoryInitOptions,
14};
15use url::Url;
16use walkdir::WalkDir;
17
18fn make_credentials_callback(
23 access_token: Option<String>,
24 config: &git2::Config,
25) -> impl (FnMut(&str, Option<&str>, CredentialType) -> Result<Cred, git2::Error>) + '_ {
26 let mut cred_helper_tried = false;
27 let mut token_tried = false;
28
29 move |url, username, allowed_types| {
30 if allowed_types.contains(CredentialType::USER_PASS_PLAINTEXT) {
31 if let Some(token) = &access_token {
32 if !token_tried {
33 token_tried = true;
34 return Cred::userpass_plaintext(&token, "");
35 }
36 } else {
37 if !cred_helper_tried {
38 cred_helper_tried = true;
39 return Cred::credential_helper(config, url, username);
40 }
41 }
42 }
43
44 if allowed_types.contains(CredentialType::DEFAULT) {
45 return Cred::default();
46 }
47
48 Err(git2::Error::from_str("no authentication available"))
49 }
50}
51
52pub fn init_test_repo(path: &Path) -> anyhow::Result<()> {
56 if path.join(".git").exists() {
58 fs_err::remove_dir_all(path.join(".git"))?;
59 }
60
61 let repository = Repository::init_opts(
62 path,
63 RepositoryInitOptions::new().initial_head("refs/heads/main"),
64 )?;
65 let mut git_index = repository.index()?;
66
67 for entry in WalkDir::new(path).min_depth(1) {
68 let entry = entry?;
69 let relative_path = entry.path().strip_prefix(path)?;
70
71 if !relative_path.starts_with(".git") && entry.file_type().is_file() {
72 git_index.add_path(relative_path)?;
74 }
75 }
76
77 let sig = git2::Signature::now("PackageUser", "PackageUser@localhost")?;
79 let tree = repository.find_tree(git_index.write_tree()?)?;
80
81 repository.commit(
82 Some("HEAD"),
83 &sig,
84 &sig,
85 "Commit all files in test repo",
86 &tree,
87 &[],
88 )?;
89
90 Ok(())
91}
92
93pub fn open_or_clone(
94 access_token: Option<String>,
95 url: &Url,
96 path: &Path,
97) -> anyhow::Result<Repository> {
98 let repo = match Repository::open(path) {
99 Ok(repo) => repo,
100 Err(_) => {
101 if let Err(err) = fs_err::remove_dir_all(path) {
102 if err.kind() != io::ErrorKind::NotFound {
106 return Err(err.into());
107 }
108 }
109
110 fs_err::create_dir_all(path)?;
111 clone(access_token, url, path)
112 .with_context(|| format!("Error cloning Git repository {}", url))?
113 }
114 };
115
116 Ok(repo)
117}
118
119pub fn clone(access_token: Option<String>, url: &Url, into: &Path) -> anyhow::Result<Repository> {
120 let git_config = git2::Config::open_default()?;
121
122 let mut callbacks = RemoteCallbacks::new();
123 callbacks.credentials(make_credentials_callback(access_token, &git_config));
124
125 let mut fetch_options = FetchOptions::new();
126 fetch_options.remote_callbacks(callbacks);
127
128 let mut builder = RepoBuilder::new();
129 builder.fetch_options(fetch_options);
130
131 let repo = builder.clone(url.as_str(), into)?;
132 Ok(repo)
133}
134
135pub fn commit_and_push(
136 repository: &Repository,
137 access_token: Option<String>,
138 message: &str,
139 index_path: &Path,
140 modified_file: &Path,
141) -> anyhow::Result<()> {
142 let git_config = git2::Config::open_default()?;
143
144 let relative_path = modified_file.strip_prefix(&index_path).with_context(|| {
146 format!(
147 "Path {} was not relative to package path {}",
148 modified_file.display(),
149 index_path.display()
150 )
151 })?;
152
153 let mut index = repository.index()?;
155 index.add_path(relative_path)?;
156 index.write()?;
157 let tree_id = index.write_tree()?;
158 let tree = repository.find_tree(tree_id)?;
159
160 let head = repository.head()?;
162 let parent = repository.find_commit(head.target().unwrap())?;
163 let sig = git2::Signature::now("PackageUser", "PackageUser@localhost")?;
164 repository.commit(Some("HEAD"), &sig, &sig, &message, &tree, &[&parent])?;
165
166 let mut ref_status = Ok(());
168 let mut callback_called = false;
169 {
170 let mut origin = repository.find_remote("origin")?;
171 let mut callbacks = RemoteCallbacks::new();
172 callbacks.credentials(make_credentials_callback(access_token, &git_config));
173 callbacks.push_update_reference(|refname, status| {
174 assert_eq!(refname, "refs/heads/main");
175 if let Some(s) = status {
176 ref_status = Err(format_err!("failed to push a ref: {}", s))
177 }
178 callback_called = true;
179 Ok(())
180 });
181 let mut opts = git2::PushOptions::new();
182 opts.remote_callbacks(callbacks);
183 origin.push(&["refs/heads/main"], Some(&mut opts))?;
184 }
185
186 if !callback_called {
187 bail!("update_reference callback was not called");
188 }
189
190 ref_status
191}
192
193pub fn update_index(access_token: Option<String>, repository: &Repository) -> anyhow::Result<()> {
194 let git_config = git2::Config::open_default()?;
195
196 let mut callbacks = RemoteCallbacks::new();
197 callbacks.credentials(make_credentials_callback(access_token, &git_config));
198
199 let mut fetch_options = FetchOptions::new();
200 fetch_options.remote_callbacks(callbacks);
201
202 repository
203 .find_remote("origin")?
204 .fetch(&["main"], Some(&mut fetch_options), None)
205 .with_context(|| format!("could not fetch Git repository"))?;
206
207 let mut options = git2::build::CheckoutBuilder::new();
208 options.force();
209
210 let commit = repository.find_reference("FETCH_HEAD")?.peel_to_commit()?;
212 repository
213 .reset(
214 &commit.into_object(),
215 git2::ResetType::Hard,
216 Some(&mut options),
217 )
218 .with_context(|| format!("could not reset git repo to fetch_head"))?;
219
220 Ok(())
221}