Skip to content

Commit 5c998ac

Browse files
Merge remote-tracking branch 'origin/main' into cursor/setup-dev-environment-0d13
2 parents a3b7f72 + 3d29d11 commit 5c998ac

15 files changed

Lines changed: 124 additions & 256 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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
"github.com/git-rain/git-rain/internal/config"
2323
"github.com/git-rain/git-rain/internal/git"
2424
"github.com/git-rain/git-rain/internal/registry"
25-
"github.com/git-rain/git-rain/internal/safety"
25+
"github.com/git-fire/git-harness/safety"
2626
"github.com/git-rain/git-rain/internal/sessionlog"
2727
"github.com/git-rain/git-rain/internal/ui"
2828
)
@@ -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/operations.go

Lines changed: 15 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,158 +2,48 @@ package git
22

33
import (
44
"fmt"
5-
"os/exec"
65
"strings"
76

8-
"github.com/git-rain/git-rain/internal/safety"
7+
harnessgit "github.com/git-fire/git-harness/git"
8+
harnessafety "github.com/git-fire/git-harness/safety"
99
)
1010

11-
// Worktree represents a git worktree
12-
type Worktree struct {
13-
Path string // Absolute path to worktree
14-
Branch string // Current branch in this worktree
15-
Head string // Current HEAD SHA
16-
IsMain bool // True if this is the main worktree
17-
}
11+
// Worktree is a git worktree (delegates to git-harness).
12+
type Worktree = harnessgit.Worktree
1813

19-
// getCommitSHA returns the SHA of a commit ref
2014
func getCommitSHA(repoPath, ref string) (string, error) {
21-
cmd := exec.Command("git", "rev-parse", ref)
22-
cmd.Dir = repoPath
23-
24-
output, err := cmd.Output()
25-
if err != nil {
26-
return "", fmt.Errorf("git rev-parse failed for %s: %w", ref, err)
27-
}
28-
29-
sha := strings.TrimSpace(string(output))
30-
return sha, nil
15+
return harnessgit.GetCommitSHA(repoPath, ref)
3116
}
3217

3318
// GetCommitSHA returns the SHA for a ref in the repository.
3419
func GetCommitSHA(repoPath, ref string) (string, error) {
35-
return getCommitSHA(repoPath, ref)
20+
return harnessgit.GetCommitSHA(repoPath, ref)
3621
}
3722

38-
// GetCurrentBranch returns the currently checked out branch
23+
// GetCurrentBranch returns the currently checked out branch.
3924
func GetCurrentBranch(repoPath string) (string, error) {
40-
cmd := exec.Command("git", "branch", "--show-current")
41-
cmd.Dir = repoPath
42-
43-
output, err := cmd.Output()
44-
if err != nil {
45-
return "", fmt.Errorf("git branch --show-current failed: %w", err)
46-
}
47-
48-
branch := strings.TrimSpace(string(output))
49-
if branch == "" {
50-
return "", fmt.Errorf("not on any branch (detached HEAD?)")
51-
}
52-
53-
return branch, nil
25+
return harnessgit.GetCurrentBranch(repoPath)
5426
}
5527

56-
// HasStagedChanges checks if there are staged changes
28+
// HasStagedChanges checks if there are staged changes.
5729
func HasStagedChanges(repoPath string) (bool, error) {
58-
cmd := exec.Command("git", "diff", "--cached", "--quiet")
59-
cmd.Dir = repoPath
60-
61-
err := cmd.Run()
62-
if err != nil {
63-
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
64-
return true, nil
65-
}
66-
return false, fmt.Errorf("git diff --cached --quiet failed: %w", err)
67-
}
68-
69-
return false, nil
30+
return harnessgit.HasStagedChanges(repoPath)
7031
}
7132

72-
// HasUnstagedChanges checks if there are unstaged changes (including untracked files)
33+
// HasUnstagedChanges checks if there are unstaged changes (including untracked files).
7334
func HasUnstagedChanges(repoPath string) (bool, error) {
74-
cmd := exec.Command("git", "diff", "--quiet")
75-
cmd.Dir = repoPath
76-
77-
err := cmd.Run()
78-
hasModified := false
79-
if err != nil {
80-
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
81-
hasModified = true
82-
} else {
83-
return false, fmt.Errorf("git diff --quiet failed: %w", err)
84-
}
85-
}
86-
87-
cmd = exec.Command("git", "ls-files", "--others", "--exclude-standard")
88-
cmd.Dir = repoPath
89-
output, err := cmd.Output()
90-
if err != nil {
91-
return false, fmt.Errorf("git ls-files failed: %w", err)
92-
}
93-
94-
hasUntracked := len(strings.TrimSpace(string(output))) > 0
95-
96-
return hasModified || hasUntracked, nil
35+
return harnessgit.HasUnstagedChanges(repoPath)
9736
}
9837

99-
// ListWorktrees returns all worktrees for a repository
38+
// ListWorktrees returns all worktrees for a repository.
10039
func ListWorktrees(repoPath string) ([]Worktree, error) {
101-
cmd := exec.Command("git", "worktree", "list", "--porcelain")
102-
cmd.Dir = repoPath
103-
104-
output, err := cmd.Output()
105-
if err != nil {
106-
return nil, fmt.Errorf("git worktree list failed: %w", err)
107-
}
108-
109-
lines := strings.Split(string(output), "\n")
110-
var worktrees []Worktree
111-
var current Worktree
112-
isFirst := true
113-
114-
for _, line := range lines {
115-
line = strings.TrimSpace(line)
116-
if line == "" {
117-
if current.Path != "" {
118-
current.IsMain = isFirst
119-
worktrees = append(worktrees, current)
120-
current = Worktree{}
121-
isFirst = false
122-
}
123-
continue
124-
}
125-
126-
parts := strings.SplitN(line, " ", 2)
127-
if len(parts) < 2 {
128-
continue
129-
}
130-
131-
key := parts[0]
132-
value := parts[1]
133-
134-
switch key {
135-
case "worktree":
136-
current.Path = value
137-
case "HEAD":
138-
current.Head = value
139-
case "branch":
140-
branch := strings.TrimPrefix(value, "refs/heads/")
141-
current.Branch = branch
142-
}
143-
}
144-
145-
if current.Path != "" {
146-
current.IsMain = isFirst
147-
worktrees = append(worktrees, current)
148-
}
149-
150-
return worktrees, nil
40+
return harnessgit.ListWorktrees(repoPath)
15141
}
15242

15343
func commandError(action string, err error, output []byte) error {
15444
out := strings.TrimSpace(string(output))
15545
if out == "" {
15646
return fmt.Errorf("%s failed: %w", action, err)
15747
}
158-
return fmt.Errorf("%s failed: %w (stderr: %s)", action, err, safety.SanitizeText(out))
48+
return fmt.Errorf("%s failed: %w (stderr: %s)", action, err, harnessafety.SanitizeText(out))
15949
}

0 commit comments

Comments
 (0)