fix(windows): build and test with native executables
Replace POSIX shell-based hub fixtures with a helper executable that resolves correctly on both Windows and Linux. This prevents Windows tests from falling through to a real hub.exe and hanging on network access. Document PowerShell installation and Windows path usage, require the .exe suffix for explicit Windows build output, and ignore generated binaries.
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
/stack-sync
|
/stack-sync
|
||||||
|
/stack-sync.exe
|
||||||
|
|||||||
@@ -19,9 +19,29 @@ Stack Sync never runs `git stash`, `git reset`, `git clean`, `git commit`, or an
|
|||||||
|
|
||||||
Stack Sync intentionally preserves `hub sync` semantics. That means `hub` may delete a local branch when its upstream branch has been deleted and it considers the local branch merged. It warns instead when it finds unpushed or apparently unmerged commits. The interactive confirmation calls this out; review `hub help sync` before using `--yes` in automation.
|
Stack Sync intentionally preserves `hub sync` semantics. That means `hub` may delete a local branch when its upstream branch has been deleted and it considers the local branch merged. It warns instead when it finds unpushed or apparently unmerged commits. The interactive confirmation calls this out; review `hub help sync` before using `--yes` in automation.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Stack Sync supports Linux and Windows. It requires:
|
||||||
|
|
||||||
|
- Go 1.24 or newer to build;
|
||||||
|
- Git on `PATH`;
|
||||||
|
- [hub](https://hub.github.com/) on `PATH` for `sync` and `tui` sync operations.
|
||||||
|
|
||||||
|
The read-only `scan` command does not require `hub`.
|
||||||
|
|
||||||
## Build and install
|
## Build and install
|
||||||
|
|
||||||
Requires Go 1.24 or newer, Git, and [hub](https://hub.github.com/).
|
`go install` is the simplest cross-platform installation method. From the repository root, run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
go install .
|
||||||
|
```
|
||||||
|
|
||||||
|
Go automatically installs `stack-sync` on Linux and `stack-sync.exe` on Windows into your Go binary directory. Make sure that directory is on `PATH` (normally `$HOME/go/bin` on Linux and `%USERPROFILE%\go\bin` on Windows).
|
||||||
|
|
||||||
|
To build a binary in the repository instead, use the command for your platform.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd stack-sync
|
cd stack-sync
|
||||||
@@ -30,6 +50,18 @@ go build -o stack-sync .
|
|||||||
install -Dm755 stack-sync ~/.local/bin/stack-sync
|
install -Dm755 stack-sync ~/.local/bin/stack-sync
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Windows (PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location stack-sync
|
||||||
|
go test ./...
|
||||||
|
go build -o stack-sync.exe .
|
||||||
|
New-Item -ItemType Directory -Force "$env:LOCALAPPDATA\Programs\stack-sync" | Out-Null
|
||||||
|
Copy-Item .\stack-sync.exe "$env:LOCALAPPDATA\Programs\stack-sync\stack-sync.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `%LOCALAPPDATA%\Programs\stack-sync` to your user `PATH` if it is not already present. The `.exe` suffix is important when choosing an explicit output name on Windows; `go build -o stack-sync .` creates an extensionless binary there.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Interactive TUI
|
### Interactive TUI
|
||||||
@@ -40,6 +72,12 @@ Launch the control deck for the workspace:
|
|||||||
stack-sync tui --root ~/Coding/jwtf
|
stack-sync tui --root ~/Coding/jwtf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
PowerShell accepts the same options with a Windows path:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
stack-sync tui --root C:\Users\you\Coding\jwtf
|
||||||
|
```
|
||||||
|
|
||||||
Every eligible repository starts selected. Move with the arrow keys or `j`/`k`, toggle the focused repository with Space, select all with `a`, clear the selection with `n`, and refresh with `r`. Press `s` or Enter to review the destructive-branch warning, then `y` to begin. Blocked repositories cannot be selected and show their first dirty paths in the detail panel. After a run, focusing a repository shows its latest `hub` output.
|
Every eligible repository starts selected. Move with the arrow keys or `j`/`k`, toggle the focused repository with Space, select all with `a`, clear the selection with `n`, and refresh with `r`. Press `s` or Enter to review the destructive-branch warning, then `y` to begin. Blocked repositories cannot be selected and show their first dirty paths in the detail panel. After a run, focusing a repository shows its latest `hub` output.
|
||||||
|
|
||||||
### CLI and automation
|
### CLI and automation
|
||||||
@@ -52,6 +90,10 @@ stack-sync scan --root ~/Coding/jwtf
|
|||||||
stack-sync --root ~/Coding/jwtf
|
stack-sync --root ~/Coding/jwtf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
stack-sync scan --root C:\Users\you\Coding\jwtf
|
||||||
|
```
|
||||||
|
|
||||||
Review the same plan, confirm it, and sync every eligible repository:
|
Review the same plan, confirm it, and sync every eligible repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -74,6 +116,8 @@ Useful options:
|
|||||||
--json emit structured output
|
--json emit structured output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both `/` and `\` are accepted in Windows paths. Quote a root or exclusion containing spaces, for example `--root "C:\Users\you\Source Repositories"`.
|
||||||
|
|
||||||
Dependency caches, build outputs, and tool-managed directories such as `node_modules`, `target`, `.claude`, and `.codex` are skipped during discovery by default. These exclusions only affect repository discovery; they never make real changes inside a discovered repository disappear from its dirty-worktree check.
|
Dependency caches, build outputs, and tool-managed directories such as `node_modules`, `target`, `.claude`, and `.codex` are skipped during discovery by default. These exclusions only affect repository discovery; they never make real changes inside a discovered repository disappear from its dirty-worktree check.
|
||||||
|
|
||||||
Exit codes are `0` for a successful scan or sync (planned dirty repositories may be safely skipped), `1` for an operational/sync failure, `2` for invalid or unconfirmed non-interactive use, and `3` when strict mode refuses the plan or a previously eligible repository fails its immediate pre-sync safety check.
|
Exit codes are `0` for a successful scan or sync (planned dirty repositories may be safely skipped), `1` for an operational/sync failure, `2` for invalid or unconfirmed non-interactive use, and `3` when strict mode refuses the plan or a previously eligible repository fails its immediate pre-sync safety check.
|
||||||
|
|||||||
+53
-8
@@ -2,14 +2,63 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if os.Getenv("STACK_SYNC_TEST_HELPER") == "hub" {
|
||||||
|
marker := os.Getenv("STACK_SYNC_TEST_MARKER")
|
||||||
|
if err := os.WriteFile(marker, []byte(strings.Join(os.Args[1:], " ")), 0o644); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
func installFakeHub(t *testing.T, dir string) string {
|
||||||
|
t.Helper()
|
||||||
|
source, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
name := "hub"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
name += ".exe"
|
||||||
|
}
|
||||||
|
destination := filepath.Join(dir, name)
|
||||||
|
if err := os.Link(source, destination); err == nil {
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(output, input); err != nil {
|
||||||
|
_ = output.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := output.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
func git(t *testing.T, dir string, args ...string) {
|
func git(t *testing.T, dir string, args ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
||||||
@@ -128,11 +177,9 @@ func TestSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
marker := filepath.Join(root, "hub-was-called")
|
marker := filepath.Join(root, "hub-was-called")
|
||||||
script := "#!/bin/sh\ntouch \"$STACK_SYNC_TEST_MARKER\"\n"
|
installFakeHub(t, bin)
|
||||||
if err := os.WriteFile(filepath.Join(bin, "hub"), []byte(script), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
|
||||||
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
||||||
|
|
||||||
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
||||||
@@ -155,11 +202,9 @@ func TestSyncInvokesHubForCleanRepository(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
marker := filepath.Join(root, "hub-was-called")
|
marker := filepath.Join(root, "hub-was-called")
|
||||||
script := "#!/bin/sh\nprintf '%s' \"$*\" > \"$STACK_SYNC_TEST_MARKER\"\n"
|
installFakeHub(t, bin)
|
||||||
if err := os.WriteFile(filepath.Join(bin, "hub"), []byte(script), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
|
||||||
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
||||||
|
|
||||||
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
||||||
|
|||||||
Reference in New Issue
Block a user