Compare commits
No commits in common. "7d5157d526c0cf532ddcbc196eb34287599cf980" and "2e0952f9fbda31de10252f6891385b9656109b11" have entirely different histories.
7d5157d526
...
2e0952f9fb
18 changed files with 85 additions and 335 deletions
111
src/bridge.rs
111
src/bridge.rs
|
|
@ -60,46 +60,18 @@ fn save_git_map(path: &std::path::Path, map: &GitMap) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn make_cred_callback()
|
||||
-> impl FnMut(&str, Option<&str>, git2::CredentialType) -> std::result::Result<git2::Cred, git2::Error>
|
||||
{
|
||||
let mut attempts = 0u32;
|
||||
move |_url, username, allowed| {
|
||||
attempts += 1;
|
||||
if attempts > 4 {
|
||||
return Err(git2::Error::from_str("authentication failed"));
|
||||
}
|
||||
|
||||
if allowed.contains(git2::CredentialType::SSH_KEY) {
|
||||
let user = username.unwrap_or("git");
|
||||
|
||||
if attempts <= 1
|
||||
&& let Ok(cred) = git2::Cred::ssh_key_from_agent(user)
|
||||
{
|
||||
return Ok(cred);
|
||||
}
|
||||
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
let ssh_dir = std::path::Path::new(&home).join(".ssh");
|
||||
for key_name in &["id_ed25519", "id_rsa", "id_ecdsa"] {
|
||||
let key_path = ssh_dir.join(key_name);
|
||||
if key_path.exists() {
|
||||
let pub_path = ssh_dir.join(format!("{key_name}.pub"));
|
||||
let pub_key = pub_path.exists().then_some(pub_path.as_path());
|
||||
if let Ok(cred) = git2::Cred::ssh_key(user, pub_key, &key_path, None) {
|
||||
return Ok(cred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(git2::Error::from_str("no SSH key found"))
|
||||
} else if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||
Err(git2::Error::from_str(
|
||||
"interactive authentication not supported",
|
||||
))
|
||||
} else {
|
||||
git2::Cred::default()
|
||||
}
|
||||
fn cred_callback(
|
||||
_url: &str,
|
||||
username: Option<&str>,
|
||||
allowed: git2::CredentialType,
|
||||
) -> std::result::Result<git2::Cred, git2::Error> {
|
||||
if allowed.contains(git2::CredentialType::SSH_KEY) {
|
||||
let user = username.unwrap_or("git");
|
||||
git2::Cred::ssh_key_from_agent(user)
|
||||
} else if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
|
||||
git2::Cred::userpass_plaintext("", "")
|
||||
} else {
|
||||
git2::Cred::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +79,6 @@ impl GitBridge {
|
|||
/// Open (or initialise) the shadow git repository and load the mapping cache.
|
||||
pub fn open(repo: &Repository) -> Result<Self> {
|
||||
let git_dir = shadow_git_dir(repo);
|
||||
debug!("opening git bridge at {}", git_dir.display());
|
||||
let git_repo = if git_dir.exists() {
|
||||
git2::Repository::open_bare(&git_dir)?
|
||||
} else {
|
||||
|
|
@ -115,7 +86,6 @@ impl GitBridge {
|
|||
};
|
||||
let map_path = git_map_path(repo);
|
||||
let map = load_git_map(&map_path)?;
|
||||
debug!("loaded git map with {} entries", map.arc_to_git.len());
|
||||
Ok(Self {
|
||||
git_repo,
|
||||
map,
|
||||
|
|
@ -132,15 +102,7 @@ impl GitBridge {
|
|||
///
|
||||
/// Returns the git OID for the newly created (or cached) git commit.
|
||||
pub fn arc_to_git(&mut self, arc_repo: &Repository, arc_id: &CommitId) -> Result<git2::Oid> {
|
||||
debug!(
|
||||
"converting arc commit {} to git",
|
||||
&arc_id.0[..12.min(arc_id.0.len())]
|
||||
);
|
||||
if let Some(hex) = self.map.arc_to_git.get(&arc_id.0) {
|
||||
debug!(
|
||||
"arc commit {} already mapped to git",
|
||||
&arc_id.0[..12.min(arc_id.0.len())]
|
||||
);
|
||||
let oid = git2::Oid::from_str(hex)?;
|
||||
return Ok(oid);
|
||||
}
|
||||
|
|
@ -169,11 +131,6 @@ impl GitBridge {
|
|||
.git_repo
|
||||
.commit(None, &sig, &sig, &c.message, &git_tree, &parent_refs)?;
|
||||
|
||||
debug!(
|
||||
"created git commit {} for arc {}",
|
||||
oid,
|
||||
&arc_id.0[..12.min(arc_id.0.len())]
|
||||
);
|
||||
self.map
|
||||
.arc_to_git
|
||||
.insert(arc_id.0.clone(), oid.to_string());
|
||||
|
|
@ -189,15 +146,7 @@ impl GitBridge {
|
|||
/// Returns the arc `CommitId` for the newly created (or cached) commit.
|
||||
pub fn git_to_arc(&mut self, arc_repo: &Repository, git_oid: git2::Oid) -> Result<CommitId> {
|
||||
let oid_hex = git_oid.to_string();
|
||||
debug!(
|
||||
"converting git commit {} to arc",
|
||||
&oid_hex[..12.min(oid_hex.len())]
|
||||
);
|
||||
if let Some(arc_id) = self.map.git_to_arc.get(&oid_hex) {
|
||||
debug!(
|
||||
"git commit {} already mapped to arc",
|
||||
&oid_hex[..12.min(oid_hex.len())]
|
||||
);
|
||||
return Ok(CommitId(arc_id.clone()));
|
||||
}
|
||||
|
||||
|
|
@ -259,11 +208,6 @@ impl GitBridge {
|
|||
};
|
||||
store::write_commit_object(arc_repo, &obj)?;
|
||||
|
||||
debug!(
|
||||
"created arc commit {} for git {}",
|
||||
&commit_id.0[..12.min(commit_id.0.len())],
|
||||
&oid_hex[..12.min(oid_hex.len())]
|
||||
);
|
||||
self.map
|
||||
.arc_to_git
|
||||
.insert(commit_id.0.clone(), oid_hex.clone());
|
||||
|
|
@ -381,7 +325,6 @@ impl GitBridge {
|
|||
///
|
||||
/// Converts all reachable commits, updates shadow refs, and pushes.
|
||||
pub fn push(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
||||
debug!("pushing to remote '{}'", remote_name);
|
||||
let remotes = remote::load(arc_repo)?;
|
||||
let entry = remotes
|
||||
.remotes
|
||||
|
|
@ -454,7 +397,7 @@ pub fn push(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
let mut git_remote = bridge.git_repo.remote_anonymous(url)?;
|
||||
let mut opts = git2::PushOptions::new();
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
callbacks.credentials(make_cred_callback());
|
||||
callbacks.credentials(cred_callback);
|
||||
opts.remote_callbacks(callbacks);
|
||||
git_remote.push(&spec_strs, Some(&mut opts))?;
|
||||
}
|
||||
|
|
@ -462,7 +405,6 @@ pub fn push(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
bridge.save_map()?;
|
||||
|
||||
let count = ref_specs.len();
|
||||
debug!("pushed {} ref(s)", count);
|
||||
Ok(format!("pushed {count} ref(s) to {remote_name}"))
|
||||
}
|
||||
|
||||
|
|
@ -471,7 +413,6 @@ pub fn push(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
/// Fetches, then imports reachable commits for each remote branch
|
||||
/// and updates local bookmarks that can be fast-forwarded.
|
||||
pub fn pull(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
||||
debug!("pulling from remote '{}'", remote_name);
|
||||
let remotes = remote::load(arc_repo)?;
|
||||
let entry = remotes
|
||||
.remotes
|
||||
|
|
@ -485,7 +426,7 @@ pub fn pull(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
let mut git_remote = bridge.git_repo.remote_anonymous(url)?;
|
||||
let mut opts = git2::FetchOptions::new();
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
callbacks.credentials(make_cred_callback());
|
||||
callbacks.credentials(cred_callback);
|
||||
opts.remote_callbacks(callbacks);
|
||||
git_remote.fetch::<&str>(&[], Some(&mut opts), None)?;
|
||||
|
||||
|
|
@ -531,7 +472,6 @@ pub fn pull(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
}
|
||||
result
|
||||
};
|
||||
debug!("fetched {} new ref(s)", refs.len());
|
||||
|
||||
for (_refname, oid) in &refs {
|
||||
if bridge.map.git_to_arc.contains_key(&oid.to_string()) {
|
||||
|
|
@ -626,7 +566,6 @@ pub fn pull(arc_repo: &Repository, remote_name: &str) -> Result<String> {
|
|||
/// Creates the target directory, initialises an arc repo, imports all
|
||||
/// git history, and sets up the worktree at the specified branch.
|
||||
pub fn clone(url: &str, path: &str, branch: &str) -> Result<String> {
|
||||
debug!("cloning from '{}'", url);
|
||||
let target = std::path::Path::new(path);
|
||||
let created_dir = !target.exists();
|
||||
if created_dir {
|
||||
|
|
@ -651,7 +590,7 @@ fn clone_inner(url: &str, target: &std::path::Path, branch: &str) -> Result<Stri
|
|||
|
||||
let git_dir = shadow_git_dir(&arc_repo);
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
callbacks.credentials(make_cred_callback());
|
||||
callbacks.credentials(cred_callback);
|
||||
let mut opts = git2::FetchOptions::new();
|
||||
opts.remote_callbacks(callbacks);
|
||||
let git_repo = git2::build::RepoBuilder::new()
|
||||
|
|
@ -747,11 +686,6 @@ fn clone_inner(url: &str, target: &std::path::Path, branch: &str) -> Result<Stri
|
|||
|
||||
bridge.save_map()?;
|
||||
|
||||
debug!(
|
||||
"cloned {} commit(s) on branch '{}'",
|
||||
bridge.map.arc_to_git.len(),
|
||||
branch
|
||||
);
|
||||
Ok(format!(
|
||||
"cloned into {} on bookmark '{branch}'",
|
||||
target.display()
|
||||
|
|
@ -763,7 +697,6 @@ fn clone_inner(url: &str, target: &std::path::Path, branch: &str) -> Result<Stri
|
|||
/// Creates `.arc/` alongside the existing `.git/`, imports all branches
|
||||
/// and tags from git history as arc commits.
|
||||
pub fn migrate(path: &std::path::Path) -> Result<String> {
|
||||
debug!("migrating git repository at {}", path.display());
|
||||
let git_repo = git2::Repository::discover(path).map_err(|_| ArcError::NotAGitRepo)?;
|
||||
|
||||
let workdir = git_repo.workdir().ok_or(ArcError::NotAGitRepo)?;
|
||||
|
|
@ -802,7 +735,6 @@ pub fn migrate(path: &std::path::Path) -> Result<String> {
|
|||
}
|
||||
result
|
||||
};
|
||||
debug!("found {} git ref(s) to import", refs.len());
|
||||
|
||||
let mut bridge = GitBridge {
|
||||
git_repo,
|
||||
|
|
@ -867,12 +799,6 @@ pub fn migrate(path: &std::path::Path) -> Result<String> {
|
|||
|
||||
bridge.save_map()?;
|
||||
|
||||
debug!(
|
||||
"migration complete: {} commit(s), {} bookmark(s), {} tag(s)",
|
||||
imported,
|
||||
bookmarks.len(),
|
||||
tags.len()
|
||||
);
|
||||
Ok(format!(
|
||||
"migrated {imported} commit(s), {} bookmark(s), {} tag(s)",
|
||||
bookmarks.len(),
|
||||
|
|
@ -885,7 +811,6 @@ pub fn migrate(path: &std::path::Path) -> Result<String> {
|
|||
/// Without `--push`, this ensures the shadow git repo mirrors arc state.
|
||||
/// With `--push`, it also pushes all refs to the default remote.
|
||||
pub fn sync(arc_repo: &Repository, do_push: bool) -> Result<String> {
|
||||
debug!("syncing refs to shadow git");
|
||||
let mut bridge = GitBridge::open(arc_repo)?;
|
||||
let mut synced = 0usize;
|
||||
|
||||
|
|
@ -930,7 +855,6 @@ pub fn sync(arc_repo: &Repository, do_push: bool) -> Result<String> {
|
|||
}
|
||||
|
||||
bridge.save_map()?;
|
||||
debug!("synced {} ref(s)", synced);
|
||||
|
||||
if do_push {
|
||||
let config = crate::config::load_effective(arc_repo);
|
||||
|
|
@ -945,11 +869,6 @@ pub fn sync(arc_repo: &Repository, do_push: bool) -> Result<String> {
|
|||
/// Check whether `ancestor` is an ancestor of `descendant` by walking
|
||||
/// the first-parent chain from `descendant`.
|
||||
fn is_ancestor(repo: &Repository, ancestor: &CommitId, descendant: &CommitId) -> bool {
|
||||
debug!(
|
||||
"checking if {} is ancestor of {}",
|
||||
&ancestor.0[..12.min(ancestor.0.len())],
|
||||
&descendant.0[..12.min(descendant.0.len())]
|
||||
);
|
||||
let mut current = descendant.clone();
|
||||
loop {
|
||||
if current == *ancestor {
|
||||
|
|
|
|||
170
src/cli.rs
170
src/cli.rs
|
|
@ -1,6 +1,5 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
|
|
@ -17,21 +16,9 @@ use crate::stash;
|
|||
use crate::tracking;
|
||||
use crate::ui;
|
||||
|
||||
static VERBOSE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Returns whether verbose/debug output is enabled.
|
||||
#[allow(dead_code)]
|
||||
pub fn verbose() -> bool {
|
||||
VERBOSE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "arc", about = "A delta-based version control system", version)]
|
||||
pub struct Cli {
|
||||
/// Enable verbose/debug output
|
||||
#[arg(short, long, global = true)]
|
||||
pub verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
|
@ -367,11 +354,8 @@ pub fn parse() -> Cli {
|
|||
}
|
||||
|
||||
pub fn dispatch(cli: Cli) {
|
||||
VERBOSE.store(cli.verbose, Ordering::Relaxed);
|
||||
debug!("dispatching command");
|
||||
match cli.command {
|
||||
Command::Init { path } => {
|
||||
debug!("command: init (path: {:?})", path);
|
||||
let target = path.as_deref().unwrap_or(".");
|
||||
let target_path = Path::new(target);
|
||||
if !target_path.exists()
|
||||
|
|
@ -397,7 +381,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Commit { message } => {
|
||||
debug!("command: commit (message: {})", message);
|
||||
let repo = open_repo_or_exit();
|
||||
match tracking::commit(&repo, &message) {
|
||||
Ok(id) => {
|
||||
|
|
@ -413,7 +396,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Log { range } => {
|
||||
debug!("command: log (range: {:?})", range);
|
||||
let repo = open_repo_or_exit();
|
||||
match inspect::log(&repo, range.as_deref()) {
|
||||
Ok(output) => print!("{output}"),
|
||||
|
|
@ -424,7 +406,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Status => {
|
||||
debug!("command: status");
|
||||
let repo = open_repo_or_exit();
|
||||
match tracking::status(&repo) {
|
||||
Ok((report, _)) => {
|
||||
|
|
@ -437,7 +418,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Diff { range: _range } => {
|
||||
debug!("command: diff (range: {:?})", _range);
|
||||
let repo = open_repo_or_exit();
|
||||
match run_diff(&repo) {
|
||||
Ok(output) => {
|
||||
|
|
@ -454,7 +434,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Switch { target } => {
|
||||
debug!("command: switch (target: {})", target);
|
||||
let repo = open_repo_or_exit();
|
||||
match refs::switch(&repo, &target) {
|
||||
Ok(msg) => println!("{msg}"),
|
||||
|
|
@ -465,7 +444,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Merge { target } => {
|
||||
debug!("command: merge (target: {})", target);
|
||||
let repo = open_repo_or_exit();
|
||||
match modify::merge_branch(&repo, &target) {
|
||||
Ok(id) => println!(
|
||||
|
|
@ -483,7 +461,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Show { target } => {
|
||||
debug!("command: show (target: {})", target);
|
||||
let repo = open_repo_or_exit();
|
||||
match inspect::show(&repo, &target) {
|
||||
Ok(output) => print!("{output}"),
|
||||
|
|
@ -494,7 +471,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::History { file, range } => {
|
||||
debug!("command: history (file: {}, range: {:?})", file, range);
|
||||
let repo = open_repo_or_exit();
|
||||
match inspect::history(&repo, &file, range.as_deref()) {
|
||||
Ok(output) => print!("{output}"),
|
||||
|
|
@ -505,7 +481,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Revert { target } => {
|
||||
debug!("command: revert (target: {})", target);
|
||||
let repo = open_repo_or_exit();
|
||||
match modify::revert(&repo, &target) {
|
||||
Ok(id) => println!(
|
||||
|
|
@ -519,7 +494,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Reset { files } => {
|
||||
debug!("command: reset (files: {:?})", files);
|
||||
let repo = open_repo_or_exit();
|
||||
match modify::reset(&repo, &files) {
|
||||
Ok(msg) => println!("{msg}"),
|
||||
|
|
@ -530,7 +504,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Push { remote } => {
|
||||
debug!("command: push (remote: {:?})", remote);
|
||||
let repo = open_repo_or_exit();
|
||||
let config = config::load_effective(&repo);
|
||||
let r = remote.as_deref().unwrap_or(&config.default_remote);
|
||||
|
|
@ -543,7 +516,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Pull { remote } => {
|
||||
debug!("command: pull (remote: {:?})", remote);
|
||||
let repo = open_repo_or_exit();
|
||||
let config = config::load_effective(&repo);
|
||||
let r = remote.as_deref().unwrap_or(&config.default_remote);
|
||||
|
|
@ -556,10 +528,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Clone { url, path, branch } => {
|
||||
debug!(
|
||||
"command: clone (url: {}, path: {:?}, branch: {:?})",
|
||||
url, path, branch
|
||||
);
|
||||
let p = path.as_deref().unwrap_or(".");
|
||||
let b = branch.as_deref().unwrap_or("main");
|
||||
match bridge::clone(&url, p, b) {
|
||||
|
|
@ -570,18 +538,14 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
}
|
||||
Command::Migrate => {
|
||||
debug!("command: migrate");
|
||||
match bridge::migrate(Path::new(".")) {
|
||||
Ok(msg) => println!("{msg}"),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
Command::Migrate => match bridge::migrate(Path::new(".")) {
|
||||
Ok(msg) => println!("{msg}"),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
},
|
||||
Command::Mark { command } => {
|
||||
debug!("command: mark");
|
||||
let repo = open_repo_or_exit();
|
||||
match command {
|
||||
MarkCommand::Add { name, commit } => {
|
||||
|
|
@ -636,7 +600,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Tag { command } => {
|
||||
debug!("command: tag");
|
||||
let repo = open_repo_or_exit();
|
||||
match command {
|
||||
TagCommand::Add { name, commit } => {
|
||||
|
|
@ -675,7 +638,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Stash { command } => {
|
||||
debug!("command: stash");
|
||||
let repo = open_repo_or_exit();
|
||||
match command {
|
||||
StashCommand::Create { name } => match stash::create(&repo, &name) {
|
||||
|
|
@ -723,7 +685,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Graft { target, onto } => {
|
||||
debug!("command: graft (target: {}, onto: {})", target, onto);
|
||||
let repo = open_repo_or_exit();
|
||||
match modify::graft(&repo, &target, &onto) {
|
||||
Ok(ids) => {
|
||||
|
|
@ -740,70 +701,66 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
}
|
||||
Command::Config { command } => {
|
||||
debug!("command: config");
|
||||
match command {
|
||||
ConfigCommand::Set { global, key, value } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Some(open_repo_or_exit())
|
||||
};
|
||||
match config::config_set(repo.as_ref(), global, &key, &value) {
|
||||
Ok(()) => println!("{}", ui::success(&format!("{key} = {value}"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Get { global, key } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Repository::discover(Path::new(".")).ok()
|
||||
};
|
||||
match config::config_get(repo.as_ref(), global, &key) {
|
||||
Ok(Some(value)) => println!("{value}"),
|
||||
Ok(None) => println!("{}", ui::info(&format!("{key} is not set"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Show { global } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Repository::discover(Path::new(".")).ok()
|
||||
};
|
||||
match config::config_show(repo.as_ref(), global) {
|
||||
Ok(yaml) => print!("{yaml}"),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Unset { global, key } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Some(open_repo_or_exit())
|
||||
};
|
||||
match config::config_unset(repo.as_ref(), global, &key) {
|
||||
Ok(()) => println!("{}", ui::success(&format!("unset {key}"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
Command::Config { command } => match command {
|
||||
ConfigCommand::Set { global, key, value } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Some(open_repo_or_exit())
|
||||
};
|
||||
match config::config_set(repo.as_ref(), global, &key, &value) {
|
||||
Ok(()) => println!("{}", ui::success(&format!("{key} = {value}"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Get { global, key } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Repository::discover(Path::new(".")).ok()
|
||||
};
|
||||
match config::config_get(repo.as_ref(), global, &key) {
|
||||
Ok(Some(value)) => println!("{value}"),
|
||||
Ok(None) => println!("{}", ui::info(&format!("{key} is not set"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Show { global } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Repository::discover(Path::new(".")).ok()
|
||||
};
|
||||
match config::config_show(repo.as_ref(), global) {
|
||||
Ok(yaml) => print!("{yaml}"),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigCommand::Unset { global, key } => {
|
||||
let repo = if global {
|
||||
None
|
||||
} else {
|
||||
Some(open_repo_or_exit())
|
||||
};
|
||||
match config::config_unset(repo.as_ref(), global, &key) {
|
||||
Ok(()) => println!("{}", ui::success(&format!("unset {key}"))),
|
||||
Err(e) => {
|
||||
eprintln!("{}", ui::error(&e.to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Command::Sync { push } => {
|
||||
debug!("command: sync (push: {})", push);
|
||||
let repo = open_repo_or_exit();
|
||||
match bridge::sync(&repo, push) {
|
||||
Ok(msg) => println!("{msg}"),
|
||||
|
|
@ -814,7 +771,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
}
|
||||
Command::Remote { command } => {
|
||||
debug!("command: remote");
|
||||
let repo = open_repo_or_exit();
|
||||
match command {
|
||||
RemoteCommand::Add { name, url } => match remote::add(&repo, &name, &url) {
|
||||
|
|
@ -853,7 +809,6 @@ pub fn dispatch(cli: Cli) {
|
|||
}
|
||||
|
||||
fn open_repo_or_exit() -> Repository {
|
||||
debug!("discovering repository from current directory");
|
||||
match Repository::discover(Path::new(".")) {
|
||||
Ok(repo) => repo,
|
||||
Err(e) => {
|
||||
|
|
@ -864,7 +819,6 @@ fn open_repo_or_exit() -> Repository {
|
|||
}
|
||||
|
||||
fn run_diff(repo: &Repository) -> crate::error::Result<String> {
|
||||
debug!("computing diff");
|
||||
let ignore = IgnoreRules::load(&repo.workdir);
|
||||
let head_commit = tracking::resolve_head_commit(repo)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -46,19 +46,11 @@ pub struct EffectiveConfig {
|
|||
|
||||
impl Config {
|
||||
pub fn load_local(repo: &Repository) -> Result<Option<Config>> {
|
||||
debug!(
|
||||
"loading local config from {}",
|
||||
repo.local_config_path().display()
|
||||
);
|
||||
let path = repo.local_config_path();
|
||||
Self::load_from(&path)
|
||||
}
|
||||
|
||||
pub fn load_global() -> Result<Option<Config>> {
|
||||
debug!(
|
||||
"loading global config from {}",
|
||||
Self::global_config_path().display()
|
||||
);
|
||||
let path = Self::global_config_path();
|
||||
Self::load_from(&path)
|
||||
}
|
||||
|
|
@ -138,7 +130,6 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn load_effective(repo: &crate::repo::Repository) -> EffectiveConfig {
|
||||
debug!("loading effective config");
|
||||
let local = Config::load_local(repo).ok().flatten();
|
||||
let global = Config::load_global().ok().flatten();
|
||||
Config::effective(local, global)
|
||||
|
|
@ -239,7 +230,6 @@ fn unset_field(config: &mut Config, section: &str, field: &str) -> Result<()> {
|
|||
|
||||
/// Set a configuration value in the local or global config file.
|
||||
pub fn config_set(repo: Option<&Repository>, global: bool, key: &str, value: &str) -> Result<()> {
|
||||
debug!("setting config {key} = {value} (global: {global})");
|
||||
let (section, field) = parse_key(key)?;
|
||||
if global {
|
||||
let path = Config::global_config_path();
|
||||
|
|
@ -257,7 +247,6 @@ pub fn config_set(repo: Option<&Repository>, global: bool, key: &str, value: &st
|
|||
|
||||
/// Get a configuration value, resolving local-first then global.
|
||||
pub fn config_get(repo: Option<&Repository>, global: bool, key: &str) -> Result<Option<String>> {
|
||||
debug!("getting config {key} (global: {global})");
|
||||
let (section, field) = parse_key(key)?;
|
||||
if global {
|
||||
let config = Config::load_global()?.unwrap_or_default();
|
||||
|
|
@ -293,7 +282,6 @@ pub fn config_show(repo: Option<&Repository>, global: bool) -> Result<String> {
|
|||
|
||||
/// Remove a configuration key from the local or global config file.
|
||||
pub fn config_unset(repo: Option<&Repository>, global: bool, key: &str) -> Result<()> {
|
||||
debug!("unsetting config {key} (global: {global})");
|
||||
let (section, field) = parse_key(key)?;
|
||||
if global {
|
||||
let path = Config::global_config_path();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ use crate::tracking::FileTree;
|
|||
use crate::ui;
|
||||
|
||||
pub fn render_diff(committed: &FileTree, changes: &[FileChange]) -> String {
|
||||
debug!("rendering diff for {} change(s)", changes.len());
|
||||
let mut output = String::new();
|
||||
|
||||
for change in changes {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ pub struct IgnorePattern {
|
|||
|
||||
impl IgnoreRules {
|
||||
pub fn load(workdir: &Path) -> Self {
|
||||
debug!("loading ignore rules from {}", workdir.display());
|
||||
let arcignore = workdir.join(".arcignore");
|
||||
let ignore = workdir.join(".ignore");
|
||||
|
||||
|
|
@ -24,14 +23,12 @@ impl IgnoreRules {
|
|||
None
|
||||
};
|
||||
|
||||
let rules = match path {
|
||||
match path {
|
||||
Some(p) => Self::parse_file(&p),
|
||||
None => Self {
|
||||
patterns: Vec::new(),
|
||||
},
|
||||
};
|
||||
debug!("loaded {} ignore pattern(s)", rules.patterns.len());
|
||||
rules
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_file(path: &Path) -> Self {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use crate::tracking;
|
|||
use crate::ui;
|
||||
|
||||
pub fn log(repo: &Repository, range: Option<&str>) -> Result<String> {
|
||||
debug!("showing log (range: {:?})", range);
|
||||
let resolved = resolve::parse_and_resolve_range(repo, range)?;
|
||||
let chain = &resolved.chain[resolved.start_idx..];
|
||||
|
||||
|
|
@ -52,7 +51,6 @@ pub fn log(repo: &Repository, range: Option<&str>) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn show(repo: &Repository, target: &str) -> Result<String> {
|
||||
debug!("showing commit '{}'", target);
|
||||
let commit_id = resolve::resolve_target(repo, target)?;
|
||||
let obj = store::read_commit_object(repo, &commit_id)?;
|
||||
let c = &obj.commit;
|
||||
|
|
@ -119,7 +117,6 @@ pub fn show(repo: &Repository, target: &str) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn history(repo: &Repository, file: &str, range: Option<&str>) -> Result<String> {
|
||||
debug!("showing history for file '{}'", file);
|
||||
let resolved = resolve::parse_and_resolve_range(repo, range)?;
|
||||
let chain = &resolved.chain[resolved.start_idx..];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
#[macro_use]
|
||||
pub mod ui;
|
||||
|
||||
pub mod bridge;
|
||||
mod cli;
|
||||
pub mod config;
|
||||
|
|
@ -19,6 +16,7 @@ pub mod signing;
|
|||
pub mod stash;
|
||||
pub mod store;
|
||||
pub mod tracking;
|
||||
pub mod ui;
|
||||
|
||||
fn main() {
|
||||
let cli = cli::parse();
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ pub fn three_way_merge(base: &FileTree, ours: &FileTree, theirs: &FileTree) -> M
|
|||
.chain(theirs.keys())
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
"performing three-way merge across {} path(s)",
|
||||
all_paths.len()
|
||||
);
|
||||
|
||||
let mut tree = FileTree::new();
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
|
|
@ -76,7 +71,6 @@ pub fn three_way_merge(base: &FileTree, ours: &FileTree, theirs: &FileTree) -> M
|
|||
}
|
||||
}
|
||||
|
||||
debug!("merge result: {} conflict(s)", conflicts.len());
|
||||
MergeOutcome { tree, conflicts }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ use crate::store::{self, CommitObject};
|
|||
use crate::tracking::{self, FileTree};
|
||||
|
||||
pub fn reset(repo: &Repository, files: &[String]) -> Result<String> {
|
||||
debug!("resetting worktree");
|
||||
let head_commit = tracking::resolve_head_commit(repo)?;
|
||||
let ignore = IgnoreRules::load(&repo.workdir);
|
||||
|
||||
|
|
@ -79,7 +78,6 @@ pub fn reset(repo: &Repository, files: &[String]) -> Result<String> {
|
|||
|
||||
refs::remove_empty_dirs(&repo.workdir)?;
|
||||
|
||||
debug!("reset {} file(s)", reset_count);
|
||||
if reset_count == 0 {
|
||||
Ok("no matching files to reset".to_string())
|
||||
} else {
|
||||
|
|
@ -88,12 +86,10 @@ pub fn reset(repo: &Repository, files: &[String]) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn revert(repo: &Repository, target: &str) -> Result<CommitId> {
|
||||
debug!("reverting target '{}'", target);
|
||||
require_clean_worktree(repo)?;
|
||||
|
||||
let head_id = tracking::resolve_head_commit(repo)?.ok_or(ArcError::NoCommitsYet)?;
|
||||
let commits = resolve_commit_or_range(repo, target)?;
|
||||
debug!("processing revert for {} commit(s)", commits.len());
|
||||
|
||||
let mut current_tree = tracking::materialize_committed_tree(repo, &head_id)?;
|
||||
|
||||
|
|
@ -127,7 +123,6 @@ pub fn revert(repo: &Repository, target: &str) -> Result<CommitId> {
|
|||
}
|
||||
|
||||
pub fn merge_branch(repo: &Repository, target: &str) -> Result<CommitId> {
|
||||
debug!("merging target '{}'", target);
|
||||
require_clean_worktree(repo)?;
|
||||
|
||||
let ours_id = tracking::resolve_head_commit(repo)?.ok_or(ArcError::NoCommitsYet)?;
|
||||
|
|
@ -138,7 +133,6 @@ pub fn merge_branch(repo: &Repository, target: &str) -> Result<CommitId> {
|
|||
}
|
||||
|
||||
let base_id = find_merge_base(repo, &ours_id, &theirs_id)?;
|
||||
debug!("merge base: {:?}", base_id);
|
||||
|
||||
let base_tree = match &base_id {
|
||||
Some(id) => tracking::materialize_committed_tree(repo, id)?,
|
||||
|
|
@ -155,13 +149,11 @@ pub fn merge_branch(repo: &Repository, target: &str) -> Result<CommitId> {
|
|||
return Err(ArcError::MergeConflicts(outcome.conflicts));
|
||||
}
|
||||
|
||||
debug!("merge completed");
|
||||
let message = format!("merge {target}");
|
||||
commit_tree(repo, &message, vec![ours_id, theirs_id], &outcome.tree)
|
||||
}
|
||||
|
||||
pub fn graft(repo: &Repository, target: &str, onto: &str) -> Result<Vec<CommitId>> {
|
||||
debug!("grafting target '{}' onto '{}'", target, onto);
|
||||
require_clean_worktree(repo)?;
|
||||
|
||||
let source_commits = resolve_commit_or_range(repo, target)?;
|
||||
|
|
@ -202,7 +194,6 @@ pub fn graft(repo: &Repository, target: &str, onto: &str) -> Result<Vec<CommitId
|
|||
new_ids.push(new_id);
|
||||
}
|
||||
|
||||
debug!("grafted {} commit(s)", new_ids.len());
|
||||
if is_bookmark {
|
||||
let bookmark_path = repo.bookmarks_dir().join(onto);
|
||||
let ref_target = RefTarget {
|
||||
|
|
@ -232,7 +223,6 @@ pub fn graft(repo: &Repository, target: &str, onto: &str) -> Result<Vec<CommitId
|
|||
}
|
||||
|
||||
fn require_clean_worktree(repo: &Repository) -> Result<()> {
|
||||
debug!("checking worktree is clean");
|
||||
let (report, _) = tracking::status(repo)?;
|
||||
if !report.is_clean() {
|
||||
return Err(ArcError::DirtyWorktree);
|
||||
|
|
@ -256,11 +246,6 @@ fn find_merge_base(
|
|||
ours: &CommitId,
|
||||
theirs: &CommitId,
|
||||
) -> Result<Option<CommitId>> {
|
||||
debug!(
|
||||
"searching for merge base between {} and {}",
|
||||
&ours.0[..12.min(ours.0.len())],
|
||||
&theirs.0[..12.min(theirs.0.len())]
|
||||
);
|
||||
let mut ours_ancestors = HashSet::new();
|
||||
collect_ancestors(repo, ours, &mut ours_ancestors)?;
|
||||
ours_ancestors.insert(ours.0.clone());
|
||||
|
|
|
|||
12
src/refs.rs
12
src/refs.rs
|
|
@ -49,13 +49,11 @@ fn short_id(id: &CommitId) -> &str {
|
|||
pub fn mark_add(repo: &Repository, name: &str, commit: Option<&str>) -> Result<CommitId> {
|
||||
crate::repo::validate_ref_name(name)?;
|
||||
let id = resolve_commit_or_head(repo, commit)?;
|
||||
debug!("adding bookmark '{}' at {id}", name);
|
||||
write_ref_target(&repo.bookmarks_dir().join(name), &id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn mark_rm(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("removing bookmark '{}'", name);
|
||||
crate::repo::validate_ref_name(name)?;
|
||||
let path = repo.bookmarks_dir().join(name);
|
||||
if !path.exists() {
|
||||
|
|
@ -71,7 +69,6 @@ pub fn mark_rm(repo: &Repository, name: &str) -> Result<()> {
|
|||
}
|
||||
|
||||
pub fn mark_list(repo: &Repository) -> Result<String> {
|
||||
debug!("listing bookmarks");
|
||||
let active = active_bookmark(repo)?;
|
||||
let dir = repo.bookmarks_dir();
|
||||
let mut entries: Vec<String> = Vec::new();
|
||||
|
|
@ -107,7 +104,6 @@ pub fn mark_list(repo: &Repository) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn mark_rename(repo: &Repository, name: &str, new_name: &str) -> Result<()> {
|
||||
debug!("renaming bookmark '{}' to '{}'", name, new_name);
|
||||
crate::repo::validate_ref_name(name)?;
|
||||
crate::repo::validate_ref_name(new_name)?;
|
||||
let old_path = repo.bookmarks_dir().join(name);
|
||||
|
|
@ -146,13 +142,11 @@ pub fn tag_add(repo: &Repository, name: &str, commit: Option<&str>) -> Result<Co
|
|||
return Err(ArcError::TagAlreadyExists(name.to_string()));
|
||||
}
|
||||
let id = resolve_commit_or_head(repo, commit)?;
|
||||
debug!("adding tag '{}' at {id}", name);
|
||||
write_ref_target(&path, &id)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn tag_rm(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("removing tag '{}'", name);
|
||||
crate::repo::validate_ref_name(name)?;
|
||||
let path = repo.tags_dir().join(name);
|
||||
if !path.exists() {
|
||||
|
|
@ -163,7 +157,6 @@ pub fn tag_rm(repo: &Repository, name: &str) -> Result<()> {
|
|||
}
|
||||
|
||||
pub fn tag_list(repo: &Repository) -> Result<String> {
|
||||
debug!("listing tags");
|
||||
let dir = repo.tags_dir();
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for entry in fs::read_dir(&dir)? {
|
||||
|
|
@ -192,7 +185,6 @@ pub fn tag_list(repo: &Repository) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn switch(repo: &Repository, target: &str) -> Result<String> {
|
||||
debug!("switching to target '{}'", target);
|
||||
let ignore = IgnoreRules::load(&repo.workdir);
|
||||
let head_commit = tracking::resolve_head_commit(repo)?;
|
||||
|
||||
|
|
@ -238,7 +230,6 @@ pub fn switch(repo: &Repository, target: &str) -> Result<String> {
|
|||
Head::Unborn { .. } => unreachable!(),
|
||||
};
|
||||
|
||||
debug!("target resolved, writing new worktree");
|
||||
clean_tracked_files(repo, &committed)?;
|
||||
|
||||
let new_tree = tracking::materialize_committed_tree(repo, &target_commit)?;
|
||||
|
|
@ -250,7 +241,6 @@ pub fn switch(repo: &Repository, target: &str) -> Result<String> {
|
|||
}
|
||||
|
||||
pub fn clean_tracked_files(repo: &Repository, tree: &tracking::FileTree) -> Result<()> {
|
||||
debug!("cleaning {} tracked file(s) from worktree", tree.len());
|
||||
for path in tree.keys() {
|
||||
crate::repo::validate_repo_path(path)?;
|
||||
let abs = repo.workdir.join(path);
|
||||
|
|
@ -288,7 +278,6 @@ pub fn remove_empty_dirs(dir: &std::path::Path) -> Result<()> {
|
|||
}
|
||||
|
||||
pub fn write_tree(repo: &Repository, tree: &tracking::FileTree) -> Result<()> {
|
||||
debug!("writing {} file(s) to worktree", tree.len());
|
||||
for (path, bytes) in tree {
|
||||
crate::repo::validate_repo_path(path)?;
|
||||
let abs = repo.workdir.join(path);
|
||||
|
|
@ -305,7 +294,6 @@ pub fn update_refs_after_commit(
|
|||
head: &Head,
|
||||
commit_id: &CommitId,
|
||||
) -> Result<()> {
|
||||
debug!("updating refs after commit {commit_id}");
|
||||
let ref_target = RefTarget {
|
||||
commit: Some(commit_id.clone()),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ fn remotes_path(repo: &Repository) -> std::path::PathBuf {
|
|||
///
|
||||
/// Returns an empty `RemotesFile` if the file does not yet exist.
|
||||
pub fn load(repo: &Repository) -> Result<RemotesFile> {
|
||||
debug!("loading remotes from {}", remotes_path(repo).display());
|
||||
let path = remotes_path(repo);
|
||||
if !path.exists() {
|
||||
return Ok(RemotesFile {
|
||||
|
|
@ -50,7 +49,6 @@ pub fn save(repo: &Repository, file: &RemotesFile) -> Result<()> {
|
|||
/// The name is validated as a ref name. Returns an error if the remote
|
||||
/// already exists.
|
||||
pub fn add(repo: &Repository, name: &str, url: &str) -> Result<()> {
|
||||
debug!("adding remote '{}' at {}", name, url);
|
||||
crate::repo::validate_ref_name(name)?;
|
||||
let mut file = load(repo)?;
|
||||
if file.remotes.contains_key(name) {
|
||||
|
|
@ -69,7 +67,6 @@ pub fn add(repo: &Repository, name: &str, url: &str) -> Result<()> {
|
|||
///
|
||||
/// Returns an error if the remote does not exist.
|
||||
pub fn rm(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("removing remote '{}'", name);
|
||||
let mut file = load(repo)?;
|
||||
if file.remotes.remove(name).is_none() {
|
||||
return Err(ArcError::RemoteNotFound(name.to_string()));
|
||||
|
|
@ -82,7 +79,6 @@ pub fn rm(repo: &Repository, name: &str) -> Result<()> {
|
|||
/// Each line has the form ` <name>\t<url>\n`.
|
||||
/// Returns an empty string if no remotes are configured.
|
||||
pub fn list(repo: &Repository) -> Result<String> {
|
||||
debug!("listing remotes");
|
||||
let file = load(repo)?;
|
||||
let mut out = String::new();
|
||||
for (name, entry) in &file.remotes {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pub struct Repository {
|
|||
|
||||
impl Repository {
|
||||
pub fn init(path: &Path) -> Result<Self> {
|
||||
debug!("initializing repository at {}", path.display());
|
||||
let workdir = path.canonicalize().map_err(|_| {
|
||||
ArcError::invalid_path(format!("cannot resolve path: {}", path.display()))
|
||||
})?;
|
||||
|
|
@ -44,12 +43,10 @@ impl Repository {
|
|||
let ref_yaml = serde_yaml::to_string(&ref_target)?;
|
||||
fs::write(repo.bookmarks_dir().join("main"), ref_yaml)?;
|
||||
|
||||
debug!("created .arc directory structure");
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
debug!("opening repository at {}", path.display());
|
||||
let workdir = path.canonicalize().map_err(|_| {
|
||||
ArcError::invalid_path(format!("cannot resolve path: {}", path.display()))
|
||||
})?;
|
||||
|
|
@ -63,14 +60,12 @@ impl Repository {
|
|||
}
|
||||
|
||||
pub fn discover(from: &Path) -> Result<Self> {
|
||||
debug!("discovering repository from {}", from.display());
|
||||
let mut current = from.canonicalize().map_err(|_| {
|
||||
ArcError::invalid_path(format!("cannot resolve path: {}", from.display()))
|
||||
})?;
|
||||
|
||||
loop {
|
||||
if current.join(ARC_DIR).is_dir() {
|
||||
debug!("found repository at {}", current.display());
|
||||
return Self::open(¤t);
|
||||
}
|
||||
if !current.pop() {
|
||||
|
|
@ -104,14 +99,12 @@ impl Repository {
|
|||
}
|
||||
|
||||
pub fn load_head(&self) -> Result<Head> {
|
||||
debug!("loading HEAD from {}", self.head_path().display());
|
||||
let contents = fs::read_to_string(self.head_path())?;
|
||||
let head: Head = serde_yaml::from_str(&contents)?;
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
pub fn save_head(&self, head: &Head) -> Result<()> {
|
||||
debug!("saving HEAD: {:?}", head);
|
||||
let yaml = serde_yaml::to_string(head)?;
|
||||
fs::write(self.head_path(), yaml)?;
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -7,11 +7,8 @@ use crate::store::CommitObject;
|
|||
use crate::tracking;
|
||||
|
||||
pub fn resolve_target(repo: &Repository, target: &str) -> Result<CommitId> {
|
||||
debug!("resolving target '{}'", target);
|
||||
if target == "HEAD" {
|
||||
let id = tracking::resolve_head_commit(repo)?.ok_or(ArcError::NoCommitsYet)?;
|
||||
debug!("resolved '{}' to {}", target, &id.0);
|
||||
return Ok(id);
|
||||
return tracking::resolve_head_commit(repo)?.ok_or(ArcError::NoCommitsYet);
|
||||
}
|
||||
|
||||
if crate::repo::validate_ref_name(target).is_ok() {
|
||||
|
|
@ -20,7 +17,6 @@ pub fn resolve_target(repo: &Repository, target: &str) -> Result<CommitId> {
|
|||
let contents = fs::read_to_string(&bookmark_path)?;
|
||||
let ref_target: RefTarget = serde_yaml::from_str(&contents)?;
|
||||
if let Some(id) = ref_target.commit {
|
||||
debug!("resolved '{}' to {}", target, &id.0);
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,19 +26,15 @@ pub fn resolve_target(repo: &Repository, target: &str) -> Result<CommitId> {
|
|||
let contents = fs::read_to_string(&tag_path)?;
|
||||
let ref_target: RefTarget = serde_yaml::from_str(&contents)?;
|
||||
if let Some(id) = ref_target.commit {
|
||||
debug!("resolved '{}' to {}", target, &id.0);
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = resolve_commit_prefix(repo, target)?;
|
||||
debug!("resolved '{}' to {}", target, &id.0);
|
||||
Ok(id)
|
||||
resolve_commit_prefix(repo, target)
|
||||
}
|
||||
|
||||
fn resolve_commit_prefix(repo: &Repository, prefix: &str) -> Result<CommitId> {
|
||||
debug!("searching commits with prefix '{}'", prefix);
|
||||
let commits_dir = repo.commits_dir();
|
||||
let entries = match fs::read_dir(&commits_dir) {
|
||||
Ok(e) => e,
|
||||
|
|
@ -59,7 +51,6 @@ fn resolve_commit_prefix(repo: &Repository, prefix: &str) -> Result<CommitId> {
|
|||
}
|
||||
}
|
||||
|
||||
debug!("found {} match(es) for prefix '{}'", matches.len(), prefix);
|
||||
match matches.len() {
|
||||
0 => Err(ArcError::UnknownRevision(prefix.to_string())),
|
||||
1 => {
|
||||
|
|
@ -79,7 +70,6 @@ pub struct ResolvedRange {
|
|||
}
|
||||
|
||||
pub fn parse_and_resolve_range(repo: &Repository, spec: Option<&str>) -> Result<ResolvedRange> {
|
||||
debug!("parsing range: {:?}", spec);
|
||||
let (start, end) = parse_range_spec(spec)?;
|
||||
|
||||
let end_target = end.as_deref().unwrap_or("HEAD");
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const NAMESPACE: &str = "arc";
|
|||
///
|
||||
/// Returns the signature as a PEM-encoded string.
|
||||
pub fn sign(key_path: &str, data: &[u8]) -> Result<String> {
|
||||
debug!("signing with key {key_path}");
|
||||
let expanded = expand_path(key_path);
|
||||
let path = Path::new(&expanded);
|
||||
|
||||
|
|
@ -28,7 +27,6 @@ pub fn sign(key_path: &str, data: &[u8]) -> Result<String> {
|
|||
///
|
||||
/// The public key is extracted from the signature itself for verification.
|
||||
pub fn verify(signature_pem: &str, data: &[u8]) -> Result<bool> {
|
||||
debug!("verifying signature");
|
||||
let sig: SshSig = signature_pem
|
||||
.parse()
|
||||
.map_err(|e| ArcError::SigningError(format!("invalid signature: {e}")))?;
|
||||
|
|
|
|||
12
src/stash.rs
12
src/stash.rs
|
|
@ -93,7 +93,6 @@ fn save_stash_file(repo: &Repository, name: &str, file: &StashFile) -> Result<()
|
|||
|
||||
/// Create a new named stash and set it as active.
|
||||
pub fn create(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("creating stash '{}'", name);
|
||||
repo::validate_ref_name(name)?;
|
||||
fs::create_dir_all(stash_named_dir(repo))?;
|
||||
|
||||
|
|
@ -115,7 +114,6 @@ pub fn create(repo: &Repository, name: &str) -> Result<()> {
|
|||
|
||||
/// Switch the active stash to an existing named stash.
|
||||
pub fn use_stash(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("switching to stash '{}'", name);
|
||||
repo::validate_ref_name(name)?;
|
||||
|
||||
let path = stash_file_path(repo, name);
|
||||
|
|
@ -133,7 +131,6 @@ pub fn use_stash(repo: &Repository, name: &str) -> Result<()> {
|
|||
|
||||
/// Push current dirty changes onto the active stash and reset the worktree.
|
||||
pub fn push(repo: &Repository) -> Result<String> {
|
||||
debug!("pushing changes to active stash");
|
||||
let state = load_state(repo)?;
|
||||
let name = state.active.ok_or(ArcError::NoActiveStash)?;
|
||||
repo::validate_ref_name(&name)?;
|
||||
|
|
@ -213,13 +210,11 @@ pub fn push(repo: &Repository) -> Result<String> {
|
|||
refs::remove_empty_dirs(&repo.workdir)?;
|
||||
|
||||
let n = changes.len();
|
||||
debug!("pushed {} change(s) to stash '{}'", n, name);
|
||||
Ok(format!("pushed {n} change(s) to stash '{name}'"))
|
||||
}
|
||||
|
||||
/// Pop the most recent entry from the active stash and apply it to the worktree.
|
||||
pub fn pop(repo: &Repository) -> Result<String> {
|
||||
debug!("popping from active stash");
|
||||
let state = load_state(repo)?;
|
||||
let name = state.active.ok_or(ArcError::NoActiveStash)?;
|
||||
repo::validate_ref_name(&name)?;
|
||||
|
|
@ -238,11 +233,6 @@ pub fn pop(repo: &Repository) -> Result<String> {
|
|||
.entries
|
||||
.pop()
|
||||
.ok_or_else(|| ArcError::StashEmpty(name.clone()))?;
|
||||
debug!(
|
||||
"popping {} change(s) from stash '{}'",
|
||||
entry.changes.len(),
|
||||
name
|
||||
);
|
||||
let head_commit = tracking::resolve_head_commit(repo)?;
|
||||
|
||||
if entry.base != head_commit {
|
||||
|
|
@ -281,7 +271,6 @@ pub fn pop(repo: &Repository) -> Result<String> {
|
|||
|
||||
/// Remove a named stash. If it was active, deactivate it.
|
||||
pub fn rm(repo: &Repository, name: &str) -> Result<()> {
|
||||
debug!("removing stash '{}'", name);
|
||||
repo::validate_ref_name(name)?;
|
||||
|
||||
let path = stash_file_path(repo, name);
|
||||
|
|
@ -302,7 +291,6 @@ pub fn rm(repo: &Repository, name: &str) -> Result<()> {
|
|||
|
||||
/// List all named stashes, marking the active one.
|
||||
pub fn list(repo: &Repository) -> Result<String> {
|
||||
debug!("listing stashes");
|
||||
let state = load_state(repo)?;
|
||||
let active = state.active.as_deref();
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ pub fn commit_object_path(repo: &Repository, id: &CommitId) -> PathBuf {
|
|||
}
|
||||
|
||||
pub fn write_commit_object(repo: &Repository, obj: &CommitObject) -> Result<()> {
|
||||
debug!("writing commit object {}", obj.commit.id.0);
|
||||
let msgpack = rmp_serde::to_vec(obj)?;
|
||||
let compressed =
|
||||
zstd::stream::encode_all(Cursor::new(&msgpack), 3).map_err(std::io::Error::other)?;
|
||||
|
|
@ -35,7 +34,6 @@ pub fn write_commit_object(repo: &Repository, obj: &CommitObject) -> Result<()>
|
|||
}
|
||||
|
||||
pub fn read_commit_object(repo: &Repository, id: &CommitId) -> Result<CommitObject> {
|
||||
debug!("reading commit object {}", id.0);
|
||||
let path = commit_object_path(repo, id);
|
||||
let compressed = fs::read(&path)?;
|
||||
let mut decoder =
|
||||
|
|
@ -70,7 +68,6 @@ struct CommitForHash<'a> {
|
|||
}
|
||||
|
||||
pub fn compute_delta_id(base: &Option<CommitId>, changes: &[FileChange]) -> Result<DeltaId> {
|
||||
debug!("computing delta id (base: {:?})", base);
|
||||
let hashable = DeltaForHash { base, changes };
|
||||
let bytes = rmp_serde::to_vec(&hashable)
|
||||
.map_err(|e| crate::error::ArcError::HashError(e.to_string()))?;
|
||||
|
|
@ -84,7 +81,6 @@ pub fn compute_commit_id(
|
|||
author: &Option<Signature>,
|
||||
timestamp: i64,
|
||||
) -> Result<CommitId> {
|
||||
debug!("computing commit id for message: {}", message);
|
||||
let hashable = CommitForHash {
|
||||
parents,
|
||||
delta,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@ use crate::ui;
|
|||
pub type FileTree = BTreeMap<String, Vec<u8>>;
|
||||
|
||||
pub fn scan_worktree(repo: &Repository, ignore: &IgnoreRules) -> Result<FileTree> {
|
||||
debug!("scanning worktree at {}", repo.workdir.display());
|
||||
let mut tree = BTreeMap::new();
|
||||
scan_dir(&repo.workdir, &repo.workdir, ignore, &mut tree)?;
|
||||
debug!("found {} file(s) in worktree", tree.len());
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
|
|
@ -63,18 +61,15 @@ fn to_rel_string(root: &Path, abs: &Path) -> String {
|
|||
}
|
||||
|
||||
pub fn materialize_committed_tree(repo: &Repository, head: &CommitId) -> Result<FileTree> {
|
||||
debug!("materializing tree at commit {}", head.0);
|
||||
let history = load_linear_history(repo, head)?;
|
||||
let mut tree = BTreeMap::new();
|
||||
for obj in &history {
|
||||
apply_delta(&mut tree, &obj.delta);
|
||||
}
|
||||
debug!("materialized tree with {} file(s)", tree.len());
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
pub fn load_linear_history(repo: &Repository, head: &CommitId) -> Result<Vec<CommitObject>> {
|
||||
debug!("loading history from {}", head.0);
|
||||
let mut chain = Vec::new();
|
||||
let mut current = head.clone();
|
||||
|
||||
|
|
@ -89,7 +84,6 @@ pub fn load_linear_history(repo: &Repository, head: &CommitId) -> Result<Vec<Com
|
|||
}
|
||||
|
||||
chain.reverse();
|
||||
debug!("loaded {} commit(s) in history chain", chain.len());
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
|
|
@ -152,31 +146,19 @@ pub fn detect_changes(committed: &FileTree, worktree: &FileTree) -> Vec<FileChan
|
|||
}
|
||||
}
|
||||
|
||||
debug!("detected {} change(s)", changes.len());
|
||||
changes
|
||||
}
|
||||
|
||||
pub fn resolve_head_commit(repo: &Repository) -> Result<Option<CommitId>> {
|
||||
debug!("resolving HEAD commit");
|
||||
let head = repo.load_head()?;
|
||||
match head {
|
||||
Head::Unborn { .. } => {
|
||||
debug!("HEAD is unborn");
|
||||
Ok(None)
|
||||
}
|
||||
Head::Attached { commit, .. } => {
|
||||
debug!("HEAD at {}", commit.0);
|
||||
Ok(Some(commit))
|
||||
}
|
||||
Head::Detached { commit } => {
|
||||
debug!("HEAD at {}", commit.0);
|
||||
Ok(Some(commit))
|
||||
}
|
||||
Head::Unborn { .. } => Ok(None),
|
||||
Head::Attached { commit, .. } => Ok(Some(commit)),
|
||||
Head::Detached { commit } => Ok(Some(commit)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit(repo: &Repository, message: &str) -> Result<CommitId> {
|
||||
debug!("committing with message: {}", message);
|
||||
let ignore = IgnoreRules::load(&repo.workdir);
|
||||
let head = repo.load_head()?;
|
||||
let head_commit = resolve_head_commit(repo)?;
|
||||
|
|
@ -188,7 +170,6 @@ pub fn commit(repo: &Repository, message: &str) -> Result<CommitId> {
|
|||
|
||||
let worktree = scan_worktree(repo, &ignore)?;
|
||||
let changes = detect_changes(&committed, &worktree);
|
||||
debug!("found {} change(s) to commit", changes.len());
|
||||
|
||||
if changes.is_empty() {
|
||||
return Err(ArcError::NothingToCommit);
|
||||
|
|
@ -250,7 +231,6 @@ pub fn commit(repo: &Repository, message: &str) -> Result<CommitId> {
|
|||
|
||||
crate::refs::update_refs_after_commit(repo, &head, &commit_id)?;
|
||||
|
||||
debug!("created commit {}", commit_id.0);
|
||||
Ok(commit_id)
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +317,6 @@ impl fmt::Display for StatusReport {
|
|||
}
|
||||
|
||||
pub fn status(repo: &Repository) -> Result<(StatusReport, Vec<FileChange>)> {
|
||||
debug!("computing status");
|
||||
let ignore = IgnoreRules::load(&repo.workdir);
|
||||
let head_commit = resolve_head_commit(repo)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
use colored::Colorize;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => {
|
||||
if $crate::cli::verbose() {
|
||||
eprintln!("{} {}", $crate::ui::SYM_ARROW, format!($($arg)*));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const SYM_ARROW: &str = "▸";
|
||||
pub const SYM_CHECK: &str = "✓";
|
||||
pub const SYM_CROSS: &str = "✗";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue