From 5c5b5d398491f118f5fa58543413388c8212495c Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 19:53:50 +0000 Subject: [PATCH 01/12] --- .trunk/trunk.yaml | 10 ++-- Cargo.lock | 124 ++++++++++++++++++++++++---------------------- Cargo.toml | 1 - src/cli.rs | 6 +++ src/config.rs | 3 ++ src/github.rs | 6 +++ src/main.rs | 33 ++++++++++-- src/trunk.rs | 43 +++++++++++----- 8 files changed, 145 insertions(+), 81 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index f17d1a0..83584bc 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -11,7 +11,7 @@ runtimes: enabled: - node@22.16.0 - python@3.10.8 - - rust@1.82.0 + - rust@1.92.0 actions: enabled: @@ -24,16 +24,16 @@ lint: enabled: - actionlint@1.7.7 - buildifier@8.2.1 - - checkov@3.2.474 + - checkov@3.2.497 - git-diff-check - - markdownlint@0.45.0 + - markdownlint@0.47.0 - osv-scanner@2.2.3 - - prettier@3.6.2 + - prettier@3.7.4 - rustfmt@1.65.0 - taplo@0.10.0 - trufflehog@3.90.8 - yamllint@1.37.1 - - clippy@1.82.0 + - clippy@1.92.0 tools: runtimes: diff --git a/Cargo.lock b/Cargo.lock index 0617369..4ce350e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -143,9 +143,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -293,9 +293,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fnv" @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -694,9 +694,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -710,15 +710,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -756,9 +756,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "linux-raw-sys" @@ -1019,9 +1019,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -1052,18 +1052,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1135,9 +1135,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", @@ -1191,9 +1191,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -1204,9 +1204,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "rustls-pki-types", @@ -1243,9 +1243,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -1320,15 +1320,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1400,9 +1400,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" dependencies = [ "proc-macro2", "quote", @@ -1452,9 +1452,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -1475,9 +1475,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -1509,9 +1509,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -1522,9 +1522,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.9+spec-1.0.0" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap", "serde_core", @@ -1537,27 +1537,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.4+spec-1.0.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.5+spec-1.0.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.5+spec-1.0.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9cd6190959dce0994aa8970cd32ab116d1851ead27e866039acaf2524ce44fa" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" @@ -1606,9 +1606,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -1616,9 +1616,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -1643,9 +1643,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2128,3 +2128,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/Cargo.toml b/Cargo.toml index a5c33a9..9a85193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" [lib] name = "gen" path = "src/lib.rs" -edition = "2021" [dependencies] anyhow = "1.0.100" diff --git a/src/cli.rs b/src/cli.rs index 77cb997..f93e57e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,6 +60,12 @@ pub struct UploadTargets { // Path to file that contains github-json block #[clap(long = "github-json")] pub github_json: String, + + /// Optional: Path to JSON file containing array of targets to upload directly + /// If provided, targets will be read from this file instead of extracting from PR body + /// File should contain a JSON array: ["target1", "target2", "target3"] + #[clap(long = "targets-json")] + pub targets_json: Option, } #[derive(Parser, Debug)] diff --git a/src/config.rs b/src/config.rs index bb45cd3..cb47b86 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,6 +98,9 @@ pub struct PullRequestConf { #[config(default = "4 hours")] pub close_stale_after: String, + + #[config(default = ["main"])] + pub protected_branches: Vec, } #[derive(Config, Serialize, Default)] diff --git a/src/github.rs b/src/github.rs index d2a344f..1c9e8ef 100644 --- a/src/github.rs +++ b/src/github.rs @@ -20,6 +20,8 @@ impl GitHub { #[derive(Serialize, Deserialize, Debug)] pub struct GitHubAction { repository: String, + #[serde(rename = "base_ref")] + pub base_ref: Option, pub event: Event, } #[derive(Serialize, Deserialize, Debug)] @@ -52,4 +54,8 @@ impl GitHubAction { let repo_parts: Vec<&str> = self.repository.split('/').collect(); repo_parts.get(1).expect("Invalid REPOSITORY format") } + + pub fn base_branch(&self) -> &str { + self.base_ref.as_deref().unwrap_or("main") + } } diff --git a/src/main.rs b/src/main.rs index 366f32f..0cb9666 100644 --- a/src/main.rs +++ b/src/main.rs @@ -296,13 +296,18 @@ fn create_pull_request( config: &Conf, dry_run: bool, gh_token: &str, + base_branch: &str, ) -> Result { let lc = maybe_add_logical_merge_conflict(last_pr, config); let current_branch = git(&["branch", "--show-current"]); + // Switch to the base branch and pull latest changes + git(&["checkout", base_branch]); + git(&["pull"]); + let branch_name = format!("change/{}", words.join("-")); - git(&["checkout", "-t", "-b", &branch_name]); + git(&["checkout", "-b", &branch_name]); let commit_msg = format!("Moving words {}", words.join(", ")); git(&["commit", "--no-verify", "-am", &commit_msg]); @@ -354,7 +359,16 @@ fn create_pull_request( first_letters.into_iter().collect::>().join(",") )); - let mut args: Vec<&str> = vec!["pr", "create", "--title", &title, "--body", &body]; + let mut args: Vec<&str> = vec![ + "pr", + "create", + "--title", + &title, + "--body", + &body, + "--base", + base_branch, + ]; for lbl in config.pullrequest.labels.split(',') { args.push("--label"); @@ -457,7 +471,18 @@ fn generate(config: &Conf, cli: &Cli) -> anyhow::Result<()> { // Select token for this PR (round-robin) let current_token = &github_tokens[token_index % github_tokens.len()]; - let pr_result = create_pull_request(&words, last_pr, config, cli.dry_run, current_token); + // Select base branch for this PR (round-robin from protected_branches) + let protected_branches = &config.pullrequest.protected_branches; + let base_branch = &protected_branches[token_index % protected_branches.len()]; + + let pr_result = create_pull_request( + &words, + last_pr, + config, + cli.dry_run, + current_token, + base_branch, + ); if pr_result.is_err() { println!("problem created pr for {:?}", words); continue; @@ -526,7 +551,7 @@ fn run() -> anyhow::Result<()> { Some(Subcommands::Generate {}) => generate(&config, &cli), Some(Subcommands::UploadTargets(ut)) => { // upload_targets(&cli, &gen::pullrequest::get_json()); // &ut.github_json); - upload_targets(&config, &cli, &ut.github_json); + upload_targets(&config, &cli, &ut.github_json, ut.targets_json.as_deref()); Ok(()) } Some(Subcommands::Enqueue(enqueue_args)) => { diff --git a/src/trunk.rs b/src/trunk.rs index 71594fb..5f908c7 100644 --- a/src/trunk.rs +++ b/src/trunk.rs @@ -62,7 +62,12 @@ mod tests { } } -pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { +pub fn upload_targets( + config: &Conf, + cli: &Cli, + github_json_path: &str, + targets_json_path: Option<&str>, +) { // Check for TRUNK_TOKEN at runtime if cli.trunk_token.is_empty() { eprintln!("TRUNK_TOKEN is required for upload-targets subcommand"); @@ -73,18 +78,31 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { let github_json = fs::read_to_string(github_json_path).expect("Failed to read file"); let ga = GitHubAction::from_json(&github_json); - if !&ga.event.pull_request.body.is_some() { - println!("No PR body content found - skipping target upload"); - println!("The PR body is required to extract dependency information using the format: deps=[target1,target2,target3]"); - return; - } + // Get targets either from JSON file or extract from PR body + let impacted_targets = if let Some(targets_path) = targets_json_path { + // Read targets directly from JSON file + let targets_content = + fs::read_to_string(targets_path).expect("Failed to read targets JSON file"); + serde_json::from_str::>(&targets_content) + .expect("Failed to parse targets JSON - expected array of strings") + } else { + // Extract from PR body (existing behavior) + if !&ga.event.pull_request.body.is_some() { + println!("No PR body content found - skipping target upload"); + println!("The PR body is required to extract dependency information using the format: deps=[target1,target2,target3]"); + println!("Alternatively, use --targets-json to provide targets directly"); + return; + } - let body = ga.event.pull_request.body.clone().unwrap(); - let impacted_targets = get_targets(&body); + let body = ga.event.pull_request.body.clone().unwrap(); + let targets = get_targets(&body); - if impacted_targets.is_empty() { - println!("No deps listed in PR body like deps=[a,b,c]"); - } + if targets.is_empty() { + println!("No deps listed in PR body like deps=[a,b,c]"); + } + + targets + }; // Validate that we have targets to upload if impacted_targets.is_empty() { @@ -98,12 +116,13 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { impacted_targets ); + let target_branch = ga.base_branch(); let result = post_targets( ga.repo_owner(), ga.repo_name(), ga.event.pull_request.number, &ga.event.pull_request.head.sha, - "main", + target_branch, impacted_targets, &config.trunk.api, &cli.trunk_token, From 3fd26e3ada5726dcc43eb45c34070df1758e3788 Mon Sep 17 00:00:00 2001 From: Eli Schleifer Date: Tue, 6 Jan 2026 19:55:47 +0000 Subject: [PATCH 02/12] --- .trunk/trunk.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 83584bc..6d91298 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -29,7 +29,7 @@ lint: - markdownlint@0.47.0 - osv-scanner@2.2.3 - prettier@3.7.4 - - rustfmt@1.65.0 + - rustfmt@1.92.0 - taplo@0.10.0 - trufflehog@3.90.8 - yamllint@1.37.1 From dbb71767af1463deef6a5deaa5a550b71e78b0bc Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:07:52 +0000 Subject: [PATCH 03/12] --- src/main.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++------ src/trunk.rs | 8 +++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0cb9666..ab62f02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -184,16 +184,22 @@ fn enqueue(pr: &str, config: &Conf, cli: &Cli, gh_token: &str) { return; } }; + // Get the PR's base branch + let target_branch = get_pr_base_branch(pr, gh_token); + println!("Enqueuing PR {} targeting branch: {}", pr, target_branch); match submit_pull_request( &owner, &name, pr_number, - "main", // Default target branch, could be made configurable - None, // Default priority, could be made configurable + &target_branch, + None, // Default priority, could be made configurable &config.trunk.api, cli, ) { - Ok(_) => println!("Successfully submitted PR {} to Trunk merge queue", pr), + Ok(_) => println!( + "Successfully submitted PR {} to Trunk merge queue (target: {})", + pr, target_branch + ), Err(e) => { eprintln!("Failed to submit PR {} to Trunk merge queue: {}", pr, e); std::process::exit(1); @@ -264,6 +270,40 @@ fn maybe_add_logical_merge_conflict(last_pr: u32, config: &Conf) -> bool { true } +fn get_pr_base_branch(pr: &str, gh_token: &str) -> String { + let result = try_gh(&["pr", "view", pr, "--json", "baseRefName"], gh_token); + if result.is_err() { + // Log the error and fallback to "main" if we can't get the PR info + eprintln!( + "Warning: Failed to get base branch for PR {}: {:?}. Falling back to 'main'", + pr, + result.as_ref().err() + ); + return "main".to_string(); + } + let json_str = result.unwrap(); + let v: Value = match serde_json::from_str(&json_str) { + Ok(val) => val, + Err(e) => { + eprintln!( + "Warning: Failed to parse PR info JSON for PR {}: {}. Falling back to 'main'", + pr, e + ); + return "main".to_string(); + } + }; + v["baseRefName"] + .as_str() + .unwrap_or_else(|| { + eprintln!( + "Warning: PR {} JSON does not contain 'baseRefName' field. Falling back to 'main'", + pr + ); + "main" + }) + .to_string() +} + fn get_last_pr(gh_token: &str) -> u32 { let result = try_gh(&["pr", "list", "--limit=1", "--json", "number"], gh_token); if result.is_err() { @@ -342,8 +382,8 @@ fn create_pull_request( config.pullrequest.close_stale_after )); body.push_str(&format!( - "\n\n[pullrequest]\nrequests per hour: {}\n", - config.pullrequest.requests_per_hour + "\n\n[pullrequest]\nrequests per hour: {}\ntarget branch: {}\n", + config.pullrequest.requests_per_hour, base_branch )); let mut first_letters: Vec<_> = words @@ -490,8 +530,9 @@ fn generate(config: &Conf, cli: &Cli) -> anyhow::Result<()> { let duration = start.elapsed(); let pr = pr_result.unwrap(); println!( - "created pr: {} in {:?} // waiting: {} mins", + "created pr: {} (target: {}) in {:?} // waiting: {} mins", pr, + base_branch, duration, (pull_request_every as f32 / 60.0) ); diff --git a/src/trunk.rs b/src/trunk.rs index 5f908c7..905f5e1 100644 --- a/src/trunk.rs +++ b/src/trunk.rs @@ -110,13 +110,14 @@ pub fn upload_targets( return; } + let target_branch = ga.base_branch(); println!( - "Uploading {} impacted targets: {:?}", + "Uploading {} impacted targets: {:?} (target branch: {})", impacted_targets.len(), - impacted_targets + impacted_targets, + target_branch ); - let target_branch = ga.base_branch(); let result = post_targets( ga.repo_owner(), ga.repo_name(), @@ -186,6 +187,7 @@ pub fn post_targets( println!(" URL: https://{}:443/v1/setImpactedTargets", api); println!(" Repository: {}/{}", repo_owner, repo_name); println!(" PR Number: {}", pr_number); + println!(" Target Branch: {}", target_branch); println!(" Impacted Targets: {:?}", impacted_targets); match status.as_u16() { From 97af5b55c7b42be4318a96a038923deeded446a7 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:15:37 +0000 Subject: [PATCH 04/12] --- src/trunk.rs | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/trunk.rs b/src/trunk.rs index 905f5e1..8def558 100644 --- a/src/trunk.rs +++ b/src/trunk.rs @@ -62,12 +62,7 @@ mod tests { } } -pub fn upload_targets( - config: &Conf, - cli: &Cli, - github_json_path: &str, - targets_json_path: Option<&str>, -) { +pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { // Check for TRUNK_TOKEN at runtime if cli.trunk_token.is_empty() { eprintln!("TRUNK_TOKEN is required for upload-targets subcommand"); @@ -78,31 +73,18 @@ pub fn upload_targets( let github_json = fs::read_to_string(github_json_path).expect("Failed to read file"); let ga = GitHubAction::from_json(&github_json); - // Get targets either from JSON file or extract from PR body - let impacted_targets = if let Some(targets_path) = targets_json_path { - // Read targets directly from JSON file - let targets_content = - fs::read_to_string(targets_path).expect("Failed to read targets JSON file"); - serde_json::from_str::>(&targets_content) - .expect("Failed to parse targets JSON - expected array of strings") - } else { - // Extract from PR body (existing behavior) - if !&ga.event.pull_request.body.is_some() { - println!("No PR body content found - skipping target upload"); - println!("The PR body is required to extract dependency information using the format: deps=[target1,target2,target3]"); - println!("Alternatively, use --targets-json to provide targets directly"); - return; - } - - let body = ga.event.pull_request.body.clone().unwrap(); - let targets = get_targets(&body); + if !&ga.event.pull_request.body.is_some() { + println!("No PR body content found - skipping target upload"); + println!("The PR body is required to extract dependency information using the format: deps=[target1,target2,target3]"); + return; + } - if targets.is_empty() { - println!("No deps listed in PR body like deps=[a,b,c]"); - } + let body = ga.event.pull_request.body.clone().unwrap(); + let impacted_targets = get_targets(&body); - targets - }; + if impacted_targets.is_empty() { + println!("No deps listed in PR body like deps=[a,b,c]"); + } // Validate that we have targets to upload if impacted_targets.is_empty() { @@ -110,6 +92,11 @@ pub fn upload_targets( return; } + println!( + "Uploading {} impacted targets: {:?}", + impacted_targets.len(), + impacted_targets + ); let target_branch = ga.base_branch(); println!( "Uploading {} impacted targets: {:?} (target branch: {})", From 1b37c5e6fb0ee7bfb9cbf48735db70cc948d9480 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:19:15 +0000 Subject: [PATCH 05/12] --- src/github.rs | 35 +++++++++++++++++++++++++++++++++++ src/main.rs | 38 ++------------------------------------ src/trunk.rs | 21 +++++++++++++-------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/github.rs b/src/github.rs index 1c9e8ef..7951263 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,5 +1,6 @@ use crate::process::try_gh; use serde::{Deserialize, Serialize}; +use serde_json::Value; pub struct GitHub; @@ -15,6 +16,40 @@ impl GitHub { pub fn add_label(pr: &str, label: &str, token: &str) -> String { try_gh(&["pr", "edit", pr, "--add-label", label], token).expect("Failed to add label to PR") } + + pub fn get_pr_base_branch(pr: &str, gh_token: &str) -> String { + let result = try_gh(&["pr", "view", pr, "--json", "baseRefName"], gh_token); + if result.is_err() { + // Log the error and fallback to "main" if we can't get the PR info + eprintln!( + "Warning: Failed to get base branch for PR {}: {:?}. Falling back to 'main'", + pr, + result.as_ref().err() + ); + return "main".to_string(); + } + let json_str = result.unwrap(); + let v: Value = match serde_json::from_str(&json_str) { + Ok(val) => val, + Err(e) => { + eprintln!( + "Warning: Failed to parse PR info JSON for PR {}: {}. Falling back to 'main'", + pr, e + ); + return "main".to_string(); + } + }; + v["baseRefName"] + .as_str() + .unwrap_or_else(|| { + eprintln!( + "Warning: PR {} JSON does not contain 'baseRefName' field. Falling back to 'main'", + pr + ); + "main" + }) + .to_string() + } } #[derive(Serialize, Deserialize, Debug)] diff --git a/src/main.rs b/src/main.rs index ab62f02..a3dcd2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -185,7 +185,7 @@ fn enqueue(pr: &str, config: &Conf, cli: &Cli, gh_token: &str) { } }; // Get the PR's base branch - let target_branch = get_pr_base_branch(pr, gh_token); + let target_branch = GitHub::get_pr_base_branch(pr, gh_token); println!("Enqueuing PR {} targeting branch: {}", pr, target_branch); match submit_pull_request( &owner, @@ -270,40 +270,6 @@ fn maybe_add_logical_merge_conflict(last_pr: u32, config: &Conf) -> bool { true } -fn get_pr_base_branch(pr: &str, gh_token: &str) -> String { - let result = try_gh(&["pr", "view", pr, "--json", "baseRefName"], gh_token); - if result.is_err() { - // Log the error and fallback to "main" if we can't get the PR info - eprintln!( - "Warning: Failed to get base branch for PR {}: {:?}. Falling back to 'main'", - pr, - result.as_ref().err() - ); - return "main".to_string(); - } - let json_str = result.unwrap(); - let v: Value = match serde_json::from_str(&json_str) { - Ok(val) => val, - Err(e) => { - eprintln!( - "Warning: Failed to parse PR info JSON for PR {}: {}. Falling back to 'main'", - pr, e - ); - return "main".to_string(); - } - }; - v["baseRefName"] - .as_str() - .unwrap_or_else(|| { - eprintln!( - "Warning: PR {} JSON does not contain 'baseRefName' field. Falling back to 'main'", - pr - ); - "main" - }) - .to_string() -} - fn get_last_pr(gh_token: &str) -> u32 { let result = try_gh(&["pr", "list", "--limit=1", "--json", "number"], gh_token); if result.is_err() { @@ -592,7 +558,7 @@ fn run() -> anyhow::Result<()> { Some(Subcommands::Generate {}) => generate(&config, &cli), Some(Subcommands::UploadTargets(ut)) => { // upload_targets(&cli, &gen::pullrequest::get_json()); // &ut.github_json); - upload_targets(&config, &cli, &ut.github_json, ut.targets_json.as_deref()); + upload_targets(&config, &cli, &ut.github_json); Ok(()) } Some(Subcommands::Enqueue(enqueue_args)) => { diff --git a/src/trunk.rs b/src/trunk.rs index 8def558..a3efa6d 100644 --- a/src/trunk.rs +++ b/src/trunk.rs @@ -1,6 +1,6 @@ use crate::cli::Cli; use crate::config::Conf; -use crate::github::GitHubAction; +use crate::github::{GitHub, GitHubAction}; use regex::Regex; use reqwest::header::{HeaderMap, CONTENT_TYPE}; use serde_json::json; @@ -73,6 +73,7 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { let github_json = fs::read_to_string(github_json_path).expect("Failed to read file"); let ga = GitHubAction::from_json(&github_json); + // Extract targets from PR body if !&ga.event.pull_request.body.is_some() { println!("No PR body content found - skipping target upload"); println!("The PR body is required to extract dependency information using the format: deps=[target1,target2,target3]"); @@ -92,12 +93,16 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { return; } - println!( - "Uploading {} impacted targets: {:?}", - impacted_targets.len(), - impacted_targets - ); - let target_branch = ga.base_branch(); + // Get the PR's base branch using GitHub API + let github_tokens = cli.get_github_tokens(); + let gh_token = github_tokens.first().map(|t| t.as_str()).unwrap_or(""); + let pr_number_str = ga.event.pull_request.number.to_string(); + let target_branch = if !gh_token.is_empty() { + GitHub::get_pr_base_branch(&pr_number_str, gh_token) + } else { + // Fallback to base_ref from JSON if no token available + ga.base_branch().to_string() + }; println!( "Uploading {} impacted targets: {:?} (target branch: {})", impacted_targets.len(), @@ -110,7 +115,7 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { ga.repo_name(), ga.event.pull_request.number, &ga.event.pull_request.head.sha, - target_branch, + &target_branch, impacted_targets, &config.trunk.api, &cli.trunk_token, From 1cdf625c7a2f5ebf5921ff2660bbfb9b6c6f596a Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:40:27 +0000 Subject: [PATCH 06/12] --- README.md | 10 ++++++++++ src/main.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3de486d..4482ed8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Usage: mq [OPTIONS] [COMMAND] Commands: generate Generate pull requests enqueue Enqueue a specific pull request to the merge queue + upload-targets Upload impacted targets for a pull request test-sim Simulate a test with flake rate in consideration housekeeping Clean out conflicting PRs and requeue failed PRs config Print current configuration content to json @@ -34,6 +35,10 @@ requests_per_run value you are specifying to either run in either distributed mo distribute the generate load across the specified `run_generate_for` value or burst mode which will attempt to create pull requests as quickly as possible given the specified `requests_per_run` value. +Generated PRs will target branches from the `protected_branches` list in round-robin fashion. Each +PR includes the target branch information in its body, and the tool automatically detects the +correct target branch when uploading impacted targets or enqueuing PRs via the API. + Burst Mode configuration to create 20 pull requests as quickly as possible ```toml @@ -126,6 +131,11 @@ assuming `mq generate` is called every 10 minutes. # Default value: "4 hours" #close_stale_after = "4 hours" +# List of protected branches that PRs should target +# PRs will be created targeting these branches in round-robin fashion +# Default value: ["main"] +#protected_branches = ["main", "develop", "release"] + [test] # Default value: 0.1 #flake_rate = 0.1 diff --git a/src/main.rs b/src/main.rs index a3dcd2e..cfc4c44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -308,9 +308,53 @@ fn create_pull_request( let current_branch = git(&["branch", "--show-current"]); - // Switch to the base branch and pull latest changes - git(&["checkout", base_branch]); - git(&["pull"]); + // Fetch latest changes to ensure we have all remote branches + let _ = try_git(&["fetch", "origin"]); + + // Check if branch exists locally + let branch_exists_locally = try_git(&[ + "rev-parse", + "--verify", + &format!("refs/heads/{}", base_branch), + ]) + .is_ok(); + + // Switch to the base branch + if branch_exists_locally { + // Branch exists locally, just checkout + if let Err(e) = try_git(&["checkout", base_branch]) { + return Err(format!( + "Failed to checkout existing branch '{}': {}", + base_branch, e + )); + } + } else { + // Branch doesn't exist locally, try to create it from origin + // First check if it exists on origin by trying to fetch it explicitly + let remote_branch = format!("origin/{}", base_branch); + let remote_exists = try_git(&["rev-parse", "--verify", &remote_branch]).is_ok(); + + if remote_exists { + // Branch exists on origin, create local tracking branch + if let Err(e) = try_git(&["checkout", "-b", base_branch, &remote_branch]) { + return Err(format!( + "Failed to create local branch '{}' tracking {}: {}", + base_branch, remote_branch, e + )); + } + } else { + // Branch doesn't exist on origin either + let available_branches = + try_git(&["branch", "-r"]).unwrap_or_else(|_| "unknown".to_string()); + return Err(format!( + "Base branch '{}' does not exist locally or on origin.\nAvailable remote branches:\n{}", + base_branch, available_branches + )); + } + } + + // Pull latest changes + let _ = try_git(&["pull"]); let branch_name = format!("change/{}", words.join("-")); git(&["checkout", "-b", &branch_name]); From 2b08d15b1772fcba468e53b0d34535d889d519c3 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:48:57 +0000 Subject: [PATCH 07/12] --- src/main.rs | 11 ++++++----- src/process.rs | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index cfc4c44..70e1e25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use gen::config::{Conf, EnqueueTrigger}; use gen::config_error::handle_config_load_error; use gen::edit::edit_files_for_pr; use gen::github::GitHub; -use gen::process::{git, run_cmd, try_gh, try_git}; +use gen::process::{git, run_cmd, try_gh, try_git, try_git_quiet}; use gen::trunk::{submit_pull_request, upload_targets}; use rand::Rng; use regex::Regex; @@ -311,8 +311,8 @@ fn create_pull_request( // Fetch latest changes to ensure we have all remote branches let _ = try_git(&["fetch", "origin"]); - // Check if branch exists locally - let branch_exists_locally = try_git(&[ + // Check if branch exists locally (quietly - we expect this to fail if branch doesn't exist) + let branch_exists_locally = try_git_quiet(&[ "rev-parse", "--verify", &format!("refs/heads/{}", base_branch), @@ -330,9 +330,10 @@ fn create_pull_request( } } else { // Branch doesn't exist locally, try to create it from origin - // First check if it exists on origin by trying to fetch it explicitly + // First check if it exists on origin (quietly - we expect this to fail if branch doesn't exist) let remote_branch = format!("origin/{}", base_branch); - let remote_exists = try_git(&["rev-parse", "--verify", &remote_branch]).is_ok(); + let remote_exists = + gen::process::try_git_quiet(&["rev-parse", "--verify", &remote_branch]).is_ok(); if remote_exists { // Branch exists on origin, create local tracking branch diff --git a/src/process.rs b/src/process.rs index a33cefd..668057d 100644 --- a/src/process.rs +++ b/src/process.rs @@ -8,6 +8,15 @@ fn exec_with_env( cmd: &str, args: &[&str], env_vars: Option<&[(&str, &str)]>, +) -> Result { + exec_with_env_quiet(cmd, args, env_vars, false) +} + +fn exec_with_env_quiet( + cmd: &str, + args: &[&str], + env_vars: Option<&[(&str, &str)]>, + quiet: bool, ) -> Result { let mut command = Command::new(cmd); command.args(args); @@ -23,8 +32,10 @@ fn exec_with_env( .unwrap_or_else(|_| panic!("Failed to execute {}", cmd)); if !output.status.success() { - eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - eprintln!("Call to {} {} failed", cmd, args.join(" ")); + if !quiet { + eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + eprintln!("Call to {} {} failed", cmd, args.join(" ")); + } return Err(String::from_utf8_lossy(&output.stderr) .into_owned() .trim() @@ -53,3 +64,7 @@ pub fn git(args: &[&str]) -> String { pub fn try_git(args: &[&str]) -> Result { exec("git", args) } + +pub fn try_git_quiet(args: &[&str]) -> Result { + exec_with_env_quiet("git", args, None, true) +} From 953b73e4583f81225271374eda649f6dfcc535f8 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 21:55:14 +0000 Subject: [PATCH 08/12] --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 70e1e25..07047ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -372,7 +372,7 @@ fn create_pull_request( } } - let mut title = words.join(", "); + let mut title = format!("[{}] {}", base_branch, words.join(", ")); if lc { title = format!("{} (logical-conflict)", title); } From e0674fb2ef99788f215b9587a03c55403aeae061 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 22:07:08 +0000 Subject: [PATCH 09/12] --- src/main.rs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 07047ac..0187b69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -308,8 +308,16 @@ fn create_pull_request( let current_branch = git(&["branch", "--show-current"]); - // Fetch latest changes to ensure we have all remote branches - let _ = try_git(&["fetch", "origin"]); + // Clean up any uncommitted changes before switching branches + // Check if there are any uncommitted changes + let status_output = try_git(&["status", "--porcelain"]); + if let Ok(output) = status_output { + if !output.trim().is_empty() { + // There are uncommitted changes, reset them to ensure clean state + let _ = try_git(&["reset", "--hard", "HEAD"]); + let _ = try_git(&["clean", "-fd"]); + } + } // Check if branch exists locally (quietly - we expect this to fail if branch doesn't exist) let branch_exists_locally = try_git_quiet(&[ @@ -329,8 +337,10 @@ fn create_pull_request( )); } } else { - // Branch doesn't exist locally, try to create it from origin - // First check if it exists on origin (quietly - we expect this to fail if branch doesn't exist) + // Branch doesn't exist locally, fetch from origin to check if it exists there + let _ = try_git(&["fetch", "origin", base_branch]); + + // Check if it exists on origin (quietly - we expect this to fail if branch doesn't exist) let remote_branch = format!("origin/{}", base_branch); let remote_exists = gen::process::try_git_quiet(&["rev-parse", "--verify", &remote_branch]).is_ok(); @@ -344,12 +354,9 @@ fn create_pull_request( )); } } else { - // Branch doesn't exist on origin either - let available_branches = - try_git(&["branch", "-r"]).unwrap_or_else(|_| "unknown".to_string()); return Err(format!( - "Base branch '{}' does not exist locally or on origin.\nAvailable remote branches:\n{}", - base_branch, available_branches + "Base branch '{}' does not exist locally or on origin.", + base_branch )); } } @@ -434,8 +441,11 @@ fn create_pull_request( let result = try_gh(args.as_slice(), gh_token); - // no matter what is result - need to reset checkout + // no matter what is result - need to reset checkout and clean up git(&["checkout", ¤t_branch]); + // Clean up any uncommitted changes and untracked files + let _ = try_git(&["reset", "--hard", "HEAD"]); + let _ = try_git(&["clean", "-fd"]); git(&["pull"]); if result.is_err() { From cb162c8f1d0b7b8760620dbc30479abdb98ec820 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 22:20:43 +0000 Subject: [PATCH 10/12] --- src/main.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0187b69..e553947 100644 --- a/src/main.rs +++ b/src/main.rs @@ -367,8 +367,21 @@ fn create_pull_request( let branch_name = format!("change/{}", words.join("-")); git(&["checkout", "-b", &branch_name]); + // Stage all changes (including untracked files) + let _ = try_git(&["add", "-A"]); + + // Check if there are any changes to commit + let status_output = try_git(&["status", "--porcelain"]); + if let Ok(output) = status_output { + if output.trim().is_empty() { + return Err(format!("No changes to commit for PR. Words: {:?}", words)); + } + } + let commit_msg = format!("Moving words {}", words.join(", ")); - git(&["commit", "--no-verify", "-am", &commit_msg]); + if let Err(e) = try_git(&["commit", "--no-verify", "-m", &commit_msg]) { + return Err(format!("Failed to commit changes: {}", e)); + } if !dry_run { let result = try_git(&["push", "--set-upstream", "origin", "HEAD"]); From 52200bdec84fff0f30679658c88400de8f953780 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 22:25:58 +0000 Subject: [PATCH 11/12] --- src/main.rs | 99 +++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/src/main.rs b/src/main.rs index e553947..8f71047 100644 --- a/src/main.rs +++ b/src/main.rs @@ -270,6 +270,48 @@ fn maybe_add_logical_merge_conflict(last_pr: u32, config: &Conf) -> bool { true } +fn checkout_branch(branch: &str) -> Result<(), String> { + // Check if branch exists locally (quietly - we expect this to fail if branch doesn't exist) + let branch_exists_locally = + try_git_quiet(&["rev-parse", "--verify", &format!("refs/heads/{}", branch)]).is_ok(); + + // Switch to the branch + if branch_exists_locally { + // Branch exists locally, just checkout + if let Err(e) = try_git(&["checkout", branch]) { + return Err(format!( + "Failed to checkout existing branch '{}': {}", + branch, e + )); + } + } else { + // Branch doesn't exist locally, fetch from origin to check if it exists there + let _ = try_git(&["fetch", "origin", branch]); + + // Check if it exists on origin (quietly - we expect this to fail if branch doesn't exist) + let remote_branch = format!("origin/{}", branch); + let remote_exists = + gen::process::try_git_quiet(&["rev-parse", "--verify", &remote_branch]).is_ok(); + + if remote_exists { + // Branch exists on origin, create local tracking branch + if let Err(e) = try_git(&["checkout", "-b", branch, &remote_branch]) { + return Err(format!( + "Failed to create local branch '{}' tracking {}: {}", + branch, remote_branch, e + )); + } + } else { + return Err(format!( + "Base branch '{}' does not exist locally or on origin.", + branch + )); + } + } + + Ok(()) +} + fn get_last_pr(gh_token: &str) -> u32 { let result = try_gh(&["pr", "list", "--limit=1", "--json", "number"], gh_token); if result.is_err() { @@ -297,7 +339,7 @@ fn get_last_pr(gh_token: &str) -> u32 { } fn create_pull_request( - words: &[String], + filenames: &[String], last_pr: u32, config: &Conf, dry_run: bool, @@ -319,51 +361,16 @@ fn create_pull_request( } } - // Check if branch exists locally (quietly - we expect this to fail if branch doesn't exist) - let branch_exists_locally = try_git_quiet(&[ - "rev-parse", - "--verify", - &format!("refs/heads/{}", base_branch), - ]) - .is_ok(); - - // Switch to the base branch - if branch_exists_locally { - // Branch exists locally, just checkout - if let Err(e) = try_git(&["checkout", base_branch]) { - return Err(format!( - "Failed to checkout existing branch '{}': {}", - base_branch, e - )); - } - } else { - // Branch doesn't exist locally, fetch from origin to check if it exists there - let _ = try_git(&["fetch", "origin", base_branch]); - - // Check if it exists on origin (quietly - we expect this to fail if branch doesn't exist) - let remote_branch = format!("origin/{}", base_branch); - let remote_exists = - gen::process::try_git_quiet(&["rev-parse", "--verify", &remote_branch]).is_ok(); - - if remote_exists { - // Branch exists on origin, create local tracking branch - if let Err(e) = try_git(&["checkout", "-b", base_branch, &remote_branch]) { - return Err(format!( - "Failed to create local branch '{}' tracking {}: {}", - base_branch, remote_branch, e - )); - } - } else { - return Err(format!( - "Base branch '{}' does not exist locally or on origin.", - base_branch - )); - } - } + // Checkout the base branch (will fetch from origin if needed) + checkout_branch(base_branch)?; // Pull latest changes let _ = try_git(&["pull"]); + // Now edit the files to create changes (after we're on the correct base branch) + let next_pr_number = last_pr + 1; + let words = edit_files_for_pr(filenames, next_pr_number, config); + let branch_name = format!("change/{}", words.join("-")); git(&["checkout", "-b", &branch_name]); @@ -538,10 +545,6 @@ fn generate(config: &Conf, cli: &Cli) -> anyhow::Result<()> { filenames.sort(); - // Use deterministic dependency count based on PR number - let next_pr_number = last_pr + 1; - let words = edit_files_for_pr(&filenames, next_pr_number, config); - // Select token for this PR (round-robin) let current_token = &github_tokens[token_index % github_tokens.len()]; @@ -550,7 +553,7 @@ fn generate(config: &Conf, cli: &Cli) -> anyhow::Result<()> { let base_branch = &protected_branches[token_index % protected_branches.len()]; let pr_result = create_pull_request( - &words, + &filenames, last_pr, config, cli.dry_run, @@ -558,7 +561,7 @@ fn generate(config: &Conf, cli: &Cli) -> anyhow::Result<()> { base_branch, ); if pr_result.is_err() { - println!("problem created pr for {:?}", words); + println!("problem created pr for files: {:?}", filenames); continue; } let duration = start.elapsed(); From 82db007c0e66928ff0caa4cb249c81453c85c745 Mon Sep 17 00:00:00 2001 From: Jane Doe Date: Tue, 6 Jan 2026 22:28:21 +0000 Subject: [PATCH 12/12] --- src/main.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8f71047..a442751 100644 --- a/src/main.rs +++ b/src/main.rs @@ -350,17 +350,6 @@ fn create_pull_request( let current_branch = git(&["branch", "--show-current"]); - // Clean up any uncommitted changes before switching branches - // Check if there are any uncommitted changes - let status_output = try_git(&["status", "--porcelain"]); - if let Ok(output) = status_output { - if !output.trim().is_empty() { - // There are uncommitted changes, reset them to ensure clean state - let _ = try_git(&["reset", "--hard", "HEAD"]); - let _ = try_git(&["clean", "-fd"]); - } - } - // Checkout the base branch (will fetch from origin if needed) checkout_branch(base_branch)?;