Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6614e0d
refactor(kw): move the kw actor into its own module folder
lorenzoberts Oct 3, 2026
bc85e55
test(kw): move kw actor tests into actor_test.rs
lorenzoberts Oct 3, 2026
b77e421
fix(lore): propagate mbox read errors instead of panicking
lorenzoberts Oct 3, 2026
a33fa7f
fix(lore): tolerate malformed patch tags and list pages
lorenzoberts Oct 3, 2026
d22e3c5
fix(infrastructure): keep error hooks from panicking on terminal restore
lorenzoberts Oct 3, 2026
1beb9fb
refactor(lore): state regex validity with expect
lorenzoberts Oct 3, 2026
103cc3a
test(input): build input contexts and key events from Default
lorenzoberts Oct 3, 2026
9865d9d
test(terminal): drop the test-only ReadEvent message
lorenzoberts Oct 3, 2026
a613d21
refactor(config): remove test-only accessors from config and lore state
lorenzoberts Oct 3, 2026
476c8ef
refactor(kw): remove unused history, readiness and handle APIs
lorenzoberts Oct 3, 2026
a03c126
refactor(lore): remove unused cache policy and feed target list
lorenzoberts Oct 3, 2026
53b0cbd
refactor(kw): gate kw modules on unix instead of allowing dead code
lorenzoberts Oct 3, 2026
822c87c
chore(lints): drop crate-wide too-many-arguments and unused-imports a…
lorenzoberts Oct 3, 2026
161103c
refactor(kw): move kw data types into kw/models
lorenzoberts Oct 3, 2026
0597ffb
refactor(app): move view-model types into app/models
lorenzoberts Oct 3, 2026
148f149
refactor(app): move popup and KwOps state types into app/models
lorenzoberts Oct 3, 2026
fe9eb33
refactor(lore): move cache types into lore/application/models
lorenzoberts Oct 3, 2026
34891a4
refactor(kw): group readiness probes into ReadinessService
lorenzoberts Oct 3, 2026
e005306
refactor(kw): group argv, remote and log-scan helpers into services
lorenzoberts Oct 3, 2026
172af35
refactor(kw): move job helpers into KwActor and KwGitService
lorenzoberts Oct 3, 2026
7348f33
refactor(app): group apply and reviewed-reply actions into services
lorenzoberts Oct 3, 2026
e127b54
refactor(app): turn screen flows into App methods
lorenzoberts Oct 3, 2026
916960c
refactor(app): move app actor helpers into AppActor
lorenzoberts Oct 3, 2026
3757077
refactor(config): group validation and parsing into config services
lorenzoberts Oct 3, 2026
f2abb43
refactor(render): group renderers into PatchRendererService and Cover…
lorenzoberts Oct 3, 2026
57e36d0
refactor(ui): group screen painting into per-screen painters
lorenzoberts Oct 3, 2026
7a5f952
refactor: share one actor reply helper across actors
lorenzoberts Oct 3, 2026
e32bb91
refactor(lore): group parsing and reply helpers into services
lorenzoberts Oct 3, 2026
ff93bc7
refactor(app): group remaining app, terminal and kw helpers into serv…
lorenzoberts Oct 3, 2026
c4473f6
refactor: split public and private impl blocks
lorenzoberts Oct 3, 2026
15672ed
refactor(kw): group unix-only helpers into cfg(unix) submodules
lorenzoberts Oct 4, 2026
b203902
refactor: import model types from their models modules
lorenzoberts Oct 4, 2026
0cb2500
refactor: name actor reply log messages once per actor
lorenzoberts Oct 4, 2026
225454e
refactor(lore): rename PatchsetParserService to PatchsetTextService
lorenzoberts Oct 4, 2026
427ac86
refactor(terminal): inline the key-press wait into the session impl
lorenzoberts Oct 4, 2026
e64d60c
test(kw): split GitStub into public and private impl blocks
lorenzoberts Oct 4, 2026
1c0f0f7
fix(infrastructure): restore the terminal from error hooks only once
lorenzoberts Oct 4, 2026
ab720eb
refactor: drop non-unix support
lorenzoberts Oct 4, 2026
c826081
ci: stop allowing too-many-arguments in clippy
lorenzoberts Oct 4, 2026
a864f0c
test(lore): cover mbox files that are not valid UTF-8
lorenzoberts Oct 4, 2026
8cae997
fix(infrastructure): keep handled errors from tearing down the terminal
lorenzoberts Oct 4, 2026
cdb1172
refactor(config): derive ConfigSnapshot from ConfigState via From
lorenzoberts Oct 4, 2026
fbd7d89
refactor(app): build view models via From<&AppState>
lorenzoberts Oct 4, 2026
2ebf661
refactor: replace remaining manual converters with From
lorenzoberts Oct 4, 2026
1eb8b01
refactor(config): validate update drafts without match guards
lorenzoberts Oct 4, 2026
7e9ad55
refactor: match small enums exhaustively instead of guards and matches!
lorenzoberts Oct 4, 2026
6a92a83
style: drop explicit types before collect
lorenzoberts Oct 4, 2026
31924f6
style: import paths instead of fully qualifying them
lorenzoberts Oct 4, 2026
89693a7
docs: remove process and TODO comments
lorenzoberts Oct 4, 2026
a3ee48a
docs: shorten long comments
lorenzoberts Oct 4, 2026
cbefca3
style: separate logical sections inside long functions
lorenzoberts Oct 4, 2026
712624b
test: replace unwrap with expect in tests
lorenzoberts Oct 4, 2026
abe8f95
test(kw): assert call counts and arguments on mock expectations
lorenzoberts Oct 4, 2026
4a436ad
test(app): assert call counts and arguments on mock expectations
lorenzoberts Oct 4, 2026
13da439
test: move fixture builders into helpers submodules
lorenzoberts Oct 4, 2026
d0c9c0d
test: share one named-field TempDir
lorenzoberts Oct 4, 2026
c68b1c0
chore(lints): deny unwrap and allow attributes in clippy
lorenzoberts Oct 4, 2026
e5cedb2
style: merge split use statements into existing groups
lorenzoberts Oct 4, 2026
096c06a
refactor: hold the kw actor handle directly in main
lorenzoberts Oct 4, 2026
b790aea
test(app): unwrap kw_ops integration tests from the unix module
lorenzoberts Oct 4, 2026
ed142d8
chore(lints): share clippy lints across the workspace
lorenzoberts Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions .github/workflows/build_and_unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,37 +54,3 @@ jobs:
printf '\e[1;33m\tPLEASE, SOLVE THEM LOCALLY W/ `cargo test`\e[0m\n'
printf '\e[1;33m\t==========================================\n\e[0m'
exit 1

