## Why Once the repo-local lint exists, `codex-rs` needs to follow the checked-in convention and CI needs to keep it from drifting. This commit applies the fallback `/*param*/` style consistently across existing positional literal call sites without changing those APIs. The longer-term preference is still to avoid APIs that require comments by choosing clearer parameter types and call shapes. This PR is intentionally the mechanical follow-through for the places where the existing signatures stay in place. After rebasing onto newer `main`, the rollout also had to cover newly introduced `tui_app_server` call sites. That made it clear the first cut of the CI job was too expensive for the common path: it was spending almost as much time installing `cargo-dylint` and re-testing the lint crate as a representative test job spends running product tests. The CI update keeps the full workspace enforcement but trims that extra overhead from ordinary `codex-rs` PRs. ## What changed - keep a dedicated `argument_comment_lint` job in `rust-ci` - mechanically annotate remaining opaque positional literals across `codex-rs` with exact `/*param*/` comments, including the rebased `tui_app_server` call sites that now fall under the lint - keep the checked-in style aligned with the lint policy by using `/*param*/` and leaving string and char literals uncommented - cache `cargo-dylint`, `dylint-link`, and the relevant Cargo registry/git metadata in the lint job - split changed-path detection so the lint crate's own `cargo test` step runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes - continue to run the repo wrapper over the `codex-rs` workspace, so product-code enforcement is unchanged Most of the code changes in this commit are intentionally mechanical comment rewrites or insertions driven by the lint itself. ## Verification - `./tools/argument-comment-lint/run.sh --workspace` - `cargo test -p codex-tui-app-server -p codex-tui` - parsed `.github/workflows/rust-ci.yml` locally with PyYAML --- * -> #14652 * #14651
256 lines
8 KiB
Rust
256 lines
8 KiB
Rust
use std::ffi::OsString;
|
|
use std::path::Path;
|
|
|
|
use crate::GitToolingError;
|
|
use crate::operations::ensure_git_repository;
|
|
use crate::operations::resolve_head;
|
|
use crate::operations::resolve_repository_root;
|
|
use crate::operations::run_git_for_stdout;
|
|
|
|
/// Returns the merge-base commit between `HEAD` and the latest version between local
|
|
/// and remote of the provided branch, if both exist.
|
|
///
|
|
/// The function mirrors `git merge-base HEAD <branch>` but returns `Ok(None)` when
|
|
/// the repository has no `HEAD` yet or when the branch cannot be resolved.
|
|
pub fn merge_base_with_head(
|
|
repo_path: &Path,
|
|
branch: &str,
|
|
) -> Result<Option<String>, GitToolingError> {
|
|
ensure_git_repository(repo_path)?;
|
|
let repo_root = resolve_repository_root(repo_path)?;
|
|
let head = match resolve_head(repo_root.as_path())? {
|
|
Some(head) => head,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let Some(branch_ref) = resolve_branch_ref(repo_root.as_path(), branch)? else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let preferred_ref =
|
|
if let Some(upstream) = resolve_upstream_if_remote_ahead(repo_root.as_path(), branch)? {
|
|
resolve_branch_ref(repo_root.as_path(), &upstream)?.unwrap_or(branch_ref)
|
|
} else {
|
|
branch_ref
|
|
};
|
|
|
|
let merge_base = run_git_for_stdout(
|
|
repo_root.as_path(),
|
|
vec![
|
|
OsString::from("merge-base"),
|
|
OsString::from(head),
|
|
OsString::from(preferred_ref),
|
|
],
|
|
/*env*/ None,
|
|
)?;
|
|
|
|
Ok(Some(merge_base))
|
|
}
|
|
|
|
fn resolve_branch_ref(repo_root: &Path, branch: &str) -> Result<Option<String>, GitToolingError> {
|
|
let rev = run_git_for_stdout(
|
|
repo_root,
|
|
vec![
|
|
OsString::from("rev-parse"),
|
|
OsString::from("--verify"),
|
|
OsString::from(branch),
|
|
],
|
|
/*env*/ None,
|
|
);
|
|
|
|
match rev {
|
|
Ok(rev) => Ok(Some(rev)),
|
|
Err(GitToolingError::GitCommand { .. }) => Ok(None),
|
|
Err(other) => Err(other),
|
|
}
|
|
}
|
|
|
|
fn resolve_upstream_if_remote_ahead(
|
|
repo_root: &Path,
|
|
branch: &str,
|
|
) -> Result<Option<String>, GitToolingError> {
|
|
let upstream = match run_git_for_stdout(
|
|
repo_root,
|
|
vec![
|
|
OsString::from("rev-parse"),
|
|
OsString::from("--abbrev-ref"),
|
|
OsString::from("--symbolic-full-name"),
|
|
OsString::from(format!("{branch}@{{upstream}}")),
|
|
],
|
|
/*env*/ None,
|
|
) {
|
|
Ok(name) => {
|
|
let trimmed = name.trim();
|
|
if trimmed.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
trimmed.to_string()
|
|
}
|
|
Err(GitToolingError::GitCommand { .. }) => return Ok(None),
|
|
Err(other) => return Err(other),
|
|
};
|
|
|
|
let counts = match run_git_for_stdout(
|
|
repo_root,
|
|
vec![
|
|
OsString::from("rev-list"),
|
|
OsString::from("--left-right"),
|
|
OsString::from("--count"),
|
|
OsString::from(format!("{branch}...{upstream}")),
|
|
],
|
|
/*env*/ None,
|
|
) {
|
|
Ok(counts) => counts,
|
|
Err(GitToolingError::GitCommand { .. }) => return Ok(None),
|
|
Err(other) => return Err(other),
|
|
};
|
|
|
|
let mut parts = counts.split_whitespace();
|
|
let _left: i64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
|
|
let right: i64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
|
|
|
|
if right > 0 {
|
|
Ok(Some(upstream))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::merge_base_with_head;
|
|
use crate::GitToolingError;
|
|
use pretty_assertions::assert_eq;
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
use tempfile::tempdir;
|
|
|
|
fn run_git_in(repo_path: &Path, args: &[&str]) {
|
|
let status = Command::new("git")
|
|
.current_dir(repo_path)
|
|
.args(args)
|
|
.status()
|
|
.expect("git command");
|
|
assert!(status.success(), "git command failed: {args:?}");
|
|
}
|
|
|
|
fn run_git_stdout(repo_path: &Path, args: &[&str]) -> String {
|
|
let output = Command::new("git")
|
|
.current_dir(repo_path)
|
|
.args(args)
|
|
.output()
|
|
.expect("git command");
|
|
assert!(output.status.success(), "git command failed: {args:?}");
|
|
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
|
}
|
|
|
|
fn init_test_repo(repo_path: &Path) {
|
|
run_git_in(repo_path, &["init", "--initial-branch=main"]);
|
|
run_git_in(repo_path, &["config", "core.autocrlf", "false"]);
|
|
}
|
|
|
|
fn commit(repo_path: &Path, message: &str) {
|
|
run_git_in(
|
|
repo_path,
|
|
&[
|
|
"-c",
|
|
"user.name=Tester",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
message,
|
|
],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_base_returns_shared_commit() -> Result<(), GitToolingError> {
|
|
let temp = tempdir()?;
|
|
let repo = temp.path();
|
|
init_test_repo(repo);
|
|
|
|
std::fs::write(repo.join("base.txt"), "base\n")?;
|
|
run_git_in(repo, &["add", "base.txt"]);
|
|
commit(repo, "base commit");
|
|
|
|
run_git_in(repo, &["checkout", "-b", "feature"]);
|
|
std::fs::write(repo.join("feature.txt"), "feature change\n")?;
|
|
run_git_in(repo, &["add", "feature.txt"]);
|
|
commit(repo, "feature commit");
|
|
|
|
run_git_in(repo, &["checkout", "main"]);
|
|
std::fs::write(repo.join("main.txt"), "main change\n")?;
|
|
run_git_in(repo, &["add", "main.txt"]);
|
|
commit(repo, "main commit");
|
|
|
|
run_git_in(repo, &["checkout", "feature"]);
|
|
|
|
let expected = run_git_stdout(repo, &["merge-base", "HEAD", "main"]);
|
|
let merge_base = merge_base_with_head(repo, "main")?;
|
|
assert_eq!(merge_base, Some(expected));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn merge_base_prefers_upstream_when_remote_ahead() -> Result<(), GitToolingError> {
|
|
let temp = tempdir()?;
|
|
let repo = temp.path().join("repo");
|
|
let remote = temp.path().join("remote.git");
|
|
std::fs::create_dir_all(&repo)?;
|
|
std::fs::create_dir_all(&remote)?;
|
|
|
|
run_git_in(&remote, &["init", "--bare"]);
|
|
run_git_in(&repo, &["init", "--initial-branch=main"]);
|
|
run_git_in(&repo, &["config", "core.autocrlf", "false"]);
|
|
|
|
std::fs::write(repo.join("base.txt"), "base\n")?;
|
|
run_git_in(&repo, &["add", "base.txt"]);
|
|
commit(&repo, "base commit");
|
|
|
|
run_git_in(
|
|
&repo,
|
|
&["remote", "add", "origin", remote.to_str().unwrap()],
|
|
);
|
|
run_git_in(&repo, &["push", "-u", "origin", "main"]);
|
|
|
|
run_git_in(&repo, &["checkout", "-b", "feature"]);
|
|
std::fs::write(repo.join("feature.txt"), "feature change\n")?;
|
|
run_git_in(&repo, &["add", "feature.txt"]);
|
|
commit(&repo, "feature commit");
|
|
|
|
run_git_in(&repo, &["checkout", "--orphan", "rewrite"]);
|
|
run_git_in(&repo, &["rm", "-rf", "."]);
|
|
std::fs::write(repo.join("new-main.txt"), "rewritten main\n")?;
|
|
run_git_in(&repo, &["add", "new-main.txt"]);
|
|
commit(&repo, "rewrite main");
|
|
run_git_in(&repo, &["branch", "-M", "rewrite", "main"]);
|
|
run_git_in(&repo, &["branch", "--set-upstream-to=origin/main", "main"]);
|
|
|
|
run_git_in(&repo, &["checkout", "feature"]);
|
|
run_git_in(&repo, &["fetch", "origin"]);
|
|
|
|
let expected = run_git_stdout(&repo, &["merge-base", "HEAD", "origin/main"]);
|
|
let merge_base = merge_base_with_head(&repo, "main")?;
|
|
assert_eq!(merge_base, Some(expected));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn merge_base_returns_none_when_branch_missing() -> Result<(), GitToolingError> {
|
|
let temp = tempdir()?;
|
|
let repo = temp.path();
|
|
init_test_repo(repo);
|
|
|
|
std::fs::write(repo.join("tracked.txt"), "tracked\n")?;
|
|
run_git_in(repo, &["add", "tracked.txt"]);
|
|
commit(repo, "initial");
|
|
|
|
let merge_base = merge_base_with_head(repo, "missing-branch")?;
|
|
assert_eq!(merge_base, None);
|
|
|
|
Ok(())
|
|
}
|
|
}
|