1use ansi_term::Colour::*;
5use anyhow::anyhow;
6use anyhow::Context;
7use getset::{CopyGetters, Getters, Setters};
8use git2::IndexAddOption;
9use git2::{
10 build::RepoBuilder, Commit, Cred, CredentialType, Direction, FetchOptions, Index, ObjectType,
11 Oid, PushOptions, RemoteCallbacks, Repository, Signature, Tree,
12};
13use git2_credentials::CredentialHandler;
14use log::{error, trace};
15use serde::Deserialize;
16use std::fmt;
17use std::path::PathBuf;
18use thiserror::Error;
19mod pull;
20
21#[cfg(test)]
22mod tests;
23#[derive(Error, Debug)]
25pub enum CodexGitError {
26 #[error("git error")]
27 Git {
28 #[from]
29 source: git2::Error,
30 },
32 #[error("IO error")]
33 IO(#[from] std::io::Error),
34 #[error("RON error")]
35 Ron(#[from] ron::Error),
36 #[error(transparent)]
37 Other(#[from] anyhow::Error),
38 #[error("codex git error")]
39 CodexGit,
40 #[error("utf8 error")]
41 Utf8Error(std::str::Utf8Error),
42}
43pub type Result<T> = std::result::Result<T, CodexGitError>;
45pub type NullResult = Result<()>;
47
48macro_rules! git_trace {
50 () => { };
51 ($($arg:tt)*) => {
52 trace!("{} ({}:{})", Black.on(Cyan).paint(format!($($arg)*)), std::file!(), std::line!());
53 };
54}
55
56#[derive(Debug, Default, Clone, Setters, Deserialize)]
58#[getset(set = "pub")]
59pub struct SshKeys {
60 public: String,
62 private: String,
64}
65#[derive(Debug, Default, Clone, Deserialize)]
68pub struct User {
69 name: String,
70 email: String,
71}
72impl User {
73 pub fn new(name: &str, email: &str) -> Self {
74 Self {
75 name: name.to_string(),
76 email: email.to_string(),
77 }
78 }
79}
80
81#[derive(Clone, Setters, Default, Deserialize, Debug)]
83pub struct CodexRepoConfig {
84 #[getset(set = "pub")]
86 user: User,
87 #[getset(set = "pub")]
89 remote_url: String,
90 #[getset(set = "pub")]
92 path: PathBuf,
93 #[getset(set = "pub")]
95 #[serde(default, skip)]
96 auto_add: Vec<String>,
97 #[getset(set = "pub")]
99 #[serde(default, skip_serializing)]
100 ssh_keys: SshKeys,
101 #[serde(default)]
103 verbose: bool,
104}
105impl CodexRepoConfig {
106 pub fn repo_name(&self) -> Result<String> {
108 let parts = self.remote_url.split("/");
109 Ok(parts.last().ok_or(CodexGitError::CodexGit)?.to_string())
110 }
111 pub fn full_path(&self) -> Result<PathBuf> {
113 Ok(PathBuf::from(format!(
114 "{}/{}",
115 self.path.to_string_lossy(),
116 self.repo_name()?
117 )))
118 }
119 pub fn has_repository(&self) -> Result<bool> {
121 let repo_head = self.full_path()?;
122 if !repo_head.exists() {
123 git_trace!("repo does not exist {:?}", &repo_head);
124 Ok(false)
125 } else if !repo_head.is_dir() {
126 error!("repo is not dir {:?}", &repo_head);
127 Ok(false)
128 } else {
129 git_trace!("repo dir exists {:?}", &repo_head);
130 Ok(true)
131 }
132 }
133 pub fn delete_repo(&self) -> Result<()> {
135 std::fs::remove_dir_all(self.full_path()?)?;
136 Ok(())
137 }
138 pub fn clone_repo(&mut self) -> Result<CodexRepository> {
140 git_trace!("cloning repo {:?} to {:?}", &self.remote_url, &self.path);
141 let fetch_options = self.fetch_options()?;
142 let repo = RepoBuilder::new()
143 .bare(false)
144 .fetch_options(fetch_options)
145 .clone(&self.remote_url, &self.full_path()?)?;
146 git_trace!("repo cloned");
147 Ok(CodexRepository::new(repo, self))
148 }
149 pub fn open(&self) -> Result<CodexRepository> {
151 git_trace!("opening existing repo {:?}", &self.full_path()?);
152 let repo = Repository::open(self.full_path()?)?;
153 Ok(CodexRepository::new(repo, self))
155 }
156 fn fetch_options(&self) -> Result<FetchOptions> {
158 let mut fo = FetchOptions::new();
159 fo.remote_callbacks(self.callbacks()?);
160 Ok(fo)
161 }
162 fn callbacks(&self) -> Result<RemoteCallbacks> {
164 let mut cb = RemoteCallbacks::new();
165 let git_config = git2::Config::open_default()?;
166 let mut ch = CredentialHandler::new(git_config);
167 let mut try_count: i8 = 0;
168 const MAX_TRIES: i8 = 5;
169 cb.credentials(move |url, username, allowed| {
170 if allowed.contains(CredentialType::SSH_MEMORY) {
171 git_trace!("trying ssh memory credential");
172 let username = username.expect("no user name");
173 let cred_res = Cred::ssh_key_from_memory(
175 username,
176 Some(&self.ssh_keys.public),
177 &self.ssh_keys.private,
178 None,
179 );
180 git_trace!("try to find ssh memory credential");
181 match &cred_res {
182 Err(e) => {
183 error!("error found in credential from memory {:?}", e);
184 }
185 Ok(_cr) => {
186 }
187 }
188 return cred_res;
189 }
190 git_trace!("look for credential {:?} ({} tries)", allowed, try_count);
191 try_count += 1;
192 if try_count > MAX_TRIES {
193 error!("too many tries for ssh key");
194 std::panic::panic_any("too many ssh tries".to_string());
195 }
196 ch.try_next_credential(url, username, allowed)
197 });
198
199 if self.verbose {
201 cb.transfer_progress(|stats| {
202 if stats.received_objects() == stats.total_objects() {
203 git_trace!(
204 "Resolving deltas {}/{} ",
205 stats.indexed_deltas(),
206 stats.total_deltas()
207 );
208 } else if stats.total_objects() > 0 {
209 git_trace!(
210 "Received {}/{} objects ({}) in {} bytes ",
211 stats.received_objects(),
212 stats.total_objects(),
213 stats.indexed_objects(),
214 stats.received_bytes()
215 );
216 }
217 true
218 });
219 cb.sideband_progress(|msg| {
220 if msg.len() == 0 {
221 return true;
222 }
223 git_trace!(
224 "git: {}",
225 std::str::from_utf8(msg).unwrap_or_else(|err| {
226 error!("bad git utf8 message {:?}", &err);
227 "bad msg"
228 })
229 );
230 true
231 });
232 }
233 Ok(cb)
234 }
235}
236#[derive(Default, Getters, CopyGetters)]
238pub struct FetchStatus {
239 #[getset(get_copy = "pub")]
240 is_changed: bool,
241 #[getset(get = "pub")]
242 index: Option<Index>,
243}
244impl FetchStatus {
245 pub fn has_conflict(&self) -> bool {
247 if let Some(i) = &self.index {
248 i.has_conflicts()
249 } else {
250 false
251 }
252 }
253}
254impl fmt::Display for FetchStatus {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 write!(f, "status (")?;
257 if let Some(i) = &self.index {
258 write!(f, "{} entries", i.len())?;
259 if self.has_conflict() {
260 write!(
261 f,
262 " {} conflicts",
263 i.conflicts().expect("bad conflicts").count()
264 )?;
265 }
266 }
267 write!(f, ")")
268 }
269}
270impl fmt::Debug for FetchStatus {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 let ix_sz = if let Some(i) = &self.index {
273 i.len()
274 } else {
275 0
276 };
277 let mut cc = Vec::<String>::new();
278 let confict_msg = if self.has_conflict() {
279 if let Some(ix) = &self.index {
280 for conflict in ix.conflicts().expect("bad conflicts") {
281 if let Ok(c) = conflict {
282 let p = if let Some(our) = c.our {
283 std::str::from_utf8(&our.path).expect("bad utf").to_string()
284 } else {
285 "?".to_string()
286 };
287 cc.push(p);
288 }
289 }
290 }
291 format!("conflicts [{}]", cc.join(" "))
292 } else {
293 "".to_string()
294 };
295 write!(
296 f,
297 "{} {} {} changes",
298 &confict_msg,
299 if self.is_changed {
300 "changed"
301 } else {
302 "unchanged"
303 },
304 &ix_sz
305 )?;
306 if f.alternate() {
307 if let Some(ix) = &self.index {
308 write!(f, " [")?;
309 for ie in ix.iter() {
310 write!(f, "{} ", std::str::from_utf8(&ie.path).expect("bad utf"))?;
311 }
312 write!(f, "]")?;
313 }
314 }
315 Ok(())
316 }
317}
318pub struct CodexRepository {
320 repo: Repository,
322 config: CodexRepoConfig,
324 needs_commit: bool,
326 needs_push: bool,
328 added: Vec<String>,
330}
331impl fmt::Display for CodexRepository {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 write!(
334 f,
335 "{} commit, {} push",
336 if self.needs_commit {
337 "needs"
338 } else {
339 "does not need"
340 },
341 if self.needs_push {
342 "needs"
343 } else {
344 "does not need"
345 }
346 )
347 }
348}
349impl Drop for CodexRepository {
350 fn drop(&mut self) {
351 git_trace!("at end (dropping repo), committing and pushing repo if required");
352 self.commit_and_push().unwrap_or_else(|err| {
353 error!("drop error: {:?}", &err);
354 panic!("drop error")
355 });
356 }
358}
359impl fmt::Debug for CodexRepository {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 write!(f, "(CodexRepository)",)
362 }
363}
364impl CodexRepository {
365 pub fn new(repo: Repository, config: &CodexRepoConfig) -> Self {
367 Self {
368 repo,
369 config: config.clone(),
370 needs_commit: false,
371 needs_push: false,
372 added: vec![],
373 }
374 }
375 pub fn fetch(&mut self) -> Result<()> {
377 let remote_name = "origin";
378 let remote_branch = "main";
379 let mut remote = self.repo.find_remote(remote_name)?;
381 let fetch_commit = pull::do_fetch(
382 &self.repo,
383 &[remote_branch],
384 &mut remote,
385 self.config.callbacks()?,
386 )?;
387 pull::do_merge(&self.repo, &remote_branch, fetch_commit)?;
388 Ok(())
389 }
390 pub fn commit_and_push(&mut self) -> Result<()> {
392 self.commit().context(format!(
393 "error in commit ({} commit)",
394 if self.needs_commit {
395 "needs"
396 } else {
397 "does not need"
398 }
399 ))?;
400 self.push(false).context(format!(
401 "error in push ({} push)",
402 if self.needs_push {
403 "needs"
404 } else {
405 "does not need"
406 }
407 ))?;
408 Ok(())
409 }
410 pub fn commit(&mut self) -> NullResult {
412 if !self.needs_commit {
413 git_trace!("no changes, do not need commit");
414 return Ok(());
415 }
416 git_trace!("adding all from: {:?}", self.config.auto_add);
418 let mut index = self.repo.index().context("cannot get the Index file")?;
419 let mut paths = vec![];
420 index.add_all(
421 self.config.auto_add.iter(),
422 IndexAddOption::DEFAULT,
423 Some(&mut |path, spec| {
424 paths.push(format!("{:?}", &path));
425 git_trace!(
426 "adding for commit {:?} for {}",
427 &path,
428 std::str::from_utf8(spec).unwrap()
429 );
430 0
431 }),
432 )?;
433 index.write().context("writing index for commit")?;
434 {
436 let tree = self.repo.find_tree(self.repo.index()?.write_tree()?)?;
437 let our_commit = self.our_commit()?;
438 let _oid = self.write_commit(
439 tree,
440 &format!(
441 "commit changes {} {}",
442 paths.join(" "),
443 self.added.join(" ")
444 ),
445 &[&our_commit],
446 )?;
447 }
448 self.added.clear();
449 self.needs_commit = false;
450 self.needs_push = true;
451 Ok(())
453 }
454 fn write_commit(
456 &self,
457 new_tree: Tree<'_>,
458 message: &str,
459 parent_commits: &[&Commit<'_>],
460 ) -> Result<Oid> {
461 let update_ref = if parent_commits.len() > 0 {
462 Some("HEAD")
463 } else {
464 None
465 };
466 let user = Signature::now(&self.config.user.name, &self.config.user.email)?;
467 let commit_oid = self.repo.commit(
468 update_ref, &user, &user, message, &new_tree, parent_commits, )?;
475 Ok(commit_oid)
476 }
477 fn our_commit(&self) -> Result<Commit<'_>> {
479 Ok(self.last_commit()?.ok_or_else(|| (anyhow!("no commit")))?)
480 }
481 fn last_commit(&self) -> Result<Option<Commit>> {
483 let head = self.repo.head()?.resolve()?.peel(ObjectType::Commit)?;
484 Ok(Some(
485 head.into_commit().map_err(|_e| anyhow!("not a commit"))?,
486 ))
487 }
488 pub fn add(&mut self, path: PathBuf) -> NullResult {
490 git_trace!("adding {:?}", &path);
491 self.repo.index()?.add_path(&path)?;
492 self.needs_commit = true;
493 self.added.push(path.to_string_lossy().to_string());
494 Ok(())
495 }
496 pub fn push(&mut self, force: bool) -> NullResult {
498 if !self.needs_push {
499 git_trace!("no commits, do not need push");
500 return Ok(());
501 }
502 git_trace!("pushing to remote");
503 let mut remote = self.repo.find_remote("origin")?;
504 let cb = self.config.callbacks()?;
505 remote.connect_auth(Direction::Push, Some(cb), None)?;
506 let mut push_options = PushOptions::new();
507 let cb = self.config.callbacks()?;
508 push_options.remote_callbacks(cb);
509 let force_marker = if force { "+" } else { "" };
510 let refspec = format!(
511 "{}refs/heads/{}:refs/heads/{}",
512 force_marker, "main", "main"
513 );
514 remote.push(&[refspec.as_str()], Some(&mut push_options))?;
515 self.needs_push = false;
516 git_trace!("pushed");
517 Ok(())
518 }
519}
520