feat(scaffold): 초기 프로젝트 골격을 구성한다
Gito를 platformless Git control plane으로 시작할 수 있도록 Go core, Flutter control surface, contracts, ops 규칙, 검증 헬퍼를 함께 구성한다.
This commit is contained in:
commit
e2bc7aa8b6
37 changed files with 1349 additions and 0 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Go
|
||||
bin/gito-*
|
||||
coverage.out
|
||||
|
||||
# Flutter/Dart
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
build/
|
||||
|
||||
# Local runtime
|
||||
.gito/
|
||||
tmp/
|
||||
|
||||
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Gito Agent Instructions
|
||||
|
||||
Read this file before making changes in this repository.
|
||||
|
||||
## Common Rules
|
||||
|
||||
- Prefer the existing structure over creating new top-level patterns.
|
||||
- Keep user-facing final answers in Korean unless the user asks otherwise.
|
||||
- Check the relevant domain rule before code changes.
|
||||
- Do not expand scope beyond the current request.
|
||||
- Do not write secrets, tokens, passwords, private endpoints, or raw credentials to tracked files.
|
||||
- Use `rg` or `rg --files` for searches when available.
|
||||
- Use root helpers under `bin/` before ad hoc commands when they cover the task.
|
||||
|
||||
## Required First Reads
|
||||
|
||||
Read these files once per session when they exist:
|
||||
|
||||
1. `agent-ops/rules/project/rules.md`
|
||||
2. `agent-ops/rules/private/rules.md`
|
||||
|
||||
## Domain Rules
|
||||
|
||||
| Path | Domain | Rules |
|
||||
| --- | --- | --- |
|
||||
| `services/core/**` | core | `agent-ops/rules/project/domain/core/rules.md` |
|
||||
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||
| `packages/contracts/**` | contracts | `agent-ops/rules/project/domain/contracts/rules.md` |
|
||||
| `bin/**`, `docs/**`, `AGENTS.md`, `README.md`, `agent-ops/**`, `agent-test/**` | workspace-ops | `agent-ops/rules/project/domain/workspace-ops/rules.md` |
|
||||
|
||||
## Test Rules
|
||||
|
||||
When validation is part of the task, read `agent-test/local/rules.md` first.
|
||||
|
||||
59
README.md
Normal file
59
README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Gito
|
||||
|
||||
Gito is a platformless Git control plane for agent-driven development workflows.
|
||||
|
||||
It manages registered repositories, workspace leases, Git operations, agent-shell
|
||||
execution, provider adapters, and normalized events. Git itself stays
|
||||
platform-neutral; GitHub, GitLab, Gitea, Plane, Jira, and similar systems are
|
||||
integrated through adapters.
|
||||
|
||||
## Direction
|
||||
|
||||
- Server: Go
|
||||
- Control surface: Flutter app and `gitoctl` in future slices
|
||||
- State: PostgreSQL as source of truth
|
||||
- Event acceleration: Redis optional, not required for the first slice
|
||||
- Internal transport: proto-socket first
|
||||
- REST: health/readiness, smoke/curl, provider callbacks, simple admin bootstrap
|
||||
- gRPC: intentionally excluded from the initial design
|
||||
|
||||
## Runtime Shape
|
||||
|
||||
```text
|
||||
Flutter UI / gitoctl
|
||||
|
|
||||
Control Plane
|
||||
|
|
||||
Core Domain
|
||||
|
|
||||
Worker / Scheduler
|
||||
|
|
||||
Agent Shell / IOP / Git Engine
|
||||
|
|
||||
Provider Adapters
|
||||
```
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```text
|
||||
apps/client/ Flutter control surface scaffold
|
||||
services/core/ Go control plane, worker, shell commands
|
||||
packages/contracts/ proto-socket, REST, event, and provider contract notes
|
||||
docs/ Architecture and operation notes
|
||||
bin/ Root test/lint/build helpers
|
||||
agent-ops/ Agent operating rules
|
||||
agent-test/ Local validation rules
|
||||
```
|
||||
|
||||
## First Commands
|
||||
|
||||
```sh
|
||||
bin/test
|
||||
bin/lint
|
||||
bin/build
|
||||
```
|
||||
|
||||
The initial Go scaffold is intentionally small but buildable. Flutter platform
|
||||
directories are not generated yet; `apps/client` currently records the control
|
||||
surface boundary and a minimal Dart entry point.
|
||||
|
||||
2
agent-ops/rules/private/rules.md
Normal file
2
agent-ops/rules/private/rules.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Gito Private Rules
|
||||
|
||||
13
agent-ops/rules/project/domain/client/rules.md
Normal file
13
agent-ops/rules/project/domain/client/rules.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# client
|
||||
|
||||
## Scope
|
||||
|
||||
The client domain covers the Flutter control surface under `apps/client`.
|
||||
|
||||
## Rules
|
||||
|
||||
- The client is a control surface, not a Git execution runtime.
|
||||
- Prefer proto-socket for operation requests, events, and log streams.
|
||||
- REST should be used only for health/readiness or compatibility endpoints.
|
||||
- Keep platform/provider-specific presentation behind adapter-friendly models.
|
||||
|
||||
14
agent-ops/rules/project/domain/contracts/rules.md
Normal file
14
agent-ops/rules/project/domain/contracts/rules.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# contracts
|
||||
|
||||
## Scope
|
||||
|
||||
The contracts domain covers `packages/contracts`.
|
||||
|
||||
## Rules
|
||||
|
||||
- Record transport-independent DTOs before binding them to one transport.
|
||||
- proto-socket is the first internal transport target.
|
||||
- REST exceptions must be documented.
|
||||
- gRPC is out of scope until explicitly added.
|
||||
- Contract notes must not include raw secrets or private credentials.
|
||||
|
||||
25
agent-ops/rules/project/domain/core/rules.md
Normal file
25
agent-ops/rules/project/domain/core/rules.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# core
|
||||
|
||||
## Scope
|
||||
|
||||
The core domain covers the Go control plane, worker, agent-shell command, core
|
||||
models, platformless Git engine, provider adapters, storage, migrations, and
|
||||
events.
|
||||
|
||||
## Included Paths
|
||||
|
||||
- `services/core/cmd/**`
|
||||
- `services/core/internal/**`
|
||||
- `services/core/migrations/**`
|
||||
- `services/core/go.mod`
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep provider-specific API details out of `internal/core`.
|
||||
- Keep GitHub/GitLab/Gitea/Plane/Jira calls behind `internal/provider`.
|
||||
- Keep platformless Git operations in `internal/gitengine`.
|
||||
- Agent command execution belongs in `internal/agentshell`.
|
||||
- Store durable state and audit trails in PostgreSQL first.
|
||||
- Redis, if added, is an acceleration layer and not the source of truth.
|
||||
- Large behavior changes require tests.
|
||||
|
||||
14
agent-ops/rules/project/domain/workspace-ops/rules.md
Normal file
14
agent-ops/rules/project/domain/workspace-ops/rules.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# workspace-ops
|
||||
|
||||
## Scope
|
||||
|
||||
Workspace operations cover root helpers, docs, agent rules, test rules, and
|
||||
repository metadata.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep helper commands simple and runnable from the repository root.
|
||||
- Do not commit local secret files.
|
||||
- Do not add generated build outputs.
|
||||
- Prefer documenting exceptions in `docs/` or `packages/contracts/notes/`.
|
||||
|
||||
52
agent-ops/rules/project/rules.md
Normal file
52
agent-ops/rules/project/rules.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Gito Project Rules
|
||||
|
||||
## Language
|
||||
|
||||
- User-facing final answers default to Korean.
|
||||
- Code identifiers, paths, commands, and commit scopes stay in the language used
|
||||
by the project.
|
||||
|
||||
## Project Summary
|
||||
|
||||
Gito is a platformless Git control plane for agent-driven development. It owns
|
||||
repo registry, workspace leases, Git operations, agent-shell execution,
|
||||
provider-neutral change requests, provider adapters, and normalized events.
|
||||
|
||||
## Main Structure
|
||||
|
||||
- `services/core/` - Go server, worker, shell, core domain, storage, adapters.
|
||||
- `apps/client/` - Flutter control surface.
|
||||
- `packages/contracts/` - proto-socket, REST, event, and DTO contract notes.
|
||||
- `bin/` - root test/lint/build helpers.
|
||||
- `docs/` - architecture and operation documents.
|
||||
- `agent-ops/` - agent rules.
|
||||
- `agent-test/` - local validation rules.
|
||||
|
||||
## Stack
|
||||
|
||||
- Backend: Go, PostgreSQL, optional Redis.
|
||||
- Client: Flutter/Dart.
|
||||
- Internal transport: proto-socket first.
|
||||
- REST: health/readiness, provider callbacks, smoke/curl, bootstrap.
|
||||
- gRPC: excluded from the initial design.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Prefer `bin/test`, `bin/lint`, and `bin/build` from the repository root.
|
||||
- Keep Git operations platformless unless a provider adapter is explicitly in
|
||||
scope.
|
||||
- Provider APIs belong behind adapter boundaries.
|
||||
- Agent-shell owns local command execution; control plane and core should not
|
||||
run arbitrary workspace commands directly.
|
||||
- Credential values must be referenced by `credential_ref` or local secret files,
|
||||
never stored in tracked docs.
|
||||
|
||||
## Domain Map
|
||||
|
||||
| Path | Domain | Rules |
|
||||
| --- | --- | --- |
|
||||
| `services/core/**` | core | `agent-ops/rules/project/domain/core/rules.md` |
|
||||
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||
| `packages/contracts/**` | contracts | `agent-ops/rules/project/domain/contracts/rules.md` |
|
||||
| `bin/**`, `docs/**`, `AGENTS.md`, `README.md`, `agent-ops/**`, `agent-test/**` | workspace-ops | `agent-ops/rules/project/domain/workspace-ops/rules.md` |
|
||||
|
||||
16
agent-test/local/rules.md
Normal file
16
agent-test/local/rules.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Local Test Rules
|
||||
|
||||
Read this file before validation work.
|
||||
|
||||
## Baseline
|
||||
|
||||
- Core: `cd services/core && go test ./...`
|
||||
- Go lint: `cd services/core && go vet ./...`
|
||||
- Root: `bin/test`, `bin/lint`, `bin/build`
|
||||
- Flutter: run `flutter test` and `flutter analyze --no-fatal-infos` when the
|
||||
Flutter toolchain is available and client changes are in scope.
|
||||
|
||||
## Reporting
|
||||
|
||||
Report executed commands, pass/fail result, skipped tools, and remaining risk.
|
||||
|
||||
8
apps/client/README.md
Normal file
8
apps/client/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Gito Client
|
||||
|
||||
Flutter control surface for Gito.
|
||||
|
||||
The first slice keeps the client scaffold light. It should consume Gito through
|
||||
proto-socket for operation requests, event streams, and log streams. REST is only
|
||||
for health/readiness and operational compatibility endpoints.
|
||||
|
||||
32
apps/client/lib/main.dart
Normal file
32
apps/client/lib/main.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const GitoClientApp());
|
||||
}
|
||||
|
||||
class GitoClientApp extends StatelessWidget {
|
||||
const GitoClientApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Gito',
|
||||
theme: ThemeData(colorSchemeSeed: Colors.teal),
|
||||
home: const GitoHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GitoHomePage extends StatelessWidget {
|
||||
const GitoHomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('Gito control surface'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
205
apps/client/pubspec.lock
Normal file
205
apps/client/pubspec.lock
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.0"
|
||||
sdks:
|
||||
dart: ">=3.9.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
20
apps/client/pubspec.yaml
Normal file
20
apps/client/pubspec.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
name: gito_client
|
||||
description: Flutter control surface for Gito.
|
||||
publish_to: none
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
sdk: ">=3.4.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
14
bin/build
Executable file
14
bin/build
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
OUT_DIR="$ROOT_DIR/bin"
|
||||
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-server" ./cmd/server)
|
||||
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-worker" ./cmd/worker)
|
||||
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-shell" ./cmd/shell)
|
||||
else
|
||||
echo "skip services/core: go not found"
|
||||
fi
|
||||
|
||||
17
bin/lint
Executable file
17
bin/lint
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
(cd "$ROOT_DIR/services/core" && go vet ./...)
|
||||
else
|
||||
echo "skip services/core: go not found"
|
||||
fi
|
||||
|
||||
if command -v flutter >/dev/null 2>&1 && [ -f "$ROOT_DIR/apps/client/pubspec.yaml" ]; then
|
||||
(cd "$ROOT_DIR/apps/client" && flutter analyze --no-fatal-infos)
|
||||
else
|
||||
echo "skip apps/client: flutter not found or app not generated"
|
||||
fi
|
||||
|
||||
16
bin/test
Executable file
16
bin/test
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
(cd "$ROOT_DIR/services/core" && go test ./...)
|
||||
else
|
||||
echo "skip services/core: go not found"
|
||||
fi
|
||||
|
||||
if command -v flutter >/dev/null 2>&1 && [ -f "$ROOT_DIR/apps/client/pubspec.yaml" ] && [ -d "$ROOT_DIR/apps/client/test" ]; then
|
||||
(cd "$ROOT_DIR/apps/client" && flutter test)
|
||||
else
|
||||
echo "skip apps/client: flutter not found or no test scaffold"
|
||||
fi
|
||||
68
docs/architecture.md
Normal file
68
docs/architecture.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Architecture
|
||||
|
||||
Gito starts as a modular monolith with separable process roles. The codebase can
|
||||
run as one service at first, but its boundaries should allow separate server,
|
||||
worker, and shell deployments later.
|
||||
|
||||
## Layers
|
||||
|
||||
### Control Plane
|
||||
|
||||
- Owns proto-socket and REST surfaces.
|
||||
- Accepts user, automation, and provider callback requests.
|
||||
- Applies auth, policy, and approval gates.
|
||||
- Creates operations and exposes event streams.
|
||||
- Does not execute Git commands directly.
|
||||
|
||||
### Core Domain
|
||||
|
||||
- Owns repository registry, workspace leases, operations, revision cursors,
|
||||
idempotency keys, audit records, and normalized events.
|
||||
- Knows provider-neutral concepts such as `Repo`, `WorkspaceLease`,
|
||||
`GitOperation`, `RevisionEvent`, and `ChangeRequest`.
|
||||
- Does not know GitHub, GitLab, Gitea, Plane, Jira, or IOP HTTP details.
|
||||
|
||||
### Worker
|
||||
|
||||
- Picks pending operations.
|
||||
- Coordinates workspace leases.
|
||||
- Calls agent-shell for filesystem and command execution.
|
||||
- Records operation outcomes and emits events.
|
||||
- Handles retry and backoff policy.
|
||||
|
||||
### Agent Shell
|
||||
|
||||
- Runs on a machine that owns local workspace access.
|
||||
- Executes Git CLI and IOP CLI commands.
|
||||
- Streams logs, handles cancellation, and enforces dirty-workspace guards.
|
||||
- Connects outbound to the control plane where possible.
|
||||
|
||||
### Git Engine
|
||||
|
||||
- Platformless Git operation layer.
|
||||
- Supports clone, fetch, pull, checkout, status, diff, commit, push, branch, tag,
|
||||
and revision scans.
|
||||
- Must be testable against local bare repositories without GitHub, GitLab, or
|
||||
Gitea.
|
||||
|
||||
### Provider Adapters
|
||||
|
||||
- Own platform APIs and DTOs.
|
||||
- Map GitHub PR, GitLab MR, and Gitea PR into provider-neutral
|
||||
`ChangeRequest` operations.
|
||||
- Treat webhooks as wakeup signals. Final state must be verified through Git
|
||||
revision or provider read APIs.
|
||||
|
||||
## Transport
|
||||
|
||||
- proto-socket is the default internal runtime transport.
|
||||
- REST is reserved for health/readiness, provider callbacks, smoke/curl, and
|
||||
simple bootstrap endpoints.
|
||||
- gRPC is intentionally out of scope for the initial architecture.
|
||||
|
||||
## Event Policy
|
||||
|
||||
All external side effects should produce durable operation and event records in
|
||||
PostgreSQL. Redis may be added later for fanout or stream acceleration, but it is
|
||||
not the source of truth.
|
||||
|
||||
8
packages/contracts/README.md
Normal file
8
packages/contracts/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Gito Contracts
|
||||
|
||||
This package records transport-independent contract candidates for Gito.
|
||||
|
||||
Use it for proto-socket channel/action notes, REST compatibility maps, event
|
||||
schemas, provider-neutral DTOs, and compatibility notes shared by the Go core and
|
||||
Flutter client.
|
||||
|
||||
100
packages/contracts/notes/gito-control-plane.md
Normal file
100
packages/contracts/notes/gito-control-plane.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Gito Control Plane Contract Candidates
|
||||
|
||||
## Transport Policy
|
||||
|
||||
- Internal runtime calls use proto-socket first.
|
||||
- REST remains for health/readiness, provider callbacks, smoke/curl, and simple
|
||||
bootstrap endpoints.
|
||||
- gRPC is excluded from the first design.
|
||||
|
||||
## proto-socket Channels
|
||||
|
||||
| Channel | Purpose |
|
||||
| --- | --- |
|
||||
| `repo` | Register, inspect, and list managed repositories. |
|
||||
| `workspace` | Lease, release, and inspect workspace slots. |
|
||||
| `operation` | Create, cancel, inspect, and stream operations. |
|
||||
| `git` | Request platformless Git operations. |
|
||||
| `change_request` | Manage provider-neutral PR/MR operations. |
|
||||
| `agent_shell` | Shell heartbeat, command dispatch, and log streaming. |
|
||||
| `event` | Subscribe to normalized events. |
|
||||
|
||||
## Core DTO Candidates
|
||||
|
||||
### Repo
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Stable Gito repo id. |
|
||||
| `name` | Display name. |
|
||||
| `remote_url` | Git remote URL. |
|
||||
| `default_branch` | Default source-of-truth branch. |
|
||||
| `workspace_root` | Local workspace root for slots. |
|
||||
| `credential_ref` | Reference to credentials, never the raw secret. |
|
||||
|
||||
### WorkspaceLease
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Lease id. |
|
||||
| `repo_id` | Managed repo id. |
|
||||
| `slot` | Zero-padded slot index. |
|
||||
| `path` | Local workspace path. |
|
||||
| `state` | `available`, `leased`, `dirty`, or `error`. |
|
||||
| `expires_at` | Optional lease expiry. |
|
||||
|
||||
### Operation
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Operation id. |
|
||||
| `repo_id` | Target repo. |
|
||||
| `type` | `clone`, `fetch`, `commit`, `push`, `agent_run`, etc. |
|
||||
| `state` | `queued`, `running`, `succeeded`, `failed`, `cancelled`. |
|
||||
| `idempotency_key` | Duplicate guard. |
|
||||
| `created_by` | Actor id or system source. |
|
||||
|
||||
### RevisionEvent
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `repo_id` | Target repo. |
|
||||
| `branch` | Branch name. |
|
||||
| `before` | Previous revision. |
|
||||
| `after` | New revision. |
|
||||
| `changed_files` | Changed file paths and change types. |
|
||||
| `observed_at` | Observation timestamp. |
|
||||
|
||||
### ChangeRequest
|
||||
|
||||
Provider-neutral abstraction for GitHub PR, GitLab MR, Gitea PR, and similar
|
||||
platform features.
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `provider` | `github`, `gitlab`, `gitea`, etc. |
|
||||
| `external_id` | Provider PR/MR id. |
|
||||
| `repo_id` | Target repo. |
|
||||
| `source_branch` | Source branch. |
|
||||
| `target_branch` | Target branch. |
|
||||
| `state` | `open`, `merged`, `closed`, `draft`, etc. |
|
||||
|
||||
## Normalized Events
|
||||
|
||||
| Event | Meaning |
|
||||
| --- | --- |
|
||||
| `repo.changed` | Repo registry changed. |
|
||||
| `branch.updated` | Branch revision changed. |
|
||||
| `workspace.leased` | Workspace lease acquired. |
|
||||
| `workspace.released` | Workspace lease released. |
|
||||
| `workspace.dirty` | Workspace has uncommitted changes. |
|
||||
| `operation.started` | Operation execution started. |
|
||||
| `operation.completed` | Operation succeeded. |
|
||||
| `operation.failed` | Operation failed. |
|
||||
| `agent.run.started` | Agent execution started. |
|
||||
| `agent.run.completed` | Agent execution completed. |
|
||||
| `change_request.opened` | PR/MR-like object opened. |
|
||||
| `change_request.updated` | PR/MR-like object changed. |
|
||||
| `change_request.merged` | PR/MR-like object merged. |
|
||||
| `provider.webhook.received` | Provider webhook received. |
|
||||
|
||||
26
services/core/cmd/server/main.go
Normal file
26
services/core/cmd/server/main.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/controlplane"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.HTTPAddr,
|
||||
Handler: controlplane.NewRouter(cfg, logger),
|
||||
}
|
||||
|
||||
logger.Info("gito control plane listening", "addr", cfg.HTTPAddr)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("server stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
20
services/core/cmd/shell/main.go
Normal file
20
services/core/cmd/shell/main.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/agentshell"
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
shell := agentshell.New(cfg, logger)
|
||||
if err := shell.Run(); err != nil {
|
||||
logger.Error("agent shell stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
20
services/core/cmd/worker/main.go
Normal file
20
services/core/cmd/worker/main.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/worker"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
|
||||
runner := worker.NewRunner(cfg, logger)
|
||||
if err := runner.Run(); err != nil {
|
||||
logger.Error("worker stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
3
services/core/go.mod
Normal file
3
services/core/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module git.toki-labs.com/toki/gito/services/core
|
||||
|
||||
go 1.25.0
|
||||
21
services/core/internal/agentshell/shell.go
Normal file
21
services/core/internal/agentshell/shell.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package agentshell
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
)
|
||||
|
||||
type Shell struct {
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func New(cfg config.Config, logger *slog.Logger) *Shell {
|
||||
return &Shell{cfg: cfg, logger: logger}
|
||||
}
|
||||
|
||||
func (s *Shell) Run() error {
|
||||
s.logger.Info("agent shell scaffold ready", "shell_id", s.cfg.ShellID)
|
||||
return nil
|
||||
}
|
||||
47
services/core/internal/config/config.go
Normal file
47
services/core/internal/config/config.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppEnv string
|
||||
HTTPAddr string
|
||||
DatabaseURL string
|
||||
RedisURL string
|
||||
ProtoSocketPath string
|
||||
WorkerEnabled bool
|
||||
ShellID string
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
return Config{
|
||||
AppEnv: getEnv("APP_ENV", "local"),
|
||||
HTTPAddr: getEnv("HTTP_ADDR", ":8080"),
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
RedisURL: os.Getenv("REDIS_URL"),
|
||||
ProtoSocketPath: getEnv("PROTO_SOCKET_PATH", "/proto-socket"),
|
||||
WorkerEnabled: getEnvBool("WORKER_ENABLED", true),
|
||||
ShellID: getEnv("GITO_SHELL_ID", "local-shell"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvBool(key string, fallback bool) bool {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
45
services/core/internal/config/config_test.go
Normal file
45
services/core/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
t.Setenv("APP_ENV", "")
|
||||
t.Setenv("HTTP_ADDR", "")
|
||||
t.Setenv("PROTO_SOCKET_PATH", "")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.AppEnv != "local" {
|
||||
t.Fatalf("AppEnv: got %q, want local", cfg.AppEnv)
|
||||
}
|
||||
if cfg.HTTPAddr != ":8080" {
|
||||
t.Fatalf("HTTPAddr: got %q, want :8080", cfg.HTTPAddr)
|
||||
}
|
||||
if cfg.ProtoSocketPath != "/proto-socket" {
|
||||
t.Fatalf("ProtoSocketPath: got %q, want /proto-socket", cfg.ProtoSocketPath)
|
||||
}
|
||||
if !cfg.WorkerEnabled {
|
||||
t.Fatal("WorkerEnabled default should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnvOverrides(t *testing.T) {
|
||||
t.Setenv("APP_ENV", "test")
|
||||
t.Setenv("HTTP_ADDR", ":18080")
|
||||
t.Setenv("PROTO_SOCKET_PATH", "/gito")
|
||||
t.Setenv("WORKER_ENABLED", "false")
|
||||
t.Setenv("GITO_SHELL_ID", "runner-1")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.AppEnv != "test" || cfg.HTTPAddr != ":18080" {
|
||||
t.Fatalf("unexpected config: %+v", cfg)
|
||||
}
|
||||
if cfg.ProtoSocketPath != "/gito" {
|
||||
t.Fatalf("ProtoSocketPath: got %q", cfg.ProtoSocketPath)
|
||||
}
|
||||
if cfg.WorkerEnabled {
|
||||
t.Fatal("WorkerEnabled should be false")
|
||||
}
|
||||
if cfg.ShellID != "runner-1" {
|
||||
t.Fatalf("ShellID: got %q", cfg.ShellID)
|
||||
}
|
||||
}
|
||||
35
services/core/internal/controlplane/router.go
Normal file
35
services/core/internal/controlplane/router.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
)
|
||||
|
||||
func NewRouter(cfg config.Config, logger *slog.Logger) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
})
|
||||
mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "ready",
|
||||
"env": cfg.AppEnv,
|
||||
})
|
||||
})
|
||||
mux.HandleFunc(cfg.ProtoSocketPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Info("proto-socket endpoint placeholder", "path", r.URL.Path)
|
||||
writeJSON(w, http.StatusNotImplemented, map[string]string{
|
||||
"error": "proto-socket endpoint is not implemented yet",
|
||||
})
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
42
services/core/internal/controlplane/router_test.go
Normal file
42
services/core/internal/controlplane/router_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package controlplane
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
)
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d", rec.Code)
|
||||
}
|
||||
var body map[string]string
|
||||
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if body["status"] != "ok" {
|
||||
t.Fatalf("body: %#v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtoSocketPlaceholder(t *testing.T) {
|
||||
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||
req := httptest.NewRequest(http.MethodGet, "/proto-socket", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("status: got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
84
services/core/internal/core/model.go
Normal file
84
services/core/internal/core/model.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package core
|
||||
|
||||
import "time"
|
||||
|
||||
type Repo struct {
|
||||
ID string
|
||||
Name string
|
||||
RemoteURL string
|
||||
DefaultBranch string
|
||||
WorkspaceRoot string
|
||||
CredentialRef string
|
||||
}
|
||||
|
||||
type WorkspaceLease struct {
|
||||
ID string
|
||||
RepoID string
|
||||
Slot string
|
||||
Path string
|
||||
State WorkspaceState
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
type WorkspaceState string
|
||||
|
||||
const (
|
||||
WorkspaceAvailable WorkspaceState = "available"
|
||||
WorkspaceLeased WorkspaceState = "leased"
|
||||
WorkspaceDirty WorkspaceState = "dirty"
|
||||
WorkspaceError WorkspaceState = "error"
|
||||
)
|
||||
|
||||
type Operation struct {
|
||||
ID string
|
||||
RepoID string
|
||||
Type OperationType
|
||||
State OperationState
|
||||
IdempotencyKey string
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type OperationType string
|
||||
|
||||
const (
|
||||
OperationClone OperationType = "clone"
|
||||
OperationFetch OperationType = "fetch"
|
||||
OperationCommit OperationType = "commit"
|
||||
OperationPush OperationType = "push"
|
||||
OperationAgentRun OperationType = "agent_run"
|
||||
)
|
||||
|
||||
type OperationState string
|
||||
|
||||
const (
|
||||
OperationQueued OperationState = "queued"
|
||||
OperationRunning OperationState = "running"
|
||||
OperationSucceeded OperationState = "succeeded"
|
||||
OperationFailed OperationState = "failed"
|
||||
OperationCancelled OperationState = "cancelled"
|
||||
)
|
||||
|
||||
type RevisionEvent struct {
|
||||
RepoID string
|
||||
Branch string
|
||||
Before string
|
||||
After string
|
||||
ChangedFiles []ChangedFile
|
||||
ObservedAt time.Time
|
||||
}
|
||||
|
||||
type ChangedFile struct {
|
||||
Path string
|
||||
ChangeType string
|
||||
}
|
||||
|
||||
type ChangeRequest struct {
|
||||
Provider string
|
||||
ExternalID string
|
||||
RepoID string
|
||||
SourceBranch string
|
||||
TargetBranch string
|
||||
State string
|
||||
}
|
||||
28
services/core/internal/events/events.go
Normal file
28
services/core/internal/events/events.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package events
|
||||
|
||||
import "time"
|
||||
|
||||
type Event struct {
|
||||
ID string
|
||||
Type string
|
||||
Subject string
|
||||
Payload []byte
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
RepoChanged = "repo.changed"
|
||||
BranchUpdated = "branch.updated"
|
||||
WorkspaceLeased = "workspace.leased"
|
||||
WorkspaceReleased = "workspace.released"
|
||||
WorkspaceDirty = "workspace.dirty"
|
||||
OperationStarted = "operation.started"
|
||||
OperationCompleted = "operation.completed"
|
||||
OperationFailed = "operation.failed"
|
||||
AgentRunStarted = "agent.run.started"
|
||||
AgentRunCompleted = "agent.run.completed"
|
||||
ChangeRequestOpened = "change_request.opened"
|
||||
ChangeRequestUpdated = "change_request.updated"
|
||||
ChangeRequestMerged = "change_request.merged"
|
||||
ProviderWebhook = "provider.webhook.received"
|
||||
)
|
||||
64
services/core/internal/gitengine/command.go
Normal file
64
services/core/internal/gitengine/command.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package gitengine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrInvalidGitInput = errors.New("invalid git input")
|
||||
|
||||
type CommandRunner interface {
|
||||
Run(workdir string, args ...string) (string, error)
|
||||
}
|
||||
|
||||
type CLI struct{}
|
||||
|
||||
func (CLI) Run(workdir string, args ...string) (string, error) {
|
||||
if len(args) == 0 {
|
||||
return "", ErrInvalidGitInput
|
||||
}
|
||||
cmd := exec.Command("git", args...)
|
||||
if strings.TrimSpace(workdir) != "" {
|
||||
cmd.Dir = workdir
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
type StatusResult struct {
|
||||
Clean bool
|
||||
Output string
|
||||
}
|
||||
|
||||
func Status(runner CommandRunner, workdir string) (StatusResult, error) {
|
||||
output, err := runner.Run(workdir, "status", "--porcelain")
|
||||
if err != nil {
|
||||
return StatusResult{}, err
|
||||
}
|
||||
return StatusResult{
|
||||
Clean: strings.TrimSpace(output) == "",
|
||||
Output: output,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ChangedFiles(runner CommandRunner, workdir, before, after string) ([]string, error) {
|
||||
before = strings.TrimSpace(before)
|
||||
after = strings.TrimSpace(after)
|
||||
if before == "" || after == "" {
|
||||
return nil, ErrInvalidGitInput
|
||||
}
|
||||
output, err := runner.Run(workdir, "diff", "--name-only", before+".."+after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
files := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
51
services/core/internal/gitengine/command_test.go
Normal file
51
services/core/internal/gitengine/command_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package gitengine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeRunner struct {
|
||||
output string
|
||||
err error
|
||||
args []string
|
||||
}
|
||||
|
||||
func (f *fakeRunner) Run(_ string, args ...string) (string, error) {
|
||||
f.args = append([]string(nil), args...)
|
||||
return f.output, f.err
|
||||
}
|
||||
|
||||
func TestStatusClean(t *testing.T) {
|
||||
runner := &fakeRunner{output: "\n"}
|
||||
result, err := Status(runner, "/repo")
|
||||
if err != nil {
|
||||
t.Fatalf("Status: %v", err)
|
||||
}
|
||||
if !result.Clean {
|
||||
t.Fatal("expected clean status")
|
||||
}
|
||||
if !reflect.DeepEqual(runner.args, []string{"status", "--porcelain"}) {
|
||||
t.Fatalf("args: %#v", runner.args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangedFiles(t *testing.T) {
|
||||
runner := &fakeRunner{output: "README.md\nservices/core/main.go\n\n"}
|
||||
files, err := ChangedFiles(runner, "/repo", "abc", "def")
|
||||
if err != nil {
|
||||
t.Fatalf("ChangedFiles: %v", err)
|
||||
}
|
||||
want := []string{"README.md", "services/core/main.go"}
|
||||
if !reflect.DeepEqual(files, want) {
|
||||
t.Fatalf("files: got %#v want %#v", files, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangedFilesRequiresRevisions(t *testing.T) {
|
||||
_, err := ChangedFiles(&fakeRunner{}, "/repo", "", "def")
|
||||
if !errors.Is(err, ErrInvalidGitInput) {
|
||||
t.Fatalf("expected ErrInvalidGitInput, got %v", err)
|
||||
}
|
||||
}
|
||||
21
services/core/internal/provider/provider.go
Normal file
21
services/core/internal/provider/provider.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/core"
|
||||
)
|
||||
|
||||
type ChangeRequestAdapter interface {
|
||||
CreateChangeRequest(ctx context.Context, input core.ChangeRequest) (core.ChangeRequest, error)
|
||||
UpdateChangeRequest(ctx context.Context, input core.ChangeRequest) (core.ChangeRequest, error)
|
||||
CommentChangeRequest(ctx context.Context, id string, body string) error
|
||||
CloseChangeRequest(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type WebhookEvent struct {
|
||||
Provider string
|
||||
EventType string
|
||||
ExternalID string
|
||||
Payload []byte
|
||||
}
|
||||
14
services/core/internal/storage/storage.go
Normal file
14
services/core/internal/storage/storage.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package storage
|
||||
|
||||
import "context"
|
||||
|
||||
type Store struct{}
|
||||
|
||||
func New() *Store {
|
||||
return &Store{}
|
||||
}
|
||||
|
||||
func (s *Store) Ping(ctx context.Context) error {
|
||||
_ = ctx
|
||||
return nil
|
||||
}
|
||||
25
services/core/internal/worker/runner.go
Normal file
25
services/core/internal/worker/runner.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
cfg config.Config
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewRunner(cfg config.Config, logger *slog.Logger) *Runner {
|
||||
return &Runner{cfg: cfg, logger: logger}
|
||||
}
|
||||
|
||||
func (r *Runner) Run() error {
|
||||
if !r.cfg.WorkerEnabled {
|
||||
r.logger.Info("worker disabled")
|
||||
return nil
|
||||
}
|
||||
r.logger.Info("worker scaffold ready")
|
||||
return nil
|
||||
}
|
||||
67
services/core/migrations/00001_initial.sql
Normal file
67
services/core/migrations/00001_initial.sql
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE IF NOT EXISTS repos (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
default_branch TEXT NOT NULL,
|
||||
workspace_root TEXT NOT NULL,
|
||||
credential_ref TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_leases (
|
||||
id TEXT PRIMARY KEY,
|
||||
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||
slot TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (repo_id, slot)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS operations (
|
||||
id TEXT PRIMARY KEY,
|
||||
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||
type TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
idempotency_key TEXT,
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_operations_idempotency
|
||||
ON operations (idempotency_key)
|
||||
WHERE idempotency_key IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS operation_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
operation_id TEXT REFERENCES operations(id),
|
||||
type TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS revision_cursors (
|
||||
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||
branch TEXT NOT NULL,
|
||||
revision TEXT NOT NULL,
|
||||
observed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (repo_id, branch)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS revision_cursors;
|
||||
DROP TABLE IF EXISTS operation_events;
|
||||
DROP TABLE IF EXISTS operations;
|
||||
DROP TABLE IF EXISTS workspace_leases;
|
||||
DROP TABLE IF EXISTS repos;
|
||||
-- +goose StatementEnd
|
||||
|
||||
Loading…
Reference in a new issue