Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 372 additions & 4 deletions rivet-cli/src/main.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion rivet-core/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pub fn evaluate_conditional_rules(
for rule in &schema.conditional_rules {
for artifact in store.iter() {
if rule.when.matches_artifact(artifact) {
diagnostics.extend(rule.then.check(artifact, &rule.name, rule.severity.clone()));
diagnostics.extend(rule.then.check(artifact, &rule.name, rule.severity));
}
}
}
Expand Down
136 changes: 51 additions & 85 deletions rivet-core/src/externals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,8 @@ pub fn check_baseline_tag(
let output = Command::new("git")
.args(["rev-parse", "--verify", &format!("refs/tags/{tag}")])
.current_dir(repo_dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.output()
.map_err(|e| crate::error::Error::Io(format!("git rev-parse: {e}")))?;

Expand All @@ -542,6 +544,8 @@ pub fn list_baseline_tags(repo_dir: &Path) -> Result<Vec<String>, crate::error::
let output = Command::new("git")
.args(["tag", "--list", "baseline/*"])
.current_dir(repo_dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.output()
.map_err(|e| crate::error::Error::Io(format!("git tag list: {e}")))?;

Expand Down Expand Up @@ -1278,48 +1282,32 @@ externals:
#[serial]
fn check_baseline_tag_in_git_repo() {
let dir = tempfile::tempdir().unwrap();

// Helper: run git in the temp repo, clearing GIT_DIR / GIT_WORK_TREE
// so the command targets the freshly-init'd repo rather than an
// enclosing worktree that may share tags.
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(dir.path())
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.output()
.unwrap()
};

// Init a git repo with a baseline tag
std::process::Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "tag.forceSignAnnotated", "false"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "tag.gpgSign", "false"])
.current_dir(dir.path())
.output()
.unwrap();
git(&["init"]);
git(&["config", "user.email", "test@test.com"]);
git(&["config", "user.name", "Test"]);
git(&["config", "tag.forceSignAnnotated", "false"]);
git(&["config", "tag.gpgSign", "false"]);
git(&["config", "commit.gpgSign", "false"]);
std::fs::write(dir.path().join("file.txt"), "hello").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["tag", "baseline/v1.0"])
.current_dir(dir.path())
.output()
.unwrap();
git(&["add", "."]);
git(&["commit", "-m", "init"]);
git(&["tag", "baseline/v1.0"]);

let status = check_baseline_tag(dir.path(), "v1.0").unwrap();
assert!(status.is_present());
Expand All @@ -1333,52 +1321,30 @@ externals:
#[serial]
fn list_baseline_tags_finds_tags() {
let dir = tempfile::tempdir().unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "tag.forceSignAnnotated", "false"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["config", "tag.gpgSign", "false"])
.current_dir(dir.path())
.output()
.unwrap();

// Helper: run git in the temp repo, clearing inherited env vars.
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(dir.path())
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_COMMON_DIR")
.output()
.unwrap()
};

git(&["init"]);
git(&["config", "user.email", "test@test.com"]);
git(&["config", "user.name", "Test"]);
git(&["config", "tag.forceSignAnnotated", "false"]);
git(&["config", "tag.gpgSign", "false"]);
git(&["config", "commit.gpgSign", "false"]);
std::fs::write(dir.path().join("file.txt"), "hello").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["tag", "baseline/v1.0"])
.current_dir(dir.path())
.output()
.unwrap();
std::process::Command::new("git")
.args(["tag", "baseline/v2.0"])
.current_dir(dir.path())
.output()
.unwrap();
git(&["add", "."]);
git(&["commit", "-m", "init"]);
git(&["tag", "baseline/v1.0"]);
git(&["tag", "baseline/v2.0"]);

