radicle: remove job cob
The Job COB is defined as separate crate: [radicle-job]. So this change removes its definition and use from the `heartwood` repository. [radicle-job]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z2UcCU1LgMshWvXj6hXSDDrwB8q8M
This commit is contained in:
parent
f4c8ff7a64
commit
e6ef767f36
|
|
@ -1,87 +0,0 @@
|
||||||
The `rad job` command lets you manage job COBs. Let's first checkout the
|
|
||||||
`heartwood` repository:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad checkout rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
|
|
||||||
✓ Repository checkout successful under ./heartwood
|
|
||||||
$ cd heartwood
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the `rad job` (or `rad job list`) command we can see that there are
|
|
||||||
currently no jobs listed:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad job
|
|
||||||
Nothing to show.
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's create a job to represent a new CI run. We check what the current `HEAD`
|
|
||||||
of the repository is, and use the `rad job trigger` to start a fresh job for
|
|
||||||
that commit:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git rev-parse HEAD
|
|
||||||
f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
|
||||||
$ rad job trigger f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354
|
|
||||||
╭──────────────────────────────────────────────────╮
|
|
||||||
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
|
|
||||||
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
|
|
||||||
│ State fresh │
|
|
||||||
╰──────────────────────────────────────────────────╯
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's check the list again, and we should we see our fresh job there:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad job
|
|
||||||
╭───────────────────────────────╮
|
|
||||||
│ ● ID Commit State │
|
|
||||||
├───────────────────────────────┤
|
|
||||||
│ ● fbbda24 f2de534 fresh │
|
|
||||||
╰───────────────────────────────╯
|
|
||||||
```
|
|
||||||
|
|
||||||
From there we can start a new job, assigning an arbitrary identifier `xyzzy`,
|
|
||||||
which would usually from the CI system that is running the job:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad job start fbbda2447c30ebbab9b746498cd41a383ff05225 xyzzy
|
|
||||||
```
|
|
||||||
|
|
||||||
Checking the job again, we can now see that the job is `running`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad job
|
|
||||||
╭─────────────────────────────────╮
|
|
||||||
│ ● ID Commit State │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ ● fbbda24 f2de534 running │
|
|
||||||
╰─────────────────────────────────╯
|
|
||||||
$ rad job show fbbda2447c30ebbab9b746498cd41a383ff05225
|
|
||||||
╭──────────────────────────────────────────────────╮
|
|
||||||
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
|
|
||||||
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
|
|
||||||
│ State running │
|
|
||||||
│ Run ID xyzzy │
|
|
||||||
╰──────────────────────────────────────────────────╯
|
|
||||||
```
|
|
||||||
|
|
||||||
When a job has finished, we can mark it as done -- either with a `--success` or
|
|
||||||
`--failed` flag -- using the `rad job finish` command:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rad job finish --success fbbda2447c30ebbab9b746498cd41a383ff05225
|
|
||||||
$ rad job
|
|
||||||
╭───────────────────────────────────╮
|
|
||||||
│ ● ID Commit State │
|
|
||||||
├───────────────────────────────────┤
|
|
||||||
│ ● fbbda24 f2de534 succeeded │
|
|
||||||
╰───────────────────────────────────╯
|
|
||||||
$ rad job show fbbda2447c30ebbab9b746498cd41a383ff05225
|
|
||||||
╭──────────────────────────────────────────────────╮
|
|
||||||
│ Job fbbda2447c30ebbab9b746498cd41a383ff05225 │
|
|
||||||
│ Commit f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 │
|
|
||||||
│ State succeeded │
|
|
||||||
│ Run ID xyzzy │
|
|
||||||
╰──────────────────────────────────────────────────╯
|
|
||||||
```
|
|
||||||
|
|
@ -32,8 +32,6 @@ pub mod rad_init;
|
||||||
pub mod rad_inspect;
|
pub mod rad_inspect;
|
||||||
#[path = "commands/issue.rs"]
|
#[path = "commands/issue.rs"]
|
||||||
pub mod rad_issue;
|
pub mod rad_issue;
|
||||||
#[path = "commands/job.rs"]
|
|
||||||
pub mod rad_job;
|
|
||||||
#[path = "commands/ls.rs"]
|
#[path = "commands/ls.rs"]
|
||||||
pub mod rad_ls;
|
pub mod rad_ls;
|
||||||
#[path = "commands/node.rs"]
|
#[path = "commands/node.rs"]
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ const COMMANDS: &[Help] = &[
|
||||||
rad_inbox::HELP,
|
rad_inbox::HELP,
|
||||||
rad_inspect::HELP,
|
rad_inspect::HELP,
|
||||||
rad_issue::HELP,
|
rad_issue::HELP,
|
||||||
rad_job::HELP,
|
|
||||||
rad_ls::HELP,
|
rad_ls::HELP,
|
||||||
rad_node::HELP,
|
rad_node::HELP,
|
||||||
rad_patch::HELP,
|
rad_patch::HELP,
|
||||||
|
|
|
||||||
|
|
@ -1,380 +0,0 @@
|
||||||
#![allow(clippy::or_fun_call)]
|
|
||||||
use std::ffi::OsString;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _};
|
|
||||||
|
|
||||||
use radicle::cob::job::{JobStore, Reason, State};
|
|
||||||
use radicle::crypto;
|
|
||||||
use radicle::node::device::Device;
|
|
||||||
use radicle::node::{Handle, NodeId};
|
|
||||||
use radicle::storage::{WriteRepository, WriteStorage};
|
|
||||||
use radicle::{cob, Node};
|
|
||||||
|
|
||||||
use crate::git::Rev;
|
|
||||||
use crate::terminal as term;
|
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
|
||||||
use crate::terminal::Element;
|
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
|
||||||
name: "job",
|
|
||||||
description: "Manage automated jobs on a repository",
|
|
||||||
version: env!("CARGO_PKG_VERSION"),
|
|
||||||
usage: r#"
|
|
||||||
Usage
|
|
||||||
|
|
||||||
rad job [<option>...]
|
|
||||||
rad job trigger <commit-id>
|
|
||||||
rad job start <job-id> <run-id> [<url>]
|
|
||||||
rad job list
|
|
||||||
rad job show <job-id>
|
|
||||||
rad job finish <job-id> [--success | --failed]
|
|
||||||
rad job delete <job-id>
|
|
||||||
|
|
||||||
Options
|
|
||||||
|
|
||||||
--no-announce Don't announce job records to peers
|
|
||||||
--quiet, -q Don't print anything
|
|
||||||
--help Print help
|
|
||||||
"#,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, PartialEq, Eq)]
|
|
||||||
pub enum OperationName {
|
|
||||||
Trigger,
|
|
||||||
Start,
|
|
||||||
#[default]
|
|
||||||
List,
|
|
||||||
Show,
|
|
||||||
Finish,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Operation {
|
|
||||||
Trigger {
|
|
||||||
commit: Rev,
|
|
||||||
},
|
|
||||||
Start {
|
|
||||||
job_id: Rev,
|
|
||||||
run_id: String,
|
|
||||||
info_url: Option<String>,
|
|
||||||
},
|
|
||||||
List,
|
|
||||||
Show {
|
|
||||||
job_id: Rev,
|
|
||||||
},
|
|
||||||
Finish {
|
|
||||||
job_id: Rev,
|
|
||||||
reason: Reason,
|
|
||||||
},
|
|
||||||
Delete {
|
|
||||||
job_id: Rev,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Options {
|
|
||||||
pub op: Operation,
|
|
||||||
pub announce: bool,
|
|
||||||
pub quiet: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Args for Options {
|
|
||||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
|
||||||
use lexopt::prelude::*;
|
|
||||||
|
|
||||||
let mut parser = lexopt::Parser::from_args(args);
|
|
||||||
let mut op: Option<OperationName> = None;
|
|
||||||
let mut commit: Option<Rev> = None;
|
|
||||||
let mut job_id: Option<Rev> = None;
|
|
||||||
let mut run_id: Option<String> = None;
|
|
||||||
let mut info_url: Option<String> = None;
|
|
||||||
let mut announce = true;
|
|
||||||
let mut quiet = false;
|
|
||||||
let mut succeeded = false;
|
|
||||||
let mut failed = false;
|
|
||||||
|
|
||||||
while let Some(arg) = parser.next()? {
|
|
||||||
match arg {
|
|
||||||
Long("help") | Short('h') => {
|
|
||||||
return Err(Error::Help.into());
|
|
||||||
}
|
|
||||||
Long("no-announce") => {
|
|
||||||
announce = false;
|
|
||||||
}
|
|
||||||
Long("quiet") | Short('q') => {
|
|
||||||
quiet = true;
|
|
||||||
}
|
|
||||||
Long("success") | Long("succeeded") | Short('s') => {
|
|
||||||
succeeded = true;
|
|
||||||
}
|
|
||||||
Long("failure") | Long("failed") | Short('f') => {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
|
||||||
"trigger" => op = Some(OperationName::Trigger),
|
|
||||||
"start" => op = Some(OperationName::Start),
|
|
||||||
"list" => op = Some(OperationName::List),
|
|
||||||
"show" => op = Some(OperationName::Show),
|
|
||||||
"finish" => op = Some(OperationName::Finish),
|
|
||||||
"delete" => op = Some(OperationName::Delete),
|
|
||||||
|
|
||||||
unknown => anyhow::bail!("unknown operation '{}'", unknown),
|
|
||||||
},
|
|
||||||
Value(val) if commit.is_none() && op == Some(OperationName::Trigger) => {
|
|
||||||
let val = term::args::oid(&val)?;
|
|
||||||
let val = Rev::from(val.to_string());
|
|
||||||
commit = Some(val);
|
|
||||||
}
|
|
||||||
Value(val)
|
|
||||||
if job_id.is_none()
|
|
||||||
&& op.is_some()
|
|
||||||
&& matches!(
|
|
||||||
op.as_ref().unwrap(),
|
|
||||||
OperationName::Start
|
|
||||||
| OperationName::Show
|
|
||||||
| OperationName::Finish
|
|
||||||
| OperationName::Delete
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
let val = term::args::oid(&val)?;
|
|
||||||
let val = Rev::from(val.to_string());
|
|
||||||
job_id = Some(val);
|
|
||||||
}
|
|
||||||
Value(val)
|
|
||||||
if job_id.is_some()
|
|
||||||
&& run_id.is_none()
|
|
||||||
&& op.is_some()
|
|
||||||
&& matches!(op.as_ref().unwrap(), OperationName::Start) =>
|
|
||||||
{
|
|
||||||
run_id = Some(val.to_str().unwrap().to_string());
|
|
||||||
}
|
|
||||||
Value(val)
|
|
||||||
if job_id.is_some()
|
|
||||||
&& run_id.is_some()
|
|
||||||
&& op.is_some()
|
|
||||||
&& matches!(op.as_ref().unwrap(), OperationName::Start) =>
|
|
||||||
{
|
|
||||||
info_url = Some(val.to_str().unwrap().to_string());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(anyhow!(arg.unexpected()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let op = match op.unwrap_or_default() {
|
|
||||||
OperationName::Trigger => Operation::Trigger {
|
|
||||||
commit: commit.ok_or_else(|| anyhow!("a commit id must be provided"))?,
|
|
||||||
},
|
|
||||||
OperationName::Start => Operation::Start {
|
|
||||||
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
|
|
||||||
run_id: run_id.ok_or_else(|| anyhow!("a run id must be provided"))?,
|
|
||||||
info_url,
|
|
||||||
},
|
|
||||||
OperationName::List => Operation::List,
|
|
||||||
OperationName::Show => Operation::Show {
|
|
||||||
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
|
|
||||||
},
|
|
||||||
OperationName::Finish => Operation::Finish {
|
|
||||||
job_id: job_id.ok_or_else(|| anyhow!("a job id must be provided"))?,
|
|
||||||
reason: if !succeeded && !failed {
|
|
||||||
return Err(anyhow!("must give one of --success or --failure"))?;
|
|
||||||
} else if succeeded && failed {
|
|
||||||
return Err(anyhow!("must give one of --success or --failure, not both"))?;
|
|
||||||
} else if succeeded {
|
|
||||||
Reason::Succeeded
|
|
||||||
} else {
|
|
||||||
Reason::Failed
|
|
||||||
},
|
|
||||||
},
|
|
||||||
OperationName::Delete => Operation::Delete {
|
|
||||||
job_id: job_id.ok_or_else(|| anyhow!("a job id to remove must be provided"))?,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
Options {
|
|
||||||
op,
|
|
||||||
announce,
|
|
||||||
quiet,
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
|
||||||
let profile = ctx.profile()?;
|
|
||||||
let (_, rid) = radicle::rad::cwd()?;
|
|
||||||
let repo = profile.storage.repository_mut(rid)?;
|
|
||||||
let announce = options.announce
|
|
||||||
&& matches!(
|
|
||||||
&options.op,
|
|
||||||
Operation::Trigger { .. }
|
|
||||||
| Operation::Start { .. }
|
|
||||||
| Operation::Finish { .. }
|
|
||||||
| Operation::Delete { .. }
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut node = Node::new(profile.socket());
|
|
||||||
let mut ci_store = JobStore::open(&repo)?;
|
|
||||||
|
|
||||||
match options.op {
|
|
||||||
Operation::Trigger { commit } => {
|
|
||||||
let signer = term::signer(&profile)?;
|
|
||||||
trigger(&commit, &mut ci_store, &repo, &signer, options.quiet)?;
|
|
||||||
}
|
|
||||||
Operation::Start {
|
|
||||||
job_id,
|
|
||||||
run_id,
|
|
||||||
info_url,
|
|
||||||
} => {
|
|
||||||
let signer = term::signer(&profile)?;
|
|
||||||
start(&job_id, &run_id, info_url, &mut ci_store, &repo, &signer)?;
|
|
||||||
}
|
|
||||||
Operation::List => {
|
|
||||||
list(&ci_store)?;
|
|
||||||
}
|
|
||||||
Operation::Show { job_id } => {
|
|
||||||
show(&job_id, &ci_store, &repo)?;
|
|
||||||
}
|
|
||||||
Operation::Finish { job_id, reason } => {
|
|
||||||
let signer = term::signer(&profile)?;
|
|
||||||
finish(&job_id, reason, &mut ci_store, &repo, &signer)?;
|
|
||||||
}
|
|
||||||
Operation::Delete { job_id } => {
|
|
||||||
let signer = term::signer(&profile)?;
|
|
||||||
let job_id = job_id.resolve(&repo.backend)?;
|
|
||||||
ci_store.remove(&job_id, &signer)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if announce {
|
|
||||||
match node.announce_refs(rid) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) if e.is_connection_err() => {}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger<R, G>(
|
|
||||||
commit: &Rev,
|
|
||||||
store: &mut JobStore<R>,
|
|
||||||
repo: &radicle::storage::git::Repository,
|
|
||||||
signer: &Device<G>,
|
|
||||||
quiet: bool,
|
|
||||||
) -> anyhow::Result<()>
|
|
||||||
where
|
|
||||||
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
let commit = commit.resolve(&repo.backend)?;
|
|
||||||
let job = store.create(commit, signer)?;
|
|
||||||
if !quiet {
|
|
||||||
term::job::show(&job, job.id())?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start<R, G>(
|
|
||||||
job_id: &Rev,
|
|
||||||
run_id: &str,
|
|
||||||
info_url: Option<String>,
|
|
||||||
store: &mut JobStore<R>,
|
|
||||||
repo: &radicle::storage::git::Repository,
|
|
||||||
signer: &Device<G>,
|
|
||||||
) -> anyhow::Result<()>
|
|
||||||
where
|
|
||||||
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
let job_id = job_id.resolve(&repo.backend)?;
|
|
||||||
let mut job = store.get_mut(&job_id)?;
|
|
||||||
|
|
||||||
job.start(run_id.to_string(), info_url, signer)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This should use the COB cache for performance.
|
|
||||||
fn list<R: WriteRepository + cob::Store<Namespace = NodeId>>(
|
|
||||||
store: &JobStore<R>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if store.is_empty()? {
|
|
||||||
term::print(term::format::italic("Nothing to show."));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut table = term::Table::new(term::table::TableOptions::bordered());
|
|
||||||
table.header([
|
|
||||||
term::format::dim(String::from("●")),
|
|
||||||
term::format::bold(String::from("ID")),
|
|
||||||
term::format::bold(String::from("Commit")),
|
|
||||||
term::format::bold(String::from("State")),
|
|
||||||
]);
|
|
||||||
table.divider();
|
|
||||||
|
|
||||||
for result in store.all()? {
|
|
||||||
let Ok((id, ci)) = result else {
|
|
||||||
// Skip COBs that failed to load.
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
table.push([
|
|
||||||
match ci.state() {
|
|
||||||
State::Fresh => term::format::positive("●").into(),
|
|
||||||
State::Running => term::format::positive("●").into(),
|
|
||||||
State::Finished(Reason::Succeeded) => term::format::positive("●").into(),
|
|
||||||
State::Finished(Reason::Failed) => term::format::negative("●").into(),
|
|
||||||
},
|
|
||||||
term::format::tertiary(term::format::cob(&id).to_string()),
|
|
||||||
term::format::tertiary(term::format::oid(ci.commit()).to_string()),
|
|
||||||
term::format::tertiary(term::format::job_state(ci.state()).to_string()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if table.is_empty() {
|
|
||||||
term::print(term::format::dim("No jobs to show."));
|
|
||||||
} else {
|
|
||||||
table.print();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show<R: WriteRepository + cob::Store<Namespace = NodeId>>(
|
|
||||||
job_id: &Rev,
|
|
||||||
store: &JobStore<R>,
|
|
||||||
repo: &radicle::storage::git::Repository,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let job_id = job_id.resolve(&repo.backend)?;
|
|
||||||
let job = store
|
|
||||||
.get(&job_id)?
|
|
||||||
.context("No job with the given ID exists")?;
|
|
||||||
|
|
||||||
term::job::show(&job, &job_id)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish<R, G>(
|
|
||||||
job_id: &Rev,
|
|
||||||
reason: Reason,
|
|
||||||
store: &mut JobStore<R>,
|
|
||||||
repo: &radicle::storage::git::Repository,
|
|
||||||
signer: &Device<G>,
|
|
||||||
) -> anyhow::Result<()>
|
|
||||||
where
|
|
||||||
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
let job_id = job_id.resolve(&repo.backend)?;
|
|
||||||
let mut job = store.get_mut(&job_id)?;
|
|
||||||
|
|
||||||
job.finish(reason, signer)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -232,13 +232,6 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
|
||||||
args.to_vec(),
|
args.to_vec(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
"job" => {
|
|
||||||
term::run_command_args::<rad_job::Options, _>(
|
|
||||||
rad_job::HELP,
|
|
||||||
rad_job::run,
|
|
||||||
args.to_vec(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"ls" => {
|
"ls" => {
|
||||||
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
|
term::run_command_args::<rad_ls::Options, _>(rad_ls::HELP, rad_ls::run, args.to_vec());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ pub mod args;
|
||||||
pub use args::{Args, Error, Help};
|
pub use args::{Args, Error, Help};
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod job;
|
|
||||||
pub use io::signer;
|
pub use io::signer;
|
||||||
pub mod cob;
|
pub mod cob;
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,6 @@ pub fn oid(oid: impl Into<radicle::git::Oid>) -> Paint<String> {
|
||||||
Paint::new(format!("{:.7}", oid.into()))
|
Paint::new(format!("{:.7}", oid.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a job COB state.
|
|
||||||
pub fn job_state(state: radicle::cob::job::State) -> Paint<String> {
|
|
||||||
Paint::new(format!("{}", state))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap parenthesis around styled input, eg. `"input"` -> `"(input)"`.
|
/// Wrap parenthesis around styled input, eg. `"input"` -> `"(input)"`.
|
||||||
pub fn parens<D: fmt::Display>(input: Paint<D>) -> Paint<String> {
|
pub fn parens<D: fmt::Display>(input: Paint<D>) -> Paint<String> {
|
||||||
Paint::new(format!("({})", input.item)).with_style(input.style)
|
Paint::new(format!("({})", input.item)).with_style(input.style)
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
use radicle::cob;
|
|
||||||
use radicle::cob::job;
|
|
||||||
use radicle_term::table::TableOptions;
|
|
||||||
use radicle_term::{Table, VStack};
|
|
||||||
|
|
||||||
use crate::terminal as term;
|
|
||||||
use crate::terminal::Element;
|
|
||||||
|
|
||||||
pub fn show(job: &job::Job, id: &cob::ObjectId) -> anyhow::Result<()> {
|
|
||||||
let mut attrs = Table::<2, term::Line>::new(TableOptions {
|
|
||||||
spacing: 2,
|
|
||||||
..TableOptions::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
attrs.push([
|
|
||||||
term::format::tertiary("Job".to_owned()).into(),
|
|
||||||
term::format::bold(id.to_string()).into(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
attrs.push([
|
|
||||||
term::format::tertiary("Commit".to_owned()).into(),
|
|
||||||
term::format::bold(job.commit().to_owned()).into(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
attrs.push([
|
|
||||||
term::format::tertiary("State".to_owned()).into(),
|
|
||||||
term::format::bold(job.state().to_string()).into(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if let Some(run_id) = job.run_id() {
|
|
||||||
attrs.push([
|
|
||||||
term::format::tertiary("Run ID".to_owned()).into(),
|
|
||||||
term::format::bold(run_id.to_string()).into(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(info_url) = job.info_url() {
|
|
||||||
attrs.push([
|
|
||||||
term::format::tertiary("Info URL".to_owned()).into(),
|
|
||||||
term::format::bold(info_url.to_string()).into(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let widget = VStack::default()
|
|
||||||
.border(Some(term::colors::FAINT))
|
|
||||||
.child(attrs);
|
|
||||||
|
|
||||||
widget.print();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -3159,17 +3159,3 @@ fn rad_workflow() {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rad_job() {
|
|
||||||
let mut environment = Environment::new();
|
|
||||||
let profile = environment.profile(config::profile("alice"));
|
|
||||||
let home = &profile.home;
|
|
||||||
let working = environment.tmp().join("working");
|
|
||||||
|
|
||||||
// Setup a test repository.
|
|
||||||
fixtures::repository(&working);
|
|
||||||
|
|
||||||
test("examples/rad-init.md", &working, Some(home), []).unwrap();
|
|
||||||
test("examples/rad-job.md", &working, Some(home), []).unwrap();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ pub mod common;
|
||||||
pub mod external;
|
pub mod external;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod issue;
|
pub mod issue;
|
||||||
pub mod job;
|
|
||||||
pub mod op;
|
pub mod op;
|
||||||
pub mod patch;
|
pub mod patch;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
|
||||||
|
|
@ -1,458 +0,0 @@
|
||||||
//! Track "jobs" related to a repository.
|
|
||||||
//!
|
|
||||||
//! The purpose of this COB is to allow users of Radicle to have a way
|
|
||||||
//! of keeping track of what automated processing of changes to a
|
|
||||||
//! repository have been done. A "job" might be a continuous
|
|
||||||
//! integration automation building the software in a repository and
|
|
||||||
//! running its automated tests. A delegate for the repository could
|
|
||||||
//! track COBs emitted by trusted nodes to help with deciding when a
|
|
||||||
//! patch is ready for them to merge.
|
|
||||||
|
|
||||||
use std::{ops::Deref, str::FromStr};
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::cob;
|
|
||||||
use crate::cob::change::store::Entry;
|
|
||||||
use crate::cob::store;
|
|
||||||
use crate::cob::store::{Cob, CobAction, Store, Transaction};
|
|
||||||
use crate::cob::{EntryId, ObjectId, TypeName};
|
|
||||||
use crate::crypto::ssh::ExtendedSignature;
|
|
||||||
use crate::git;
|
|
||||||
use crate::node::device::Device;
|
|
||||||
use crate::node::NodeId;
|
|
||||||
use crate::prelude::ReadRepository;
|
|
||||||
use crate::storage::{Oid, WriteRepository};
|
|
||||||
|
|
||||||
use super::store::CobWithType;
|
|
||||||
|
|
||||||
/// The name of this COB type. Note that this is a "beta" COB, which
|
|
||||||
/// means it's not meant for others to rely on yet, and we may change
|
|
||||||
/// it without warning.
|
|
||||||
pub static TYPENAME: Lazy<TypeName> =
|
|
||||||
Lazy::new(|| FromStr::from_str("xyz.radicle.beta.job").expect("type name is valid"));
|
|
||||||
|
|
||||||
/// An identifier for the job.
|
|
||||||
pub type JobId = ObjectId;
|
|
||||||
|
|
||||||
/// All the possible errors from this type of COB.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("initialization failed: {0}")]
|
|
||||||
Init(&'static str),
|
|
||||||
#[error("op decoding failed: {0}")]
|
|
||||||
Op(#[from] cob::op::OpEncodingError),
|
|
||||||
#[error("store: {0}")]
|
|
||||||
Store(#[from] store::Error),
|
|
||||||
#[error("can't trigger a job which is not fresh")]
|
|
||||||
TriggerWhenNotFresh,
|
|
||||||
#[error("can't start a job which is not triggered")]
|
|
||||||
StartWhenNotFresh,
|
|
||||||
#[error("can't finish a job which is not running")]
|
|
||||||
FinishWhenNotRunning,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All the possible states of this COB.
|
|
||||||
///
|
|
||||||
/// This is primarily modeled for CI, for now. A COB is created by a
|
|
||||||
/// node when it triggers a CI run, and then updated when the CI
|
|
||||||
/// system actually starts executing the run, and finishes. The CI
|
|
||||||
/// system assigns an identifier to the run, and may have a URL for
|
|
||||||
/// the log. These can also be stored in the COB.
|
|
||||||
///
|
|
||||||
/// This COB is essentially a state machine that tracks the state of
|
|
||||||
/// an automated run Ci. When the COB is created, in state `Fresh`, it
|
|
||||||
/// just records the git commit the run uses. This can't be changed.
|
|
||||||
/// The COB may be created before the run actually starts. Once the
|
|
||||||
/// run starts, the COB state changes to `Running`. When the run
|
|
||||||
/// finished, the state changes to `Finished`, and the state records
|
|
||||||
/// why the run finished: `Succeeded` or `Failed`.
|
|
||||||
///
|
|
||||||
/// No other state changes are allowed for the COB.
|
|
||||||
///
|
|
||||||
/// Note that if CI runs again for the same commit, a new COB is
|
|
||||||
/// created. The two runs may result in different outcomes, even if
|
|
||||||
/// nothing in the source code has changed. For example, the CI system
|
|
||||||
/// may run out of disk space, or use different versions of the
|
|
||||||
/// software used in the run.
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase", tag = "status")]
|
|
||||||
pub enum State {
|
|
||||||
/// COB has been created, job has not yet started running.
|
|
||||||
#[default]
|
|
||||||
Fresh,
|
|
||||||
/// Job has started running.
|
|
||||||
Running,
|
|
||||||
/// Job has finished.
|
|
||||||
Finished(Reason),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for State {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Fresh => write!(f, "fresh"),
|
|
||||||
Self::Running => write!(f, "running"),
|
|
||||||
Self::Finished(Reason::Succeeded) => write!(f, "succeeded"),
|
|
||||||
Self::Finished(Reason::Failed) => write!(f, "failed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Why did build finish?
|
|
||||||
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Reason {
|
|
||||||
/// Build was successful.
|
|
||||||
Succeeded,
|
|
||||||
/// Build failed for some reason.
|
|
||||||
Failed,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Actions to update this COB.
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
|
||||||
pub enum Action {
|
|
||||||
/// Initialize the COB for a new job.
|
|
||||||
Trigger { commit: git::Oid },
|
|
||||||
|
|
||||||
/// Start a job.
|
|
||||||
Start {
|
|
||||||
run_id: String,
|
|
||||||
info_url: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Finish a job.
|
|
||||||
Finish { reason: Reason },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CobAction for Action {}
|
|
||||||
|
|
||||||
/// Type of COB operation.
|
|
||||||
pub type Op = cob::Op<Action>;
|
|
||||||
|
|
||||||
/// The COB with actions applied.
|
|
||||||
///
|
|
||||||
/// A job is based on a specific commit. This is set when the COB is
|
|
||||||
/// created and can't be changed.
|
|
||||||
///
|
|
||||||
/// A job has a specific [`State`].
|
|
||||||
///
|
|
||||||
/// A job may have a "run id", which is an arbitrary string. It might
|
|
||||||
/// be the identifier for a CI run, set by an external CI system, for
|
|
||||||
/// example. The id is informational and there no guarantees what it
|
|
||||||
/// means, or that it's unique.
|
|
||||||
///
|
|
||||||
/// A job may also store a URL to more information. This might be a
|
|
||||||
/// link to a run log in a CI system, for example.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Job {
|
|
||||||
commit: git::Oid,
|
|
||||||
state: State,
|
|
||||||
run_id: Option<String>,
|
|
||||||
info_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Job {
|
|
||||||
/// Create a new `Job` in the `Fresh` state, using the provided `commit`.
|
|
||||||
fn new(commit: git::Oid) -> Self {
|
|
||||||
Self {
|
|
||||||
commit,
|
|
||||||
state: State::default(),
|
|
||||||
run_id: None,
|
|
||||||
info_url: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the commit that this `Job` was created with.
|
|
||||||
pub fn commit(&self) -> git::Oid {
|
|
||||||
self.commit
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the run identifier, if any, that was associated with this `Job`.
|
|
||||||
pub fn run_id(&self) -> Option<&str> {
|
|
||||||
self.run_id.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the info URL, if any, that was associated with this `Job`.
|
|
||||||
pub fn info_url(&self) -> Option<&str> {
|
|
||||||
self.info_url.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `State` of this `Job`.
|
|
||||||
pub fn state(&self) -> State {
|
|
||||||
self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a single action to the job.
|
|
||||||
fn action(&mut self, action: Action) -> Result<(), Error> {
|
|
||||||
match action {
|
|
||||||
Action::Trigger { .. } => {
|
|
||||||
if self.state != State::Fresh {
|
|
||||||
return Err(Error::TriggerWhenNotFresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Action::Start { run_id, info_url } => {
|
|
||||||
if self.state != State::Fresh {
|
|
||||||
return Err(Error::StartWhenNotFresh);
|
|
||||||
}
|
|
||||||
self.state = State::Running;
|
|
||||||
self.run_id = Some(run_id);
|
|
||||||
self.info_url = info_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Action::Finish { reason } => {
|
|
||||||
if self.state != State::Running {
|
|
||||||
return Err(Error::FinishWhenNotRunning);
|
|
||||||
}
|
|
||||||
self.state = State::Finished(reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cob for Job {
|
|
||||||
type Action = Action;
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn from_root<R: ReadRepository>(op: Op, _repo: &R) -> Result<Self, Self::Error> {
|
|
||||||
let mut actions = op.actions.into_iter();
|
|
||||||
let Some(Action::Trigger { commit }) = actions.next() else {
|
|
||||||
return Err(Error::Init("the first action must be of type `trigger`"));
|
|
||||||
};
|
|
||||||
let mut job = Job::new(commit);
|
|
||||||
|
|
||||||
for action in actions {
|
|
||||||
job.action(action)?;
|
|
||||||
}
|
|
||||||
Ok(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
|
|
||||||
&mut self,
|
|
||||||
op: Op,
|
|
||||||
_concurrent: I,
|
|
||||||
_repo: &R,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
// Some day this needs to check authorization. However, we
|
|
||||||
// don't yet know what the rules should be.
|
|
||||||
for action in op.actions {
|
|
||||||
self.action(action)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CobWithType for Job {
|
|
||||||
fn type_name() -> &'static TypeName {
|
|
||||||
&TYPENAME
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: ReadRepository> cob::Evaluate<R> for Job {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
|
|
||||||
let op = Op::try_from(entry)?;
|
|
||||||
let job = Job::from_root(op, repo)?;
|
|
||||||
Ok(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply<'a, I>(
|
|
||||||
&mut self,
|
|
||||||
entry: &cob::Entry,
|
|
||||||
concurrent: I,
|
|
||||||
repo: &R,
|
|
||||||
) -> Result<(), Self::Error>
|
|
||||||
where
|
|
||||||
I: Iterator<Item = (&'a Oid, &'a Entry<Oid, Oid, ExtendedSignature>)>,
|
|
||||||
{
|
|
||||||
let op = Op::try_from(entry)?;
|
|
||||||
self.op(op, concurrent.map(|(_, e)| e), repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: ReadRepository> Transaction<Job, R> {
|
|
||||||
/// Push an [`Action::Trigger`] which will create a new `Job` with the
|
|
||||||
/// provided `commit` in the [`State::Fresh`] state.
|
|
||||||
pub fn trigger(&mut self, commit: git::Oid) -> Result<(), store::Error> {
|
|
||||||
self.push(Action::Trigger { commit })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push an [`Action::Start`] which will start the `Job` with the provided
|
|
||||||
/// metadata and move the `Job` into the [`State::Running`] state.
|
|
||||||
pub fn start(&mut self, run_id: String, info_url: Option<String>) -> Result<(), store::Error> {
|
|
||||||
self.push(Action::Start { run_id, info_url })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push an [`Action::Finish`] which will finish the `Job` with the provided
|
|
||||||
/// reason, moving the `Job` into the [`State::Finished`] state.
|
|
||||||
pub fn finish(&mut self, reason: Reason) -> Result<(), store::Error> {
|
|
||||||
self.push(Action::Finish { reason })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct JobMut<'a, 'g, R> {
|
|
||||||
id: ObjectId,
|
|
||||||
job: Job,
|
|
||||||
store: &'g mut JobStore<'a, R>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'g, R> From<JobMut<'a, 'g, R>> for (JobId, Job) {
|
|
||||||
fn from(value: JobMut<'a, 'g, R>) -> Self {
|
|
||||||
(value.id, value.job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> std::fmt::Debug for JobMut<'_, '_, R> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
f.debug_struct("JobMut")
|
|
||||||
.field("id", &self.id)
|
|
||||||
.field("job", &self.job)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> JobMut<'_, '_, R>
|
|
||||||
where
|
|
||||||
R: WriteRepository + cob::Store<Namespace = NodeId>,
|
|
||||||
{
|
|
||||||
/// Reload the COB from storage.
|
|
||||||
pub fn reload(&mut self) -> Result<(), store::Error> {
|
|
||||||
self.job = self
|
|
||||||
.store
|
|
||||||
.get(&self.id)?
|
|
||||||
.ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &ObjectId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transition the `Job` into a running state, storing the provided
|
|
||||||
/// metadata.
|
|
||||||
pub fn start<G>(
|
|
||||||
&mut self,
|
|
||||||
run_id: String,
|
|
||||||
info_url: Option<String>,
|
|
||||||
signer: &Device<G>,
|
|
||||||
) -> Result<EntryId, Error>
|
|
||||||
where
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
self.transaction("Start", signer, |tx| {
|
|
||||||
tx.start(run_id, info_url)?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transition the `Job` into a finished state, with the provided `reason`.
|
|
||||||
pub fn finish<G>(&mut self, reason: Reason, signer: &Device<G>) -> Result<EntryId, Error>
|
|
||||||
where
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
self.transaction("Finish", signer, |tx| tx.finish(reason))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn transaction<G, F>(
|
|
||||||
&mut self,
|
|
||||||
message: &str,
|
|
||||||
signer: &Device<G>,
|
|
||||||
operations: F,
|
|
||||||
) -> Result<EntryId, Error>
|
|
||||||
where
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
F: FnOnce(&mut Transaction<Job, R>) -> Result<(), store::Error>,
|
|
||||||
{
|
|
||||||
let mut tx = Transaction::default();
|
|
||||||
operations(&mut tx)?;
|
|
||||||
|
|
||||||
let (job, id) = tx.commit(message, self.id, &mut self.store.raw, signer)?;
|
|
||||||
self.job = job;
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R> Deref for JobMut<'_, '_, R> {
|
|
||||||
type Target = Job;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct JobStore<'a, R> {
|
|
||||||
raw: Store<'a, Job, R>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R> Deref for JobStore<'a, R> {
|
|
||||||
type Target = Store<'a, Job, R>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.raw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R> JobStore<'a, R>
|
|
||||||
where
|
|
||||||
R: WriteRepository + ReadRepository + cob::Store<Namespace = NodeId>,
|
|
||||||
{
|
|
||||||
pub fn open(repository: &'a R) -> Result<Self, store::Error> {
|
|
||||||
let raw = store::Store::open(repository)?;
|
|
||||||
Ok(Self { raw })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `Job`, if any, identified by `id`.
|
|
||||||
pub fn get(&self, id: &JobId) -> Result<Option<Job>, store::Error> {
|
|
||||||
self.raw.get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `Job`, identified by `id`, which can be mutated.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// This will fail if the `Job` could not be found.
|
|
||||||
pub fn get_mut<'g>(&'g mut self, id: &JobId) -> Result<JobMut<'a, 'g, R>, store::Error> {
|
|
||||||
let job = self
|
|
||||||
.raw
|
|
||||||
.get(id)?
|
|
||||||
.ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), *id))?;
|
|
||||||
Ok(JobMut {
|
|
||||||
id: *id,
|
|
||||||
job,
|
|
||||||
store: self,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a fresh `Job` with the provided `commit_id`.
|
|
||||||
pub fn create<'g, G>(
|
|
||||||
&'g mut self,
|
|
||||||
commit_id: git::Oid,
|
|
||||||
signer: &Device<G>,
|
|
||||||
) -> Result<JobMut<'a, 'g, R>, Error>
|
|
||||||
where
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
let (id, job) = Transaction::initial("Create job", &mut self.raw, signer, |tx, _| {
|
|
||||||
tx.trigger(commit_id)?;
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(JobMut {
|
|
||||||
id,
|
|
||||||
job,
|
|
||||||
store: self,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete the `Job` identified by `id`.
|
|
||||||
pub fn remove<G>(&self, id: &JobId, signer: &Device<G>) -> Result<(), store::Error>
|
|
||||||
where
|
|
||||||
G: crypto::signature::Signer<crypto::Signature>,
|
|
||||||
{
|
|
||||||
self.raw.remove(id, signer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue