Skip to content

Commit ee01295

Browse files
Merge remote-tracking branch 'origin/main' into design/tui-status-log-export-spec
2 parents a66ebff + 3d29d11 commit ee01295

21 files changed

Lines changed: 518 additions & 256 deletions

.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:

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@ visual density (more or fewer seeds, longer or shorter blooms) for clarity.
367367

368368
`git-rain --rain` opens an interactive picker. Repositories stream in as the filesystem scan finds them — no waiting for the full scan to complete before you can start picking. After you confirm, the tool runs the **default full fetch** (`git fetch --all`, prune opt-in) unless you passed **`--fetch-mainline`**, or **full branch hydration** is implied by **`--sync`**, **`--risky`**, **`risky_mode`** in config, a **non-mainline `branch_mode`**, or **any `--branch-mode`** on the CLI. Quitting (**`q`** or **`ctrl+c`**) cancels the in-progress scan (in-flight `git` subprocesses are aborted via the scan context); **`ctrl+c`** outside raw TTY mode is treated like cancel.
369369

370+
Status strip and optional log panel are driven from structured session events. Confirming selection still exits TUI and runs fetch/sync work in normal CLI output.
371+
370372
**Key bindings:**
371373

372374
| Key | Action |
@@ -377,6 +379,8 @@ visual density (more or fewer seeds, longer or shorter blooms) for clarity.
377379
| `q` / `ctrl+c` | Abort picker |
378380
| `c` / `Esc` | Back from settings (ignored list uses `Esc` / `i` / `b`) |
379381
| `` / `` | Navigate |
382+
| `Shift+L` | Toggle in-TUI log panel |
383+
| `e` | Export visible TUI log buffer to `~/.cache/git-rain/exports/` |
380384

381385
## Safe Mode vs Risky Mode
382386

cmd/root.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ 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"
26+
"github.com/git-rain/git-rain/internal/sessionlog"
2627
"github.com/git-rain/git-rain/internal/ui"
2728
)
2829

@@ -508,6 +509,11 @@ func runRainTUIStream(cfg *config.Config, reg *registry.Registry, regPath string
508509

509510
userCfgDir, _ := config.UserGitRainDir()
510511
cfgPath := filepath.Join(userCfgDir, "config.toml")
512+
logger, err := sessionlog.NewLogger(sessionlog.DefaultLogDir())
513+
if err != nil {
514+
return fmt.Errorf("init rain logger: %w", err)
515+
}
516+
defer func() { _ = logger.Close() }()
511517

512518
selected, err := ui.RunRepoSelectorStream(
513519
tuiRepoChan,
@@ -518,6 +524,7 @@ func runRainTUIStream(cfg *config.Config, reg *registry.Registry, regPath string
518524
cfgPath,
519525
reg,
520526
regPath,
527+
logger,
521528
)
522529

523530
// Cancel scan first so filepath walk and in-flight git subprocesses unwind.
@@ -881,6 +888,7 @@ func fetchOnly(repoPath string, opts git.RainOptions) error {
881888
}
882889
cmd := exec.Command("git", fetchArgs...)
883890
cmd.Dir = repoPath
891+
git.PrepareNetworkGit(cmd)
884892
if out, err := cmd.CombinedOutput(); err != nil {
885893
return fmt.Errorf("%s: %w (output: %s)", strings.Join(append([]string{"git"}, fetchArgs...), " "), err, strings.TrimSpace(string(out)))
886894
}

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
}

0 commit comments

Comments
 (0)