Skip to content

Commit 241c851

Browse files
Use git-harness v0.3.1 for non-interactive network git (#19)
* Disable interactive git credential prompts on fetch operations. Apply GIT_TERMINAL_PROMPT=0 to all network fetch paths so parallel git-rain runs fail fast with a frozen auth message instead of interleaved TTY prompts. Co-authored-by: Cursor <cursoragent@cursor.com> * Use git-harness v0.3.1 for non-interactive network git and fix GoReleaser CI. Delegate PrepareNetworkGit to git-harness instead of duplicating the helper, pin github.com/git-fire/git-harness v0.3.1, and migrate stable releases from deprecated brews to homebrew_casks so release config validation passes. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix release gate to verify Homebrew cask publish path. GoReleaser stable config publishes homebrew_casks under Casks/, so the release workflow must wait on Casks/git-rain.rb instead of Formula/. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 96af267 commit 241c851

10 files changed

Lines changed: 77 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,25 +153,25 @@ jobs:
153153
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154154
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
155155

156-
- name: Verify Homebrew formula publish
156+
- name: Verify Homebrew cask publish
157157
env:
158158
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
159159
RELEASE_TAG: ${{ steps.release_meta.outputs.release_tag }}
160160
run: |
161161
set -euo pipefail
162162
RELEASE_VERSION="${RELEASE_TAG#v}"
163163
for attempt in $(seq 1 18); do
164-
FORMULA_B64="$(gh api repos/git-fire/homebrew-tap/contents/Formula/git-rain.rb --jq '.content' 2>/dev/null || true)"
165-
if [ -n "$FORMULA_B64" ]; then
166-
FORMULA_CONTENT="$(printf '%s' "$FORMULA_B64" | tr -d '\n' | base64 -d)"
167-
if printf '%s' "$FORMULA_CONTENT" | grep -q "version \"${RELEASE_VERSION}\"" && \
168-
printf '%s' "$FORMULA_CONTENT" | grep -q "/download/${RELEASE_TAG}/"; then
169-
echo "Homebrew formula updated for ${RELEASE_TAG}."
164+
CASK_B64="$(gh api repos/git-fire/homebrew-tap/contents/Casks/git-rain.rb --jq '.content' 2>/dev/null || true)"
165+
if [ -n "$CASK_B64" ]; then
166+
CASK_CONTENT="$(printf '%s' "$CASK_B64" | tr -d '\n' | base64 -d)"
167+
if printf '%s' "$CASK_CONTENT" | grep -q "version \"${RELEASE_VERSION}\"" && \
168+
printf '%s' "$CASK_CONTENT" | grep -q "/download/${RELEASE_TAG}/"; then
169+
echo "Homebrew cask updated for ${RELEASE_TAG}."
170170
exit 0
171171
fi
172172
fi
173-
echo "Homebrew formula not updated for ${RELEASE_TAG} yet (attempt ${attempt}/18)."
173+
echo "Homebrew cask not updated for ${RELEASE_TAG} yet (attempt ${attempt}/18)."
174174
sleep 10
175175
done
176-
echo "Homebrew formula did not update for ${RELEASE_TAG}; failing release gate."
176+
echo "Homebrew cask did not update for ${RELEASE_TAG}; failing release gate."
177177
exit 1

.goreleaser.stable.yaml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,29 @@ changelog:
9797
- title: Other
9898
order: 999
9999

100-
brews:
100+
homebrew_casks:
101101
- name: git-rain
102102
repository:
103103
owner: git-fire
104104
name: homebrew-tap
105105
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
106106
pull_request:
107107
enabled: false
108-
directory: Formula
109108
commit_author:
110109
name: goreleaserbot
111110
email: bot@goreleaser.com
112111
commit_msg_template: "chore: update git-rain to {{ .Tag }}"
113112
homepage: "https://github.com/git-fire/git-rain"
114113
description: "Sync local git repositories from their remotes — the reverse of git-fire"
115114
license: MIT
116-
install: bin.install "git-rain"
117-
test: system "#{bin}/git-rain", "--version"
115+
binaries:
116+
- git-rain
117+
hooks:
118+
post:
119+
install: |
120+
if OS.mac?
121+
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/git-rain"]
122+
end
118123
119124
release:
120125
github:

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,7 @@ func fetchOnly(repoPath string, opts git.RainOptions) error {
888888
}
889889
cmd := exec.Command("git", fetchArgs...)
890890
cmd.Dir = repoPath
891+
git.PrepareNetworkGit(cmd)
891892
if out, err := cmd.CombinedOutput(); err != nil {
892893
return fmt.Errorf("%s: %w (output: %s)", strings.Join(append([]string{"git"}, fetchArgs...), " "), err, strings.TrimSpace(string(out)))
893894
}

cmd/root_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ func TestFetchFailureMessage(t *testing.T) {
487487
{"authentication", "Authentication failed for git@github.com", "could not authenticate with remote — check your credentials and try again"},
488488
{"permission denied", "git@github.com: Permission denied (publickey)", "could not authenticate with remote — check your credentials and try again"},
489489
{"could not read", "fatal: could not read from remote", "could not authenticate with remote — check your credentials and try again"},
490+
{"terminal prompts disabled", "fatal: could not read Username for 'https://github.com': terminal prompts disabled", "could not authenticate with remote — check your credentials and try again"},
490491
{"401", "fatal: HTTP 401: Unauthorized", "could not authenticate with remote — check your credentials and try again"},
491492
{"403", "fatal: HTTP 403 forbidden", "could not authenticate with remote — check your credentials and try again"},
492493
{"could not resolve", "fatal: unable to access ...: Could not resolve host", "could not reach remote — check your network and try again"},

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v1.0.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/lipgloss v1.1.0
9+
github.com/git-fire/git-harness v0.3.1
910
github.com/git-fire/git-testkit v0.2.0
1011
github.com/gofrs/flock v0.12.1
1112
github.com/mattn/go-runewidth v0.0.19

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
2929
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
3030
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
3131
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
32+
github.com/git-fire/git-harness v0.3.1 h1:+fCQ9nMS3YI7r9iVGrU95oefH8N86XjxcYSqsin/1aE=
33+
github.com/git-fire/git-harness v0.3.1/go.mod h1:cNvjYdbowpoO/KdJwtGenhySx+EkgoQJujuANJpRpj4=
3234
github.com/git-fire/git-testkit v0.2.0 h1:IFzOxMdNTE5A4lnzbFz62h2R44+3qVy27Xj1KWyGi1Y=
3335
github.com/git-fire/git-testkit v0.2.0/go.mod h1:YlJlkY9JfGdYTe9o9W3l+gv9BPj05FGu6HK36Z5jwVA=
3436
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=

internal/git/command.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package git
2+
3+
import (
4+
"os/exec"
5+
6+
harnessgit "github.com/git-fire/git-harness/git"
7+
)
8+
9+
// PrepareNetworkGit configures cmd for fetch/push operations that may contact
10+
// a remote. Delegates to git-harness so credential behavior stays aligned with git-fire.
11+
func PrepareNetworkGit(cmd *exec.Cmd) {
12+
harnessgit.PrepareNetworkGit(cmd)
13+
}

internal/git/command_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package git
2+
3+
import (
4+
"os/exec"
5+
"strings"
6+
"testing"
7+
8+
testutil "github.com/git-fire/git-testkit"
9+
)
10+
11+
func TestFetchFailureReason_TerminalPromptsDisabled(t *testing.T) {
12+
got := fetchFailureReason([]byte("fatal: could not read Username for 'https://github.com': terminal prompts disabled"))
13+
want := "could not authenticate with remote — check your credentials and try again"
14+
if got != want {
15+
t.Fatalf("fetchFailureReason() = %q, want %q", got, want)
16+
}
17+
}
18+
19+
func TestNetworkFetch_UnauthenticatedHTTPSFailsWithoutPrompt(t *testing.T) {
20+
repo := testutil.CreateTestRepo(t, testutil.RepoOptions{
21+
Name: "https-fetch-repo",
22+
Remotes: map[string]string{
23+
"origin": "https://github.com/git-rain/nonexistent-repo-auth-test.git",
24+
},
25+
})
26+
27+
cmd := exec.Command("git", "fetch", "--all")
28+
cmd.Dir = repo
29+
PrepareNetworkGit(cmd)
30+
output, err := cmd.CombinedOutput()
31+
if err == nil {
32+
t.Fatal("expected fetch to fail without credentials")
33+
}
34+
msg := strings.ToLower(string(output) + " " + err.Error())
35+
if !strings.Contains(msg, "terminal prompts disabled") &&
36+
!strings.Contains(msg, "could not read username") {
37+
t.Fatalf("expected non-interactive auth failure, got output=%q err=%v", strings.TrimSpace(string(output)), err)
38+
}
39+
}

internal/git/fetch_mainline.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ func MainlineFetchRemotes(repoPath string, opts RainOptions) (RainResult, error)
146146
}
147147
cmd := exec.Command("git", args...)
148148
cmd.Dir = repoPath
149+
PrepareNetworkGit(cmd)
149150
if output, err := cmd.CombinedOutput(); err != nil {
150151
failedReason[remote] = fetchFailureReason(output)
151152
}

internal/git/rain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ func RainRepository(repoPath string, opts RainOptions) (RainResult, error) {
201201
}
202202
cmd := exec.Command("git", fetchArgs...)
203203
cmd.Dir = repoPath
204+
PrepareNetworkGit(cmd)
204205
if output, fetchErr := cmd.CombinedOutput(); fetchErr != nil {
205206
// Freeze gracefully — could not reach remote (auth, network, etc.).
206207
// This is not a hard failure; the repo is untouched. Try again later.

0 commit comments

Comments
 (0)