Split implementation per struct
This commit is contained in:
parent
4ad1e3310b
commit
f817634959
1 changed files with 420 additions and 385 deletions
805
src/main.rs
805
src/main.rs
|
|
@ -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(¤t_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 == ¤t_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(¤t_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,6 +241,283 @@ struct FormatPatch {
|
||||||
extra_args: Vec<String>,
|
extra_args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FormatPatch {
|
||||||
|
pub fn run(
|
||||||
|
self,
|
||||||
|
config: GsmConfig,
|
||||||
|
git_cd: impl Fn(&[&str]) -> Result<String>,
|
||||||
|
patch_dir: &Path,
|
||||||
|
) -> Result<()> {
|
||||||
|
if config.ci_url.is_some() && self.ci.is_none() {
|
||||||
|
eprintln!("WARNING: CI was not specified\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch = self
|
||||||
|
.branch
|
||||||
|
.try_m_unwrap_or_else(|| Ok(git_cd(&["branch", "--show-current"])?))?;
|
||||||
|
|
||||||
|
let component = config.component.try_m_unwrap_or_else(|| {
|
||||||
|
let url = git_cd(&["remote", "get-url", "origin"])?;
|
||||||
|
Ok(url
|
||||||
|
.strip_prefix(&config.repo_url_base)
|
||||||
|
.ok_or(miette!(
|
||||||
|
"remote {url} does not start with url base {}",
|
||||||
|
config.repo_url_base
|
||||||
|
))?
|
||||||
|
.trim_end_matches(".git")
|
||||||
|
.to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!("Component: {component}");
|
||||||
|
println!("Branch: {branch}");
|
||||||
|
|
||||||
|
let ci_link = match (config.ci_url, self.ci) {
|
||||||
|
(Some(ci_template), Some(id)) => Some(
|
||||||
|
ci_template
|
||||||
|
.replace("${component}", &component)
|
||||||
|
.replace("${branch}", &branch)
|
||||||
|
.replace("${ci_job}", &id.to_string()),
|
||||||
|
),
|
||||||
|
(Some(ci_template), None) => Some(
|
||||||
|
ci_template
|
||||||
|
.replace("${component}", &component)
|
||||||
|
.replace("${branch}", &branch),
|
||||||
|
),
|
||||||
|
(None, _) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let branch_dir = patch_dir.join(&branch);
|
||||||
|
std::fs::create_dir_all(&branch_dir)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("could not create branch dir")?;
|
||||||
|
|
||||||
|
let version = match self.version {
|
||||||
|
Some(v) => Some(v),
|
||||||
|
None => latest_version(&branch_dir)
|
||||||
|
.wrap_err("could not get version")?
|
||||||
|
.map(|v| v + 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_dir = branch_dir.join(version.unwrap_or(1).to_string());
|
||||||
|
|
||||||
|
if version_dir.exists() {
|
||||||
|
if !self.force {
|
||||||
|
return Err(miette!(
|
||||||
|
"Patch dir {version_dir:?} exists, pass --force to delete it"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
std::fs::remove_dir_all(&version_dir).into_diagnostic()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let version_dir = version_dir
|
||||||
|
.to_str()
|
||||||
|
.ok_or(miette!("Temp dir is not utf-8"))?;
|
||||||
|
|
||||||
|
struct VersionDir {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VersionDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
std::fs::remove_dir_all(&self.path).expect("could not delete version dir on error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let format_patch = |extra_args: &[&str]| -> Result<_> {
|
||||||
|
let mut format_patch_args = vec!["format-patch", "-o", &version_dir];
|
||||||
|
let version_str = version.map(|s| s.to_string());
|
||||||
|
|
||||||
|
if let Some(version) = &version_str {
|
||||||
|
format_patch_args.push("-v");
|
||||||
|
format_patch_args.push(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(extra_args);
|
||||||
|
format_patch_args.extend(self.extra_args.iter().map(|s| s.deref()));
|
||||||
|
|
||||||
|
git_cd(&format_patch_args)?;
|
||||||
|
|
||||||
|
Ok(VersionDir {
|
||||||
|
path: version_dir.into(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let _version_dir = if let Some(interdiff) = self.diff {
|
||||||
|
let base = self
|
||||||
|
.base_diff
|
||||||
|
.or(config.interdiff_base)
|
||||||
|
.unwrap_or_else(|| String::from("origin/master"));
|
||||||
|
|
||||||
|
struct TempBranch<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
git: &'a dyn Fn(&[&str]) -> Result<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Drop for TempBranch<'a> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
(self.git)(&["branch", "-D", self.name]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch = {
|
||||||
|
let name = "__patch_old";
|
||||||
|
git_cd(&["branch", name, &base])?;
|
||||||
|
TempBranch { name, git: &git_cd }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GitWorktree {
|
||||||
|
_dir: TempDir,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitWorktree {
|
||||||
|
pub fn exec(&self, args: &[&str]) -> Result<String> {
|
||||||
|
let mut a = vec!["-C", &self.path];
|
||||||
|
a.extend_from_slice(args);
|
||||||
|
|
||||||
|
git_bare(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GitWorktree {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.exec(&["worktree", "remove", &self.path]).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wt = {
|
||||||
|
let worktree = temp_dir::TempDir::new()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not create worktree directory")?;
|
||||||
|
|
||||||
|
let worktree_path = worktree
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(miette!("Temp dir is not utf-8"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
git_cd(&["worktree", "add", "--detach", &worktree_path])?;
|
||||||
|
|
||||||
|
GitWorktree {
|
||||||
|
path: worktree_path,
|
||||||
|
_dir: worktree,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wt.exec(&["switch", branch.name])?;
|
||||||
|
|
||||||
|
let patches = branch_dir
|
||||||
|
.join(&interdiff.to_string())
|
||||||
|
.read_dir()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not read interdiff folder")?
|
||||||
|
.map(|e| -> Result<_> {
|
||||||
|
let e = e
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not read interdiff entry")?;
|
||||||
|
|
||||||
|
let path = e
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(miette!("Interdiff patch path is not utf-8"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
match path.contains("cover-letter") {
|
||||||
|
true => Ok(None),
|
||||||
|
false => Ok(Some(path)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter_map(|e| e.transpose())
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
let mut apply_args = vec!["am"];
|
||||||
|
apply_args.extend(patches.iter().map(|s| s.deref()));
|
||||||
|
wt.exec(&apply_args)?;
|
||||||
|
|
||||||
|
let interdiff_branch = format!("--interdiff={}", branch.name);
|
||||||
|
format_patch(&[&interdiff_branch])?
|
||||||
|
} else {
|
||||||
|
format_patch(&[])?
|
||||||
|
};
|
||||||
|
|
||||||
|
let cover_letter = branch_dir.join(COVER_LETTER_NAME);
|
||||||
|
if !cover_letter.exists() {
|
||||||
|
let mut cover_letter_template = format!("Title: \n\nBranch: {branch}\n");
|
||||||
|
if let Some(ci_link) = &ci_link {
|
||||||
|
cover_letter_template += &format!("CI: {ci_link}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(&cover_letter, cover_letter_template)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not write cover letter")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::Command::new(config.editor)
|
||||||
|
.arg(&cover_letter)
|
||||||
|
.status()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not edit cover letter")?;
|
||||||
|
|
||||||
|
let cover_letter = std::fs::read_to_string(cover_letter)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Error while reading back the cover letter")?;
|
||||||
|
|
||||||
|
let Some((title, body)) = cover_letter.split_once('\n') else {
|
||||||
|
return Err(miette!("Missing title newline"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(title) = title.strip_prefix("Title: ") else {
|
||||||
|
return Err(miette!("Missing `Title: ` prefix"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cover_letter = None;
|
||||||
|
for entry in Path::new(version_dir)
|
||||||
|
.read_dir()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not read patch directory")?
|
||||||
|
{
|
||||||
|
let entry = entry
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Error while reading patch directory")?;
|
||||||
|
|
||||||
|
if entry
|
||||||
|
.file_name()
|
||||||
|
.as_encoded_bytes()
|
||||||
|
.ends_with(b"cover-letter.patch")
|
||||||
|
{
|
||||||
|
cover_letter = Some(entry.file_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cover_letter = Path::new(version_dir)
|
||||||
|
.join(cover_letter.ok_or(miette!("Did not find cover letter in {version_dir}"))?);
|
||||||
|
|
||||||
|
let cover_letter_content = std::fs::read_to_string(&cover_letter)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not read patchset cover letter")?;
|
||||||
|
|
||||||
|
let cover_letter_content = cover_letter_content
|
||||||
|
.replace("*** SUBJECT HERE ***", title.trim())
|
||||||
|
.replace("*** BLURB HERE ***", body.trim());
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(cover_letter)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not re-open cover letter")?;
|
||||||
|
file.write(cover_letter_content.as_bytes())
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("Could not save cover letter")?;
|
||||||
|
|
||||||
|
std::mem::forget(_version_dir);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct GsmConfig {
|
struct GsmConfig {
|
||||||
sendmail_args: Option<Vec<String>>,
|
sendmail_args: Option<Vec<String>>,
|
||||||
|
|
@ -209,390 +625,9 @@ fn main() -> Result<()> {
|
||||||
.wrap_err("could not create patch directory")?;
|
.wrap_err("could not create patch directory")?;
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Command::FormatPatch(args) => {
|
Command::FormatPatch(args) => args.run(config, git_cd, &patch_dir),
|
||||||
if config.ci_url.is_some() && args.ci.is_none() {
|
Command::List(list) => list.run(config, git_cd, &patch_dir),
|
||||||
eprintln!("WARNING: CI was not specified\n");
|
Command::Send(send) => send.run(config, git_cd, &patch_dir),
|
||||||
}
|
Command::Delete(delete) => delete.run(config, git_cd, &patch_dir),
|
||||||
|
|
||||||
let branch = args
|
|
||||||
.branch
|
|
||||||
.try_m_unwrap_or_else(|| Ok(git_cd(&["branch", "--show-current"])?))?;
|
|
||||||
|
|
||||||
let component = config.component.try_m_unwrap_or_else(|| {
|
|
||||||
let url = git_cd(&["remote", "get-url", "origin"])?;
|
|
||||||
Ok(url
|
|
||||||
.strip_prefix(&config.repo_url_base)
|
|
||||||
.ok_or(miette!(
|
|
||||||
"remote {url} does not start with url base {}",
|
|
||||||
config.repo_url_base
|
|
||||||
))?
|
|
||||||
.trim_end_matches(".git")
|
|
||||||
.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!("Component: {component}");
|
|
||||||
println!("Branch: {branch}");
|
|
||||||
|
|
||||||
let ci_link = match (config.ci_url, args.ci) {
|
|
||||||
(Some(ci_template), Some(id)) => Some(
|
|
||||||
ci_template
|
|
||||||
.replace("${component}", &component)
|
|
||||||
.replace("${branch}", &branch)
|
|
||||||
.replace("${ci_job}", &id.to_string()),
|
|
||||||
),
|
|
||||||
(Some(ci_template), None) => Some(
|
|
||||||
ci_template
|
|
||||||
.replace("${component}", &component)
|
|
||||||
.replace("${branch}", &branch),
|
|
||||||
),
|
|
||||||
(None, _) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let branch_dir = patch_dir.join(&branch);
|
|
||||||
std::fs::create_dir_all(&branch_dir)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("could not create branch dir")?;
|
|
||||||
|
|
||||||
let version = match args.version {
|
|
||||||
Some(v) => Some(v),
|
|
||||||
None => latest_version(&branch_dir)
|
|
||||||
.wrap_err("could not get version")?
|
|
||||||
.map(|v| v + 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
let version_dir = branch_dir.join(version.unwrap_or(1).to_string());
|
|
||||||
|
|
||||||
if version_dir.exists() {
|
|
||||||
if !args.force {
|
|
||||||
return Err(miette!(
|
|
||||||
"Patch dir {version_dir:?} exists, pass --force to delete it"
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
std::fs::remove_dir_all(&version_dir).into_diagnostic()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let version_dir = version_dir
|
|
||||||
.to_str()
|
|
||||||
.ok_or(miette!("Temp dir is not utf-8"))?;
|
|
||||||
|
|
||||||
struct VersionDir {
|
|
||||||
path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for VersionDir {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
std::fs::remove_dir_all(&self.path)
|
|
||||||
.expect("could not delete version dir on error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let format_patch = |extra_args: &[&str]| -> Result<_> {
|
|
||||||
let mut format_patch_args = vec!["format-patch", "-o", &version_dir];
|
|
||||||
let version_str = version.map(|s| s.to_string());
|
|
||||||
|
|
||||||
if let Some(version) = &version_str {
|
|
||||||
format_patch_args.push("-v");
|
|
||||||
format_patch_args.push(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(extra_args);
|
|
||||||
format_patch_args.extend(args.extra_args.iter().map(|s| s.deref()));
|
|
||||||
|
|
||||||
git_cd(&format_patch_args)?;
|
|
||||||
|
|
||||||
Ok(VersionDir {
|
|
||||||
path: version_dir.into(),
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let _version_dir = if let Some(interdiff) = args.diff {
|
|
||||||
let base = args
|
|
||||||
.base_diff
|
|
||||||
.or(config.interdiff_base)
|
|
||||||
.unwrap_or_else(|| String::from("origin/master"));
|
|
||||||
|
|
||||||
struct TempBranch<'a> {
|
|
||||||
name: &'a str,
|
|
||||||
git: &'a dyn Fn(&[&str]) -> Result<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Drop for TempBranch<'a> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
(self.git)(&["branch", "-D", self.name]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let branch = {
|
|
||||||
let name = "__patch_old";
|
|
||||||
git_cd(&["branch", name, &base])?;
|
|
||||||
TempBranch { name, git: &git_cd }
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GitWorktree {
|
|
||||||
_dir: TempDir,
|
|
||||||
path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitWorktree {
|
|
||||||
pub fn exec(&self, args: &[&str]) -> Result<String> {
|
|
||||||
let mut a = vec!["-C", &self.path];
|
|
||||||
a.extend_from_slice(args);
|
|
||||||
|
|
||||||
git_bare(a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for GitWorktree {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.exec(&["worktree", "remove", &self.path]).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let wt = {
|
|
||||||
let worktree = temp_dir::TempDir::new()
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not create worktree directory")?;
|
|
||||||
|
|
||||||
let worktree_path = worktree
|
|
||||||
.path()
|
|
||||||
.to_str()
|
|
||||||
.ok_or(miette!("Temp dir is not utf-8"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
git_cd(&["worktree", "add", "--detach", &worktree_path])?;
|
|
||||||
|
|
||||||
GitWorktree {
|
|
||||||
path: worktree_path,
|
|
||||||
_dir: worktree,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
wt.exec(&["switch", branch.name])?;
|
|
||||||
|
|
||||||
let patches = branch_dir
|
|
||||||
.join(&interdiff.to_string())
|
|
||||||
.read_dir()
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not read interdiff folder")?
|
|
||||||
.map(|e| -> Result<_> {
|
|
||||||
let e = e
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not read interdiff entry")?;
|
|
||||||
|
|
||||||
let path = e
|
|
||||||
.path()
|
|
||||||
.to_str()
|
|
||||||
.ok_or(miette!("Interdiff patch path is not utf-8"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
match path.contains("cover-letter") {
|
|
||||||
true => Ok(None),
|
|
||||||
false => Ok(Some(path)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter_map(|e| e.transpose())
|
|
||||||
.collect::<Result<Vec<_>>>()?;
|
|
||||||
let mut apply_args = vec!["am"];
|
|
||||||
apply_args.extend(patches.iter().map(|s| s.deref()));
|
|
||||||
wt.exec(&apply_args)?;
|
|
||||||
|
|
||||||
let interdiff_branch = format!("--interdiff={}", branch.name);
|
|
||||||
format_patch(&[&interdiff_branch])?
|
|
||||||
} else {
|
|
||||||
format_patch(&[])?
|
|
||||||
};
|
|
||||||
|
|
||||||
let cover_letter = branch_dir.join(COVER_LETTER_NAME);
|
|
||||||
if !cover_letter.exists() {
|
|
||||||
let mut cover_letter_template = format!("Title: \n\nBranch: {branch}\n");
|
|
||||||
if let Some(ci_link) = &ci_link {
|
|
||||||
cover_letter_template += &format!("CI: {ci_link}\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::write(&cover_letter, cover_letter_template)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not write cover letter")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::process::Command::new(config.editor)
|
|
||||||
.arg(&cover_letter)
|
|
||||||
.status()
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not edit cover letter")?;
|
|
||||||
|
|
||||||
let cover_letter = std::fs::read_to_string(cover_letter)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Error while reading back the cover letter")?;
|
|
||||||
|
|
||||||
let Some((title, body)) = cover_letter.split_once('\n') else {
|
|
||||||
return Err(miette!("Missing title newline"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(title) = title.strip_prefix("Title: ") else {
|
|
||||||
return Err(miette!("Missing `Title: ` prefix"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cover_letter = None;
|
|
||||||
for entry in Path::new(version_dir)
|
|
||||||
.read_dir()
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not read patch directory")?
|
|
||||||
{
|
|
||||||
let entry = entry
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Error while reading patch directory")?;
|
|
||||||
|
|
||||||
if entry
|
|
||||||
.file_name()
|
|
||||||
.as_encoded_bytes()
|
|
||||||
.ends_with(b"cover-letter.patch")
|
|
||||||
{
|
|
||||||
cover_letter = Some(entry.file_name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cover_letter = Path::new(version_dir)
|
|
||||||
.join(cover_letter.ok_or(miette!("Did not find cover letter in {version_dir}"))?);
|
|
||||||
|
|
||||||
let cover_letter_content = std::fs::read_to_string(&cover_letter)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not read patchset cover letter")?;
|
|
||||||
|
|
||||||
let cover_letter_content = cover_letter_content
|
|
||||||
.replace("*** SUBJECT HERE ***", title.trim())
|
|
||||||
.replace("*** BLURB HERE ***", body.trim());
|
|
||||||
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(cover_letter)
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not re-open cover letter")?;
|
|
||||||
file.write(cover_letter_content.as_bytes())
|
|
||||||
.into_diagnostic()
|
|
||||||
.wrap_err("Could not save cover letter")?;
|
|
||||||
|
|
||||||
std::mem::forget(_version_dir);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Command::List(list) => {
|
|
||||||
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 list.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(())
|
|
||||||
}
|
|
||||||
Command::Send(send) => {
|
|
||||||
let current_branch = git_cd(&["branch", "--show-current"])?;
|
|
||||||
let branch = send
|
|
||||||
.series
|
|
||||||
.as_ref()
|
|
||||||
.try_m_unwrap_or_else(|| Ok(¤t_branch))?;
|
|
||||||
|
|
||||||
let branch_dir = patch_dir.join(&branch);
|
|
||||||
let version = match send.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(())
|
|
||||||
}
|
|
||||||
Command::Delete(delete) => {
|
|
||||||
let current_branch = git_cd(&["branch", "--show-current"])?;
|
|
||||||
|
|
||||||
let branch = delete
|
|
||||||
.branch
|
|
||||||
.as_ref()
|
|
||||||
.try_m_unwrap_or_else(|| Ok(¤t_branch))?;
|
|
||||||
|
|
||||||
let has_remote = git_cd(&["rev-parse", "@{u}"]).is_ok();
|
|
||||||
if has_remote && !delete.local_only {
|
|
||||||
println!("Removing branch from remote repository");
|
|
||||||
git_cd(&["push", "-d", "origin", branch])?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch == ¤t_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 delete.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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue