feat: track workspace repo default branches
This commit is contained in:
parent
e8a2438291
commit
fa71a7dda5
|
|
@ -44,6 +44,7 @@ type WorkspaceRepoRecord struct {
|
|||
Name string
|
||||
RelativePath string
|
||||
RepoOriginURL string
|
||||
DefaultBranch string
|
||||
RegisteredAt time.Time
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2560,6 +2560,8 @@ components:
|
|||
type: object
|
||||
WorkspaceRepo:
|
||||
properties:
|
||||
defaultBranch:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
relativePath:
|
||||
|
|
@ -2570,6 +2572,7 @@ components:
|
|||
- name
|
||||
- relativePath
|
||||
- repo
|
||||
- defaultBranch
|
||||
type: object
|
||||
tags:
|
||||
- description: Project registry, configuration, and lifecycle administration
|
||||
|
|
|
|||
|
|
@ -530,7 +530,12 @@ func configureCommitter(t *testing.T) {
|
|||
|
||||
func gitRepoWithCommit(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
if out, err := exec.Command("git", "init", "-b", "main", dir).CombinedOutput(); err != nil {
|
||||
return gitRepoWithCommitOnBranch(t, dir, "main")
|
||||
}
|
||||
|
||||
func gitRepoWithCommitOnBranch(t *testing.T, dir, branch string) string {
|
||||
t.Helper()
|
||||
if out, err := exec.Command("git", "init", "-b", branch, dir).CombinedOutput(); err != nil {
|
||||
t.Fatalf("git init: %v (%s)", err, out)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hello\n"), 0o644); err != nil {
|
||||
|
|
@ -566,6 +571,11 @@ func TestManager_AddWorkspaceInitializesPlainParent(t *testing.T) {
|
|||
if len(proj.WorkspaceRepos) != 2 || proj.WorkspaceRepos[0].Name != "api" || proj.WorkspaceRepos[1].Name != "cli" {
|
||||
t.Fatalf("WorkspaceRepos = %#v", proj.WorkspaceRepos)
|
||||
}
|
||||
for _, repo := range proj.WorkspaceRepos {
|
||||
if repo.DefaultBranch != "main" {
|
||||
t.Fatalf("repo %s default branch = %q, want main", repo.Name, repo.DefaultBranch)
|
||||
}
|
||||
}
|
||||
ignored, err := os.ReadFile(filepath.Join(parent, ".gitignore"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -595,6 +605,35 @@ func TestManager_AddWorkspaceInitializesPlainParent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestManager_AddWorkspaceDetectsChildDefaultBranches(t *testing.T) {
|
||||
configureCommitter(t)
|
||||
ctx := context.Background()
|
||||
m := newManager(t)
|
||||
parent := t.TempDir()
|
||||
gitRepoWithCommitOnBranch(t, filepath.Join(parent, "api"), "master")
|
||||
gitRepoWithCommitOnBranch(t, filepath.Join(parent, "cli"), "develop")
|
||||
|
||||
proj, err := m.Add(ctx, project.AddInput{Path: parent, ProjectID: ptr("ws"), AsWorkspace: true})
|
||||
if err != nil {
|
||||
t.Fatalf("Add workspace: %v", err)
|
||||
}
|
||||
want := map[string]string{"api": "master", "cli": "develop"}
|
||||
for _, repo := range proj.WorkspaceRepos {
|
||||
if repo.DefaultBranch != want[repo.Name] {
|
||||
t.Fatalf("repo %s default branch = %q, want %q", repo.Name, repo.DefaultBranch, want[repo.Name])
|
||||
}
|
||||
}
|
||||
got, err := m.Get(ctx, "ws")
|
||||
if err != nil {
|
||||
t.Fatalf("Get workspace: %v", err)
|
||||
}
|
||||
for _, repo := range got.Project.WorkspaceRepos {
|
||||
if repo.DefaultBranch != want[repo.Name] {
|
||||
t.Fatalf("stored repo %s default branch = %q, want %q", repo.Name, repo.DefaultBranch, want[repo.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_AddWorkspaceRejectsUncommittedChild(t *testing.T) {
|
||||
configureCommitter(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ type Degraded struct {
|
|||
|
||||
// WorkspaceRepo is the project-detail read shape for a registered child repo.
|
||||
type WorkspaceRepo struct {
|
||||
Name string `json:"name"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
Repo string `json:"repo"`
|
||||
Name string `json:"name"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
Repo string `json:"repo"`
|
||||
DefaultBranch string `json:"defaultBranch"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ func detectWorkspaceChildren(ctx context.Context, parent string, projectID domai
|
|||
Name: name,
|
||||
RelativePath: filepath.ToSlash(name),
|
||||
RepoOriginURL: resolveGitOriginURL(child),
|
||||
DefaultBranch: resolveDefaultBranch(child),
|
||||
RegisteredAt: registeredAt,
|
||||
})
|
||||
}
|
||||
|
|
@ -206,8 +207,7 @@ func validateWorkspaceChild(ctx context.Context, child string) error {
|
|||
"suggestedFix": "Run `git init -b main`, add the initial files, and create the first commit before registering the workspace.",
|
||||
})
|
||||
}
|
||||
branch, err := gitOutput(ctx, child, "symbolic-ref", "--quiet", "--short", "HEAD")
|
||||
if err != nil || strings.TrimSpace(branch) == "" {
|
||||
if branch := resolveDefaultBranch(child); branch == "" {
|
||||
return apierr.Invalid("WORKSPACE_CHILD_DEFAULT_BRANCH_UNKNOWN", "Workspace child repositories must have an identifiable default branch", map[string]any{
|
||||
"path": child,
|
||||
"suggestedFix": "Check out the repository's default branch (for example `main`) and retry.",
|
||||
|
|
@ -352,7 +352,16 @@ func guardNoGitlinks(ctx context.Context, repo string) error {
|
|||
func workspaceReposFromRecords(records []domain.WorkspaceRepoRecord) []WorkspaceRepo {
|
||||
out := make([]WorkspaceRepo, 0, len(records))
|
||||
for _, rec := range records {
|
||||
out = append(out, WorkspaceRepo{Name: rec.Name, RelativePath: rec.RelativePath, Repo: rec.RepoOriginURL})
|
||||
defaultBranch := rec.DefaultBranch
|
||||
if defaultBranch == "" {
|
||||
defaultBranch = domain.DefaultBranchName
|
||||
}
|
||||
out = append(out, WorkspaceRepo{
|
||||
Name: rec.Name,
|
||||
RelativePath: rec.RelativePath,
|
||||
Repo: rec.RepoOriginURL,
|
||||
DefaultBranch: defaultBranch,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ func (m *Manager) createSessionWorkspace(ctx context.Context, project domain.Pro
|
|||
Name: repo.Name,
|
||||
RelativePath: repo.RelativePath,
|
||||
RepoPath: filepath.Join(project.Path, filepath.FromSlash(repo.RelativePath)),
|
||||
BaseBranch: project.Config.WithDefaults().DefaultBranch,
|
||||
BaseBranch: firstNonEmptyString(repo.DefaultBranch, project.Config.WithDefaults().DefaultBranch),
|
||||
})
|
||||
}
|
||||
info, err := workspaceProject.CreateWorkspaceProject(ctx, ports.WorkspaceProjectConfig{
|
||||
|
|
|
|||
|
|
@ -616,8 +616,8 @@ func TestSpawn_WorkspaceProjectRecordsRootAndChildWorktrees(t *testing.T) {
|
|||
Config: testRoleAgents(),
|
||||
}
|
||||
st.workspaceRepo["mer"] = []domain.WorkspaceRepoRecord{
|
||||
{Name: "api", RelativePath: "services/api"},
|
||||
{Name: "web", RelativePath: "apps/web"},
|
||||
{Name: "api", RelativePath: "services/api", DefaultBranch: "master"},
|
||||
{Name: "web", RelativePath: "apps/web", DefaultBranch: "develop"},
|
||||
}
|
||||
rt := &fakeRuntime{}
|
||||
ws := &fakeWorkspace{path: "/managed/mer-1"}
|
||||
|
|
@ -649,6 +649,12 @@ func TestSpawn_WorkspaceProjectRecordsRootAndChildWorktrees(t *testing.T) {
|
|||
if got := ws.lastProjectCfg.Repos[1].RepoPath; got != "/repo/mer/apps/web" {
|
||||
t.Fatalf("web repo path = %q, want /repo/mer/apps/web", got)
|
||||
}
|
||||
if got := ws.lastProjectCfg.Repos[0].BaseBranch; got != "master" {
|
||||
t.Fatalf("api base branch = %q, want master", got)
|
||||
}
|
||||
if got := ws.lastProjectCfg.Repos[1].BaseBranch; got != "develop" {
|
||||
t.Fatalf("web base branch = %q, want develop", got)
|
||||
}
|
||||
rows := st.worktrees["mer-1"]
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("session worktree rows = %d, want 3: %#v", len(rows), rows)
|
||||
|
|
|
|||
|
|
@ -210,5 +210,6 @@ type WorkspaceRepo struct {
|
|||
Name string
|
||||
RelativePath string
|
||||
RepoOriginURL string
|
||||
DefaultBranch string
|
||||
RegisteredAt time.Time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ func (q *Queries) ListSessionWorktrees(ctx context.Context, sessionID domain.Ses
|
|||
}
|
||||
|
||||
const listWorkspaceRepos = `-- name: ListWorkspaceRepos :many
|
||||
SELECT project_id, name, relative_path, repo_origin_url, registered_at
|
||||
SELECT project_id, name, relative_path, repo_origin_url, default_branch, registered_at
|
||||
FROM workspace_repos
|
||||
WHERE project_id = ?
|
||||
ORDER BY name
|
||||
|
|
@ -115,6 +115,7 @@ func (q *Queries) ListWorkspaceRepos(ctx context.Context, projectID domain.Proje
|
|||
&i.Name,
|
||||
&i.RelativePath,
|
||||
&i.RepoOriginURL,
|
||||
&i.DefaultBranch,
|
||||
&i.RegisteredAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -165,11 +166,12 @@ func (q *Queries) UpsertSessionWorktree(ctx context.Context, arg UpsertSessionWo
|
|||
}
|
||||
|
||||
const upsertWorkspaceRepo = `-- name: UpsertWorkspaceRepo :exec
|
||||
INSERT INTO workspace_repos (project_id, name, relative_path, repo_origin_url, registered_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO workspace_repos (project_id, name, relative_path, repo_origin_url, default_branch, registered_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET
|
||||
relative_path = excluded.relative_path,
|
||||
repo_origin_url = excluded.repo_origin_url,
|
||||
default_branch = excluded.default_branch,
|
||||
registered_at = excluded.registered_at
|
||||
`
|
||||
|
||||
|
|
@ -178,6 +180,7 @@ type UpsertWorkspaceRepoParams struct {
|
|||
Name string
|
||||
RelativePath string
|
||||
RepoOriginURL string
|
||||
DefaultBranch string
|
||||
RegisteredAt time.Time
|
||||
}
|
||||
|
||||
|
|
@ -187,6 +190,7 @@ func (q *Queries) UpsertWorkspaceRepo(ctx context.Context, arg UpsertWorkspaceRe
|
|||
arg.Name,
|
||||
arg.RelativePath,
|
||||
arg.RepoOriginURL,
|
||||
arg.DefaultBranch,
|
||||
arg.RegisteredAt,
|
||||
)
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ CREATE TABLE workspace_repos (
|
|||
name TEXT NOT NULL,
|
||||
relative_path TEXT NOT NULL,
|
||||
repo_origin_url TEXT NOT NULL DEFAULT '',
|
||||
default_branch TEXT NOT NULL DEFAULT 'main',
|
||||
registered_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (project_id, name),
|
||||
UNIQUE (project_id, relative_path)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
DELETE FROM workspace_repos WHERE project_id = ?;
|
||||
|
||||
-- name: UpsertWorkspaceRepo :exec
|
||||
INSERT INTO workspace_repos (project_id, name, relative_path, repo_origin_url, registered_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO workspace_repos (project_id, name, relative_path, repo_origin_url, default_branch, registered_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (project_id, name) DO UPDATE SET
|
||||
relative_path = excluded.relative_path,
|
||||
repo_origin_url = excluded.repo_origin_url,
|
||||
default_branch = excluded.default_branch,
|
||||
registered_at = excluded.registered_at;
|
||||
|
||||
-- name: ListWorkspaceRepos :many
|
||||
SELECT project_id, name, relative_path, repo_origin_url, registered_at
|
||||
SELECT project_id, name, relative_path, repo_origin_url, default_branch, registered_at
|
||||
FROM workspace_repos
|
||||
WHERE project_id = ?
|
||||
ORDER BY name;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ func (s *Store) UpsertWorkspaceProject(ctx context.Context, r domain.ProjectReco
|
|||
Name: repo.Name,
|
||||
RelativePath: repo.RelativePath,
|
||||
RepoOriginURL: repo.RepoOriginURL,
|
||||
DefaultBranch: repo.DefaultBranch,
|
||||
RegisteredAt: repo.RegisteredAt,
|
||||
}); err != nil {
|
||||
return err
|
||||
|
|
@ -67,6 +68,7 @@ func (s *Store) ListWorkspaceRepos(ctx context.Context, projectID string) ([]dom
|
|||
Name: row.Name,
|
||||
RelativePath: row.RelativePath,
|
||||
RepoOriginURL: row.RepoOriginURL,
|
||||
DefaultBranch: row.DefaultBranch,
|
||||
RegisteredAt: row.RegisteredAt,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -916,6 +916,7 @@ export interface components {
|
|||
reviews: components["schemas"]["PRReviewState"][];
|
||||
};
|
||||
WorkspaceRepo: {
|
||||
defaultBranch: string;
|
||||
name: string;
|
||||
relativePath: string;
|
||||
repo: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue