1use std::{collections::BTreeSet, fmt::Display, path::PathBuf, str::FromStr};
4
5use anyhow::{Result, anyhow, bail};
6use cj_path_util::unix::fixup_path::CURRENT_DIRECTORY;
7use itertools::Itertools;
8use run_git::git::GitWorkingDir;
9
10use crate::{
11 config_file::backend_from_path,
12 git::GitHash,
13 git_ext::MoreGitWorkingDir,
14 info,
15 run::{
16 benchmarking_job::{
17 BenchmarkingJob, BenchmarkingJobOpts, BenchmarkingJobReasonOpt,
18 BenchmarkingJobSettingsOpts,
19 },
20 config::{JobTemplate, RunConfigBundle, ShareableConfig},
21 insert_jobs::{DryRunOpt, ForceOpt, QuietOpt, insert_jobs},
22 polling_pool::PollingPool,
23 run_queues::RunQueues,
24 sub_command::open_polling_pool,
25 working_directory::REMOTE_NAME,
26 },
27 serde_types::{
28 date_and_time::DateTimeWithOffset, git_branch_name::GitBranchName,
29 git_reference::GitReference, priority::Priority,
30 },
31 serde_util::serde_read_json,
32 utillib::fallback::FallingBackTo,
33};
34
35#[derive(Debug, Clone, clap::Args)]
36pub struct ForceInvalidOpt {
37 #[clap(long)]
40 pub force_invalid: bool,
41}
42
43#[derive(Debug, Clone, Copy, clap::ValueEnum)]
46pub enum LocalOrRemote {
50 Local,
54 Remote,
56}
57
58impl LocalOrRemote {
59 pub fn as_str(self) -> &'static str {
60 match self {
61 LocalOrRemote::Local => "local",
62 LocalOrRemote::Remote => "remote",
63 }
64 }
65
66 pub fn as_char(self) -> char {
67 match self {
68 LocalOrRemote::Local => 'L',
69 LocalOrRemote::Remote => 'R',
70 }
71 }
72
73 pub fn load(self, shareable_config: &ShareableConfig) -> Result<LocalOrRemoteGitWorkingDir> {
74 match self {
75 LocalOrRemote::Local => {
76 let git_working_dir = GitWorkingDir {
77 working_dir_path: CURRENT_DIRECTORY.to_owned().into(),
78 };
79 Ok(LocalOrRemoteGitWorkingDir::Local { git_working_dir })
80 }
81 LocalOrRemote::Remote => {
82 let polling_pool = open_polling_pool(shareable_config)?;
83 Ok(LocalOrRemoteGitWorkingDir::Remote { polling_pool })
84 }
85 }
86 }
87}
88
89impl Display for LocalOrRemote {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95pub enum LocalOrRemoteGitWorkingDir {
96 Local { git_working_dir: GitWorkingDir },
97 Remote { polling_pool: PollingPool },
98}
99
100impl LocalOrRemoteGitWorkingDir {
101 pub fn resolve_references<R: AsRef<GitReference>>(
104 &mut self,
105 remote_name: &str,
106 references: impl IntoIterator<Item = R>,
107 ) -> Result<Vec<Option<GitHash>>> {
108 match self {
109 LocalOrRemoteGitWorkingDir::Local { git_working_dir } => references
110 .into_iter()
111 .map(|reference| -> Result<Option<GitHash>> {
112 let reference = reference.as_ref();
113 Ok(git_working_dir
114 .git_rev_parse(reference.as_str(), true)?
115 .map(|s| {
116 GitHash::from_str(&s).expect("git rev-parse always returns hashes")
117 }))
118 })
119 .try_collect(),
120 LocalOrRemoteGitWorkingDir::Remote { polling_pool } => {
121 let working_dir_id = polling_pool.updated_working_dir()?;
122 polling_pool.resolve_references(working_dir_id, Some(remote_name), references)
123 }
124 }
125 }
126
127 pub fn get_branch_default(&mut self) -> Result<Option<GitBranchName>> {
128 match self {
129 LocalOrRemoteGitWorkingDir::Local { git_working_dir } => {
130 git_working_dir.get_current_branch()
131 }
132 LocalOrRemoteGitWorkingDir::Remote { polling_pool } => {
133 let id = polling_pool.updated_working_dir()?;
134 polling_pool.process_in_working_directory(
135 id,
136 &DateTimeWithOffset::now(None),
137 |wdwp| {
138 let wd = wdwp.into_inner().expect("still there?");
139 wd.git_working_dir.get_current_branch()
140 },
141 "LocalOrRemoteGitWorkingDir.get_branch_default",
142 )
143 }
144 }
145 }
146}
147
148impl FromStr for LocalOrRemote {
151 type Err = anyhow::Error;
152
153 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
154 match s {
155 "local" => Ok(LocalOrRemote::Local),
156 "remote" => Ok(LocalOrRemote::Remote),
157 _ => bail!("invalid argument {s:?}, expecting 'local' or 'remote'"),
158 }
159 }
160}
161
162#[derive(clap::Args, Debug)]
164pub struct InsertBehaviourOpts {
165 #[clap(flatten)]
166 force_opt: ForceOpt,
167 #[clap(flatten)]
168 quiet_opt: QuietOpt,
169 #[clap(flatten)]
170 dry_run_opt: DryRunOpt,
171}
172
173#[derive(Debug, Clone, clap::Args)]
175#[command(allow_hyphen_values = true)]
176pub struct InsertBenchmarkingJobOpts {
177 #[clap(flatten)]
178 pub reason: BenchmarkingJobReasonOpt,
179
180 #[clap(flatten)]
181 pub benchmarking_job_settings: BenchmarkingJobSettingsOpts,
182
183 #[clap(long)]
185 pub priority: Option<Priority>,
186
187 #[clap(long)]
190 pub initial_boost: Option<Priority>,
191}
192
193impl InsertBenchmarkingJobOpts {
194 pub fn complete_with(self, fallback: &BenchmarkingJobSettingsOpts) -> Self {
197 let Self {
198 reason,
199 benchmarking_job_settings,
200 priority,
201 initial_boost,
202 } = self;
203 let benchmarking_job_settings = benchmarking_job_settings.falling_back_to(fallback);
204 Self {
205 reason,
206 benchmarking_job_settings,
207 priority,
208 initial_boost,
209 }
210 }
211}
212
213#[derive(clap::Args, Debug)]
236pub struct InsertOpts {
237 #[clap(flatten)]
238 insert_behaviour_opts: InsertBehaviourOpts,
239
240 #[clap(flatten)]
241 insert_benchmarking_job_opts: InsertBenchmarkingJobOpts,
242}
243
244#[derive(clap::Subcommand, Debug)]
245pub enum Insert {
246 #[command(after_help = " Note: more job‑setting options are available in the parent command!")]
250 Templates {
251 #[clap(flatten)]
252 opts: InsertOpts,
253
254 job_template_lists_name: String,
257 local_or_remote: LocalOrRemote,
261 reference_names: Vec<GitReference>,
265 },
266
267 #[command(after_help = " Note: more job‑setting options are available in the parent command!")]
272 TemplatesOfBranch {
273 #[clap(flatten)]
274 opts: InsertOpts,
275
276 branch_name: GitBranchName,
277 #[clap(value_enum)]
278 local_or_remote: LocalOrRemote,
282 reference_names: Vec<GitReference>,
286 },
287
288 #[command(after_help = " Note: more job‑setting options are available in the parent command!")]
295 Branch {
296 #[clap(flatten)]
297 opts: InsertOpts,
298
299 #[clap(value_enum)]
303 local_or_remote: LocalOrRemote,
304 branch_name: Option<GitBranchName>,
310 more_reference_names: Vec<GitReference>,
314 },
315
316 #[command(after_help = " Note: more job‑setting options are available in the parent command!")]
320 JobFiles {
321 #[clap(flatten)]
322 opts: InsertOpts,
323
324 #[clap(flatten)]
325 force_invalid_opt: ForceInvalidOpt,
326
327 #[clap(long)]
329 commit: Option<GitHash>,
330
331 paths: Vec<PathBuf>,
337 },
338}
339
340fn insert_templates_with_references(
341 shareable_config: &ShareableConfig,
342 insert_opts: InsertOpts,
343 queues: &RunQueues,
344 mut gwd: LocalOrRemoteGitWorkingDir,
345 job_templates: &[JobTemplate],
346 reference_names: BTreeSet<GitReference>,
347) -> Result<usize> {
348 let InsertOpts {
349 insert_behaviour_opts:
350 InsertBehaviourOpts {
351 force_opt,
352 quiet_opt,
353 dry_run_opt,
354 },
355 insert_benchmarking_job_opts,
356 } = insert_opts;
357
358 let insert_benchmarking_job_opts = insert_benchmarking_job_opts
361 .complete_with(&shareable_config.run_config.benchmarking_job_settings);
362
363 let commits: Vec<Option<GitHash>> = gwd.resolve_references(REMOTE_NAME, &reference_names)?;
364 let commits: BTreeSet<GitHash> = commits.into_iter().filter_map(|v| v).collect();
365 info!("reference_names {reference_names:?} resolve to commits {commits:?}");
366
367 let benchmarking_jobs: Vec<BenchmarkingJob> = commits
368 .into_iter()
369 .map(|commit_id| {
370 let benchmarking_job_opts = BenchmarkingJobOpts {
371 insert_benchmarking_job_opts: insert_benchmarking_job_opts.clone(),
372 commit_id,
373 };
374 benchmarking_job_opts.complete_jobs(job_templates)
375 })
376 .flatten()
377 .collect();
378
379 insert_jobs(
380 benchmarking_jobs,
381 shareable_config,
382 dry_run_opt,
383 force_opt,
384 quiet_opt,
385 queues,
386 )
387}
388
389impl Insert {
390 pub fn run(self, run_config_bundle: &RunConfigBundle, queues: &RunQueues) -> Result<usize> {
391 let conf = &run_config_bundle.shareable.run_config;
392
393 match self {
394 Insert::Templates {
395 mut opts,
396 job_template_lists_name,
397 local_or_remote,
398 reference_names,
399 } => {
400 let job_templates = conf
401 .job_template_lists
402 .get(&*job_template_lists_name)
403 .ok_or_else(|| {
404 anyhow!(
405 "there is no entry under `job_template_lists_name` for name \
406 {job_template_lists_name:?} in config file at {:?}",
407 run_config_bundle.config_file.path()
408 )
409 })?;
410
411 let reference_names: BTreeSet<GitReference> = reference_names.into_iter().collect();
412
413 opts.insert_benchmarking_job_opts
414 .reason
415 .reason
416 .get_or_insert(format!("T {job_template_lists_name}"));
417
418 let gwd = local_or_remote.load(&run_config_bundle.shareable)?;
419 insert_templates_with_references(
420 &run_config_bundle.shareable,
421 opts,
422 queues,
423 gwd,
424 job_templates,
425 reference_names,
426 )
427 }
428
429 Insert::TemplatesOfBranch {
430 mut opts,
431 branch_name,
432 local_or_remote,
433 reference_names,
434 } => {
435 let job_templates = conf
436 .remote_repository
437 .remote_branch_names_for_poll
438 .get(&branch_name)
439 .ok_or_else(|| {
440 anyhow!(
441 "there is no entry under \
442 `remote_repository.remote_branch_names_for_poll` \
443 for branch name {branch_name}"
444 )
445 })?;
446
447 let reference_names: BTreeSet<GitReference> = reference_names.into_iter().collect();
449
450 opts.insert_benchmarking_job_opts
451 .reason
452 .reason
453 .get_or_insert(format!("{} {branch_name}", local_or_remote.as_char()));
454
455 let gwd = local_or_remote.load(&run_config_bundle.shareable)?;
456 insert_templates_with_references(
457 &run_config_bundle.shareable,
458 opts,
459 queues,
460 gwd,
461 job_templates,
462 reference_names,
463 )
464 }
465
466 Insert::Branch {
467 mut opts,
468 local_or_remote,
469 branch_name,
470 more_reference_names,
471 } => {
472 let mut gwd = local_or_remote.load(&run_config_bundle.shareable)?;
473 let branch_name = if let Some(branch_name) = branch_name {
474 branch_name
475 } else {
476 gwd.get_branch_default()?.ok_or_else(|| {
477 anyhow!("{local_or_remote} Git repository has no default/current branch")
478 })?
479 };
480 info!("using {local_or_remote} branch {branch_name}");
481
482 let job_templates = conf
483 .remote_repository
484 .remote_branch_names_for_poll
485 .get(&branch_name)
486 .ok_or_else(|| {
487 anyhow!(
488 "there is no entry under \
489 `remote_repository.remote_branch_names_for_poll` \
490 for branch name {branch_name}"
491 )
492 })?;
493
494 let mut reference_names: BTreeSet<GitReference> =
495 more_reference_names.into_iter().collect();
496 reference_names.insert(branch_name.to_reference());
497
498 opts.insert_benchmarking_job_opts
500 .reason
501 .reason
502 .get_or_insert(format!("{} {branch_name}", local_or_remote.as_char()));
503
504 insert_templates_with_references(
505 &run_config_bundle.shareable,
506 opts,
507 queues,
508 gwd,
509 job_templates,
510 reference_names,
511 )
512 }
513
514 Insert::JobFiles {
515 opts,
516 force_invalid_opt,
517 commit,
518 paths,
519 } => {
520 let InsertOpts {
521 insert_behaviour_opts:
522 InsertBehaviourOpts {
523 force_opt,
524 quiet_opt,
525 dry_run_opt,
526 },
527 insert_benchmarking_job_opts,
528 } = opts;
529
530 let mut benchmarking_jobs = Vec::new();
531 for path in &paths {
532 let mut job: BenchmarkingJob = if let Ok(backend) = backend_from_path(&path) {
533 backend.load_config_file(&path)?
534 } else {
535 serde_read_json(&path)?
536 };
537
538 job.check_and_init(
539 conf,
540 &insert_benchmarking_job_opts,
541 commit.as_ref(),
542 &force_invalid_opt,
543 )?;
544
545 benchmarking_jobs.push(job);
546 }
547
548 insert_jobs(
549 benchmarking_jobs,
550 &run_config_bundle.shareable,
551 dry_run_opt,
552 force_opt,
553 quiet_opt,
554 queues,
555 )
556 }
557 }
558 }
559}