update roadmap and gitengine command implementation
This commit is contained in:
parent
9f8608333a
commit
e657bce398
5 changed files with 78 additions and 23 deletions
|
|
@ -11,7 +11,7 @@ GitHub/GitLab/Gitea 같은 플랫폼 API 없이 Git CLI와 local bare repository
|
|||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
[완료]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -45,19 +45,23 @@ Provider API에 의존하지 않는 Git command 기능을 만든다.
|
|||
|
||||
branch revision 변화와 changed file을 normalized event 입력으로 만든다.
|
||||
|
||||
- [ ] [cursor] repo/branch별 last seen revision cursor를 저장한다.
|
||||
- [x] [cursor] repo/branch별 last seen revision cursor를 저장한다. 검증: `go test ./internal/controlplane ./internal/storage ./internal/gitengine`에서 cursor store와 scan cursor 상태를 확인한다.
|
||||
- [x] [changed-files] `before..after` changed file scan을 구현한다. 검증: 여러 commit 사이 변경 파일 목록이 기대와 일치한다.
|
||||
- [ ] [event-input] scan 결과를 `RevisionEvent` 후보로 변환한다.
|
||||
- [x] [event-input] scan 결과를 `RevisionEvent` 후보로 변환한다. 검증: `TestRuntimeScanBranchRevisionBuildsRevisionEvent`에서 revision bounds와 changed files를 확인한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 없음
|
||||
- 상태: 승인됨
|
||||
- 요청일: 2026-06-13
|
||||
- 완료 근거:
|
||||
- `services/core/internal/storage`의 `RevisionCursorStore`와 Postgres 구현이 repo/branch cursor 저장과 조회를 지원한다.
|
||||
- `services/core/internal/controlplane`의 `ScanBranchRevision`이 cursor, changed file scan, `RevisionEvent` 변환을 연결한다.
|
||||
- 코드 레벨 완료 리뷰에서 `gitengine.CLI` timeout 경계와 `--name-status` path 파싱 edge case를 보완했다.
|
||||
- `go test ./...`와 `go vet ./...` 통과.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 코드 레벨 리뷰와 보완 검증이 완료되어 archive 이동 대상으로 확정했다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -18,19 +18,19 @@ Gito의 초기 control plane을 build 가능한 modular monolith로 구성한다
|
|||
- 경로: `agent-roadmap/archive/phase/control-plane-foundation/milestones/forgejo-branch-event-mvp.md`
|
||||
- 요약: Forgejo push webhook을 watched branch `branch.updated` event로 정규화하고 NomadCode가 proto-socket으로 받을 수 있는 MVP slice를 만든다.
|
||||
|
||||
- [검토중] Runtime Scaffold and Contract Baseline
|
||||
- 경로: `agent-roadmap/phase/control-plane-foundation/milestones/runtime-scaffold-and-contract-baseline.md`
|
||||
- 요약: Go command, Flutter control surface placeholder, contracts, agent-ops, README, architecture 문서를 실행 가능한 기준선으로 정리한다.
|
||||
|
||||
- [완료] Repo Registry and Workspace Lease
|
||||
- 경로: `agent-roadmap/archive/phase/control-plane-foundation/milestones/repo-registry-and-workspace-lease.md`
|
||||
- 요약: 관리 대상 repo와 workspace slot/lease를 Postgres source of truth로 저장하고 안전하게 할당한다.
|
||||
|
||||
- [진행중] Platformless Git Operation Engine
|
||||
- 경로: `agent-roadmap/phase/control-plane-foundation/milestones/platformless-git-operation-engine.md`
|
||||
- [완료] Platformless Git Operation Engine
|
||||
- 경로: `agent-roadmap/archive/phase/control-plane-foundation/milestones/platformless-git-operation-engine.md`
|
||||
- 요약: clone, fetch, status, diff, commit, push, revision scan을 provider 없이 local bare repo 기반으로 검증한다.
|
||||
|
||||
- [계획] Operation Event Outbox
|
||||
- [검토중] Runtime Scaffold and Contract Baseline
|
||||
- 경로: `agent-roadmap/phase/control-plane-foundation/milestones/runtime-scaffold-and-contract-baseline.md`
|
||||
- 요약: Go command, Flutter control surface placeholder, contracts, agent-ops, README, architecture 문서를 실행 가능한 기준선으로 정리한다.
|
||||
|
||||
- [진행중] Operation Event Outbox
|
||||
- 경로: `agent-roadmap/phase/control-plane-foundation/milestones/operation-event-outbox.md`
|
||||
- 요약: operation lifecycle, idempotency, audit event, outbox 발행을 PostgreSQL 기준으로 구성한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ operation lifecycle과 normalized event를 PostgreSQL 원장에 기록하고, pr
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -72,4 +72,3 @@ operation 결과를 durable event로 남기고 fanout 대상이 읽을 수 있
|
|||
- 선행 작업: Platformless Git Operation Engine
|
||||
- 후속 작업: Agent Shell Runtime Channel
|
||||
- 확인 필요: 없음
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,45 @@
|
|||
package gitengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidGitInput = errors.New("invalid git input")
|
||||
|
||||
const DefaultCommandTimeout = 2 * time.Minute
|
||||
|
||||
type CommandRunner interface {
|
||||
Run(workdir string, args ...string) (string, error)
|
||||
}
|
||||
|
||||
type CLI struct{}
|
||||
type CLI struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (CLI) Run(workdir string, args ...string) (string, error) {
|
||||
func (c CLI) Run(workdir string, args ...string) (string, error) {
|
||||
if len(args) == 0 {
|
||||
return "", ErrInvalidGitInput
|
||||
}
|
||||
cmd := exec.Command("git", args...)
|
||||
timeout := c.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultCommandTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "git", args...)
|
||||
if strings.TrimSpace(workdir) != "" {
|
||||
cmd.Dir = workdir
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return string(out), fmt.Errorf("git command timed out after %s: %w", timeout, ctx.Err())
|
||||
}
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
|
|
@ -226,11 +243,19 @@ func ChangedFilesWithStatus(runner CommandRunner, workdir, before, after string)
|
|||
}
|
||||
|
||||
func parseNameStatus(line string) (ChangedFile, bool) {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return ChangedFile{}, false
|
||||
}
|
||||
fields := strings.Split(line, "\t")
|
||||
if len(fields) < 2 {
|
||||
return ChangedFile{}, false
|
||||
}
|
||||
changeType := changeTypeFromGitStatus(fields[0])
|
||||
status := strings.TrimSpace(fields[0])
|
||||
if status == "" {
|
||||
return ChangedFile{}, false
|
||||
}
|
||||
changeType := changeTypeFromGitStatus(status)
|
||||
path := fields[len(fields)-1]
|
||||
if path == "" {
|
||||
return ChangedFile{}, false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package gitengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
|
|
@ -38,6 +40,13 @@ func (f *fakeRunner) Run(workdir string, args ...string) (string, error) {
|
|||
return output, err
|
||||
}
|
||||
|
||||
func TestCLIRunTimesOut(t *testing.T) {
|
||||
_, err := (CLI{Timeout: 10 * time.Millisecond}).Run("", "-c", "alias.wait=!sleep 1", "wait")
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected context deadline exceeded, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusClean(t *testing.T) {
|
||||
runner := &fakeRunner{outputs: []string{"\n"}}
|
||||
result, err := Status(runner, "/repo")
|
||||
|
|
@ -184,6 +193,24 @@ func TestChangedFilesWithStatus(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChangedFilesWithStatusPreservesWhitespaceInPaths(t *testing.T) {
|
||||
runner := &fakeRunner{outputs: []string{
|
||||
"A\tdocs/user guide.md\nM\tpath with spaces/file name.go\nR100\told name.md\tnew name.md\n",
|
||||
}}
|
||||
files, err := ChangedFilesWithStatus(runner, "/repo", "abc", "def")
|
||||
if err != nil {
|
||||
t.Fatalf("ChangedFilesWithStatus: %v", err)
|
||||
}
|
||||
want := []ChangedFile{
|
||||
{Path: "docs/user guide.md", ChangeType: "added"},
|
||||
{Path: "path with spaces/file name.go", ChangeType: "modified"},
|
||||
{Path: "new name.md", ChangeType: "renamed"},
|
||||
}
|
||||
if !reflect.DeepEqual(files, want) {
|
||||
t.Fatalf("files:\n got %#v\nwant %#v", files, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangedFilesWithStatusLocalCommits(t *testing.T) {
|
||||
_, seedPath, initialRev := setupBareRemote(t)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue