Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a

## 0.32.1 - 2026-07-07
### Added
- Add version namespaces to Git version dependencies, allowing versioned forks to be tagged and resolved under their own prefix (e.g. `companyX-v1.2.0`). The prefix may be given via the `version_prefix` field or embedded in the version string (`version: "companyX-v1.2.0"`). Defaults to `v` for full backwards compatibility, and `version_prefix: ""` selects unprefixed tags; namespaces are strict and never mix -- a dependency required under two namespaces is reported like any other conflicting requirement, offering the usual interactive choice on a terminal -- and the resolved prefix is recorded in `Bender.lock`. The field is only meaningful on git version dependencies; elsewhere it is rejected rather than dropped silently.
- `bender audit` now aligns its version-bump suggestions to the namespace a dependency is currently resolved under, falling back to the default `v` namespace when the current checkout is not a version, and names that namespace in its output when it is not the default. `bender audit --check-upstream` additionally reports the highest release in the default `v` namespace when a dependency pinned to a custom namespace has fallen behind it numerically.
- Add `git_submodules` config field and `--git-submodules <true|false>` flag (env `BENDER_GIT_SUBMODULES`) to control cloning of dependency submodules; defaults to `true`, the flag overrides the configured value in either direction (https://github.com/pulp-platform/bender/pull/314).

### Fixed
Expand Down
37 changes: 36 additions & 1 deletion book/src/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,42 @@ dependencies:
axi: { version: ">=0.23.0, <0.26.0" }
```

> **Note:** Bender only recognizes Git tags that follow the `vX.Y.Z` format (e.g., `v1.2.1`).
> **Note:** By default, Bender only recognizes Git tags that follow the `vX.Y.Z` format (e.g., `v1.2.1`). See [Version Namespaces](#version-namespaces) if you need a different prefix.

#### Version Namespaces

The leading `v` in a version tag is simply the default *prefix*. If you maintain your own versioned fork of an open-source IP — for example to ship internally patched releases — you can tag those releases under your own namespace and depend on them with `version_prefix`:

```yaml
dependencies:
# Resolves only tags of the form `companyX-v<semver>`, e.g. `companyX-v1.2.0`.
common_cells: { git: "...", version: "1.21.0", version_prefix: "companyX-v" }
```

You can equivalently embed the prefix directly in the version string:

```yaml
dependencies:
common_cells: { git: "...", version: "companyX-v1.21.0" }
```

The embedded form requires the version requirement to begin with a number (e.g. `companyX-v1.21.0`, `companyX-v1.*`). For operator-based ranges such as `>=1.21.0`, use the `version_prefix` field alongside a plain `version`. If both a field and an embedded prefix are given, they must agree.

The prefix is the entire literal string preceding the semantic version, so you are free to choose any convention (`companyX-v`, `acme-`, …). Setting `version_prefix: ""` selects tags carrying no prefix at all (`1.2.0` rather than `v1.2.0`). A dependency without a prefix keeps the default `v`, so existing manifests and lockfiles are unaffected.

Prefixes only apply to Git version dependencies. On a path or revision dependency the field has nothing to act on, so Bender rejects it rather than ignoring it silently — the same treatment `version` and `rev` get where they cannot apply. A dependency given only a `version` resolves through the default remote and is a Git version dependency, so `version_prefix` is accepted there too.

Namespaces are **strict and never mix**:

- A dependency resolves *only* tags carrying its own prefix. There is no fallback to the default `v` namespace (or any other).
- A dependency pinned to a custom namespace never sees releases in the default one. `bender audit --check-upstream` reports the highest default-namespace release when it carries a higher version number, so a fork that has fallen behind upstream stays visible.
- If the same dependency is required with two different prefixes anywhere in the dependency tree, Bender never guesses. It reports the clash like any other conflicting requirement: on a terminal it asks you to pick one of the requirements, and without one (in CI, say) it fails. To settle it permanently, add an [`overrides`](./configuration.md) entry pinning the dependency to a single namespace — note that overrides live in `.bender.yml`, not in `Bender.yml`:

```yaml
# .bender.yml
overrides:
common_cells: { git: "...", version: "1.21.0", version_prefix: "companyX-v" }
```

#### Revision-based
Use this for specific commits, branches, or tags that don't follow SemVer.
Expand Down
1 change: 1 addition & 0 deletions book/src/lockfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ packages:

- **revision:** The full 40-character Git commit hash.
- **version:** The SemVer version that was resolved.
- **version_prefix:** The version [namespace](./dependencies.md#version-namespaces) the version was resolved under. Omitted for the default `v` prefix.
- **source:** Where to download the package from.
- **dependencies:** A list of other packages that this specific package depends on, ensuring the entire tree is captured.

Expand Down
105 changes: 80 additions & 25 deletions src/cmd/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ use std::io::Write;
use clap::Args;
use futures::future::join_all;
use miette::IntoDiagnostic as _;
use semver::VersionReq;
use tabwriter::TabWriter;
use tokio::runtime::Runtime;

use crate::Result;
use crate::cmd::parents::get_parent_array;
use crate::sess::{DependencyVersions, Session, SessionIo};
use crate::cmd::parents::get_parent_requirements;
use crate::sess::{DependencyConstraint, DependencyVersions, Session, SessionIo};

/// Get information about version conflicts and possible updates.
#[derive(Args, Debug)]
Expand All @@ -31,6 +30,11 @@ pub struct AuditArgs {
/// Ignore URL conflicts when auditing.
#[arg(long)]
pub ignore_url_conflict: bool,

/// For dependencies pinned to a custom version namespace, also report the highest release in
/// the default `v` namespace when it carries a higher version number.
#[arg(long)]
pub check_upstream: bool,
}

/// Execute the `audit` subcommand.
Expand Down Expand Up @@ -77,43 +81,85 @@ pub fn run(sess: &Session, args: &AuditArgs) -> Result<()> {

for pkg in pkgs {
let pkg_name = sess.dependency_name(*pkg);
let parent_array = get_parent_array(sess, &rt, &io, pkg_name, false)?;
let parents = get_parent_requirements(sess, &rt, &io, pkg_name)?;
let current_version = sess.dependency(*pkg).version.clone();
let current_version_unwrapped = current_version
.as_ref()
.map(|v| v.to_string())
.unwrap_or_default();
let current_revision = sess.dependency(*pkg).revision.clone();
let current_revision_unwrapped = current_revision.as_deref().unwrap_or_default();
// Align update suggestions with the namespace the dependency is
// currently resolved under. When the current checkout is not a version
// (e.g. a path or revision), fall back to the default `v` namespace.
let current_prefix = if current_version.is_some() {
sess.dependency(*pkg)
.version_prefix
.as_deref()
.unwrap_or(crate::config::DEFAULT_VERSION_PREFIX)
} else {
crate::config::DEFAULT_VERSION_PREFIX
};
// Every version on this package's lines comes from `current_prefix`, so name the
// namespace once rather than prefixing each number. Left off for the default `v`, which
// keeps existing output unchanged.
let namespace_note = match current_prefix {
crate::config::DEFAULT_VERSION_PREFIX => String::new(),
"" => " (unprefixed namespace)".to_string(),
prefix => format!(" (namespace `{}`)", prefix),
};
let available_versions = match dep_versions.get(pkg).unwrap() {
DependencyVersions::Git(versions) => {
versions.versions.iter().map(|(v, _)| v.clone()).collect()
}
DependencyVersions::Git(versions) => versions
.versions
.iter()
.filter(|tv| tv.prefix == current_prefix)
.map(|tv| tv.version.clone())
.collect(),
_ => vec![],
};
let highest_version = available_versions.iter().max();

// `--check-upstream`: a dependency pinned to a fork's namespace never sees releases in
// the default one, by design. Surface the highest of those so a fork that has fallen
// behind is visible, without letting it influence the suggestion itself.
let upstream_version =
if args.check_upstream && current_prefix != crate::config::DEFAULT_VERSION_PREFIX {
match dep_versions.get(pkg).unwrap() {
DependencyVersions::Git(versions) => versions
.versions
.iter()
.filter(|tv| tv.prefix == crate::config::DEFAULT_VERSION_PREFIX)
.map(|tv| &tv.version)
.max()
.filter(|upstream| Some(*upstream) > current_version.as_ref()),
_ => None,
}
} else {
None
};

let mut conflicting = false;
let mut version_req_exists = false;
let mut compatible_versions = available_versions.clone();
let (default_version, url) = parent_array
let (default_constraint, url) = parents
.values()
.next()
.map(|v| (v[0].clone(), v[1].clone()))
.unwrap_or_else(|| ("".to_string(), "".to_string()));
for parent in parent_array.values() {
match VersionReq::parse(&parent[0]) {
Ok(parent_version) => {
compatible_versions.retain(|v| parent_version.matches(v));
.map(|p| (Some(p.constraint.clone()), p.source.clone()))
.unwrap_or((None, String::new()));
for parent in parents.values() {
// The namespace is already fixed by resolution, so only the requirement matters here.
match &parent.constraint {
DependencyConstraint::Version { req, .. } => {
compatible_versions.retain(|v| req.matches(v));
version_req_exists = true;
}
Err(_) => {
if parent[0] != default_version {
other => {
if Some(other) != default_constraint.as_ref() {
conflicting = true;
}
}
}
if parent[1] != url && !args.ignore_url_conflict {
if parent.source != url && !args.ignore_url_conflict {
conflicting = true;
}
}
Expand Down Expand Up @@ -143,8 +189,8 @@ pub fn run(sess: &Session, args: &AuditArgs) -> Result<()> {
));
if let Some(highest_version) = highest_version {
audit_str.push_str(&format!(
"\t\x1B[31;1m\x1B[m\thighest version: {}\n",
highest_version
"\t\x1B[31;1m\x1B[m\thighest version: {}{}\n",
highest_version, namespace_note
));
}
}
Expand All @@ -157,8 +203,8 @@ pub fn run(sess: &Session, args: &AuditArgs) -> Result<()> {
&& !args.only_update
{
audit_str.push_str(&format!(
" is \x1B[32;1mUp-to-date\x1B[m:\t@ {}\n",
current_version_unwrapped
" is \x1B[32;1mUp-to-date\x1B[m:\t@ {}{}\n",
current_version_unwrapped, namespace_note
));
}

Expand All @@ -169,8 +215,8 @@ pub fn run(sess: &Session, args: &AuditArgs) -> Result<()> {
&& *max_compatible > *current_version
{
audit_str.push_str(&format!(
"can \x1B[32;1mAuto-update\x1B[m:\t{} -> {}\n",
current_version_unwrapped, max_compatible
"can \x1B[32;1mAuto-update\x1B[m:\t{} -> {}{}\n",
current_version_unwrapped, max_compatible, namespace_note
));
}

Expand All @@ -182,8 +228,17 @@ pub fn run(sess: &Session, args: &AuditArgs) -> Result<()> {
&& (max_compatible.is_none() || *max_compatible.unwrap() < *highest_version)
{
audit_str.push_str(&format!(
" can \x1B[33;1mUpdate\x1B[m:\t{} -> {}\n",
current_version_unwrapped, highest_version
" can \x1B[33;1mUpdate\x1B[m:\t{} -> {}{}\n",
current_version_unwrapped, highest_version, namespace_note
));
}

// Reported after the package's own status, since it is context rather than a suggestion:
// the two namespaces are separate release lines and bender will not cross between them.
if let Some(upstream_version) = upstream_version {
audit_str.push_str(&format!(
"\t has \x1B[36;1mUpstream\x1B[m:\t{} in the default `v` namespace\n",
upstream_version
));
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/cmd/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,14 @@ pub fn run(sess: &Session, args: &PackagesArgs) -> Result<()> {
"{}:\t{}\tat {}\t{}\n",
pkg_source.name,
match pkg_source.version {
Some(ref v) => format!("v{}", v),
Some(ref v) => format!(
"{}{}",
pkg_source
.version_prefix
.as_deref()
.unwrap_or(crate::config::DEFAULT_VERSION_PREFIX),
v
),
None => "".to_string(),
},
pkg_source.source,
Expand Down
Loading