scaffold: CLI surface with stubbed kernel and patch verbs
Add the spectral command tree -- kernel quest/test and patch check/format/commit/create/submit/update -- with every verb wired through main to a todo!() body, plus the pieces they will share: - config: resolve the kernel tree from $SPECTRAL_KERNEL, falling back to ~/.spectral/linux, patches under ~/.spectral/patches, and confirm a tree really is one before anything uses it - git: a thin wrapper over the git binary, currently only run - error: one thiserror enum, converted once in main - patch/checkpatch and patch/maintainers: seams around the tree's own scripts/checkpatch.pl and scripts/get_maintainer.pl, so spectral never carries a stale copy of the kernel's rules 20 functions are todo!(); the #[allow(dead_code)] sites mark the items that are unreachable only until their caller is written. fmt, clippy -D warnings, and test are all clean.
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
//! The whole command surface, in one file, so the shape of the CLI reads at a
|
||||
//! glance. Nothing here does any work — every variant is dispatched in `main`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "spectral",
|
||||
version,
|
||||
about = "A monolithic kernel work wrapper to make it easy.",
|
||||
subcommand_required = true,
|
||||
arg_required_else_help = true
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Find work, build it, boot it
|
||||
Kernel {
|
||||
#[command(subcommand)]
|
||||
command: KernelCommand,
|
||||
},
|
||||
/// Carry a change from working tree to mailing list
|
||||
Patch {
|
||||
#[command(subcommand)]
|
||||
command: PatchCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum KernelCommand {
|
||||
/// Fetch a random open issue to work on
|
||||
Quest(QuestArgs),
|
||||
/// Build the kernel and boot it under qemu
|
||||
Test(TestArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct QuestArgs {
|
||||
/// Print the picked issue as JSON
|
||||
#[arg(long)]
|
||||
pub json: bool,
|
||||
/// Only consider issues whose title contains this text
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub filter: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct TestArgs {
|
||||
/// Boot the existing build instead of rebuilding first
|
||||
#[arg(long)]
|
||||
pub no_build: bool,
|
||||
/// Rootfs image to boot with; defaults to the tree's own test image
|
||||
#[arg(long, value_name = "PATH")]
|
||||
pub rootfs: Option<PathBuf>,
|
||||
/// Extra argument passed through to qemu (repeatable)
|
||||
#[arg(long = "qemu-arg", value_name = "ARG", allow_hyphen_values = true)]
|
||||
pub qemu_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum PatchCommand {
|
||||
/// Run checkpatch.pl over the work in progress
|
||||
Check(CheckArgs),
|
||||
/// Auto-fix what checkpatch.pl reports
|
||||
Format(FormatArgs),
|
||||
/// Commit the work in progress with a kernel-style message
|
||||
Commit(CommitArgs),
|
||||
/// Write the diff against the base branch out to <NAME>.patch
|
||||
Create(CreateArgs),
|
||||
/// Send a patch, with To/CC taken from get_maintainer.pl
|
||||
Submit(SubmitArgs),
|
||||
/// Re-roll a patch as v<N>, renaming the file to match
|
||||
Update(UpdateArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CheckArgs {
|
||||
/// Check this patch file instead of the working tree
|
||||
#[arg(value_name = "PATCH")]
|
||||
pub patch: Option<PathBuf>,
|
||||
/// Pass --strict to checkpatch.pl
|
||||
#[arg(long)]
|
||||
pub strict: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct FormatArgs {
|
||||
/// Fix this patch file instead of the working tree
|
||||
#[arg(value_name = "PATCH")]
|
||||
pub patch: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CommitArgs {
|
||||
/// Commit message, without the Signed-off-by line
|
||||
#[arg(value_name = "MESSAGE")]
|
||||
pub message: String,
|
||||
/// Append your Signed-off-by, which the kernel requires
|
||||
#[arg(short = 's', long)]
|
||||
pub signoff: bool,
|
||||
/// Amend the previous commit instead of adding one
|
||||
#[arg(long)]
|
||||
pub amend: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CreateArgs {
|
||||
/// Patch name; `.patch` is appended if you leave it off
|
||||
#[arg(value_name = "NAME")]
|
||||
pub name: String,
|
||||
/// Branch or revision to diff against
|
||||
#[arg(long, value_name = "REV", default_value = "master")]
|
||||
pub base: String,
|
||||
/// Directory to write the patch into; defaults to the patch dir
|
||||
#[arg(long, value_name = "DIR")]
|
||||
pub out: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct SubmitArgs {
|
||||
/// Patch file to send
|
||||
#[arg(value_name = "PATCH")]
|
||||
pub patch: PathBuf,
|
||||
/// Print the recipients and command instead of sending
|
||||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
/// Extra address to CC (repeatable)
|
||||
#[arg(long = "cc", value_name = "ADDR")]
|
||||
pub cc: Vec<String>,
|
||||
/// Message-Id of the patch this one re-rolls, so mail threads correctly
|
||||
#[arg(long, value_name = "MSGID")]
|
||||
pub in_reply_to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct UpdateArgs {
|
||||
/// Patch file to re-roll
|
||||
#[arg(value_name = "PATCH")]
|
||||
pub patch: PathBuf,
|
||||
/// Revision to re-roll as; defaults to one past the file's current vN
|
||||
#[arg(short = 'v', long, value_name = "N")]
|
||||
pub revision: Option<u32>,
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Where the kernel tree and the patches live.
|
||||
//!
|
||||
//! Everything else asks this module for paths, so the day a config file or
|
||||
//! `spectral init` arrives, it is the only thing that changes.
|
||||
|
||||
#![allow(dead_code)] // accessors are read once the patch verbs stop being stubs
|
||||
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Environment variable that points at the kernel checkout.
|
||||
pub const ENV_KERNEL_TREE: &str = "SPECTRAL_KERNEL";
|
||||
|
||||
/// Tree used when that variable is unset, relative to `$HOME`.
|
||||
const DEFAULT_KERNEL_TREE: &str = ".spectral/linux";
|
||||
|
||||
/// Where written patches land, relative to `$HOME`.
|
||||
const DEFAULT_PATCH_DIR: &str = ".spectral/patches";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
kernel_tree: PathBuf,
|
||||
patch_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Resolve the tree from `$SPECTRAL_KERNEL`, falling back to
|
||||
/// `~/.spectral/linux`.
|
||||
///
|
||||
/// The tree is not checked for existence here — commands that need it call
|
||||
/// [`Config::require_kernel_tree`], so `kernel quest` still works before
|
||||
/// anything is cloned.
|
||||
///
|
||||
/// TODO: also read `~/.config/spectral/config.toml` once `spectral init`
|
||||
/// exists. The environment variable should keep winning over the file.
|
||||
pub fn load() -> Result<Self> {
|
||||
let home = home_dir();
|
||||
let kernel_tree = env::var_os(ENV_KERNEL_TREE)
|
||||
.map_or_else(|| home.join(DEFAULT_KERNEL_TREE), PathBuf::from);
|
||||
|
||||
Ok(Self {
|
||||
kernel_tree,
|
||||
patch_dir: home.join(DEFAULT_PATCH_DIR),
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn kernel_tree(&self) -> &Path {
|
||||
&self.kernel_tree
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn patch_dir(&self) -> &Path {
|
||||
&self.patch_dir
|
||||
}
|
||||
|
||||
/// The kernel tree, having confirmed it is one.
|
||||
pub fn require_kernel_tree(&self) -> Result<&Path> {
|
||||
if self.kernel_tree.join("scripts/checkpatch.pl").is_file() {
|
||||
Ok(&self.kernel_tree)
|
||||
} else if self.kernel_tree.exists() {
|
||||
Err(Error::NotAKernelTree {
|
||||
path: self.kernel_tree.clone(),
|
||||
})
|
||||
} else {
|
||||
Err(Error::KernelTreeMissing {
|
||||
path: self.kernel_tree.clone(),
|
||||
env: ENV_KERNEL_TREE,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn home_dir() -> PathBuf {
|
||||
env::var_os("HOME").map_or_else(|| PathBuf::from("."), PathBuf::from)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//! One error type for the whole CLI.
|
||||
//!
|
||||
//! Anything that can go wrong ends up as an [`Error`], gets a `?` at the call
|
||||
//! site, and is printed once in `main` — no `unwrap` in the command paths.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Shorthand for the crate's result type.
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
// Variants that no command constructs yet, but that the command paths below
|
||||
// are written to return. Safe to drop the allow once they are all reached.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Nothing at the configured path at all.
|
||||
#[error("no kernel tree at `{path}`; set ${env} or clone one there")]
|
||||
KernelTreeMissing { path: PathBuf, env: &'static str },
|
||||
|
||||
/// Something is there, but it is not a kernel source tree.
|
||||
#[error("`{path}` is not a kernel source tree (no scripts/checkpatch.pl)")]
|
||||
NotAKernelTree { path: PathBuf },
|
||||
|
||||
/// An external tool ran and failed.
|
||||
#[error("`{program} {args}` failed (exit {code}):\n{stderr}")]
|
||||
ExternalCommand {
|
||||
program: String,
|
||||
args: String,
|
||||
code: String,
|
||||
stderr: String,
|
||||
},
|
||||
|
||||
/// An external tool could not even be started.
|
||||
#[error("could not run `{program}`")]
|
||||
CommandSpawn {
|
||||
program: String,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
/// A patch file the user named does not exist.
|
||||
#[error("no patch named `{name}` in `{dir}`")]
|
||||
PatchNotFound { name: String, dir: PathBuf },
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
//! Thin plumbing over the `git` binary.
|
||||
//!
|
||||
//! Nothing in here knows what a kernel is: it starts processes, hands back
|
||||
//! stdout, and turns a non-zero exit into an [`Error`].
|
||||
|
||||
#![allow(dead_code)] // reachable as soon as the patch verbs stop being stubs
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// A git invocation rooted at one repository.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Git {
|
||||
repo: PathBuf,
|
||||
}
|
||||
|
||||
impl Git {
|
||||
pub fn new(repo: impl Into<PathBuf>) -> Self {
|
||||
Self { repo: repo.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn repo(&self) -> &Path {
|
||||
&self.repo
|
||||
}
|
||||
|
||||
/// Run git in the repository and return its trimmed stdout.
|
||||
///
|
||||
/// This is the only place a git process is started; the semantic
|
||||
/// operations below are written in terms of it.
|
||||
pub fn run(&self, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&self.repo)
|
||||
.args(args)
|
||||
.output()
|
||||
.map_err(|source| Error::CommandSpawn {
|
||||
program: format!("git {}", args.join(" ")),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::ExternalCommand {
|
||||
program: "git".to_owned(),
|
||||
args: args.join(" "),
|
||||
code: output
|
||||
.status
|
||||
.code()
|
||||
.map_or_else(|| "signal".to_owned(), |code| code.to_string()),
|
||||
stderr: String::from_utf8_lossy(&output.stderr)
|
||||
.trim_end()
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout)
|
||||
.trim_end()
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
// TODO: each of these is a few lines over `run`, still unwritten.
|
||||
|
||||
/// The branch currently checked out, or `HEAD` when detached.
|
||||
pub fn current_branch(&self) -> Result<String> {
|
||||
todo!("git rev-parse --abbrev-ref HEAD")
|
||||
}
|
||||
|
||||
/// The working tree diff against `base`, for piping into a patch file.
|
||||
pub fn diff_against(&self, base: &str) -> Result<String> {
|
||||
todo!("git diff {base}: needs --stat/--no-prefix decisions before it is real")
|
||||
}
|
||||
|
||||
/// Commit the index, optionally signing off and optionally amending.
|
||||
pub fn commit(&self, message: &str, signoff: bool, amend: bool) -> Result<()> {
|
||||
todo!("git commit -m {message:?} -s={signoff} --amend={amend}")
|
||||
}
|
||||
|
||||
/// Resolve a revision to a commit hash.
|
||||
pub fn rev_parse(&self, rev: &str) -> Result<String> {
|
||||
todo!("git rev-parse {rev}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Finding kernel work, and seeing it boot.
|
||||
|
||||
pub mod qemu;
|
||||
pub mod quest;
|
||||
@@ -0,0 +1,44 @@
|
||||
//! `spectral kernel test` — build the tree, then boot it under qemu.
|
||||
|
||||
#![allow(dead_code)] // nothing is reachable until `run` stops being a stub
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
|
||||
use crate::cli::TestArgs;
|
||||
use crate::config::Config;
|
||||
use crate::error::Result;
|
||||
|
||||
/// One qemu run.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Boot {
|
||||
/// Image to boot, normally `arch/x86/boot/bzImage`.
|
||||
pub kernel: PathBuf,
|
||||
/// Rootfs or initramfs to hand the kernel.
|
||||
pub rootfs: Option<PathBuf>,
|
||||
/// Arguments appended to the qemu command line verbatim.
|
||||
pub extra_args: Vec<String>,
|
||||
}
|
||||
|
||||
/// Build the tree and hand back the path to the bootable image.
|
||||
pub fn build(kernel_tree: &Path) -> Result<PathBuf> {
|
||||
todo!(
|
||||
"make -j$(nproc) in {}, then locate arch/x86/boot/bzImage",
|
||||
kernel_tree.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// Boot an image and wait for qemu to exit.
|
||||
pub async fn boot(kernel_tree: &Path, boot: &Boot) -> Result<ExitStatus> {
|
||||
// TODO: qemu-system-x86_64 -kernel {boot.kernel} -append "console=ttyS0" \
|
||||
// -nographic, with -initrd when `rootfs` is set and `extra_args` on
|
||||
// the end. Stream serial output instead of buffering it.
|
||||
let _ = (kernel_tree, boot);
|
||||
todo!("boot under qemu and stream the console")
|
||||
}
|
||||
|
||||
/// `spectral kernel test`: build unless told not to, then boot.
|
||||
pub async fn run(config: &Config, args: TestArgs) -> Result<()> {
|
||||
let _ = (config, args);
|
||||
todo!("build + boot")
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! `spectral kernel quest` — go and find something worth fixing.
|
||||
//!
|
||||
//! The source is behind [`QuestSource`] so swapping bugzilla for syzbot, a
|
||||
//! lore.kernel.org thread, or a local TODO file is one new impl and no change
|
||||
//! to the command.
|
||||
|
||||
#![allow(dead_code)] // nothing is reachable until `run` stops being a stub
|
||||
|
||||
use std::future::Future;
|
||||
|
||||
use crate::cli::QuestArgs;
|
||||
use crate::config::Config;
|
||||
use crate::error::Result;
|
||||
|
||||
/// One piece of work, normalised across whatever source it came from.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Issue {
|
||||
/// Source-specific identifier, e.g. a bugzilla bug id.
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
/// Subsystem or component, when the source names one.
|
||||
pub component: Option<String>,
|
||||
/// Where a human reads the rest.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Anything spectral can pull open work from.
|
||||
///
|
||||
/// Written as `impl Future` rather than a bare `async fn` so the `Send` bound
|
||||
/// the async entry point needs is stated in the trait, not inferred at each
|
||||
/// call site.
|
||||
pub trait QuestSource {
|
||||
fn fetch_open(&self) -> impl Future<Output = Result<Vec<Issue>>> + Send;
|
||||
}
|
||||
|
||||
/// Open bugs on bugzilla.kernel.org.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bugzilla {
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl Default for Bugzilla {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "https://bugzilla.kernel.org".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QuestSource for Bugzilla {
|
||||
async fn fetch_open(&self) -> Result<Vec<Issue>> {
|
||||
// TODO: GET `{base_url}/buglist.cgi` with status=NEW/ASSIGNED/REOPENED,
|
||||
// walk the paginated result tables, and pull id + summary + product out
|
||||
// of each row with `scraper`.
|
||||
let _ = &self.base_url;
|
||||
todo!("scrape open bugs from bugzilla.kernel.org")
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the open issues and pick one.
|
||||
///
|
||||
/// TODO: apply `args.filter` to the title or component before picking, and
|
||||
/// print JSON when `args.json` is set.
|
||||
pub async fn run(config: &Config, args: QuestArgs) -> Result<Issue> {
|
||||
let _ = (config, args);
|
||||
todo!("pick one open bug at random")
|
||||
}
|
||||
|
||||
/// Choose one issue at random. `seed` keeps the pick reproducible in tests.
|
||||
pub fn pick_random(issues: &[Issue], seed: u64) -> Option<&Issue> {
|
||||
todo!(
|
||||
"index into {} candidate issues with seed {seed}",
|
||||
issues.len()
|
||||
)
|
||||
}
|
||||
+72
-2
@@ -1,3 +1,73 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
//! spectral — a monolithic kernel work wrapper to make it easy.
|
||||
//!
|
||||
//! `main` does three things: parse, dispatch, render. Anything with a kernel
|
||||
//! or git in it lives behind one of the modules below.
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod error;
|
||||
mod git;
|
||||
mod kernel;
|
||||
mod patch;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::cli::{Cli, Command, KernelCommand, PatchCommand};
|
||||
use crate::config::Config;
|
||||
use crate::error::Result;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> ExitCode {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match run(cli).await {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("spectral: {error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The command modules do the work and hand back what they produced; printing
|
||||
/// it is `main`'s job.
|
||||
async fn run(cli: Cli) -> Result<()> {
|
||||
let config = Config::load()?;
|
||||
|
||||
match cli.command {
|
||||
Command::Kernel { command } => match command {
|
||||
KernelCommand::Quest(args) => {
|
||||
let issue = kernel::quest::run(&config, args).await?;
|
||||
println!("{} {}", issue.id, issue.title);
|
||||
println!("{}", issue.url);
|
||||
Ok(())
|
||||
}
|
||||
KernelCommand::Test(args) => kernel::qemu::run(&config, args).await,
|
||||
},
|
||||
|
||||
Command::Patch { command } => match command {
|
||||
PatchCommand::Check(args) => {
|
||||
let report = patch::check(&config, args)?;
|
||||
print!("{}", report.output);
|
||||
Ok(())
|
||||
}
|
||||
PatchCommand::Format(args) => {
|
||||
let report = patch::format(&config, args)?;
|
||||
print!("{}", report.output);
|
||||
Ok(())
|
||||
}
|
||||
PatchCommand::Commit(args) => patch::commit(&config, args),
|
||||
PatchCommand::Create(args) => {
|
||||
println!("{}", patch::create(&config, args)?.display());
|
||||
Ok(())
|
||||
}
|
||||
PatchCommand::Submit(args) => patch::submit(&config, args),
|
||||
PatchCommand::Update(args) => {
|
||||
println!("{}", patch::update(&config, args)?.display());
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//! `scripts/checkpatch.pl`, run the two ways spectral needs it.
|
||||
|
||||
#![allow(dead_code)] // reachable as soon as the patch verbs stop being stubs
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
/// What checkpatch.pl should look at.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Target {
|
||||
/// The uncommitted working tree, via `--git`.
|
||||
WorkingTree,
|
||||
/// A committed range, e.g. `HEAD~1`.
|
||||
Rev(String),
|
||||
/// A patch file, via `--file`.
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
/// What checkpatch.pl said.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Report {
|
||||
pub errors: usize,
|
||||
pub warnings: usize,
|
||||
/// The full checkpatch output, for printing or for `format` to act on.
|
||||
pub output: String,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Whether the patch is clean by checkpatch's standards.
|
||||
#[must_use]
|
||||
pub fn is_clean(&self) -> bool {
|
||||
self.errors == 0 && self.warnings == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Run checkpatch.pl over `target`.
|
||||
///
|
||||
/// `fix` adds `--fix`, which rewrites the patch file in place — only
|
||||
/// `Target::File` supports it, and the caller is responsible for having the
|
||||
/// change committed first so a bad fix is one `git checkout` away.
|
||||
pub fn run(kernel_tree: &Path, target: &Target, strict: bool, fix: bool) -> Result<Report> {
|
||||
todo!(
|
||||
"scripts/checkpatch.pl --no-tree in {} on {target:?} (strict={strict}, fix={fix})",
|
||||
kernel_tree.display()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! `scripts/get_maintainer.pl`, so `patch submit` knows who to mail.
|
||||
|
||||
#![allow(dead_code)] // reachable as soon as the patch verbs stop being stubs
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
/// Who a patch goes to, already split into the two headers git send-email wants.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Recipients {
|
||||
/// Maintainers listed with a `(M)` role, plus the lists they own.
|
||||
pub to: Vec<String>,
|
||||
/// Reviewers, lists, and everyone else worth a courtesy copy.
|
||||
pub cc: Vec<String>,
|
||||
/// Files the patch touches, which is what the lookup runs against.
|
||||
pub touched: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Look up recipients for a patch.
|
||||
///
|
||||
/// Runs `get_maintainer.pl --roles=... --git` over the patch's diff, which is
|
||||
/// what gives us the files it touches as a side effect.
|
||||
pub fn lookup(kernel_tree: &Path, patch: &Path) -> Result<Recipients> {
|
||||
todo!("get_maintainer.pl --git on {patch:?} inside {kernel_tree:?}")
|
||||
}
|
||||
|
||||
/// Add extra addresses to the CC list, skipping ones already there.
|
||||
pub fn add_cc(recipients: &mut Recipients, extra: &[String]) {
|
||||
let _ = (recipients, extra);
|
||||
todo!("push the extra addresses onto the CC list, deduped")
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! `spectral patch …` — carry a change from working tree to mailing list.
|
||||
//!
|
||||
//! The verbs are all stubs, but the order they are meant to be run in is the
|
||||
//! point of the module:
|
||||
//!
|
||||
//! ```text
|
||||
//! check ─▶ format ─▶ commit ─▶ create ─▶ submit
|
||||
//! ▲ │
|
||||
//! └─ update┘ (v2, v3, …)
|
||||
//! ```
|
||||
//!
|
||||
//! Each verb resolves the tree through [`Config::require_kernel_tree`] and
|
||||
//! builds a [`crate::git::Git`] over it; that is how they reach the plumbing.
|
||||
|
||||
pub mod checkpatch;
|
||||
pub mod maintainers;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::cli::{CheckArgs, CommitArgs, CreateArgs, FormatArgs, SubmitArgs, UpdateArgs};
|
||||
use crate::config::Config;
|
||||
use crate::error::Result;
|
||||
|
||||
/// `checkpatch.pl` over the work in progress.
|
||||
pub fn check(config: &Config, args: CheckArgs) -> Result<checkpatch::Report> {
|
||||
let _ = (config, args);
|
||||
todo!("resolve the target, run checkpatch, report errors and warnings")
|
||||
}
|
||||
|
||||
/// Auto-fix whatever `check` reported.
|
||||
pub fn format(config: &Config, args: FormatArgs) -> Result<checkpatch::Report> {
|
||||
let _ = (config, args);
|
||||
todo!("checkpatch --fix, then re-run check to show what is left")
|
||||
}
|
||||
|
||||
/// Commit the work in progress with a kernel-style message.
|
||||
pub fn commit(config: &Config, args: CommitArgs) -> Result<()> {
|
||||
let _ = (config, args);
|
||||
todo!("git commit, appending Signed-off-by when asked")
|
||||
}
|
||||
|
||||
/// Write the diff against the base branch out as a patch file.
|
||||
///
|
||||
/// The name is normalised to end in `.patch` and the file lands in
|
||||
/// [`Config::patch_dir`] unless `--out` says otherwise. Returns the path
|
||||
/// written.
|
||||
pub fn create(config: &Config, args: CreateArgs) -> Result<PathBuf> {
|
||||
let _ = (config, args);
|
||||
todo!("git diff <base> > <patch_dir>/<name>.patch")
|
||||
}
|
||||
|
||||
/// Send a patch to whoever `get_maintainer.pl` names.
|
||||
///
|
||||
/// `--dry-run` stops one step short: print the recipients and the exact
|
||||
/// `git send-email` invocation instead of sending it, which is the honest way
|
||||
/// to review a first submission to a list.
|
||||
pub fn submit(config: &Config, args: SubmitArgs) -> Result<()> {
|
||||
let _ = (config, args);
|
||||
todo!("look up recipients, then git send-email")
|
||||
}
|
||||
|
||||
/// Re-roll a patch as `v<N>`.
|
||||
///
|
||||
/// `000-kernel-patch.patch` becomes `v2-000-kernel-patch.patch`; an existing
|
||||
/// `vN-` prefix is replaced rather than stacked, so re-running it at the same
|
||||
/// revision is a no-op. `args.revision` overrides the inferred next number.
|
||||
/// Returns the path of the renamed file.
|
||||
pub fn update(config: &Config, args: UpdateArgs) -> Result<PathBuf> {
|
||||
let _ = (config, args);
|
||||
todo!("work out N, rename the file, leave the contents alone")
|
||||
}
|
||||
|
||||
/// The path `patch` should have once it is revision `revision`.
|
||||
///
|
||||
/// Kept separate from [`update`] because it is the rule worth having a test
|
||||
/// for: strip one leading `vN-`, then prepend `v<N>-`.
|
||||
#[allow(dead_code)] // called by `update` once that is written
|
||||
pub fn reroll_path(patch: &Path, revision: u32) -> PathBuf {
|
||||
todo!(
|
||||
"strip the vN- prefix from {} if present, then prepend v{revision}-",
|
||||
patch.display()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user