Initial port of gpt
This commit is contained in:
parent
d55df58fab
commit
29dfad3809
4 changed files with 1645 additions and 1 deletions
1125
Cargo.lock
generated
Normal file
1125
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "todo_change_name"
|
name = "gsm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["traxys <quentin@familleboyer.net>"]
|
authors = ["traxys <quentin@familleboyer.net>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
|
config = "0.14.0"
|
||||||
|
directories = "5.0.1"
|
||||||
|
duct = "0.13.7"
|
||||||
|
miette = { version = "7.0.0", features = ["fancy"] }
|
||||||
|
serde = { version = "1.0.196", features = ["derive"] }
|
||||||
|
temp-dir = "0.1.12"
|
||||||
|
|
|
||||||
429
src/main.rs
Normal file
429
src/main.rs
Normal file
|
|
@ -0,0 +1,429 @@
|
||||||
|
use std::{
|
||||||
|
fs::OpenOptions,
|
||||||
|
io::Write,
|
||||||
|
ops::Deref,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use config::Config;
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use miette::{miette, Context, IntoDiagnostic, Result};
|
||||||
|
|
||||||
|
use temp_dir::TempDir;
|
||||||
|
use utils::OptExt;
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
const COVER_LETTER_NAME: &str = "cover-letter";
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Arg {
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
help = "git repository to use, defaults to current repository"
|
||||||
|
)]
|
||||||
|
repo: Option<PathBuf>,
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Command {
|
||||||
|
/// Format a patch. alias "p"
|
||||||
|
#[command(alias = "p")]
|
||||||
|
FormatPatch(FormatPatch),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
struct FormatPatch {
|
||||||
|
#[arg(short, long, help = "Branch to use (defaults to the current branch)")]
|
||||||
|
branch: Option<String>,
|
||||||
|
#[arg(short, long, help = "Jenkins job number")]
|
||||||
|
ci: Option<u64>,
|
||||||
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
help = "Version of the patchset. Defaults to last patchset + 1"
|
||||||
|
)]
|
||||||
|
version: Option<u64>,
|
||||||
|
#[arg(long, help = "Override the current version of the patchset")]
|
||||||
|
force: bool,
|
||||||
|
#[arg(long, help = "Silence the CI warning")]
|
||||||
|
no_ci: bool,
|
||||||
|
#[arg(short, long, help = "Send the patches directly")]
|
||||||
|
send: bool,
|
||||||
|
#[arg(short, long, help = "Perform the interdiff with the supplied version")]
|
||||||
|
diff: Option<u64>,
|
||||||
|
#[arg(
|
||||||
|
short = 'B',
|
||||||
|
long,
|
||||||
|
help = "Reference for the interdiff (defaults to origin/master)"
|
||||||
|
)]
|
||||||
|
base_diff: Option<String>,
|
||||||
|
extra_args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct GsmConfig {
|
||||||
|
editor: String,
|
||||||
|
repo_url_base: String,
|
||||||
|
component: Option<String>,
|
||||||
|
ci_url: Option<String>,
|
||||||
|
interdiff_base: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
let mut dir_content = branch_dir
|
||||||
|
.read_dir()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("could not read branch dir")?
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
match dir_content.peek() {
|
||||||
|
Some(_) => Some(
|
||||||
|
dir_content
|
||||||
|
.into_iter()
|
||||||
|
.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))
|
||||||
|
})?
|
||||||
|
+ 1,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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"))?;
|
||||||
|
|
||||||
|
let format_patch = |extra_args: &[&str]| {
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
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")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/utils.rs
Normal file
83
src/utils.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
use std::mem::{self, ManuallyDrop};
|
||||||
|
|
||||||
|
/// Allows transmuting between types of different sizes.
|
||||||
|
///
|
||||||
|
/// Necessary for transmuting in generic functions, since (as of Rust 1.51.0)
|
||||||
|
/// transmute doesn't work well with generic types.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This function has the same safety requirements as [`std::mem::transmute_copy`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use core_extensions::utils::transmute_ignore_size;
|
||||||
|
///
|
||||||
|
/// use std::mem::MaybeUninit;
|
||||||
|
///
|
||||||
|
/// unsafe fn transmute_into_init<T>(array: [MaybeUninit<T>; 3]) -> [T; 3] {
|
||||||
|
/// transmute_ignore_size(array)
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let array = [MaybeUninit::new(3), MaybeUninit::new(5), MaybeUninit::new(8)];
|
||||||
|
///
|
||||||
|
/// unsafe{ assert_eq!(transmute_into_init(array), [3, 5, 8]); }
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This is the error you get if you tried to use `std::mem::transmute`.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
|
||||||
|
/// --> src/lib.rs:4:5
|
||||||
|
/// |
|
||||||
|
/// 4 | std::mem::transmute(array)
|
||||||
|
/// | ^^^^^^^^^^^^^^^^^^^
|
||||||
|
/// |
|
||||||
|
/// = note: source type: `[MaybeUninit<T>; 3]` (size can vary because of T)
|
||||||
|
/// = note: target type: `[T; 3]` (size can vary because of T)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// [`std::mem::transmute_copy`]: https://doc.rust-lang.org/std/mem/fn.transmute_copy.html
|
||||||
|
#[inline(always)]
|
||||||
|
pub unsafe fn transmute_ignore_size<T, U>(v: T) -> U {
|
||||||
|
let v = ManuallyDrop::new(v);
|
||||||
|
mem::transmute_copy::<T, U>(&v)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TypeIdent {
|
||||||
|
type Type: ?Sized;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn into_type(self) -> Self::Type
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
Self::Type: Sized,
|
||||||
|
{
|
||||||
|
unsafe { transmute_ignore_size(self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> TypeIdent for T {
|
||||||
|
type Type = T;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OptExt<T>: TypeIdent<Type = Option<T>> + Sized {
|
||||||
|
fn try_unwrap_or_else<F, E>(self, f: F) -> Result<T, E>
|
||||||
|
where
|
||||||
|
F: Fn() -> Result<T, E>,
|
||||||
|
{
|
||||||
|
self.into_type().map(Ok::<T, E>).unwrap_or_else(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_m_unwrap_or_else<F>(self, f: F) -> Result<T, miette::Report>
|
||||||
|
where
|
||||||
|
F: Fn() -> Result<T, miette::Report>,
|
||||||
|
{
|
||||||
|
self.try_unwrap_or_else(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OptExt<T> for Option<T> {}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue