1
0
Fork 0
mirror of https://codeberg.org/icewind/haze.git synced 2026-06-03 09:04:12 +02:00
haze/src/git.rs
2026-05-08 22:23:40 +02:00

205 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::config::HazeConfig;
use crate::Result;
use git2::build::CheckoutBuilder;
use git2::{Branch, BranchType, Repository, RepositoryState};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use miette::{Context, IntoDiagnostic};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::ThreadPoolBuilder;
use std::fs::read_dir;
use std::iter::once;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
fn find_app_repos(config: &HazeConfig) -> Result<impl Iterator<Item = PathBuf>> {
let apps_dirs = once(config.sources_root.as_path().join("apps"))
.chain(config.app_directories.iter().cloned());
let dir_handles = apps_dirs
.map(|dir| read_dir(dir).into_diagnostic())
.collect::<Result<Vec<_>>>()?;
Ok(dir_handles
.into_iter()
.flatten()
.flatten()
.filter(|app| app.path().join(".git").is_dir())
.map(|app| app.path()))
}
fn longest_app_branch(config: &HazeConfig) -> Result<(usize, usize)> {
Ok(find_app_repos(config)?
.filter_map(|app_dir| {
let app_name = app_dir.file_name()?.to_str()?;
let repo = Repository::init(&app_dir).ok()?;
let branch_name = current_branch_name(&repo)?;
Some((app_name.len(), branch_name.len()))
})
.max()
.unwrap_or_default())
}
pub fn checkout_all(config: &HazeConfig, mut name: &str, verbose: bool) -> Result<()> {
// "main" and "master" are interchangeable
if name == "main" {
name = "master";
}
for app_dir in find_app_repos(config)? {
let repo = Repository::init(&app_dir)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to open repository {}", app_dir.display()))?;
let app_name = app_dir.file_name().unwrap().to_string_lossy();
if let Some(branch) = get_branch(&repo, name)? {
if !branch.is_head() {
let is_remote = branch.get().is_remote();
print!("{app_name}");
if let Err(e) = checkout(&repo, &branch, is_remote.then_some(name)) {
println!(": {:#}", e);
} else {
println!("");
}
} else if verbose {
println!("{app_name} -");
}
} else if verbose {
println!("{app_name} 🛇 Branch not found");
};
}
Ok(())
}
fn current_branch_name(repo: &Repository) -> Option<String> {
let (branch, _) = repo
.branches(None)
.ok()?
.flatten()
.find(|(b, _)| b.is_head())?;
branch.name().ok().flatten().map(String::from)
}
const GIT_BINARY: &str = match option_env!("GIT_BINARY") {
Some(git) => git,
None => "git",
};
pub fn pull_all(config: &HazeConfig) -> Result<()> {
let (max_app, max_branch) = longest_app_branch(config)?;
let progress = MultiProgress::new();
let pull_style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap();
let pool = ThreadPoolBuilder::new()
.num_threads(8)
.build()
.into_diagnostic()?;
let repos = find_app_repos(config)?.collect::<Vec<_>>();
pool.install(|| {
repos.par_iter().for_each(|app_dir| {
let app_name = app_dir.file_name().unwrap().to_string_lossy();
let Ok(repo) = Repository::init(app_dir) else {
return;
};
let branch_name = current_branch_name(&repo).unwrap_or("unknown".into());
let bar = ProgressBar::new_spinner().with_style(pull_style.clone());
bar.enable_steady_tick(Duration::from_millis(100));
let bar = progress.add(bar);
let msg = |state: &str| {
format!(
"{app_name:<app_width$} - {branch_name:<branch_width$}{state}",
app_width = max_app,
branch_width = max_branch
)
};
if repo.state() != RepositoryState::Clean {
bar.set_message(msg(" repository not clean"));
bar.finish();
return;
}
bar.set_message(msg(""));
let output = match Command::new(GIT_BINARY)
.arg("pull")
.current_dir(app_dir)
.output()
{
Ok(output) => output,
Err(error) => {
bar.set_message(msg(&format!(" {error}")));
return;
}
};
if output.status.success() {
bar.set_message(msg(""));
} else {
let err = String::from_utf8_lossy(&output.stderr);
let err_line = err.lines().next().unwrap();
bar.set_message(msg(&format!(" {err_line}")));
}
bar.finish();
});
});
Ok(())
}
fn get_branch<'repo>(repo: &'repo Repository, name: &str) -> Result<Option<Branch<'repo>>> {
let local_branches = repo.branches(Some(BranchType::Local)).into_diagnostic()?;
if let Some(local_branch) = local_branches.flatten().find_map(|(branch, _)| {
match (branch.name(), name) {
(Ok(Some("main")), "master") => Some(branch), // make "main" synonymous with "master"
(Ok(Some(branch_name)), _) if branch_name == name => Some(branch),
_ => None,
}
}) {
return Ok(Some(local_branch));
};
let remote_branches = repo.branches(Some(BranchType::Remote)).into_diagnostic()?;
let remote_name = format!("origin/{name}");
Ok(remote_branches
.flatten()
.find_map(|(branch, _)| match branch.name() {
Ok(Some(branch_name)) if branch_name == remote_name => Some(branch),
_ => None,
}))
}
fn checkout(repo: &Repository, branch: &Branch, target_name: Option<&str>) -> Result<()> {
let commit = branch.get().peel_to_commit().into_diagnostic()?;
let mut name = branch
.name()
.into_diagnostic()?
.expect("we already know the name if utf8");
let mut checkout = CheckoutBuilder::default();
checkout.update_index(true);
repo.checkout_tree(commit.as_object(), Some(&mut checkout))
.into_diagnostic()
.wrap_err("Failed to checkout tree")?;
if let Some(target_name) = target_name {
let mut new_branch = repo
.branch(target_name, &commit, false)
.into_diagnostic()
.wrap_err("Failed to create local branch")?;
new_branch
.set_upstream(Some(name))
.into_diagnostic()
.wrap_err("Failed to set local branch upstream")?;
name = target_name;
}
repo.set_head(&format!("refs/heads/{name}"))
.into_diagnostic()
.wrap_err("Failed to set HEAD")?;
Ok(())
}