From 9e96e2f8116eeb6e743bfa0fb4c6a6f105fc9f49 Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 25 Jul 2026 16:29:13 +1000 Subject: [PATCH 1/3] test(windows): run full suite on CI; normalise editor paths for git's shell git runs GIT_SEQUENCE_EDITOR/GIT_EDITOR through a POSIX shell (its bundled sh on Windows), which accepts forward slashes but mishandles backslash separators. Route every editor command path (cp-based todo/message editors and the current_exe reorder/stamp editors) through a shell_path() helper that normalises separators. No-op on Unix. Also flip the Windows CI job from --lib-only to the full test suite so the git-driven rebase machinery is actually exercised. --- .github/workflows/ci.yml | 13 ++++++++----- src/git.rs | 26 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6b2a25..51fce6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,9 +45,10 @@ jobs: - name: Test run: cargo test --verbose - # Guard Windows portability: the code must compile and the pure unit tests - # (ident / render / engine) must pass. The git-driven integration and view - # tests assume a POSIX shell and aren't run here yet. + # Verify Windows portability: the code must compile AND the full test suite + # (including the git-driven integration + view tests, which exercise the + # `cp`/binary-based rebase editors) must pass. Tests self-skip any git feature + # the runner's git lacks. windows: name: Windows runs-on: windows-latest @@ -56,7 +57,9 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 + - name: Git version + run: git --version - name: Build run: cargo build --verbose - - name: Test (portable unit tests) - run: cargo test --lib -- --skip view + - name: Test + run: cargo test --verbose diff --git a/src/git.rs b/src/git.rs index 0473477..aedcc9e 100644 --- a/src/git.rs +++ b/src/git.rs @@ -554,6 +554,7 @@ pub(crate) fn rebase_reorder_persist( after: Option<&str>, ) -> Result<()> { let exe = std::env::current_exe().context("cannot locate the git-queue executable")?; + let exe = shell_path(&exe); let mut initial = Command::new("git"); initial.args([ "-c", @@ -570,10 +571,7 @@ pub(crate) fn rebase_reorder_persist( quiet_git(&mut initial); // Our own binary rewrites the todo; the spec travels via the environment. initial - .env( - "GIT_SEQUENCE_EDITOR", - format!("\"{}\" reorder-todo", exe.display()), - ) + .env("GIT_SEQUENCE_EDITOR", format!("\"{exe}\" reorder-todo")) .env("GIT_QUEUE_MOVE_SHAS", move_shas.join(" ")) .env("GIT_QUEUE_MOVE_AFTER", after.unwrap_or("")); let _ = initial.status().context("failed to spawn `git rebase`")?; @@ -604,7 +602,7 @@ pub(crate) fn rebase_with_todo_stop(base: &str, top_branch: &str, todo: &str) -> // git invokes `$GIT_SEQUENCE_EDITOR `, so `cp ` becomes // `cp ` — overwriting git's generated todo with ours. - let editor = format!("cp {}", shell_single_quote(&todo_path.to_string_lossy())); + let editor = format!("cp {}", shell_single_quote(&shell_path(&todo_path))); let mut cmd = Command::new("git"); cmd.args([ "rebase", @@ -638,6 +636,14 @@ fn shell_single_quote(s: &str) -> String { format!("'{}'", s.replace('\'', r"'\''")) } +/// Render a filesystem path for embedding in a shell command that git will run. +/// git executes `GIT_SEQUENCE_EDITOR`/`GIT_EDITOR` through a POSIX shell — its +/// bundled `sh` on Windows — which accepts forward slashes but mishandles +/// backslash separators, so normalise them. On Unix this is a no-op. +fn shell_path(p: &std::path::Path) -> String { + p.to_string_lossy().replace('\\', "/") +} + /// Rewrite `base..top_branch` with a caller-supplied todo (one commit marked /// `reword`) and supply the new commit message from `message`. Both are /// installed via `cp`-based editors, so no helper binary is needed. A reword is @@ -655,8 +661,8 @@ pub(crate) fn rebase_with_todo_message( std::fs::write(&todo_path, todo).context("failed to stage the rebase todo")?; std::fs::write(&msg_path, message).context("failed to stage the commit message")?; - let seq_editor = format!("cp {}", shell_single_quote(&todo_path.to_string_lossy())); - let msg_editor = format!("cp {}", shell_single_quote(&msg_path.to_string_lossy())); + let seq_editor = format!("cp {}", shell_single_quote(&shell_path(&todo_path))); + let msg_editor = format!("cp {}", shell_single_quote(&shell_path(&msg_path))); let mut cmd = Command::new("git"); cmd.args([ "rebase", @@ -700,8 +706,8 @@ pub(crate) fn rebase_squash_stop( std::fs::write(&todo_path, todo).context("failed to stage the rebase todo")?; std::fs::write(&msg_path, message).context("failed to stage the commit message")?; - let seq_editor = format!("cp {}", shell_single_quote(&todo_path.to_string_lossy())); - let msg_editor = format!("cp {}", shell_single_quote(&msg_path.to_string_lossy())); + let seq_editor = format!("cp {}", shell_single_quote(&shell_path(&todo_path))); + let msg_editor = format!("cp {}", shell_single_quote(&shell_path(&msg_path))); let mut cmd = Command::new("git"); cmd.args([ "rebase", @@ -799,7 +805,7 @@ fn drive_cherry_pick_to_completion(what: &str) -> Result<()> { /// trailer and exits). Content is untouched, so no conflicts can arise. pub(crate) fn rebase_stamp_ids(upstream: &str, branch: &str, shas: &[String]) -> Result<()> { let exe = std::env::current_exe().context("cannot locate the git-queue executable")?; - let exe = exe.display(); + let exe = shell_path(&exe); let mut initial = Command::new("git"); initial.args(["rebase", "-i", "--update-refs", upstream, branch]); initial From 6e9f858865533c5b591d4b8cfe44762ddd96c92e Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 25 Jul 2026 16:35:16 +1000 Subject: [PATCH 2/3] test(windows): pin core.autocrlf=false in test repos The one Windows failure was a test artifact, not a product bug: git-for-windows defaults core.autocrlf=true, so a checked-out `v2\n` file reads back as `v2\r\n` and a working-tree byte assertion failed. Pin autocrlf/eol in the hermetic repo setup so disk-content assertions hold on every platform. --- tests/integration.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration.rs b/tests/integration.rs index bf93bdf..aafb9ee 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -59,6 +59,11 @@ fn new_repo() -> TempDir { // with a key that isn't loaded. git(dir, &["config", "commit.gpgsign", "false"]); git(dir, &["config", "tag.gpgsign", "false"]); + // Pin line endings so working-tree byte assertions hold cross-platform + // (git-for-windows defaults core.autocrlf=true, which would rewrite LF→CRLF + // on checkout). + git(dir, &["config", "core.autocrlf", "false"]); + git(dir, &["config", "core.eol", "lf"]); commit(dir, "seed.txt"); tmp } From aa8af16fe857665c765c74e6af74d1ca23854ddf Mon Sep 17 00:00:00 2001 From: James Sadler Date: Sat, 25 Jul 2026 16:42:47 +1000 Subject: [PATCH 3/3] feat(dist): ship Windows binaries + PowerShell installer Windows portability is now verified end-to-end (full test suite green on windows-latest / git 2.55, 0 skips), so add x86_64-pc-windows-msvc to dist targets and the powershell installer. Release now produces git-queue-x86_64-pc-windows-msvc.zip and git-queue-installer.ps1. Docs: website Install section shows the PowerShell installer for Windows and includes Windows in the prebuilt-binary card; drops the 'source-only, #11' note. README documents the PowerShell installer. Closes #11. --- README.md | 17 +++++++++++------ dist-workspace.toml | 5 ++--- index.html | 12 +++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b3c8a4a..5f53981 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,9 @@ works immediately (git's standard subcommand mechanism). `git queue setup` then walks through the optional extras interactively (see below). Prefer a prebuilt binary? Each [GitHub -release](https://github.com/freshtonic/git-queue/releases) ships archives and a -shell installer (built by [dist](https://opensource.axo.dev/cargo-dist/)) for -Linux and macOS (x86-64 and arm64): +release](https://github.com/freshtonic/git-queue/releases) ships archives and +install scripts (built by [dist](https://opensource.axo.dev/cargo-dist/)) for +macOS, Linux, and Windows. On macOS/Linux: ```sh curl --proto '=https' --tlsv1.2 -LsSf \ @@ -67,9 +67,14 @@ On macOS or Linux you can also use Homebrew: brew install freshtonic/git-queue/git-queue ``` -Or just download an archive, extract it, and put `git-queue` on your `PATH`. -(Windows isn't packaged as a prebuilt binary yet — build it with `cargo install -git-queue`. Windows support is still in progress; see issue #11.) +On Windows, use the PowerShell installer: + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://github.com/freshtonic/git-queue/releases/latest/download/git-queue-installer.ps1 | iex" +``` + +Or just download an archive for your platform (macOS/Linux `.tar.xz`, Windows +`.zip`), extract it, and put `git-queue` on your `PATH`. ### `git queue setup` diff --git a/dist-workspace.toml b/dist-workspace.toml index ef7720b..2539dc1 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -8,15 +8,14 @@ cargo-dist-version = "0.32.0" # CI backends to support ci = "github" # The installers to generate for each app. -# PowerShell/Windows are omitted until git-queue is Windows-portable (issue #11). -installers = ["homebrew", "shell"] +installers = ["homebrew", "shell", "powershell"] # Homebrew tap to publish the generated formula to. dist pushes the formula on # release using a token in the HOMEBREW_TAP_TOKEN secret (write access to the tap). tap = "freshtonic/homebrew-git-queue" # Actually run the formula-publishing job (not just generate the formula locally). publish-jobs = ["homebrew"] # Target platforms to build apps for (Rust target-triple syntax). -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Path that installers should place binaries in install-path = "CARGO_HOME" # Where to host releases diff --git a/index.html b/index.html index 80e08d9..2dad869 100644 --- a/index.html +++ b/index.html @@ -524,6 +524,10 @@

