aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Audron <audron@cocaine.farm>2024-03-27 12:57:30 +0100
committerMax Audron <audron@cocaine.farm>2024-03-27 12:57:30 +0100
commit3a88527328952ddffef0bf228f0832e81fcfdf19 (patch)
tree8de19b16c5038537a714d2bfe34892d20e6d5626 /src
parentrelease 1.0.4 (diff)
implement basic cloning and updating with gix
Diffstat (limited to 'src')
-rw-r--r--src/git/mod.rs46
-rw-r--r--src/repo/aggregate.rs22
-rw-r--r--src/repo/mod.rs325
-rw-r--r--src/sync/mod.rs32
-rw-r--r--src/update/mod.rs77
5 files changed, 307 insertions, 195 deletions
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 60d0b46..0653072 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -1,27 +1,27 @@
-pub fn git_credentials_callback(
- _user: &str,
- user_from_url: Option<&str>,
- _cred: git2::CredentialType,
-) -> Result<git2::Cred, git2::Error> {
- if let Some(user) = user_from_url {
- git2::Cred::ssh_key_from_agent(user)
- } else {
- Err(git2::Error::from_str("no url username found"))
- }
-}
+// pub fn git_credentials_callback(
+// _user: &str,
+// user_from_url: Option<&str>,
+// _cred: git2::CredentialType,
+// ) -> Result<git2::Cred, git2::Error> {
+// if let Some(user) = user_from_url {
+// git2::Cred::ssh_key_from_agent(user)
+// } else {
+// Err(git2::Error::from_str("no url username found"))
+// }
+// }
-pub fn callbacks<'g>() -> git2::RemoteCallbacks<'g> {
- let mut callbacks = git2::RemoteCallbacks::new();
- callbacks.credentials(git_credentials_callback);
+// pub fn callbacks<'g>() -> git2::RemoteCallbacks<'g> {
+// let mut callbacks = git2::RemoteCallbacks::new();
+// callbacks.credentials(git_credentials_callback);
- callbacks
-}
+// callbacks
+// }
-#[tracing::instrument(level = "trace")]
-pub fn fetch_options<'g>() -> git2::FetchOptions<'g> {
- let mut opts = git2::FetchOptions::new();
- opts.remote_callbacks(callbacks());
- opts.download_tags(git2::AutotagOption::All);
+// #[tracing::instrument(level = "trace")]
+// pub fn fetch_options<'g>() -> git2::FetchOptions<'g> {
+// let mut opts = git2::FetchOptions::new();
+// opts.remote_callbacks(callbacks());
+// opts.download_tags(git2::AutotagOption::All);
- opts
-}
+// opts
+// }
diff --git a/src/repo/aggregate.rs b/src/repo/aggregate.rs
index d5a59ba..f8e44a5 100644
--- a/src/repo/aggregate.rs
+++ b/src/repo/aggregate.rs
@@ -1,11 +1,7 @@
-use std::{
- collections::HashMap,
- sync::RwLock,
-};
+use std::{collections::HashMap, os::unix::ffi::OsStrExt, sync::RwLock};
-use git2::Repository;
-
-use tracing::error;
+use gix::bstr::ByteSlice;
+use tracing::{debug, error};
use walkdir::WalkDir;
use crate::forge::Project;
@@ -25,7 +21,7 @@ impl Aggregator for Repos {
fn from_local(root: &str, scope: &str) -> Repos {
let mut repos = HashMap::new();
- let path: std::path::PathBuf = [root, scope].iter().collect();
+ let path: std::path::PathBuf = root.into();
if !path.exists() {
return repos;
@@ -40,7 +36,8 @@ impl Aggregator for Repos {
Some(Ok(entry)) => entry,
};
- if entry.file_type().is_dir() {
+ if entry.file_type().is_dir() && entry.path().as_os_str().as_bytes().contains_str(scope)
+ {
let mut dir = std::fs::read_dir(entry.path()).unwrap();
if dir.any(|dir| {
@@ -52,7 +49,9 @@ impl Aggregator for Repos {
}) {
walker.skip_current_dir();
- match Repository::open(entry.path()) {
+ debug!("found git repo {:?} trying to open...", entry.path());
+
+ match gix::open(entry.path()) {
Ok(repo) => {
let name = entry
.path()
@@ -104,8 +103,7 @@ impl Aggregator for Repos {
local = local
.into_iter()
.map(|(left_name, left)| {
- if let Some(right) = remote.remove(&left_name)
- {
+ if let Some(right) = remote.remove(&left_name) {
left.write().unwrap().forge = right.into_inner().unwrap().forge;
}
diff --git a/src/repo/mod.rs b/src/repo/mod.rs
index aaf0177..4eb9043 100644
--- a/src/repo/mod.rs
+++ b/src/repo/mod.rs
@@ -1,14 +1,18 @@
-use std::{
- collections::HashMap,
- fmt::Debug,
- path::PathBuf,
- sync::RwLock,
-};
+use std::{collections::HashMap, fmt::Debug, path::PathBuf, sync::RwLock};
+use anyhow::Context;
use thiserror::Error;
-use git2::{Branch, Remote, Repository};
-use tracing::{debug, trace};
+use gix::{
+ bstr::BString,
+ clone::checkout::main_worktree::ProgressId,
+ refs::{
+ transaction::{LogChange, PreviousValue, RefEdit},
+ FullName,
+ },
+ remote, Id, ObjectId, Progress, Remote, Repository,
+};
+use tracing::{debug, error};
use crate::forge::Project;
@@ -30,125 +34,216 @@ pub struct Repo {
}
impl Repo {
- pub fn is_clean(&self) -> Result<bool, RepoError> {
- if let Some(repo) = &self.repo {
- debug!("repo state: {:?}", repo.state());
- let statuses: Vec<git2::Status> = repo
- .statuses(None)?
- .iter()
- .filter_map(|status| {
- if status.status().is_ignored() {
- None
- } else {
- Some(status.status())
- }
- })
- .collect();
-
- debug!("got repo statuses: {:?}", statuses);
-
- if repo.state() == git2::RepositoryState::Clean && statuses.is_empty() {
- Ok(true)
- } else {
- Ok(false)
- }
- } else {
- Err(RepoError::NoLocalRepo)
+ pub fn repo(&self) -> Result<&Repository, RepoError> {
+ match &self.repo {
+ Some(repo) => Ok(repo),
+ None => Err(RepoError::NoLocalRepo),
+ }
+ }
+
+ pub fn repo_mut(&mut self) -> Result<&mut Repository, RepoError> {
+ match &mut self.repo {
+ Some(repo) => Ok(repo),
+ None => Err(RepoError::NoLocalRepo),
}
}
- pub fn main_remote<'a>(&self, repo: &'a Repository) -> Result<git2::Remote<'a>, RepoError> {
- let remotes = repo.remotes()?;
+ #[tracing::instrument(level = "debug")]
+ pub fn is_clean(&self) -> Result<LocalRepoState, RepoError> {
+ let repo = self.repo()?;
- let remote = if remotes.iter().any(|x| x == Some("origin")) {
- "origin"
- } else if let Some(remote) = remotes.get(0) {
- remote
+ if let Some(state) = repo.state() {
+ Ok(LocalRepoState::InProgress(state))
} else {
- return Err(RepoError::NoRemoteFound);
- };
+ let head = repo.head().unwrap();
+
+ if head.is_detached() {
+ return Ok(LocalRepoState::DetachedHead);
+ }
+
+ if head.is_unborn() {
+ return Ok(LocalRepoState::UnbornHead);
+ }
- return Ok(repo.find_remote(remote)?);
+ Ok(LocalRepoState::Clean)
+ }
}
- #[tracing::instrument(level = "trace", skip(remote))]
- pub fn fetch<'a>(&self, remote: &mut Remote) -> Result<(), RepoError> {
- // Pass an empty array as the refspec to fetch to fetch the "default" refspecs
- // Type annotation is needed because type can't be guessed from the empty array
- remote.fetch::<&str>(
- &[],
- Some(&mut crate::git::fetch_options()),
- Some("gtree fetch"),
- )?;
+ pub fn default_remote(&self) -> Result<Remote, RepoError> {
+ Ok(self
+ .repo()?
+ .find_default_remote(gix::remote::Direction::Fetch)
+ .ok_or(RepoError::NoRemoteFound)?
+ .context("fetch: failed to find default remote")?)
+ }
- Ok(())
+ pub fn default_branch(&self) -> Result<BString, RepoError> {
+ let repo = self.repo()?;
+ let remote = self.default_remote()?;
+ let remote_name = remote.name().context("remote does not have name")?;
+
+ let origin_ref = repo
+ .find_reference(&format!("remotes/{}/HEAD", remote_name.as_bstr()))
+ .context("the remotes HEAD references does not exist")?;
+
+ if let Some(origin_ref) = origin_ref.target().try_name() {
+ Ok(origin_ref.shorten().to_owned())
+ } else {
+ Err(RepoError::NoDefaultBranch)
+ }
}
#[tracing::instrument(level = "trace")]
- pub fn clone(&self, url: &str) -> Result<Repository, RepoError> {
- let mut builder = git2::build::RepoBuilder::new();
- builder.fetch_options(crate::git::fetch_options());
+ pub fn clone(&self, url: &str) -> Result<(), RepoError> {
+ std::fs::create_dir_all(&self.path).unwrap();
+
+ let mut prepare_clone = gix::prepare_clone(url, &self.path).unwrap();
+
+ let (mut prepare_checkout, _) = prepare_clone
+ .fetch_then_checkout(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
+ .unwrap();
- builder.clone(url, &self.path).map_err(RepoError::GitError)
+ let (_repo, _) = prepare_checkout
+ .main_worktree(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
+ .unwrap();
+
+ Ok(())
}
#[tracing::instrument(level = "trace")]
- pub fn checkout(&self) -> Result<(), RepoError> {
- if let Some(repo) = &self.repo {
- repo.checkout_head(None).map_err(|e| e.into())
- } else {
- Err(RepoError::NoLocalRepo)
+ pub fn fetch<'a>(&mut self) -> Result<bool, RepoError> {
+ let remote = self.default_remote()?;
+ let conn = remote.connect(gix::remote::Direction::Fetch).unwrap();
+ let outcome = conn
+ .prepare_fetch(
+ gix::progress::Discard,
+ gix::remote::ref_map::Options::default(),
+ )
+ .context("fetch: failed to prepare patch")?
+ .receive(gix::progress::Discard, &gix::interrupt::IS_INTERRUPTED)
+ .context("fetch: failed to receive")?;
+
+ match outcome.status {
+ gix::remote::fetch::Status::NoPackReceived {
+ dry_run: _,
+ negotiate: _,
+ update_refs: _,
+ } => Ok(false),
+ gix::remote::fetch::Status::Change {
+ negotiate: _,
+ write_pack_bundle: _,
+ update_refs: _,
+ } => Ok(true),
}
}
- pub fn branch_name(branch: &Branch) -> String {
- match branch.name().unwrap() {
- Some(s) => s.to_string(),
- None => String::from_utf8_lossy(branch.name_bytes().unwrap()).to_string(),
+ pub fn refedit(target: ObjectId, name: &str, message: &str) -> RefEdit {
+ RefEdit {
+ change: gix::refs::transaction::Change::Update {
+ log: LogChange {
+ mode: gix::refs::transaction::RefLog::AndReference,
+ force_create_reflog: false,
+ message: message.into(),
+ },
+ expected: PreviousValue::Any,
+ new: gix::refs::Target::Peeled(target),
+ },
+ name: FullName::try_from(name).unwrap(),
+ deref: true,
}
}
- #[tracing::instrument(level = "trace", skip(repo, local, upstream))]
- pub fn merge(
+ pub fn update_default_branch_ref(
&self,
- repo: &Repository,
- local: &mut Branch,
- upstream: &Branch,
- ) -> Result<bool, RepoError> {
- let local_name = Repo::branch_name(local);
- let upstream_name = Repo::branch_name(upstream);
-
- let local_ref = local.get_mut();
- let upstream_ref = upstream.get();
-
- let analysis = repo.merge_analysis_for_ref(
- local_ref,
- &[&repo.reference_to_annotated_commit(upstream_ref)?],
- )?;
-
- if analysis.0.is_fast_forward() {
- trace!("Doing a fast forward");
-
- let msg = format!(
- "gtree: update repo branch: {} to {}",
- local_name, upstream_name
- );
- debug!("{}", msg);
-
- // sets the branch to target the new commit
- // of the remote branch it's tracking
- local_ref.set_target(upstream_ref.target().unwrap(), &msg)?;
- // Apply these changes in the working dir if the branch is currently checked out.
- if format!("refs/heads/{}", local_name) == self.default_branch {
- repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?
- }
+ remote: remote::Name,
+ head: Id,
+ ) -> Result<(), RepoError> {
+ let default_branch = self.default_branch()?;
+ let repo = self.repo()?;
+
+ repo.edit_reference(Repo::refedit(
+ head.into(),
+ &format!("heads/{}", default_branch),
+ &format!("checkout: {}/HEAD with gtree", remote.as_bstr()),
+ ))
+ .context("checkout: failed to edit ref")?;
- Ok(true)
- } else if analysis.0.is_up_to_date() {
- Ok(false)
- } else {
- Err(RepoError::NoFF)
- }
+ Ok(())
+ }
+
+ pub fn default_remote_head(&self) -> Result<(remote::Name, Id), RepoError> {
+ let repo = self.repo()?;
+
+ let remote = repo
+ .find_fetch_remote(None)
+ .context("could not find remote to fetch")?;
+ let remote = remote.name().context("remote does not have name")?;
+
+ let head_ref = repo
+ .find_reference(&format!("remotes/{}/HEAD", remote.as_bstr()))
+ .context("the remotes HEAD references does not exist")?;
+ let head = head_ref
+ .into_fully_peeled_id()
+ .context("failed to peel ref")?;
+
+ Ok((remote.to_owned(), head.to_owned()))
+ }
+
+ #[tracing::instrument(level = "trace", skip(progress))]
+ pub fn checkout(
+ &self,
+ remote: remote::Name,
+ head: Id,
+ progress: &mut dyn gix::progress::DynNestedProgress,
+ ) -> Result<(), RepoError> {
+ let repo = self.repo()?;
+
+ let workdir = repo.work_dir().ok_or(RepoError::NoWorktree)?;
+ let head_tree = head
+ .object()
+ .context("could not find object HEAD points to")?
+ .peel_to_tree()
+ .context("failed to peel HEAD object")?
+ .id();
+
+ let index =
+ gix_index::State::from_tree(&head_tree, &repo.objects).context("index from tree")?;
+ let mut index = gix_index::File::from_state(index, repo.index_path());
+
+ let mut files =
+ progress.add_child_with_id("checkout".to_string(), ProgressId::CheckoutFiles.into());
+ let mut bytes =
+ progress.add_child_with_id("writing".to_string(), ProgressId::BytesWritten.into());
+
+ files.init(Some(index.entries().len()), gix::progress::count("files"));
+ bytes.init(None, gix::progress::bytes());
+
+ let start = std::time::Instant::now();
+
+ debug!("workdir: {:?}", workdir);
+ let opts = gix_worktree_state::checkout::Options::default();
+ let outcome = gix_worktree_state::checkout(
+ &mut index,
+ workdir,
+ repo.objects.clone().into_arc().unwrap(),
+ &files,
+ &bytes,
+ &gix::interrupt::IS_INTERRUPTED,
+ opts,
+ )
+ .context("checkout: failed");
+
+ files.show_throughput(start);
+ bytes.show_throughput(start);
+
+ debug!("outcome: {:?}", outcome);
+ debug!("is interrupted: {:?}", &gix::interrupt::IS_INTERRUPTED);
+
+ index
+ .write(Default::default())
+ .context("checkout: write index")?;
+
+ Ok(())
}
}
@@ -158,16 +253,32 @@ pub enum RepoError {
NoLocalRepo,
#[error("local git repo does not have a remote")]
NoRemoteFound,
- #[error("repository is dirty")]
- Dirty,
+ #[error("could not determine default branch based on remote HEAD")]
+ NoDefaultBranch,
+ #[error("repo is not checked out")]
+ NoWorktree,
+ #[error("repository is dirty: {0}")]
+ Dirty(LocalRepoState),
#[error("fast-forward merge was not possible")]
NoFF,
- #[error("error during git operation {0}")]
- GitError(#[from] git2::Error),
+ #[error("error: {0}")]
+ Anyhow(#[from] anyhow::Error),
#[error("unknown repo error")]
Unknown,
}
+#[derive(Error, Debug, PartialEq)]
+pub enum LocalRepoState {
+ #[error("operation in progress: {0:?}")]
+ InProgress(gix::state::InProgress),
+ #[error("head is detached")]
+ DetachedHead,
+ #[error("head is unborn")]
+ UnbornHead,
+ #[error("repo is clean")]
+ Clean,
+}
+
impl Ord for Repo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(&other.name)
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index 4bfe322..a1dab53 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -2,7 +2,7 @@ use std::fmt::{Debug, Display};
use crate::{
batch::batch,
- repo::{Repo, RepoError, Repos},
+ repo::{LocalRepoState, Repo, RepoError, Repos},
};
impl crate::GTree {
@@ -22,12 +22,11 @@ impl Repo {
pub fn sync(&mut self) -> Result<SyncResult, SyncResult> {
let repo_name = self.name.clone();
- if self.repo.is_some()
- && !self
- .is_clean()
- .map_err(|err| SyncResult::err(repo_name.clone(), err))?
- {
- return Ok(SyncResult::dirty(repo_name));
+ let repo_state = self
+ .is_clean()
+ .map_err(|err| SyncResult::err(repo_name.clone(), err))?;
+ if self.repo.is_some() && repo_state != LocalRepoState::Clean {
+ return Ok(SyncResult::dirty(repo_name, repo_state));
};
if self.repo.is_some() && self.forge.is_some() {
@@ -44,14 +43,12 @@ impl Repo {
.as_ref()
.ok_or_else(|| SyncResult::err(self.name.clone(), RepoError::NoRemoteFound))?;
- let repo = self
- .clone(url)
+ self.clone(url)
.map_err(|err| SyncResult::err(repo_name.clone(), err))?;
// TODO detect moved repos based on first commit
// ???? how to detect and not move forks?
- self.repo = Some(repo);
Ok(SyncResult::cloned(repo_name))
} else {
Ok(SyncResult::no_changes(repo_name))
@@ -62,7 +59,7 @@ impl Repo {
#[derive(Debug)]
pub enum SyncResult {
NoChanges { name: String },
- Dirty { name: String },
+ Dirty { name: String, state: LocalRepoState },
Cloned { name: String },
Pushed { name: String },
Error { name: String, error: RepoError },
@@ -81,8 +78,8 @@ impl SyncResult {
SyncResult::Pushed { name }
}
- pub fn dirty(name: String) -> SyncResult {
- SyncResult::Dirty { name }
+ pub fn dirty(name: String, state: LocalRepoState) -> SyncResult {
+ SyncResult::Dirty { name, state }
}
pub fn no_changes(name: String) -> SyncResult {
@@ -98,9 +95,12 @@ impl Display for SyncResult {
SyncResult::NoChanges { name } => {
f.write_fmt(format_args!("{} {}", Blue.paint("NOCHANGE"), name))
}
- SyncResult::Dirty { name } => {
- f.write_fmt(format_args!("{} {}", Yellow.paint("DIRTY "), name))
- }
+ SyncResult::Dirty { name, state } => f.write_fmt(format_args!(
+ "{} {} [{}]",
+ Yellow.paint("DIRTY "),
+ name,
+ state
+ )),
SyncResult::Cloned { name } => {
f.write_fmt(format_args!("{} {}", Green.paint("CLONED "), name))
}
diff --git a/src/update/mod.rs b/src/update/mod.rs
index 8f10663..7b51136 100644
--- a/src/update/mod.rs
+++ b/src/update/mod.rs
@@ -1,11 +1,10 @@
use std::fmt::{Debug, Display};
-use git2::BranchType;
use tracing::debug;
use crate::{
batch::batch,
- repo::{Repo, RepoError, Repos},
+ repo::{LocalRepoState, Repo, RepoError, Repos},
};
impl crate::GTree {
@@ -35,51 +34,52 @@ impl Repo {
}
fn update_inner(&mut self) -> Result<UpdateResult, RepoError> {
- let repo = self.repo.as_ref().unwrap();
- let mut remote = self.main_remote(repo)?;
+ let repo_state = self.is_clean()?;
+ if self.repo.is_some() && repo_state != LocalRepoState::Clean {
+ return Ok(UpdateResult::dirty(self.name.clone(), repo_state));
+ };
- self.fetch(&mut remote)?;
+ debug!("repo is clean");
- self.default_branch = remote.default_branch()?.as_str().unwrap().to_string();
+ let mut progress = gix::progress::Discard {};
- debug!("default branch: {}", self.default_branch);
+ let _fetched = self.fetch()?;
+ let (remote, head) = self.default_remote_head()?;
+ self.update_default_branch_ref(remote.clone(), head)?;
+ self.checkout(remote, head, &mut progress)?;
- if self.is_clean()? {
- debug!("repo is clean");
+ // let merged = repo.branches(Some(BranchType::Local))?
+ // .filter_map(|x| x.ok())
+ // .try_fold(false, |mut merged, (mut branch, _)| {
+ // let name = format!("refs/heads/{}", Repo::branch_name(&branch));
- let merged = repo.branches(Some(BranchType::Local))?
- .filter_map(|x| x.ok())
- .try_fold(false, |mut merged, (mut branch, _)| {
- let name = format!("refs/heads/{}", Repo::branch_name(&branch));
+ // if branch.upstream().is_ok() {
+ // let upstream = branch.upstream().unwrap();
- if branch.upstream().is_ok() {
- let upstream = branch.upstream().unwrap();
+ // debug!("branch: {}", name);
- debug!("branch: {}", name);
+ // merged |= self.merge(repo, &mut branch, &upstream)?;
+ // Ok::<bool, RepoError>(merged)
+ // } else {
+ // debug!("not updating branch: {}: branch does not have upstream tracking branch set", name);
+ // Ok(merged)
+ // }
+ // })?;
- merged |= self.merge(repo, &mut branch, &upstream)?;
- Ok::<bool, RepoError>(merged)
- } else {
- debug!("not updating branch: {}: branch does not have upstream tracking branch set", name);
- Ok(merged)
- }
- })?;
+ // if merged {
+ // Ok(UpdateResult::merged(self.name.clone()))
+ // } else {
+ // Ok(UpdateResult::no_changes(self.name.clone()))
+ // }
- if merged {
- Ok(UpdateResult::merged(self.name.clone()))
- } else {
- Ok(UpdateResult::no_changes(self.name.clone()))
- }
- } else {
- Ok(UpdateResult::dirty(self.name.clone()))
- }
+ Ok(UpdateResult::no_changes(self.name.clone()))
}
}
#[derive(Debug)]
pub enum UpdateResult {
NoChanges { name: String },
- Dirty { name: String },
+ Dirty { name: String, state: LocalRepoState },
Merged { name: String },
Error { name: String, error: RepoError },
}
@@ -93,8 +93,8 @@ impl UpdateResult {
UpdateResult::Merged { name }
}
- pub fn dirty(name: String) -> UpdateResult {
- UpdateResult::Dirty { name }
+ pub fn dirty(name: String, state: LocalRepoState) -> UpdateResult {
+ UpdateResult::Dirty { name, state }
}
pub fn no_changes(name: String) -> UpdateResult {
@@ -110,9 +110,12 @@ impl Display for UpdateResult {
UpdateResult::NoChanges { name } => {
f.write_fmt(format_args!("{} {}", Blue.paint("FETCHED"), name))
}
- UpdateResult::Dirty { name } => {
- f.write_fmt(format_args!("{} {}", Yellow.paint("DIRTY "), name))
- }
+ UpdateResult::Dirty { name, state } => f.write_fmt(format_args!(
+ "{} {} [{}]",
+ Yellow.paint("DIRTY "),
+ name,
+ state
+ )),
UpdateResult::Merged { name } => {
f.write_fmt(format_args!("{} {}", Green.paint("PULLED "), name))
}