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
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ permissions:

jobs:
verify:
name: Verify
runs-on: ubuntu-24.04
name: Verify (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-latest, windows-latest]
steps:
- name: Check out repository
uses: actions/checkout@v5
Expand Down
34 changes: 31 additions & 3 deletions src/cli/commit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,35 @@ mod tests {
use crate::core::commit::{CommitEntry, CommitOptions, CommitOutcome};
use crate::core::restack::RestackPreview;
use clap::FromArgMatches;
use std::os::unix::process::ExitStatusExt;
use std::process::ExitStatus;

/// Create an `ExitStatus` representing a successful (code 0) process.
fn exit_status_success() -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(0)
}
#[cfg(windows)]
{
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(0)
}
}

/// Create an `ExitStatus` representing a failed (non-zero) process.
fn exit_status_failure() -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(1 << 8) // encodes exit code 1
}
#[cfg(windows)]
{
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(1)
}
}

#[test]
fn converts_cli_args_into_core_commit_options() {
Expand Down Expand Up @@ -197,7 +225,7 @@ mod tests {
#[test]
fn formats_commit_output_with_summary_and_blank_line_before_log() {
let outcome = CommitOutcome {
status: std::process::ExitStatus::from_raw(0),
status: exit_status_success(),
commit_succeeded: true,
summary_line: Some("10 files changed, 2245 insertions(+)".into()),
recent_commits: vec![CommitEntry {
Expand All @@ -220,7 +248,7 @@ mod tests {
#[test]
fn formats_commit_output_with_restack_section() {
let outcome = CommitOutcome {
status: std::process::ExitStatus::from_raw(1 << 8),
status: exit_status_failure(),
commit_succeeded: true,
summary_line: Some("1 file changed, 1 insertion(+)".into()),
recent_commits: vec![CommitEntry {
Expand Down
16 changes: 15 additions & 1 deletion src/cli/merge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,22 @@ mod tests {
use super::{MergeArgs, format_merge_plan, format_merge_success_output};
use crate::core::merge::{MergeMode, MergeOptions, MergePlan, MergeTreeNode};
use crate::core::restack::RestackPreview;
use std::process::ExitStatus;
use uuid::Uuid;

fn exit_status_success() -> ExitStatus {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(0)
}
#[cfg(windows)]
{
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(0)
}
}

#[test]
fn converts_cli_args_into_core_merge_options() {
let options = MergeOptions::from(MergeArgs {
Expand Down Expand Up @@ -307,7 +321,7 @@ mod tests {
restack_plan: vec![],
},
&crate::core::merge::MergeOutcome {
status: std::os::unix::process::ExitStatusExt::from_raw(0),
status: exit_status_success(),
switched_to_target_from: Some("feat/auth-api".into()),
restacked_branches: vec![RestackPreview {
branch_name: "feat/auth-api-tests".into(),
Expand Down
15 changes: 12 additions & 3 deletions src/core/gh.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io;
use std::io::{Read, Write};
use std::process::{Command, ExitStatus, Output, Stdio};
use std::thread;
use std::{env, thread};

use serde::Deserialize;

Expand Down Expand Up @@ -352,8 +352,17 @@ fn pull_request_number_from_url(url: &str) -> Option<u64> {
(!digits.is_empty()).then(|| digits.parse().ok()).flatten()
}

/// Returns the program name used to invoke the GitHub CLI.
///
/// Defaults to `"gh"` but can be overridden via the `DAGGER_GH_BIN` environment
/// variable, which is useful for testing on platforms where `Command::new("gh")`
/// does not resolve non-`.exe` scripts (e.g. `.cmd` wrappers on Windows).
fn gh_program() -> String {
env::var("DAGGER_GH_BIN").unwrap_or_else(|_| "gh".to_string())
}

fn run_gh_capture_output(args: &[String]) -> io::Result<GhCommandOutput> {
let output = Command::new("gh")
let output = Command::new(gh_program())
.args(args)
.output()
.map_err(normalize_gh_spawn_error)?;
Expand All @@ -375,7 +384,7 @@ fn run_gh_command(command_name: &str, args: &[String]) -> io::Result<()> {
}

fn run_gh_with_live_output(args: &[String]) -> io::Result<GhCommandOutput> {
let mut child = Command::new("gh")
let mut child = Command::new(gh_program())
.args(args)
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
Expand Down
34 changes: 24 additions & 10 deletions src/core/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,22 +1579,29 @@ mod tests {

fn install_fake_executable(bin_dir: &Path, name: &str, script: &str) {
fs::create_dir_all(bin_dir).unwrap();
let path = bin_dir.join(name);
fs::write(&path, script).unwrap();
#[cfg(unix)]
{
let path = bin_dir.join(name);
fs::write(&path, script).unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(path, permissions).unwrap();
}
#[cfg(windows)]
{
// On Windows, Command::new("gh") finds gh.cmd in PATH
let path = bin_dir.join(format!("{name}.cmd"));
fs::write(&path, script).unwrap();
}
}

fn path_with_prepend(dir: &Path) -> String {
let existing_path = env::var("PATH").unwrap_or_default();
if existing_path.is_empty() {
dir.display().to_string()
} else {
format!("{}:{existing_path}", dir.display())
let sep = if cfg!(windows) { ";" } else { ":" };
format!("{}{sep}{existing_path}", dir.display())
}
}

Expand Down Expand Up @@ -1836,16 +1843,23 @@ mod tests {

let bin_dir = repo.join("fake-bin");
let log_path = repo.join("gh.log");
install_fake_executable(
&bin_dir,
"gh",
&format!(
"#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"{}\"\n",
log_path.display()
),
#[cfg(unix)]
let script = format!(
"#!/bin/sh\nset -eu\nprintf '%s\\n' \"$*\" >> \"{}\"\n",
log_path.display()
);
#[cfg(windows)]
let script = format!("@echo off\r\necho %* >> \"{}\"\r\n", log_path.display());
install_fake_executable(&bin_dir, "gh", &script);
fs::write(&log_path, "").unwrap();
let _path_guard = EnvVarGuard::set("PATH", path_with_prepend(&bin_dir));
// On Windows, Command::new("gh") only resolves gh.exe, not gh.cmd.
// Point DAGGER_GH_BIN at the .cmd wrapper so gh_program() uses it directly.
#[cfg(windows)]
let _gh_bin_guard = EnvVarGuard::set(
"DAGGER_GH_BIN",
bin_dir.join("gh.cmd").display().to_string(),
);

let plan = PullRequestUpdatePlan {
actions: vec![
Expand Down
31 changes: 28 additions & 3 deletions tests/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ use support::{
load_state_json, path_with_prepend, strip_ansi, with_temp_repo,
};

fn install_fake_gh(repo: &Path, script: &str) -> (PathBuf, String) {
fn install_fake_gh(repo: &Path, unix_script: &str, windows_script: &str) -> (PathBuf, String) {
let bin_dir = repo.join("fake-bin");
let script = if cfg!(windows) {
windows_script
} else {
unix_script
};
install_fake_executable(&bin_dir, "gh", script);

let path = path_with_prepend(&bin_dir);
Expand Down Expand Up @@ -58,7 +63,7 @@ fn init_lineage_shows_tracked_pull_request_numbers() {
dgr_ok(repo, &["init"]);
dgr_ok(repo, &["branch", "feat/auth"]);

let (_, path) = install_fake_gh(
let (bin_dir, path) = install_fake_gh(
repo,
r#"#!/bin/sh
set -eu
Expand All @@ -72,10 +77,30 @@ if [ "$1" = "pr" ] && [ "$2" = "create" ]; then
fi
echo "unexpected gh args: $*" >&2
exit 1
"#,
r#"@echo off
if "%1"=="pr" if "%2"=="list" (
echo []
exit /b 0
)
if "%1"=="pr" if "%2"=="create" (
echo https://github.com/oneirosoft/dagger/pull/123
exit /b 0
)
echo unexpected gh args: %* 1>&2
exit /b 1
"#,
);

dgr_ok_with_env(repo, &["pr"], &[("PATH", path.as_str())]);
let gh_bin = bin_dir.join(if cfg!(windows) { "gh.cmd" } else { "gh" });
dgr_ok_with_env(
repo,
&["pr"],
&[
("PATH", path.as_str()),
("DAGGER_GH_BIN", gh_bin.to_str().unwrap()),
],
);

let output = dgr_ok(repo, &["init"]);
let stdout = strip_ansi(&String::from_utf8(output.stdout).unwrap());
Expand Down
Loading
Loading