diff --git a/Cargo.lock b/Cargo.lock index a0691ac..922df7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e" + [[package]] name = "assert_cmd" version = "2.0.2" @@ -383,9 +389,9 @@ checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "predicates" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" +checksum = "95e5a7689e456ab905c22c2b48225bb921aba7c8dfa58440d68ba13f6222a715" dependencies = [ "difflib", "float-cmp", @@ -585,6 +591,7 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" name = "stc" version = "0.1.0" dependencies = [ + "anyhow", "assert_cmd", "assert_fs", "clap", @@ -593,13 +600,14 @@ dependencies = [ "phf", "predicates", "serde", + "thiserror", ] [[package]] name = "syn" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", @@ -632,6 +640,26 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index 29c8212..d919bb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ keywords = ["git"] exclude = [".gitignore", ".github", "target"] [dependencies] +anyhow = "1.0.48" const_format = "0.2.22" serde = { version = "1.0.130", features = ["derive"] } csv = "1.1.6" +thiserror = "1.0.30" [dependencies.clap] version = "3.0.0-beta.5" diff --git a/src/git.rs b/src/git.rs index 22d942f..80821fb 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,21 +1,22 @@ +use ::anyhow::Result; use ::const_format::concatcp; use ::csv::ReaderBuilder; use ::serde::Deserialize; use ::std::{ + self, borrow::{Cow, ToOwned}, clone::Clone, collections::{BTreeSet, HashMap}, - convert::AsRef, - default::Default, - error::Error, + convert::{AsRef, Into}, format, iter::{IntoIterator, Iterator}, - option::Option::{self, None}, - result::Result::{self, Err, Ok}, + option::Option::{self, Some}, + result::Result::{Err, Ok}, string::{String, ToString}, vec::Vec, write, }; +use ::thiserror::Error; pub const NON_EXISTANT_OBJECT: ObjectName<'static> = ObjectName::new("0000000000000000000000000000000000000000"); @@ -27,50 +28,46 @@ pub const STC_REMOTE_REF_PREFIX: &str = concatcp!(STC_REF_PREFIX, "remote/"); pub const BRANCH_REF_PREFIX: &str = "refs/heads/"; -#[derive(Debug)] -pub struct Status { +#[derive(Error, Debug)] +#[error("exitcode={exitcode:?}, stdout={stdout:?}, stderr={stderr:?}")] +pub struct ExecStatus { pub exitcode: i32, - pub stdout: Vec, - pub stderr: Vec, + pub stdout: String, + pub stderr: String, } -impl Status { - pub fn new(exitcode: i32, stdout: Vec, stderr: Vec) -> Self { - Status { +impl ExecStatus { + pub fn new(exitcode: i32, stdout: String, stderr: String) -> Self { + ExecStatus { exitcode, stdout, stderr, } } - pub fn with(exitcode: i32) -> Self { - Status { + pub fn from(exitcode: i32, stdout: Vec, stderr: Vec) -> Self { + // TODO: use OsString. + ExecStatus { exitcode, - stdout: Default::default(), - stderr: Default::default(), + stdout: String::from_utf8_lossy(stdout.as_slice()).to_string(), + stderr: String::from_utf8_lossy(stderr.as_slice()).to_string(), } } -} - -impl ::std::fmt::Display for Status { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - write!(f, "SuperError is here!") - } -} -impl Error for Status { - fn source(&self) -> Option<&(dyn Error + 'static)> { - // XXX - None //Some(&self.side) + pub fn result(self) -> Result<()> { + match self.exitcode { + 0 => Ok(()), + _ => Err(self.into()), + } } } pub trait Git { - fn exec(&self, args: &[&str]) -> Result; + fn exec(&self, args: &[&str]) -> Result; - fn snapshot(&self) -> Result { + fn snapshot(&self) -> Result { let status = self.exec(&["for-each-ref", "--format", FIELD_FORMATS.join(",").as_str()])?; - let refs = parse_ref(status.stdout.as_slice()).map_err(|_err| Status::with(1))?; + let refs = parse_ref(status.stdout.as_bytes())?; let head = refs .values() .find(|r| r.head) @@ -78,36 +75,31 @@ pub trait Git { Ok(Repository { refs, head }) } - fn check_branchname<'a>(&self, name: &'a str) -> Result, Status> { + fn check_branchname<'a>(&self, name: &'a str) -> Result> { self.exec(&["check-ref-format", "--branch", name])?; Ok(BranchName(Cow::Owned(name.to_string()))) } - fn create_branch(&self, name: &BranchName, base: &BranchName) -> Result<(), Status> { + fn create_branch(&self, name: &BranchName, base: &BranchName) -> Result<()> { self.exec(&["branch", "--create-reflog", name.as_str(), base.as_str()]) .map(|_| {}) } - fn switch_branch(&self, b: &BranchName) -> Result<(), Status> { + fn switch_branch(&self, b: &BranchName) -> Result<()> { self.exec(&["switch", "--no-guess", b.as_str()]).map(|_| {}) } - fn create_symref( - &self, - name: &RefName, - target: &RefName, - reason: &'static str, - ) -> Result<(), Status> { + fn create_symref(&self, name: &RefName, target: &RefName, reason: &'static str) -> Result<()> { self.exec(&["symbolic-ref", "-m", reason, name.as_str(), target.as_str()]) .map(|_| {}) } - fn delete_symref(&self, name: &RefName) -> Result<(), Status> { + fn delete_symref(&self, name: &RefName) -> Result<()> { self.exec(&["symbolic-ref", "--delete", name.as_str()]) .map(|_| {}) } - fn create_ref(&self, name: &RefName, commit: &ObjectName) -> Result<(), Status> { + fn create_ref(&self, name: &RefName, commit: &ObjectName) -> Result<()> { self.exec(&[ "update-ref", "--no-deref", @@ -124,7 +116,7 @@ pub trait Git { name: &RefName, new_commit: &ObjectName, cur_commit: &ObjectName, - ) -> Result<(), Status> { + ) -> Result<()> { self.exec(&[ "update-ref", "--no-deref", @@ -136,7 +128,7 @@ pub trait Git { .map(|_| {}) } - fn delete_ref(&self, name: &RefName, cur_commit: &ObjectName) -> Result<(), Status> { + fn delete_ref(&self, name: &RefName, cur_commit: &ObjectName) -> Result<()> { self.exec(&[ "update-ref", "--no-deref", @@ -147,7 +139,7 @@ pub trait Git { .map(|_| {}) } - fn rebase_onto(&self, name: &BranchName) -> Result<(), Status> { + fn rebase_onto(&self, name: &BranchName) -> Result<()> { self.exec(&[ "rebase", "--committer-date-is-author-date", @@ -159,12 +151,7 @@ pub trait Git { .map(|_| {}) } - fn push( - &self, - name: &BranchName, - remote: &RemoteName, - expect: &ObjectName, - ) -> Result<(), Status> { + fn push(&self, name: &BranchName, remote: &RemoteName, expect: &ObjectName) -> Result<()> { self.exec(&[ "push", "--set-upstream", @@ -175,16 +162,16 @@ pub trait Git { .map(|_| {}) } - fn config_set(&self, key: &str, value: &str) -> Result<(), Status> { + fn config_set(&self, key: &str, value: &str) -> Result<()> { self.exec(&["config", "--local", key, value]).map(|_| {}) } - fn config_add(&self, key: &str, value: &str) -> Result<(), Status> { + fn config_add(&self, key: &str, value: &str) -> Result<()> { self.exec(&["config", "--local", "--add", key, value]) .map(|_| {}) } - fn config_unset_pattern(&self, key: &str, pattern: &str) -> Result<(), Status> { + fn config_unset_pattern(&self, key: &str, pattern: &str) -> Result<()> { match self.exec(&[ "config", "--local", @@ -194,22 +181,23 @@ pub trait Git { pattern, ]) { // 5 means the nothing matched. - Err(status) if status.exitcode != 5 => Err(status), + Err(err) => match err.downcast_ref::() { + Some(status) if status.exitcode != 5 => Err(err), + _ => Ok(()), + }, _ => Ok(()), } } - fn fetch_all_prune(&self) -> Result<(), Status> { + fn fetch_all_prune(&self) -> Result<()> { self.exec(&["fetch", "--all", "--prune"]).map(|_| {}) } - fn forkpoint(&self, base: &RefName, branch: &RefName) -> Result { + fn forkpoint(&self, base: &RefName, branch: &RefName) -> Result { self.exec(&["merge-base", "--fork-point", base.as_str(), branch.as_str()]) .map(move |status| { // TODO: handle not found - ObjectName(Cow::Owned( - String::from_utf8_lossy(&status.stdout).to_string(), - )) + ObjectName(Cow::Owned(status.stdout)) }) } } diff --git a/src/main.rs b/src/main.rs index 14be093..7d802f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ #![allow(missing_docs)] // TODO: change to warn/deny #![allow(dead_code)] // TODO: remove +use ::anyhow::Result; use ::clap::{self, Parser, Subcommand}; use ::std::{ option::Option::{self, None, Some}, - result::Result, string::String, }; @@ -81,9 +81,9 @@ enum Command { Sync, } -fn main() -> Result<(), git::Status> { +fn main() -> Result<()> { let root = Root::parse(); - let runner = runner::Runner::new("git"); + let runner = runner::Runner::new("git")?; let stc = stc::Stc::new(runner); match root.subcommand { Command::Clean => stc.clean(), diff --git a/src/runner.rs b/src/runner.rs index 2a5c474..5b369ad 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,11 +1,13 @@ use crate::git; +use ::anyhow::Result; use ::std::{ assert_ne, collections::HashMap, + convert::Into, option::Option::Some, path::PathBuf, process::{Command, Stdio}, - result::Result::{self, Err, Ok}, + result::Result::{Err, Ok}, }; pub struct Runner<'a> { @@ -15,17 +17,17 @@ pub struct Runner<'a> { } impl<'a> Runner<'a> { - pub fn new(gitpath: &'a str) -> Self { - Runner { + pub fn new(gitpath: &'a str) -> Result { + Ok(Runner { gitpath, - workdir: ::std::env::current_dir().expect("cannot determine current working directory"), + workdir: ::std::env::current_dir()?, env: HashMap::<&'a str, &'a str>::new(), - } + }) } } impl<'a> git::Git for Runner<'a> { - fn exec(&self, args: &[&str]) -> Result { + fn exec(&self, args: &[&str]) -> Result { let cmd = Command::new(self.gitpath) .args(args) .current_dir(&self.workdir) @@ -33,20 +35,19 @@ impl<'a> git::Git for Runner<'a> { .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .spawn() - .expect("failed to start git"); + .spawn()?; - let output = cmd.wait_with_output().expect("failed to wait on git"); + let output = cmd.wait_with_output()?; if output.status.success() { ::std::eprintln!("[OK] git {:?}", args); - Ok(git::Status::new(0, output.stdout, output.stderr)) + Ok(git::ExecStatus::from(0, output.stdout, output.stderr)) } else if let Some(code) = output.status.code() { assert_ne!(code, 0); ::std::eprintln!("[ERR {:?}] git {:?}", code, args); - Err(git::Status::new(code, output.stdout, output.stderr)) + Err(git::ExecStatus::from(code, output.stdout, output.stderr).into()) } else { ::std::eprintln!("[ERR] git {:?}", args); - Err(git::Status::new(1, output.stdout, output.stderr)) + Err(git::ExecStatus::from(1, output.stdout, output.stderr).into()) } } } diff --git a/src/stc.rs b/src/stc.rs index 6d90186..471e516 100644 --- a/src/stc.rs +++ b/src/stc.rs @@ -1,10 +1,26 @@ use crate::git; +use ::anyhow::Result; use ::std::{ + self, + convert::Into, option::Option::{self, Some}, - result::Result::{self, Err, Ok}, - string::String, - vec::Vec, + result::Result::{Err, Ok}, + string::{String, ToString}, + write, }; +use ::thiserror::Error; + +#[derive(Error, Debug)] +pub enum StcError { + #[error("base branch already defined")] + BaseBranchAlreadyDefined, + #[error("base not specified")] + BaseNotSpecified, + #[error("HEAD not defined")] + HeadNotDefined, + #[error("ref not found: {name:?}")] + RefNotFound { name: String }, +} pub struct Stc { git: G, @@ -15,7 +31,7 @@ impl Stc { Stc { git } } - pub fn init(&self) -> Result<(), git::Status> { + pub fn init(&self) -> Result<()> { let g = &self.git; g.config_add("transfer.hideRefs", git::STC_REF_PREFIX)?; g.config_add("log.excludeDecoration", git::STC_REF_PREFIX)?; @@ -28,7 +44,7 @@ impl Stc { Ok(()) } - pub fn clean(&self) -> Result<(), git::Status> { + pub fn clean(&self) -> Result<()> { let g = &self.git; g.config_unset_pattern("transfer.hideRefs", git::STC_REF_PREFIX)?; g.config_unset_pattern("log.excludeDecoration", git::STC_REF_PREFIX)?; @@ -42,10 +58,10 @@ impl Stc { Ok(()) } - pub fn start(&self, name: String) -> Result<(), git::Status> { + pub fn start(&self, name: String) -> Result<()> { let g = &self.git; let repo = g.snapshot()?; - let base_branch = repo.head().ok_or_else(|| git::Status::with(1))?; + let base_branch = repo.head().ok_or(StcError::HeadNotDefined)?; let new_name = g.check_branchname(&name)?; g.create_branch(&new_name, base_branch)?; g.switch_branch(&new_name)?; @@ -57,26 +73,32 @@ impl Stc { let base_refname = base_branch.refname(); let base_ref = repo .get_ref(&base_refname) - .ok_or_else(|| git::Status::with(1))?; + .ok_or_else(|| StcError::RefNotFound { + name: base_refname.as_str().to_string(), + })?; g.create_ref(&new_name.stc_start_refname(), &base_ref.objectname)?; Ok(()) } - pub fn push(&self) -> Result<(), git::Status> { + pub fn push(&self) -> Result<()> { let g = &self.git; let expected_commit: git::ObjectName; { let repo = g.snapshot()?; - let cur_branch = repo.head().ok_or_else(|| git::Status::with(1))?; + let cur_branch = repo.head().ok_or(StcError::HeadNotDefined)?; let stc_base_refname = cur_branch.stc_base_refname(); - let base_symref = repo - .get_ref(&stc_base_refname) - .ok_or_else(|| git::Status::with(1))?; - let base_ref = repo - .get_ref(&base_symref.symref_target) - .ok_or_else(|| git::Status::with(1))?; + let base_symref = + repo.get_ref(&stc_base_refname) + .ok_or_else(|| StcError::RefNotFound { + name: stc_base_refname.as_str().to_string(), + })?; + let base_ref = + repo.get_ref(&base_symref.symref_target) + .ok_or_else(|| StcError::RefNotFound { + name: base_symref.symref_target.as_str().to_string(), + })?; if let Some(remote_ref) = repo.get_ref(&cur_branch.stc_remote_refname()) { expected_commit = remote_ref.objectname.owning_clone(); } else { @@ -86,11 +108,13 @@ impl Stc { } { let repo = g.snapshot()?; - let cur_branch = repo.head().ok_or_else(|| git::Status::with(1))?; + let cur_branch = repo.head().ok_or(StcError::HeadNotDefined)?; let cur_refname = cur_branch.refname(); let cur_ref = repo .get_ref(&cur_refname) - .ok_or_else(|| git::Status::with(1))?; + .ok_or_else(|| StcError::RefNotFound { + name: cur_refname.as_str().to_string(), + })?; g.update_ref( &cur_branch.stc_remote_refname(), &cur_ref.objectname, @@ -101,19 +125,23 @@ impl Stc { Ok(()) } - pub fn rebase(&self) -> Result<(), git::Status> { + pub fn rebase(&self) -> Result<()> { let g = &self.git; let repo = g.snapshot()?; - let branch = repo.head().ok_or_else(|| git::Status::with(1))?; + let branch = repo.head().ok_or(StcError::HeadNotDefined)?; let stc_base_refname = branch.stc_base_refname(); let stc_start_refname = branch.stc_start_refname(); let base_ref = repo .get_ref(&stc_base_refname) - .ok_or_else(|| git::Status::with(1))?; + .ok_or_else(|| StcError::RefNotFound { + name: stc_base_refname.as_str().to_string(), + })?; let start_ref = repo .get_ref(&stc_start_refname) - .ok_or_else(|| git::Status::with(1))?; + .ok_or_else(|| StcError::RefNotFound { + name: stc_start_refname.as_str().to_string(), + })?; g.rebase_onto(branch)?; g.update_ref( &branch.stc_start_refname(), @@ -124,7 +152,7 @@ impl Stc { Ok(()) } - pub fn sync(&self) -> Result<(), git::Status> { + pub fn sync(&self) -> Result<()> { let g = &self.git; g.fetch_all_prune()?; @@ -132,7 +160,7 @@ impl Stc { Ok(()) } - pub fn fix(&self, branch: Option, base: Option) -> Result<(), git::Status> { + pub fn fix(&self, branch: Option, base: Option) -> Result<()> { let g = &self.git; let repo = g.snapshot()?; @@ -143,11 +171,7 @@ impl Stc { let base_branch = g.check_branchname(&base_branchname)?; if let Some(base_symref) = repo.get_ref(&branch.stc_base_refname()) { if base_symref.symref_target != base_branch.refname() { - return Err(git::Status::new( - 1, - Vec::::new(), - "base branch already defined".as_bytes().to_vec(), - )); + return Err(StcError::BaseBranchAlreadyDefined.into()); } } else { g.create_symref( @@ -163,11 +187,7 @@ impl Stc { g.create_ref(&branch.stc_start_refname(), &forkpoint)?; } } else { - return Err(git::Status::new( - 1, - Vec::::new(), - "base not specified".as_bytes().to_vec(), - )); + return Err(StcError::BaseNotSpecified.into()); } }