# The kw actor and the process infrastructure it uses are unix-only
# (#[cfg(unix)]). This job exists so a cfg leak — non-gated code
# referencing them — fails mechanically instead of in review.
check-non-unix:
runs-on: ubuntu-latest
timeout-minutes: 4
if: '!github.event.pull_request.draft'
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Update rustup and install rustc and cargo
shell: bash
run: |
rustup update
rustup install stable

# rustls/ring needs a C toolchain for the target; windows-gnu builds
# with mingw on Ubuntu, unlike the MSVC target.
- name: Install the mingw cross toolchain
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64

- name: Check non-unix compilation
shell: bash
env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
run: |
rustup target add x86_64-pc-windows-gnu
cargo check --target x86_64-pc-windows-gnu --verbose
2 changes: 1 addition & 1 deletion .github/workflows/format_and_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
- name: Check clippy errors
shell: bash
run: |
cargo clippy --all-features --all-targets --tests -- --allow=clippy::too-many-arguments --deny=warnings --deny=clippy::map_unwrap_or --deny=unconditional_recursion
cargo clippy --all-features --all-targets --tests -- --deny=warnings --deny=clippy::map_unwrap_or --deny=unconditional_recursion
3 changes: 0 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
submodules: recursive
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repos:
entry: cargo clippy
args: [
"--all-features", "--all-targets", "--tests",
"--", "--allow=clippy::too-many-arguments", "--deny=warnings",
"--", "--deny=warnings",
"--deny=clippy::map_unwrap_or", "--deny=unconditional_recursion"
]
types: [rust]
Expand Down
21 changes: 13 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,21 @@ targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
# Publish jobs to run in CI
pr-run-mode = "plan"

