evobench_tools/run/sub_command/
insert.rs

1//! -- see insert.md -- todo: copy here automatically via script?
2
3use 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    /// Normally, values from a job file are checked for validity
38    /// against the configuration. This disables that check.
39    #[clap(long)]
40    pub force_invalid: bool,
41}
42
43// (Note: clap::ArgEnum is only for the CLI help texts--FromStr is
44// still necessary!)
45#[derive(Debug, Clone, Copy, clap::ValueEnum)]
46/// Whether to look up Git references in the remote repository
47/// or in a local clone (in which case the current working dir
48/// must be inside it)
49pub enum LocalOrRemote {
50    /// Resolve branch names and references in the local repository
51    /// (fails if the current working directory is not inside a clone
52    /// of the target project repository)
53    Local,
54    /// Resolve branch names and references in the remote repository
55    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    /// `remote_name` (e.g. "origin") is only used for
102    /// LocalOrRemoteGitWorkingDir::Remote
103    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
148// (Note: strum is useless as its FromStr has no helpful error
149// message, thus derive our own.)
150impl 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/// Options to change insertion behaviour
163#[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/// Options to set or override job settings from elsewhere
174#[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    /// The priority (overrides the priority given elsewhere).
184    #[clap(long)]
185    pub priority: Option<Priority>,
186
187    /// The initial priority boost (overrides the boost given
188    /// elsewhere).
189    #[clap(long)]
190    pub initial_boost: Option<Priority>,
191}
192
193impl InsertBenchmarkingJobOpts {
194    /// Fill in fallback values (from the RunConfig) for the only part
195    /// that has those
196    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//Unused
214// impl FallingBackTo for InsertBenchmarkingJobOpts {
215//     fn falling_back_to(self, fallback: &Self) -> Self {
216//         let Self {
217//             reason,
218//             benchmarking_job_settings,
219//             priority,
220//             initial_boost,
221//         } = self;
222//         fallback_to_trait!(fallback.reason);
223//         fallback_to_trait!(fallback.benchmarking_job_settings);
224//         fallback_to_option!(fallback.priority);
225//         fallback_to_option!(fallback.initial_boost);
226//         Self {
227//             reason,
228//             benchmarking_job_settings,
229//             priority,
230//             initial_boost,
231//         }
232//     }
233// }
234
235#[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    /// Take template definitions of a given named entry from the
247    /// configuration file, and commits from explicitly specified
248    /// references.
249    #[command(after_help = "  Note: more job‑setting options are available in the parent command!")]
250    Templates {
251        #[clap(flatten)]
252        opts: InsertOpts,
253
254        /// The name of the entry in the `job_template_lists_name`
255        /// field in the configuration file (RunConfig).
256        job_template_lists_name: String,
257        /// Whether to look up Git references in the remote repository
258        /// or in a local clone (in which case the current working dir
259        /// must be inside it)
260        local_or_remote: LocalOrRemote,
261        /// Git references to the commits that should be benchmarked
262        /// (commit ids, branch oder tag names, and other syntax like
263        /// `HEAD^`).
264        reference_names: Vec<GitReference>,
265    },
266
267    /// Take template definitions of a branch name from the
268    /// configuration file, and commits specified separately (if you
269    /// want to take the commit from the same branch that you specify,
270    /// you can use the `branch` subcommand instead).
271    #[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        /// Whether to look up Git references in the remote repository
279        /// or in a local clone (in which case the current working dir
280        /// must be inside it)
281        local_or_remote: LocalOrRemote,
282        /// Git references to the commits that should be benchmarked
283        /// (commit ids, branch oder tag names, and other syntax like
284        /// `HEAD^`).
285        reference_names: Vec<GitReference>,
286    },
287
288    /// Take template definitions of a branch name from the
289    /// configuration file. If no branch name is given, takes the
290    /// currently checked-out branch for 'local' or the default branch
291    /// for 'remote'.  If you just want to take the configuration from
292    /// a branch, but specify the commit independently, use the
293    /// `template-of-branch` subcommand instead.
294    #[command(after_help = "  Note: more job‑setting options are available in the parent command!")]
295    Branch {
296        #[clap(flatten)]
297        opts: InsertOpts,
298
299        /// Whether to look up Git references in the remote repository
300        /// or in a local clone (in which case the current working dir
301        /// must be inside it)
302        #[clap(value_enum)]
303        local_or_remote: LocalOrRemote,
304        /// Branch name to use for template lookup and commit id. If
305        /// not given, the currently checked-out branch name is tried
306        /// (fails if that branch has no template configuration). Be
307        /// careful when using `local` mode if your local branch
308        /// naming conventions differ from the remote ones.
309        branch_name: Option<GitBranchName>,
310        /// Further commits to insert, specified via Git references
311        /// (commit ids, branch oder tag names, and other syntax like
312        /// `HEAD^`).
313        more_reference_names: Vec<GitReference>,
314    },
315
316    /// Take template definitions and commit from job specification
317    /// files (e.g. to re-use files of failed jobs from queues, or
318    /// edit manually)
319    #[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        /// Override the commit id found in the file
328        #[clap(long)]
329        commit: Option<GitHash>,
330
331        /// Path(s) to the JSON file(s) to insert. The format is the
332        /// one used in the `~/.evobench/queues/` directories,
333        /// except you can alternatively choose JSON5, RON, or one of
334        /// the other formats shown in `config-formats` if the file
335        /// has a corresponding file extension.
336        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    // Do not forget to use the config entries! (XX how to improve the
359    // code to enforce this?)
360    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                // Do *not* add branch_name to those!
448                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                // Add a reason if there isn't one yet
499                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}