Slice 6: right-click command menu + git write actions
git boundary: Pull/Push/Commit/DiscardAll (discard is 1.4-destructive). Scanner.RefreshRepo re-scans one repo after a mutation. Service GitFetch/GitPull/GitPush/GitCommit/GitDiscard record a git-* activity event and refresh on success; service.New takes a refresh hook. HTTP POST /api/repo/git. New <repo-menu> overlay with plain-language commands (Get latest/Publish/Check for updates/Save my work/Set active/Ask Claude to switch/Copy path/Discard all changes), summoned by repo-list's repo:contextmenu. Added service test for commit/discard on a temp repo. Verified the menu live for safe commands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitmanager/internal/activity"
|
||||
"gitmanager/internal/forge"
|
||||
@@ -18,16 +19,18 @@ import (
|
||||
|
||||
// Service holds the shared dependencies the capabilities need.
|
||||
type Service struct {
|
||||
git *git.CLI
|
||||
index *repos.Index
|
||||
feed *activity.Feed
|
||||
forge *forge.Gitea // nil when no forge is configured
|
||||
git *git.CLI
|
||||
index *repos.Index
|
||||
feed *activity.Feed
|
||||
forge *forge.Gitea // nil when no forge is configured
|
||||
refresh func(context.Context, string) // re-scan one repo after a mutation (may be nil)
|
||||
}
|
||||
|
||||
// New builds a Service over the git boundary, the scanner's repo index, the
|
||||
// activity feed, and (optionally) a forge provider.
|
||||
func New(g *git.CLI, index *repos.Index, feed *activity.Feed, fg *forge.Gitea) *Service {
|
||||
return &Service{git: g, index: index, feed: feed, forge: fg}
|
||||
// activity feed, (optionally) a forge provider, and a single-repo refresh hook
|
||||
// (may be nil) used to re-scan a repo after a mutating action.
|
||||
func New(g *git.CLI, index *repos.Index, feed *activity.Feed, fg *forge.Gitea, refresh func(context.Context, string)) *Service {
|
||||
return &Service{git: g, index: index, feed: feed, forge: fg, refresh: refresh}
|
||||
}
|
||||
|
||||
// ListRepos returns a snapshot of every discovered repository.
|
||||
@@ -154,6 +157,64 @@ func (s *Service) MergeAndCleanup(ctx context.Context, actor activity.Actor, rep
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// --- Git actions (the plain-language commands, §6) --------------------------
|
||||
|
||||
// gitAction runs one mutating git op through the boundary, records the outcome
|
||||
// on the activity feed, and refreshes the repo in the index on success. Callers
|
||||
// are responsible for §1.4 confirmation of destructive ops (e.g. discard).
|
||||
func (s *Service) gitAction(ctx context.Context, actor activity.Actor, repoPath, kind string, run func(dir string) (string, error)) (string, error) {
|
||||
base, ok := s.index.Get(filepath.Clean(repoPath))
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unknown repository %q", repoPath)
|
||||
}
|
||||
out, err := run(base.Path)
|
||||
if err != nil {
|
||||
s.feed.Record(actor, kind, base.Path, "failed: "+err.Error())
|
||||
return "", err
|
||||
}
|
||||
s.feed.Record(actor, kind, base.Path, "ok")
|
||||
if s.refresh != nil {
|
||||
s.refresh(ctx, base.Path)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GitFetch, GitPull, GitPush, GitCommit, GitDiscard are the mutating commands
|
||||
// the right-click menu (and, later, MCP) invoke.
|
||||
func (s *Service) GitFetch(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||
return s.gitAction(ctx, actor, repoPath, "git-fetch", func(d string) (string, error) {
|
||||
return "", s.git.Fetch(ctx, d)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) GitPull(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||
return s.gitAction(ctx, actor, repoPath, "git-pull", func(d string) (string, error) {
|
||||
return s.git.Pull(ctx, d)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) GitPush(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||
return s.gitAction(ctx, actor, repoPath, "git-push", func(d string) (string, error) {
|
||||
return s.git.Push(ctx, d)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) GitCommit(ctx context.Context, actor activity.Actor, repoPath, message string) (string, error) {
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return "", fmt.Errorf("a commit message is required")
|
||||
}
|
||||
return s.gitAction(ctx, actor, repoPath, "git-commit", func(d string) (string, error) {
|
||||
return s.git.Commit(ctx, d, message)
|
||||
})
|
||||
}
|
||||
|
||||
// GitDiscard is DESTRUCTIVE (§1.4) — the caller must confirm with the user first.
|
||||
func (s *Service) GitDiscard(ctx context.Context, actor activity.Actor, repoPath string) (string, error) {
|
||||
return s.gitAction(ctx, actor, repoPath, "git-discard", func(d string) (string, error) {
|
||||
return s.git.DiscardAll(ctx, d)
|
||||
})
|
||||
}
|
||||
|
||||
// resolveForge maps a repo path to (owner, repo) on the configured forge host via
|
||||
// its git remotes, preferring "origin".
|
||||
func (s *Service) resolveForge(ctx context.Context, repoPath string) (owner, repo string, err error) {
|
||||
|
||||
Reference in New Issue
Block a user