diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index f17d1a0..6d91298 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 - - rustfmt@1.65.0 + - prettier@3.7.4 + - rustfmt@1.92.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/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/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..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,11 +16,47 @@ 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)] pub struct GitHubAction { repository: String, + #[serde(rename = "base_ref")] + pub base_ref: Option, pub event: Event, } #[derive(Serialize, Deserialize, Debug)] @@ -52,4 +89,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..a442751 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; @@ -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 = GitHub::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,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() { @@ -291,21 +339,45 @@ fn get_last_pr(gh_token: &str) -> u32 { } fn create_pull_request( - words: &[String], + filenames: &[String], last_pr: u32, 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"]); + // 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", "-t", "-b", &branch_name]); + 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"]); @@ -316,7 +388,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); } @@ -337,8 +409,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 @@ -354,7 +426,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"); @@ -369,8 +450,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() { @@ -450,23 +534,31 @@ 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()]; - 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( + &filenames, + last_pr, + config, + cli.dry_run, + current_token, + 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(); 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/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) +} diff --git a/src/trunk.rs b/src/trunk.rs index 71594fb..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,10 +93,21 @@ pub fn upload_targets(config: &Conf, cli: &Cli, github_json_path: &str) { return; } + // 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: {:?}", + "Uploading {} impacted targets: {:?} (target branch: {})", impacted_targets.len(), - impacted_targets + impacted_targets, + target_branch ); let result = post_targets( @@ -103,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, - "main", + &target_branch, impacted_targets, &config.trunk.api, &cli.trunk_token, @@ -167,6 +179,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() {