Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"github.com/git-rain/git-rain/internal/config"
"github.com/git-rain/git-rain/internal/git"
"github.com/git-rain/git-rain/internal/registry"
"github.com/git-rain/git-rain/internal/safety"
"github.com/git-fire/git-harness/safety"
"github.com/git-rain/git-rain/internal/sessionlog"
"github.com/git-rain/git-rain/internal/ui"
)
Expand Down
140 changes: 15 additions & 125 deletions internal/git/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,158 +2,48 @@ package git

import (
"fmt"
"os/exec"
"strings"

"github.com/git-rain/git-rain/internal/safety"
harnessgit "github.com/git-fire/git-harness/git"
harnessafety "github.com/git-fire/git-harness/safety"
)

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

// getCommitSHA returns the SHA of a commit ref
func getCommitSHA(repoPath, ref string) (string, error) {
cmd := exec.Command("git", "rev-parse", ref)
cmd.Dir = repoPath

output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git rev-parse failed for %s: %w", ref, err)
}

sha := strings.TrimSpace(string(output))
return sha, nil
return harnessgit.GetCommitSHA(repoPath, ref)
}

// GetCommitSHA returns the SHA for a ref in the repository.
func GetCommitSHA(repoPath, ref string) (string, error) {
return getCommitSHA(repoPath, ref)
return harnessgit.GetCommitSHA(repoPath, ref)
}

// GetCurrentBranch returns the currently checked out branch
// GetCurrentBranch returns the currently checked out branch.
func GetCurrentBranch(repoPath string) (string, error) {
cmd := exec.Command("git", "branch", "--show-current")
cmd.Dir = repoPath

output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git branch --show-current failed: %w", err)
}

branch := strings.TrimSpace(string(output))
if branch == "" {
return "", fmt.Errorf("not on any branch (detached HEAD?)")
}

return branch, nil
return harnessgit.GetCurrentBranch(repoPath)
}

// HasStagedChanges checks if there are staged changes
// HasStagedChanges checks if there are staged changes.
func HasStagedChanges(repoPath string) (bool, error) {
cmd := exec.Command("git", "diff", "--cached", "--quiet")
cmd.Dir = repoPath

err := cmd.Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
return true, nil
}
return false, fmt.Errorf("git diff --cached --quiet failed: %w", err)
}

return false, nil
return harnessgit.HasStagedChanges(repoPath)
}

// HasUnstagedChanges checks if there are unstaged changes (including untracked files)
// HasUnstagedChanges checks if there are unstaged changes (including untracked files).
func HasUnstagedChanges(repoPath string) (bool, error) {
cmd := exec.Command("git", "diff", "--quiet")
cmd.Dir = repoPath

err := cmd.Run()
hasModified := false
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
hasModified = true
} else {
return false, fmt.Errorf("git diff --quiet failed: %w", err)
}
}

cmd = exec.Command("git", "ls-files", "--others", "--exclude-standard")
cmd.Dir = repoPath
output, err := cmd.Output()
if err != nil {
return false, fmt.Errorf("git ls-files failed: %w", err)
}

hasUntracked := len(strings.TrimSpace(string(output))) > 0

return hasModified || hasUntracked, nil
return harnessgit.HasUnstagedChanges(repoPath)
}

// ListWorktrees returns all worktrees for a repository
// ListWorktrees returns all worktrees for a repository.
func ListWorktrees(repoPath string) ([]Worktree, error) {
cmd := exec.Command("git", "worktree", "list", "--porcelain")
cmd.Dir = repoPath

output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git worktree list failed: %w", err)
}

lines := strings.Split(string(output), "\n")
var worktrees []Worktree
var current Worktree
isFirst := true

for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
if current.Path != "" {
current.IsMain = isFirst
worktrees = append(worktrees, current)
current = Worktree{}
isFirst = false
}
continue
}

parts := strings.SplitN(line, " ", 2)
if len(parts) < 2 {
continue
}

key := parts[0]
value := parts[1]

switch key {
case "worktree":
current.Path = value
case "HEAD":
current.Head = value
case "branch":
branch := strings.TrimPrefix(value, "refs/heads/")
current.Branch = branch
}
}

if current.Path != "" {
current.IsMain = isFirst
worktrees = append(worktrees, current)
}

return worktrees, nil
return harnessgit.ListWorktrees(repoPath)
}

func commandError(action string, err error, output []byte) error {
out := strings.TrimSpace(string(output))
if out == "" {
return fmt.Errorf("%s failed: %w", action, err)
}
return fmt.Errorf("%s failed: %w (stderr: %s)", action, err, safety.SanitizeText(out))
return fmt.Errorf("%s failed: %w (stderr: %s)", action, err, harnessafety.SanitizeText(out))
}
30 changes: 30 additions & 0 deletions internal/git/safety_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package git_test

import (
"strings"
"testing"

"github.com/git-fire/git-harness/safety"
)

func TestHarnessSanitizeText_FreezeMessagesPreserved(t *testing.T) {
msgs := []string{
"could not reach remote — check your network and try again",
"could not authenticate with remote — check your credentials and try again",
"fetch did not complete — try again when the remote is reachable",
}
for _, msg := range msgs {
got := safety.SanitizeText(msg)
if got != msg {
t.Errorf("freeze message was modified:\n input: %q\n got: %q", msg, got)
}
}
}

func TestHarnessSanitizeText_RedactsCredentials(t *testing.T) {
input := "fatal: https://user:supersecret@github.com/org/repo.git"
got := safety.SanitizeText(input)
if strings.Contains(got, "supersecret") {
t.Errorf("password not redacted: %q", got)
}
}
32 changes: 0 additions & 32 deletions internal/safety/redact.go

This file was deleted.

84 changes: 0 additions & 84 deletions internal/safety/redact_test.go

This file was deleted.

2 changes: 1 addition & 1 deletion internal/sessionlog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"path/filepath"
"time"

"github.com/git-rain/git-rain/internal/safety"
"github.com/git-fire/git-harness/safety"
)

// LogEntry matches git-fire's structured session event shape.
Expand Down
Loading