Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
with:
key: partition-${{ matrix.partition }}
- uses: taiki-e/install-action@nextest
- uses: taiki-e/install-action@v2
with:
tool: jj-cli
- name: Build
run: cargo build
- name: Run tests
Expand Down
8 changes: 6 additions & 2 deletions crates/dropshot-api-manager/src/cmd/debug.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025 Oxide Computer Company
// Copyright 2026 Oxide Computer Company

use crate::{
apis::ManagedApis,
Expand Down Expand Up @@ -113,10 +113,14 @@ fn dump_structure<T: AsRawFiles>(
for (version, files) in info.versions() {
println!(" version {}:", version);
for api_spec in files.as_raw_files() {
let version_str = api_spec
.version()
.map(|v| v.to_string())
.unwrap_or_else(|| "unparseable".to_string());
println!(
" file {} (v{})",
api_spec.spec_file_name().path(),
api_spec.version()
version_str
);
}
}
Expand Down
76 changes: 73 additions & 3 deletions crates/dropshot-api-manager/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025 Oxide Computer Company
// Copyright 2026 Oxide Computer Company

//! Helpers for accessing data stored in git

Expand Down Expand Up @@ -90,13 +90,51 @@ pub enum CommitHashParseError {
InvalidHex(hex::FromHexError),
}

/// Given a revision, return its merge base with HEAD
/// Given a revision, return its merge base with the current working state.
///
/// If we're in the middle of a merge (MERGE_HEAD exists), we compute merge
/// bases for both HEAD and MERGE_HEAD, then use whichever is the descendant
/// (more recent). This handles both merge directions correctly:
///
/// - Merging main into branch: HEAD (p1) = branch, MERGE_HEAD (p2) = main.
/// We want main's merge base (which is main itself, containing all blessed
/// files).
/// - Merging branch into main: HEAD (p1) = main, MERGE_HEAD (p2) = branch. We
/// want main's merge base (main itself), not branch's merge base (the common
/// ancestor before main's changes).
///
/// In the rare case where the two merge bases are independent (neither is an
/// ancestor of the other), we fall back to HEAD's merge base.
pub fn git_merge_base_head(
repo_root: &Utf8Path,
revision: &GitRevision,
) -> anyhow::Result<GitRevision> {
if git_merge_head_exists(repo_root) {
// We're in a merge. Compute merge bases for both HEAD and MERGE_HEAD.
let mb_head = git_merge_base(repo_root, "HEAD", revision)?;
let mb_merge_head = git_merge_base(repo_root, "MERGE_HEAD", revision)?;

// Use whichever merge base is the descendant (more recent). If mb_head
// is an ancestor of mb_merge_head, use mb_merge_head (it's newer).
// Otherwise, use mb_head (either it's newer, or they're parallel).
if git_is_ancestor(repo_root, &mb_head, &mb_merge_head)? {
Ok(mb_merge_head)
} else {
Ok(mb_head)
}
} else {
git_merge_base(repo_root, "HEAD", revision)
}
}

/// Compute the merge base between a reference and a revision.
fn git_merge_base(
repo_root: &Utf8Path,
base_ref: &str,
revision: &GitRevision,
) -> anyhow::Result<GitRevision> {
let mut cmd = git_start(repo_root);
cmd.arg("merge-base").arg("--all").arg("HEAD").arg(revision.as_str());
cmd.arg("merge-base").arg("--all").arg(base_ref).arg(revision.as_str());
let label = cmd_label(&cmd);
let stdout = do_run(&mut cmd)?;
let stdout = stdout.trim();
Expand All @@ -110,6 +148,33 @@ pub fn git_merge_base_head(
Ok(GitRevision::from(stdout.to_owned()))
}

/// Check if `potential_ancestor` is an ancestor of `commit`.
fn git_is_ancestor(
repo_root: &Utf8Path,
potential_ancestor: &GitRevision,
commit: &GitRevision,
) -> anyhow::Result<bool> {
let mut cmd = git_start(repo_root);
cmd.args([
"merge-base",
"--is-ancestor",
potential_ancestor.as_str(),
commit.as_str(),
]);
// --is-ancestor returns exit code 0 if true, 1 if false.
let status =
cmd.status().context("running git merge-base --is-ancestor")?;
Ok(status.success())
}

/// Returns true if MERGE_HEAD exists, indicating we're in the middle of a
/// merge.
fn git_merge_head_exists(repo_root: &Utf8Path) -> bool {
let mut cmd = git_start(repo_root);
cmd.args(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
matches!(cmd.status(), Ok(status) if status.success())
}

/// List files recursively under some path `path` in Git revision `revision`.
pub fn git_ls_tree(
repo_root: &Utf8Path,
Expand Down Expand Up @@ -180,8 +245,13 @@ pub fn git_first_commit_for_file(
// We intentionally don't use --follow because Git's rename detection can
// incorrectly match unrelated files with similar content, causing it to
// return the wrong commit.
//
// We use -m to split merge commits, so that files added in merge commits
// are properly detected. Without -m, git log may not show files that were
// added in merge commits.
let mut cmd = git_start(repo_root);
cmd.arg("log")
.arg("-m")
.arg("--diff-filter=A")
.arg("--format=%H")
.arg(revision.as_str())
Expand Down
Loading
Loading