# Linter configurations
[lints.rust]
warnings = "deny"
unconditional_recursion = "deny"

[lints.clippy]
too-many-arguments = "allow"
map_unwrap_or = "deny"
[lints]
workspace = true

[workspace]
members = [
"patch_hub_proc_macros",
]

[workspace.lints.rust]
warnings = "deny"
unconditional_recursion = "deny"

[workspace.lints.clippy]
map_unwrap_or = "deny"
unwrap_used = "deny"
allow_attributes = "deny"
redundant_type_annotations = "warn"
match_wildcard_for_single_variants = "warn"
3 changes: 3 additions & 0 deletions patch_hub_proc_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ proc-macro2 = "1.0.95"
[lib]
proc-macro = true
doctest = false # otherwise tests will fail

[lints]
workspace = true
88 changes: 55 additions & 33 deletions patch_hub_proc_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput};
use syn::{Data, DeriveInput, Fields};

/// This procedural macro create default deserealization functions for each
/// structure attribute based on std::Default impl.
Expand Down Expand Up @@ -36,19 +36,40 @@ use syn::{parse_macro_input, Data, DeriveInput};
/// "test_1": 500,
/// "test_2": 100
/// });
/// let example_struct_1: Example = serde_json::from_value(json_data_1).unwrap();
/// let example_struct_1: Example = serde_json::from_value(json_data_1)
/// .expect("example json deserializes");
/// assert_eq!(example_struct_1.test_1, 500);
/// assert_eq!(example_struct_1.test_2, 100);
/// assert_eq!(example_struct_1.test_3, "a".to_string());
/// ```
#[proc_macro_attribute]
pub fn serde_individual_default(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_serde_individual_default(input) {
Ok(tokens) => tokens,
Err(error) => error.to_compile_error().into(),
}
}

fn expand_serde_individual_default(input: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse::<DeriveInput>(input)?;
let struct_name = &input.ident;
let struct_generics = &input.generics;
let struct_fields = match &input.data {
Data::Struct(s) => &s.fields,
_ => panic!("SerdeIndividualDefault can only be used with structs"),
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => fields,
Fields::Unnamed(_) | Fields::Unit => {
return Err(syn::Error::new_spanned(
struct_name,
"SerdeIndividualDefault can only be used with structs that have named fields",
));
}
},
Data::Enum(_) | Data::Union(_) => {
return Err(syn::Error::new_spanned(
struct_name,
"SerdeIndividualDefault can only be used with structs",
));
}
};
let struct_attrs = &input.attrs;
let struct_visibility = &input.vis;
Expand All @@ -69,36 +90,37 @@ pub fn serde_individual_default(_attr: TokenStream, input: TokenStream) -> Token
};

// build struct attributes with #[serde(default = "")] and build the default function itself
let (all_field_attrs, default_deserialize_function_definitions) = struct_fields.iter().fold(
(vec![], vec![]),
|(mut all_field_attrs, mut default_deserialize_function_definitions), field| {
let field_name = &field.ident;
let field_type = &field.ty;
let field_vis = &field.vis;
let field_attrs = &field.attrs;
let field_name_str = field_name.as_ref().unwrap().to_string();

// default function name will be named default_{struct_name}_{field_name}
let default_deserialize_function_name =
format_ident!("default_{}_{}", struct_name_str, field_name_str);
let mut all_field_attrs = Vec::new();
let mut default_deserialize_function_definitions = Vec::new();
for field in &struct_fields.named {
let field_name = field.ident.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
field,
"SerdeIndividualDefault requires every field to be named",
)
})?;
let field_type = &field.ty;
let field_vis = &field.vis;
let field_attrs = &field.attrs;
let field_name_str = field_name.to_string();

