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