- gitoevents: Gito 이벤트 클라이언트 및 이벤트 모델 구현 - gitosync: 브리지, 러너, 스캐너 구현으로 브랜치 이벤트 및 작업 항목 동기화 - config: 새 설정 항목 추가 및 테스트 - roadmapsync: plane projection 수정 및 테스트 - agent-task: G07 관련 계획 및 코드 리뷰 로그 추가 - roadmap: PHASE 및 마일스톤 업데이트 - contracts: Flutter core API 후보 노트 업데이트
110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
package gitosync
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"nhooyr.io/websocket"
|
|
|
|
"github.com/nomadcode/nomadcode-core/internal/gitoevents"
|
|
"github.com/nomadcode/nomadcode-core/internal/protosocket"
|
|
)
|
|
|
|
// ExecCommandRunner is the production CommandRunner backed by os/exec. It runs
|
|
// git in the given directory and returns stdout with the trailing newline left
|
|
// intact for the caller to trim.
|
|
type ExecCommandRunner struct{}
|
|
|
|
func (ExecCommandRunner) Run(ctx context.Context, dir, name string, args ...string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
cmd.Dir = dir
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// websocketTransport is the production gitoevents.Transport that dials the Gito
|
|
// proto-socket endpoint over a websocket and exchanges JSON envelopes. It is
|
|
// kept unexported; callers wire it through NewRunner.
|
|
type websocketTransport struct {
|
|
url string
|
|
conn *websocket.Conn
|
|
}
|
|
|
|
func (t *websocketTransport) Connect(ctx context.Context) error {
|
|
conn, _, err := websocket.Dial(ctx, t.url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.conn = conn
|
|
return nil
|
|
}
|
|
|
|
func (t *websocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
|
|
if t.conn == nil {
|
|
return errors.New("gitosync: transport not connected")
|
|
}
|
|
data, err := json.Marshal(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.conn.Write(ctx, websocket.MessageText, data)
|
|
}
|
|
|
|
func (t *websocketTransport) Read(ctx context.Context) (protosocket.Envelope, error) {
|
|
if t.conn == nil {
|
|
return protosocket.Envelope{}, errors.New("gitosync: transport not connected")
|
|
}
|
|
_, data, err := t.conn.Read(ctx)
|
|
if err != nil {
|
|
return protosocket.Envelope{}, err
|
|
}
|
|
var env protosocket.Envelope
|
|
if err := json.Unmarshal(data, &env); err != nil {
|
|
return protosocket.Envelope{}, err
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
func (t *websocketTransport) Close() error {
|
|
if t.conn == nil {
|
|
return nil
|
|
}
|
|
return t.conn.Close(websocket.StatusNormalClosure, "")
|
|
}
|
|
|
|
// NewRunner builds the gitoevents client wired to forward on-target branch
|
|
// events into the bridge. It returns a *gitoevents.Client whose Run blocks
|
|
// until the context is cancelled or the stream ends; the caller owns starting
|
|
// it in a goroutine and cancelling the context on shutdown. The bridge's Handle
|
|
// error is logged (not propagated) so a single failed event does not tear down
|
|
// the long-lived subscription.
|
|
func NewRunner(url, repoID, branch string, bridge *Bridge, logger *slog.Logger) (*gitoevents.Client, error) {
|
|
if strings.TrimSpace(url) == "" {
|
|
return nil, errors.New("gitosync: proto-socket url is required")
|
|
}
|
|
if bridge == nil {
|
|
return nil, errors.New("gitosync: bridge is nil")
|
|
}
|
|
transport := &websocketTransport{url: url}
|
|
handler := func(ctx context.Context, ev gitoevents.BranchUpdatedEvent) {
|
|
if err := bridge.Handle(ctx, ev); err != nil {
|
|
if logger != nil {
|
|
logger.Error("gito creation sync handle failed",
|
|
"error", err, "repo_id", ev.RepoID, "after", ev.After)
|
|
}
|
|
}
|
|
}
|
|
return gitoevents.NewClient(transport, gitoevents.Options{
|
|
RepoID: repoID,
|
|
Branch: branch,
|
|
Handler: handler,
|
|
Logger: logger,
|
|
})
|
|
}
|