let tags = list_baseline_tags(dir.path()).unwrap();
assert!(tags.contains(&"v1.0".to_string()));
Expand Down
31 changes: 19 additions & 12 deletions rivet-core/src/links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ pub struct LinkGraph {
impl LinkGraph {
/// Build the link graph from a store and schema.
pub fn build(store: &Store, schema: &Schema) -> Self {
let mut forward: HashMap<ArtifactId, Vec<ResolvedLink>> = HashMap::new();
let mut backward: HashMap<ArtifactId, Vec<Backlink>> = HashMap::new();
let n = store.len();
let mut forward: HashMap<ArtifactId, Vec<ResolvedLink>> = HashMap::with_capacity(n);
let mut backward: HashMap<ArtifactId, Vec<Backlink>> = HashMap::with_capacity(n);
let mut broken = Vec::new();
let mut graph = DiGraph::new();
let mut node_map: HashMap<ArtifactId, NodeIndex> = HashMap::new();
let mut graph = DiGraph::with_capacity(n, n * 2);
let mut node_map: HashMap<ArtifactId, NodeIndex> = HashMap::with_capacity(n);

// Create nodes for all artifacts
for artifact in store.iter() {
Expand All @@ -61,18 +62,16 @@ impl LinkGraph {
// Create edges for all links
for artifact in store.iter() {
for link in &artifact.links {
let resolved = ResolvedLink {
source: artifact.id.clone(),
target: link.target.clone(),
link_type: link.link_type.clone(),
};

if store.contains(&link.target) {
// Valid link — add forward, backward, and graph edge
forward
.entry(artifact.id.clone())
.or_default()
.push(resolved);
.push(ResolvedLink {
source: artifact.id.clone(),
target: link.target.clone(),
link_type: link.link_type.clone(),
});

let inverse_type = schema.inverse_of(&link.link_type).map(|s| s.to_string());
backward
Expand All @@ -90,7 +89,11 @@ impl LinkGraph {
graph.add_edge(src, dst, link.link_type.clone());
}
} else {
broken.push(resolved);
broken.push(ResolvedLink {
source: artifact.id.clone(),
target: link.target.clone(),
link_type: link.link_type.clone(),
});
}
}
}
Expand All @@ -105,21 +108,25 @@ impl LinkGraph {
}

/// Access the underlying petgraph directed graph.
#[inline]
pub fn graph(&self) -> &DiGraph<ArtifactId, String> {
&self.graph
}

/// Access the mapping from artifact ID to petgraph node index.
#[inline]
pub fn node_map(&self) -> &HashMap<ArtifactId, NodeIndex> {
&self.node_map
}

/// Get forward links from an artifact.
#[inline]
pub fn links_from(&self, id: &str) -> &[ResolvedLink] {
self.forward.get(id).map(|v| v.as_slice()).unwrap_or(&[])
}

/// Get backlinks (incoming links) to an artifact.
#[inline]
pub fn backlinks_to(&self, id: &str) -> &[Backlink] {
self.backward.get(id).map(|v| v.as_slice()).unwrap_or(&[])
}
Expand Down
2 changes: 2 additions & 0 deletions rivet-core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub struct Artifact {

impl Artifact {
/// Return all link targets of a given link type.
#[inline]
pub fn links_of_type(&self, link_type: &str) -> Vec<&ArtifactId> {
self.links
.iter()
Expand All @@ -74,6 +75,7 @@ impl Artifact {
}

/// Check whether this artifact has any link of the given type.
#[inline]
pub fn has_link_type(&self, link_type: &str) -> bool {
self.links.iter().any(|l| l.link_type == link_type)
}
Expand Down
11 changes: 8 additions & 3 deletions rivet-core/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ pub struct TraceabilityRule {
pub severity: Severity,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Expand Down Expand Up @@ -220,6 +220,7 @@ impl TryFrom<ConditionRaw> for Condition {
// Manual Serialize implementation for Condition → flat YAML output
impl Condition {
/// Check whether an artifact satisfies this condition.
#[inline]
pub fn matches_artifact(&self, artifact: &Artifact) -> bool {
match self {
Condition::Equals { field, value } => {
Expand All @@ -237,6 +238,7 @@ impl Condition {
}

/// Get a string value for a field from an artifact, checking base fields first.
#[inline]
fn get_field_value(artifact: &Artifact, field: &str) -> Option<String> {
match field {
"status" => artifact.status.clone(),
Expand Down Expand Up @@ -326,7 +328,7 @@ impl Requirement {
let has_field = get_field_value(artifact, field_name).is_some();
if !has_field {
diags.push(crate::validate::Diagnostic {
severity: severity.clone(),
severity,
artifact_id: Some(artifact.id.clone()),
rule: rule_name.to_string(),
message: format!(
Expand All @@ -341,7 +343,7 @@ impl Requirement {
for lt in link_types {
if !artifact.has_link_type(lt) {
diags.push(crate::validate::Diagnostic {
severity: severity.clone(),
severity,
artifact_id: Some(artifact.id.clone()),
rule: rule_name.to_string(),
message: format!(
Expand Down Expand Up @@ -532,16 +534,19 @@ impl Schema {
}

/// Look up an artifact type definition by name.
#[inline]
pub fn artifact_type(&self, name: &str) -> Option<&ArtifactTypeDef> {
self.artifact_types.get(name)
}

/// Look up a link type definition by name.
#[inline]
pub fn link_type(&self, name: &str) -> Option<&LinkTypeDef> {
self.link_types.get(name)
}

/// Get the inverse link type name, if one is defined.
#[inline]
pub fn inverse_of(&self, link_type: &str) -> Option<&str> {
self.inverse_map.get(link_type).map(|s| s.as_str())
}
Expand Down
6 changes: 6 additions & 0 deletions rivet-core/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ impl Store {
}

/// Look up an artifact by ID.
#[inline]
pub fn get(&self, id: &str) -> Option<&Artifact> {
self.artifacts.get(id)
}

/// Get all artifact IDs of a given type.
#[inline]
pub fn by_type(&self, artifact_type: &str) -> &[ArtifactId] {
self.by_type
.get(artifact_type)
Expand All @@ -69,15 +71,18 @@ impl Store {
}

/// Iterate over all artifacts.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Artifact> {
self.artifacts.values()
}

/// Total number of artifacts.
#[inline]
pub fn len(&self) -> usize {
self.artifacts.len()
}

#[inline]
pub fn is_empty(&self) -> bool {
self.artifacts.is_empty()
}
Expand All @@ -96,6 +101,7 @@ impl Store {
}

/// Check whether an artifact ID exists in the store.
#[inline]
pub fn contains(&self, id: &str) -> bool {
self.artifacts.contains_key(id)
}
Expand Down
Loading
Loading