$ curl --proto '=https' --tlsv1.2 -LsSf \ https://github.com/freshtonic/git-queue/releases/latest/download/git-queue-installer.sh | sh +
+
install script · recommended
+
> powershell -ExecutionPolicy ByPass -c "irm https://github.com/freshtonic/git-queue/releases/latest/download/git-queue-installer.ps1 | iex"
+
homebrew
$ brew install freshtonic/git-queue/git-queue
@@ -532,12 +536,9 @@

cargo · from crates.io

$ cargo install git-queue
-
+
prebuilt binary
-

Grab an archive for your platform (x86-64 or arm64) from the latest GitHub release, extract it, and put git-queue on your PATH. Archives and the install script are built by dist.

-
-
-

Windows support is still in progress (#11) — no prebuilt Windows binary or install script yet. cargo install git-queue above builds it from source.

+

Grab an archive for your platform from the latest GitHub release (macOS/Linux x86-64 & arm64 .tar.xz, Windows x86-64 .zip), extract it, and put git-queue on your PATH. Archives and install scripts are built by dist.

Because the binary is named git-queue, Git runs git queue … for you via its standard subcommand mechanism. Then git queue setup installs the man page — afterwards both man git-queue and git queue --help work (Git routes --help to the man page; use git queue help for inline output).

@@ -764,6 +765,7 @@