let default_deserialize_function_name_str =
default_deserialize_function_name.to_string();
// default function name will be named default_{struct_name}_{field_name}
let default_deserialize_function_name =
format_ident!("default_{}_{}", struct_name_str, field_name_str);

all_field_attrs.push(quote! {
#(#field_attrs)*
#[serde(default = #default_deserialize_function_name_str)]
#field_vis #field_name: #field_type,
});
default_deserialize_function_definitions.push(quote! {
fn #default_deserialize_function_name() -> #field_type {
#default_config_struct_name.#field_name.clone()
}
});
let default_deserialize_function_name_str = default_deserialize_function_name.to_string();

(all_field_attrs, default_deserialize_function_definitions)
},
);
all_field_attrs.push(quote! {
#(#field_attrs)*
#[serde(default = #default_deserialize_function_name_str)]
#field_vis #field_name: #field_type,
});
default_deserialize_function_definitions.push(quote! {
fn #default_deserialize_function_name() -> #field_type {
#default_config_struct_name.#field_name.clone()
}
});
}

// build final struct.
//We have to explicitly derive Deserialize here so the serde attribute works
Expand All @@ -113,5 +135,5 @@ pub fn serde_individual_default(_attr: TokenStream, input: TokenStream) -> Token

#(#default_deserialize_function_definitions)*
};
TokenStream::from(expanded_token_stream)
Ok(TokenStream::from(expanded_token_stream))
}
14 changes: 9 additions & 5 deletions patch_hub_proc_macros/tests/serde_individual_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ fn should_have_default_serialization() {
"test_2": 100
});

let example_struct_1: Example = serde_json::from_value(json_data_1).unwrap();
let example_struct_1: Example =
serde_json::from_value(json_data_1).expect("example with a missing field deserializes");

// Assert that`test_1` and `test_2` are set to the custom value
assert_eq!(example_struct_1.test_1, 500);
Expand All @@ -79,7 +80,8 @@ fn should_have_default_serialization() {
"test_3": "test".to_string()
});

let example_struct_2: Example = serde_json::from_value(json_data_2).unwrap();
let example_struct_2: Example = serde_json::from_value(json_data_2)
.expect("example with another missing field deserializes");

// Assert that`test_1` and `test_3` are set to the custom value
assert_eq!(example_struct_2.test_1, 999);
Expand All @@ -98,7 +100,8 @@ fn should_preserve_other_attributes() {
"test_3": "b".to_string()
});

let example_struct: Example = serde_json::from_value(json_data).unwrap();
let example_struct: Example =
serde_json::from_value(json_data).expect("fully populated example deserializes");

// Assert that`test_2` and `test_3` have getters
assert_eq!(example_struct.test_1, 500);
Expand All @@ -113,7 +116,7 @@ fn test_struct_without_serialize() {
});

let example_without_serialize: ExampleWithoutSerialize =
serde_json::from_value(json_data).unwrap();
serde_json::from_value(json_data).expect("struct without Serialize deserializes");

assert_eq!(example_without_serialize.test_1, 765);
assert_eq!(example_without_serialize.test_2, 123);
Expand All @@ -125,7 +128,8 @@ fn test_public_struct() {
"test_1": 345,
});

let example_public: ExamplePublic = serde_json::from_value(json_data).unwrap();
let example_public: ExamplePublic =
serde_json::from_value(json_data).expect("public struct deserializes");

assert_eq!(example_public.test_1, 345);
assert_eq!(example_public.test_2, 403);
Expand Down
Loading
Loading