1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 mod android_utils;
6 mod patch_parsing;
7 mod version_control;
8
9 use std::borrow::ToOwned;
10 use std::collections::BTreeSet;
11 use std::path::{Path, PathBuf};
12
13 use anyhow::{Context, Result};
14 use structopt::StructOpt;
15
16 use patch_parsing::{filter_patches_by_platform, PatchCollection, PatchDictSchema, VersionRange};
17 use version_control::RepoSetupContext;
18
main() -> Result<()>19 fn main() -> Result<()> {
20 match Opt::from_args() {
21 Opt::Show {
22 cros_checkout_path,
23 android_checkout_path,
24 sync,
25 keep_unmerged,
26 } => show_subcmd(ShowOpt {
27 cros_checkout_path,
28 android_checkout_path,
29 sync,
30 keep_unmerged,
31 }),
32 Opt::Transpose {
33 cros_checkout_path,
34 cros_reviewers,
35 old_cros_ref,
36 android_checkout_path,
37 android_reviewers,
38 old_android_ref,
39 sync,
40 verbose,
41 dry_run,
42 no_commit,
43 wip,
44 disable_cq,
45 } => transpose_subcmd(TransposeOpt {
46 cros_checkout_path,
47 cros_reviewers: cros_reviewers
48 .map(|r| r.split(',').map(ToOwned::to_owned).collect())
49 .unwrap_or_default(),
50 old_cros_ref,
51 android_checkout_path,
52 android_reviewers: android_reviewers
53 .map(|r| r.split(',').map(ToOwned::to_owned).collect())
54 .unwrap_or_default(),
55 old_android_ref,
56 sync,
57 verbose,
58 dry_run,
59 no_commit,
60 wip,
61 disable_cq,
62 }),
63 }
64 }
65
66 struct ShowOpt {
67 cros_checkout_path: PathBuf,
68 android_checkout_path: PathBuf,
69 keep_unmerged: bool,
70 sync: bool,
71 }
72
show_subcmd(args: ShowOpt) -> Result<()>73 fn show_subcmd(args: ShowOpt) -> Result<()> {
74 let ShowOpt {
75 cros_checkout_path,
76 android_checkout_path,
77 keep_unmerged,
78 sync,
79 } = args;
80 let ctx = RepoSetupContext {
81 cros_checkout: cros_checkout_path,
82 android_checkout: android_checkout_path,
83 sync_before: sync,
84 wip_mode: true, // Has no effect, as we're not making changes
85 enable_cq: false, // Has no effect, as we're not uploading anything
86 };
87 ctx.setup()?;
88 let make_collection = |platform: &str, patches_fp: &Path| -> Result<PatchCollection> {
89 let parsed_collection = PatchCollection::parse_from_file(patches_fp)
90 .with_context(|| format!("could not parse {} PATCHES.json", platform))?;
91 Ok(if keep_unmerged {
92 parsed_collection
93 } else {
94 filter_patches_by_platform(&parsed_collection, platform).map_patches(|p| {
95 // Need to do this platforms creation as Rust 1.55 cannot use "from".
96 let mut platforms = BTreeSet::new();
97 platforms.insert(platform.to_string());
98 PatchDictSchema {
99 platforms,
100 ..p.clone()
101 }
102 })
103 })
104 };
105 let cur_cros_collection = make_collection("chromiumos", &ctx.cros_patches_path())?;
106 let cur_android_collection = make_collection("android", &ctx.android_patches_path())?;
107 let merged = cur_cros_collection.union(&cur_android_collection)?;
108 println!("{}", merged.serialize_patches()?);
109 Ok(())
110 }
111
112 struct TransposeOpt {
113 cros_checkout_path: PathBuf,
114 old_cros_ref: String,
115 android_checkout_path: PathBuf,
116 old_android_ref: String,
117 sync: bool,
118 verbose: bool,
119 dry_run: bool,
120 no_commit: bool,
121 cros_reviewers: Vec<String>,
122 android_reviewers: Vec<String>,
123 wip: bool,
124 disable_cq: bool,
125 }
126
transpose_subcmd(args: TransposeOpt) -> Result<()>127 fn transpose_subcmd(args: TransposeOpt) -> Result<()> {
128 let ctx = RepoSetupContext {
129 cros_checkout: args.cros_checkout_path,
130 android_checkout: args.android_checkout_path,
131 sync_before: args.sync,
132 wip_mode: args.wip,
133 enable_cq: !args.disable_cq,
134 };
135 ctx.setup()?;
136 let cros_patches_path = ctx.cros_patches_path();
137 let android_patches_path = ctx.android_patches_path();
138
139 // Get new Patches -------------------------------------------------------
140 let patch_parsing::PatchTemporalDiff {
141 cur_collection: cur_cros_collection,
142 new_patches: new_cros_patches,
143 version_updates: cros_version_updates,
144 } = patch_parsing::new_patches(
145 &cros_patches_path,
146 &ctx.old_cros_patch_contents(&args.old_cros_ref)?,
147 "chromiumos",
148 )
149 .context("finding new patches for chromiumos")?;
150 let patch_parsing::PatchTemporalDiff {
151 cur_collection: cur_android_collection,
152 new_patches: new_android_patches,
153 version_updates: android_version_updates,
154 } = patch_parsing::new_patches(
155 &android_patches_path,
156 &ctx.old_android_patch_contents(&args.old_android_ref)?,
157 "android",
158 )
159 .context("finding new patches for android")?;
160
161 // Have to ignore patches that are already at the destination, even if
162 // the patches are new.
163 let new_cros_patches = new_cros_patches.subtract(&cur_android_collection)?;
164 let new_android_patches = new_android_patches.subtract(&cur_cros_collection)?;
165
166 // Need to do an extra filtering step for Android, as AOSP doesn't
167 // want patches outside of the start/end bounds.
168 let android_llvm_version: u64 = {
169 let android_llvm_version_str =
170 android_utils::get_android_llvm_version(&ctx.android_checkout)?;
171 android_llvm_version_str.parse::<u64>().with_context(|| {
172 format!(
173 "converting llvm version to u64: '{}'",
174 android_llvm_version_str
175 )
176 })?
177 };
178 let new_android_patches = new_android_patches.filter_patches(|p| {
179 match (p.get_from_version(), p.get_until_version()) {
180 (Some(start), Some(end)) => start <= android_llvm_version && android_llvm_version < end,
181 (Some(start), None) => start <= android_llvm_version,
182 (None, Some(end)) => android_llvm_version < end,
183 (None, None) => true,
184 }
185 });
186
187 // Need to filter version updates to only existing patches to the other platform.
188 let cros_version_updates =
189 filter_version_changes(cros_version_updates, &cur_android_collection);
190 let android_version_updates =
191 filter_version_changes(android_version_updates, &cur_cros_collection);
192
193 if args.verbose {
194 display_patches("New patches from ChromiumOS", &new_cros_patches);
195 display_version_updates("Version updates from ChromiumOS", &cros_version_updates);
196 display_patches("New patches from Android", &new_android_patches);
197 display_version_updates("Version updates from Android", &android_version_updates);
198 }
199
200 if args.dry_run {
201 println!("--dry-run specified; skipping modifications");
202 return Ok(());
203 }
204
205 modify_repos(
206 &ctx,
207 args.no_commit,
208 ModifyOpt {
209 new_cros_patches,
210 cur_cros_collection,
211 cros_version_updates,
212 cros_reviewers: args.cros_reviewers,
213 new_android_patches,
214 cur_android_collection,
215 android_version_updates,
216 android_reviewers: args.android_reviewers,
217 },
218 )
219 }
220
221 struct ModifyOpt {
222 new_cros_patches: PatchCollection,
223 cur_cros_collection: PatchCollection,
224 cros_version_updates: Vec<(String, Option<VersionRange>)>,
225 cros_reviewers: Vec<String>,
226 new_android_patches: PatchCollection,
227 cur_android_collection: PatchCollection,
228 android_version_updates: Vec<(String, Option<VersionRange>)>,
229 android_reviewers: Vec<String>,
230 }
231
modify_repos(ctx: &RepoSetupContext, no_commit: bool, opt: ModifyOpt) -> Result<()>232 fn modify_repos(ctx: &RepoSetupContext, no_commit: bool, opt: ModifyOpt) -> Result<()> {
233 // Cleanup on scope exit.
234 scopeguard::defer! {
235 ctx.cleanup();
236 }
237 // Transpose Patches -----------------------------------------------------
238 let mut cur_android_collection = opt.cur_android_collection;
239 let mut cur_cros_collection = opt.cur_cros_collection;
240 // Apply any version ranges and new patches, then write out.
241 if !opt.new_cros_patches.is_empty() || !opt.cros_version_updates.is_empty() {
242 cur_android_collection =
243 cur_android_collection.update_version_ranges(&opt.cros_version_updates);
244 opt.new_cros_patches
245 .transpose_write(&mut cur_android_collection)?;
246 }
247 if !opt.new_android_patches.is_empty() || !opt.android_version_updates.is_empty() {
248 cur_cros_collection =
249 cur_cros_collection.update_version_ranges(&opt.android_version_updates);
250 opt.new_android_patches
251 .transpose_write(&mut cur_cros_collection)?;
252 }
253
254 if no_commit {
255 println!("--no-commit specified; not committing or uploading");
256 return Ok(());
257 }
258 // Commit and upload for review ------------------------------------------
259 // Note we want to check if the android patches are empty for CrOS, and
260 // vice versa. This is a little counterintuitive.
261 if !opt.new_android_patches.is_empty() {
262 ctx.cros_repo_upload(&opt.cros_reviewers)
263 .context("uploading chromiumos changes")?;
264 }
265 if !opt.new_cros_patches.is_empty() {
266 if let Err(e) = android_utils::sort_android_patches(&ctx.android_checkout) {
267 eprintln!(
268 "Couldn't sort Android patches; continuing. Caused by: {}",
269 e
270 );
271 }
272 ctx.android_repo_upload(&opt.android_reviewers)
273 .context("uploading android changes")?;
274 }
275 Ok(())
276 }
277
278 /// Filter version changes that can't apply to a given collection.
filter_version_changes<T>( version_updates: T, other_platform_collection: &PatchCollection, ) -> Vec<(String, Option<VersionRange>)> where T: IntoIterator<Item = (String, Option<VersionRange>)>,279 fn filter_version_changes<T>(
280 version_updates: T,
281 other_platform_collection: &PatchCollection,
282 ) -> Vec<(String, Option<VersionRange>)>
283 where
284 T: IntoIterator<Item = (String, Option<VersionRange>)>,
285 {
286 version_updates
287 .into_iter()
288 .filter(|(rel_patch_path, _)| {
289 other_platform_collection
290 .patches
291 .iter()
292 .any(|p| &p.rel_patch_path == rel_patch_path)
293 })
294 .collect()
295 }
296
display_patches(prelude: &str, collection: &PatchCollection)297 fn display_patches(prelude: &str, collection: &PatchCollection) {
298 println!("{}", prelude);
299 if collection.patches.is_empty() {
300 println!(" [No Patches]");
301 return;
302 }
303 println!("{}", collection);
304 }
305
display_version_updates(prelude: &str, version_updates: &[(String, Option<VersionRange>)])306 fn display_version_updates(prelude: &str, version_updates: &[(String, Option<VersionRange>)]) {
307 println!("{}", prelude);
308 if version_updates.is_empty() {
309 println!(" [No Version Changes]");
310 return;
311 }
312 for (rel_patch_path, _) in version_updates {
313 println!("* {}", rel_patch_path);
314 }
315 }
316
317 #[derive(Debug, structopt::StructOpt)]
318 #[structopt(name = "patch_sync", about = "A pipeline for syncing the patch code")]
319 enum Opt {
320 /// Show a combined view of the PATCHES.json file, without making any changes.
321 #[allow(dead_code)]
322 Show {
323 #[structopt(parse(from_os_str))]
324 cros_checkout_path: PathBuf,
325 #[structopt(parse(from_os_str))]
326 android_checkout_path: PathBuf,
327
328 /// Keep a patch's platform field even if it's not merged at that platform.
329 #[structopt(long)]
330 keep_unmerged: bool,
331
332 /// Run repo sync before transposing.
333 #[structopt(short, long)]
334 sync: bool,
335 },
336 /// Transpose patches from two PATCHES.json files
337 /// to each other.
338 Transpose {
339 /// Path to the ChromiumOS source repo checkout.
340 #[structopt(long = "cros-checkout", parse(from_os_str))]
341 cros_checkout_path: PathBuf,
342
343 /// Emails to send review requests to during ChromiumOS upload.
344 /// Comma separated.
345 #[structopt(long = "cros-rev")]
346 cros_reviewers: Option<String>,
347
348 /// Git ref (e.g. hash) for the ChromiumOS overlay to use as the base.
349 #[structopt(long = "overlay-base-ref")]
350 old_cros_ref: String,
351
352 /// Path to the Android Open Source Project source repo checkout.
353 #[structopt(long = "aosp-checkout", parse(from_os_str))]
354 android_checkout_path: PathBuf,
355
356 /// Emails to send review requests to during Android upload.
357 /// Comma separated.
358 #[structopt(long = "aosp-rev")]
359 android_reviewers: Option<String>,
360
361 /// Git ref (e.g. hash) for the llvm_android repo to use as the base.
362 #[structopt(long = "aosp-base-ref")]
363 old_android_ref: String,
364
365 /// Run repo sync before transposing.
366 #[structopt(short, long)]
367 sync: bool,
368
369 /// Print information to stdout
370 #[structopt(short, long)]
371 verbose: bool,
372
373 /// Do not change any files. Useful in combination with `--verbose`
374 /// Implies `--no-commit`.
375 #[structopt(long)]
376 dry_run: bool,
377
378 /// Do not commit or upload any changes made.
379 #[structopt(long)]
380 no_commit: bool,
381
382 /// Upload and send things for review, but mark as WIP and send no
383 /// emails.
384 #[structopt(long)]
385 wip: bool,
386
387 /// Don't run CQ if set. Only has an effect if uploading.
388 #[structopt(long)]
389 disable_cq: bool,
390 },
391 }
392