Split implementation per struct

This commit is contained in:
Quentin Boyer 2024-05-27 16:36:30 +02:00
parent 4ad1e3310b
commit f817634959

View file

@ -55,12 +55,106 @@ struct Delete {
branch: Option<String>, branch: Option<String>,
} }
impl Delete {
pub fn run(
self,
config: GsmConfig,
git_cd: impl Fn(&[&str]) -> Result<String>,
patch_dir: &Path,
) -> Result<()> {
let current_branch = git_cd(&["branch", "--show-current"])?;
let branch = self
.branch
.as_ref()
.try_m_unwrap_or_else(|| Ok(&current_branch))?;
let has_remote = git_cd(&["rev-parse", "@{u}"]).is_ok();
if has_remote && !self.local_only {
println!("Removing branch from remote repository");
git_cd(&["push", "-d", "origin", branch])?;
}
if branch == &current_branch {
println!("Branch {branch} currently checked out, switching to master");
git_cd(&[
"switch",
config.interdiff_base.as_deref().unwrap_or("master"),
])?;
}
let branch_delete = match self.force {
true => "-D",
false => "-d",
};
git_cd(&["branch", branch_delete, branch])?;
let branch_dir = patch_dir.join(&branch);
std::fs::remove_dir_all(branch_dir).into_diagnostic()?;
Ok(())
}
}
#[derive(Args, Debug)] #[derive(Args, Debug)]
struct List { struct List {
#[arg(short, long)] #[arg(short, long)]
verbose: bool, verbose: bool,
} }
impl List {
pub fn run(
self,
_config: GsmConfig,
_git_cd: impl Fn(&[&str]) -> Result<String>,
patch_dir: &Path,
) -> Result<()> {
for entry in patch_dir
.read_dir()
.into_diagnostic()
.wrap_err("Could not read patch dir")?
{
let entry = entry
.into_diagnostic()
.wrap_err("Could not read patch dir entry")?;
if entry.file_name() == "config.toml" {
continue;
}
let branch_dir = patch_dir.join(entry.file_name());
let Some(branch_version) =
latest_version(&branch_dir).wrap_err("Could not fetch latest version")?
else {
continue;
};
println!(
" - {}: v{branch_version}",
entry.file_name().to_string_lossy()
);
if self.verbose {
println!(" Patches:");
for entry in branch_dir
.join(branch_version.to_string())
.read_dir()
.into_diagnostic()
.wrap_err("Could not read patchset dir")?
{
let entry = entry
.into_diagnostic()
.wrap_err("Could not read patchset entry")?;
println!(" - {}", entry.file_name().to_string_lossy());
}
}
}
Ok(())
}
}
#[derive(Args, Debug)] #[derive(Args, Debug)]
struct Send { struct Send {
#[arg( #[arg(
@ -73,6 +167,51 @@ struct Send {
series: Option<String>, series: Option<String>,
} }
impl Send {
pub fn run(
self,
config: GsmConfig,
git_cd: impl Fn(&[&str]) -> Result<String>,
patch_dir: &Path,
) -> Result<()> {
let current_branch = git_cd(&["branch", "--show-current"])?;
let branch = self
.series
.as_ref()
.try_m_unwrap_or_else(|| Ok(&current_branch))?;
let branch_dir = patch_dir.join(&branch);
let version = match self.version {
Some(v) => v,
None => match latest_version(&branch_dir)? {
None => return Err(miette!("No patch set for the branch {branch}")),
Some(v) => v,
},
};
let version_dir = &branch_dir.join(&version.to_string());
let mut cmd = std::process::Command::new("git");
cmd.arg("send-email");
if let Some(args) = &config.sendmail_args {
cmd.args(args.iter());
}
cmd.arg(version_dir);
let status = cmd
.status()
.into_diagnostic()
.wrap_err("Could not send emails")?;
if !status.success() {
return Err(miette!("Could not send emails"));
}
Ok(())
}
}
#[derive(Args, Debug)] #[derive(Args, Debug)]
struct FormatPatch { struct FormatPatch {
#[arg(short, long, help = "Branch to use (defaults to the current branch)")] #[arg(short, long, help = "Branch to use (defaults to the current branch)")]
@ -102,119 +241,18 @@ struct FormatPatch {
extra_args: Vec<String>, extra_args: Vec<String>,
} }
#[derive(Debug, serde::Deserialize)] impl FormatPatch {
struct GsmConfig { pub fn run(
sendmail_args: Option<Vec<String>>, self,
editor: String, config: GsmConfig,
repo_url_base: String, git_cd: impl Fn(&[&str]) -> Result<String>,
component: Option<String>, patch_dir: &Path,
ci_url: Option<String>, ) -> Result<()> {
interdiff_base: Option<String>, if config.ci_url.is_some() && self.ci.is_none() {
}
fn latest_version(branch_dir: &Path) -> Result<Option<u64>> {
let mut dir_content = branch_dir
.read_dir()
.into_diagnostic()
.wrap_err("could not read branch dir")?
.peekable();
match dir_content.peek() {
Some(_) => Ok(Some(
dir_content
.filter_map(|e| {
let entry = match e.into_diagnostic().wrap_err("Could not read entry") {
Ok(e) => e,
Err(e) => return Some(Err(e)),
};
let name = entry.file_name();
let name = name.to_str().expect("patch set entry is not utf8");
if name == "cover-letter" {
None
} else {
Some(Ok(name.parse().expect("version is not an int")))
}
})
.try_fold(0, |cur, version| -> Result<_> {
let version = version?;
Ok(std::cmp::max(version, cur))
})?,
)),
None => Ok(None),
}
}
fn git_bare(args: Vec<&str>) -> Result<String> {
let out = duct::cmd("git", args)
.stderr_to_stdout()
.unchecked()
.stdout_capture()
.run()
.into_diagnostic()
.wrap_err("failed to launch git")?;
let output = String::from_utf8_lossy(&out.stdout);
let output = output.trim();
if !out.status.success() {
return Err(miette!("{output}").wrap_err("git command failed"));
} else {
Ok(output.to_string())
}
}
fn main() -> Result<()> {
let args = Arg::parse();
let project_dir = ProjectDirs::from("net", "traxys", "git-series-manager")
.ok_or(miette!("Could not create project dirs"))?;
let repo_root = args.repo.try_m_unwrap_or_else(|| {
Ok(PathBuf::from(git_bare(vec![
"rev-parse",
"--show-toplevel",
])?))
})?;
let patch_dir = repo_root.join(".patches");
let git_cd = |args: &[&str]| {
let mut a = vec![
"-C",
repo_root
.to_str()
.ok_or(miette!("{repo_root:?} is not a valid string"))?,
];
a.extend_from_slice(args);
git_bare(a)
};
let config: GsmConfig = Config::builder()
.add_source(
config::File::from(project_dir.config_dir().join("config.toml")).required(false),
)
.add_source(config::File::from(patch_dir.join("config.toml")).required(false))
.build()
.into_diagnostic()
.wrap_err("Failed to read the configuration")?
.try_deserialize()
.into_diagnostic()
.wrap_err("Failed to deserialize the configuration")?;
std::fs::create_dir_all(&patch_dir)
.into_diagnostic()
.wrap_err("could not create patch directory")?;
match args.command {
Command::FormatPatch(args) => {
if config.ci_url.is_some() && args.ci.is_none() {
eprintln!("WARNING: CI was not specified\n"); eprintln!("WARNING: CI was not specified\n");
} }
let branch = args let branch = self
.branch .branch
.try_m_unwrap_or_else(|| Ok(git_cd(&["branch", "--show-current"])?))?; .try_m_unwrap_or_else(|| Ok(git_cd(&["branch", "--show-current"])?))?;
@ -233,7 +271,7 @@ fn main() -> Result<()> {
println!("Component: {component}"); println!("Component: {component}");
println!("Branch: {branch}"); println!("Branch: {branch}");
let ci_link = match (config.ci_url, args.ci) { let ci_link = match (config.ci_url, self.ci) {
(Some(ci_template), Some(id)) => Some( (Some(ci_template), Some(id)) => Some(
ci_template ci_template
.replace("${component}", &component) .replace("${component}", &component)
@ -253,7 +291,7 @@ fn main() -> Result<()> {
.into_diagnostic() .into_diagnostic()
.wrap_err("could not create branch dir")?; .wrap_err("could not create branch dir")?;
let version = match args.version { let version = match self.version {
Some(v) => Some(v), Some(v) => Some(v),
None => latest_version(&branch_dir) None => latest_version(&branch_dir)
.wrap_err("could not get version")? .wrap_err("could not get version")?
@ -263,7 +301,7 @@ fn main() -> Result<()> {
let version_dir = branch_dir.join(version.unwrap_or(1).to_string()); let version_dir = branch_dir.join(version.unwrap_or(1).to_string());
if version_dir.exists() { if version_dir.exists() {
if !args.force { if !self.force {
return Err(miette!( return Err(miette!(
"Patch dir {version_dir:?} exists, pass --force to delete it" "Patch dir {version_dir:?} exists, pass --force to delete it"
)); ));
@ -282,8 +320,7 @@ fn main() -> Result<()> {
impl Drop for VersionDir { impl Drop for VersionDir {
fn drop(&mut self) { fn drop(&mut self) {
std::fs::remove_dir_all(&self.path) std::fs::remove_dir_all(&self.path).expect("could not delete version dir on error");
.expect("could not delete version dir on error");
} }
} }
@ -299,7 +336,7 @@ fn main() -> Result<()> {
let subject_prefix = format!(r#"--subject-prefix=PATCH {component}"#); let subject_prefix = format!(r#"--subject-prefix=PATCH {component}"#);
format_patch_args.extend_from_slice(&[&subject_prefix, "--cover-letter"]); format_patch_args.extend_from_slice(&[&subject_prefix, "--cover-letter"]);
format_patch_args.extend_from_slice(extra_args); format_patch_args.extend_from_slice(extra_args);
format_patch_args.extend(args.extra_args.iter().map(|s| s.deref())); format_patch_args.extend(self.extra_args.iter().map(|s| s.deref()));
git_cd(&format_patch_args)?; git_cd(&format_patch_args)?;
@ -308,8 +345,8 @@ fn main() -> Result<()> {
}) })
}; };
let _version_dir = if let Some(interdiff) = args.diff { let _version_dir = if let Some(interdiff) = self.diff {
let base = args let base = self
.base_diff .base_diff
.or(config.interdiff_base) .or(config.interdiff_base)
.unwrap_or_else(|| String::from("origin/master")); .unwrap_or_else(|| String::from("origin/master"));
@ -479,120 +516,118 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
Command::List(list) => { }
for entry in patch_dir
#[derive(Debug, serde::Deserialize)]
struct GsmConfig {
sendmail_args: Option<Vec<String>>,
editor: String,
repo_url_base: String,
component: Option<String>,
ci_url: Option<String>,
interdiff_base: Option<String>,
}
fn latest_version(branch_dir: &Path) -> Result<Option<u64>> {
let mut dir_content = branch_dir
.read_dir() .read_dir()
.into_diagnostic() .into_diagnostic()
.wrap_err("Could not read patch dir")? .wrap_err("could not read branch dir")?
{ .peekable();
let entry = entry
.into_diagnostic()
.wrap_err("Could not read patch dir entry")?;
if entry.file_name() == "config.toml" { match dir_content.peek() {
continue; Some(_) => Ok(Some(
} dir_content
.filter_map(|e| {
let branch_dir = patch_dir.join(entry.file_name()); let entry = match e.into_diagnostic().wrap_err("Could not read entry") {
let Some(branch_version) = Ok(e) => e,
latest_version(&branch_dir).wrap_err("Could not fetch latest version")? Err(e) => return Some(Err(e)),
else {
continue;
}; };
println!( let name = entry.file_name();
" - {}: v{branch_version}", let name = name.to_str().expect("patch set entry is not utf8");
entry.file_name().to_string_lossy()
);
if list.verbose { if name == "cover-letter" {
println!(" Patches:"); None
for entry in branch_dir } else {
.join(branch_version.to_string()) Some(Ok(name.parse().expect("version is not an int")))
.read_dir()
.into_diagnostic()
.wrap_err("Could not read patchset dir")?
{
let entry = entry
.into_diagnostic()
.wrap_err("Could not read patchset entry")?;
println!(" - {}", entry.file_name().to_string_lossy());
}
}
} }
})
.try_fold(0, |cur, version| -> Result<_> {
let version = version?;
Ok(()) Ok(std::cmp::max(version, cur))
} })?,
Command::Send(send) => { )),
let current_branch = git_cd(&["branch", "--show-current"])?; None => Ok(None),
let branch = send }
.series }
.as_ref()
.try_m_unwrap_or_else(|| Ok(&current_branch))?; fn git_bare(args: Vec<&str>) -> Result<String> {
let out = duct::cmd("git", args)
let branch_dir = patch_dir.join(&branch); .stderr_to_stdout()
let version = match send.version { .unchecked()
Some(v) => v, .stdout_capture()
None => match latest_version(&branch_dir)? { .run()
None => return Err(miette!("No patch set for the branch {branch}")), .into_diagnostic()
Some(v) => v, .wrap_err("failed to launch git")?;
},
}; let output = String::from_utf8_lossy(&out.stdout);
let output = output.trim();
let version_dir = &branch_dir.join(&version.to_string());
if !out.status.success() {
let mut cmd = std::process::Command::new("git"); return Err(miette!("{output}").wrap_err("git command failed"));
} else {
cmd.arg("send-email"); Ok(output.to_string())
if let Some(args) = &config.sendmail_args { }
cmd.args(args.iter()); }
}
cmd.arg(version_dir); fn main() -> Result<()> {
let args = Arg::parse();
let status = cmd
.status() let project_dir = ProjectDirs::from("net", "traxys", "git-series-manager")
.into_diagnostic() .ok_or(miette!("Could not create project dirs"))?;
.wrap_err("Could not send emails")?;
let repo_root = args.repo.try_m_unwrap_or_else(|| {
if !status.success() { Ok(PathBuf::from(git_bare(vec![
return Err(miette!("Could not send emails")); "rev-parse",
} "--show-toplevel",
])?))
Ok(()) })?;
} let patch_dir = repo_root.join(".patches");
Command::Delete(delete) => {
let current_branch = git_cd(&["branch", "--show-current"])?; let git_cd = |args: &[&str]| {
let mut a = vec![
let branch = delete "-C",
.branch repo_root
.as_ref() .to_str()
.try_m_unwrap_or_else(|| Ok(&current_branch))?; .ok_or(miette!("{repo_root:?} is not a valid string"))?,
];
let has_remote = git_cd(&["rev-parse", "@{u}"]).is_ok(); a.extend_from_slice(args);
if has_remote && !delete.local_only {
println!("Removing branch from remote repository"); git_bare(a)
git_cd(&["push", "-d", "origin", branch])?; };
}
let config: GsmConfig = Config::builder()
if branch == &current_branch { .add_source(
println!("Branch {branch} currently checked out, switching to master"); config::File::from(project_dir.config_dir().join("config.toml")).required(false),
git_cd(&[ )
"switch", .add_source(config::File::from(patch_dir.join("config.toml")).required(false))
config.interdiff_base.as_deref().unwrap_or("master"), .build()
])?; .into_diagnostic()
} .wrap_err("Failed to read the configuration")?
.try_deserialize()
let branch_delete = match delete.force { .into_diagnostic()
true => "-D", .wrap_err("Failed to deserialize the configuration")?;
false => "-d",
}; std::fs::create_dir_all(&patch_dir)
.into_diagnostic()
git_cd(&["branch", branch_delete, branch])?; .wrap_err("could not create patch directory")?;
let branch_dir = patch_dir.join(&branch);
std::fs::remove_dir_all(branch_dir).into_diagnostic()?; match args.command {
Command::FormatPatch(args) => args.run(config, git_cd, &patch_dir),
Ok(()) Command::List(list) => list.run(config, git_cd, &patch_dir),
} Command::Send(send) => send.run(config, git_cd, &patch_dir),
Command::Delete(delete) => delete.run(config, git_cd, &patch_dir),
} }
} }