solokit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- solokit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git workflow integration for Session-Driven Development.
|
|
4
|
+
|
|
5
|
+
Handles:
|
|
6
|
+
- Branch creation for work items
|
|
7
|
+
- Branch continuation for multi-session work
|
|
8
|
+
- Commit generation
|
|
9
|
+
- Push to remote
|
|
10
|
+
- Branch merging (local or PR-based)
|
|
11
|
+
- PR creation and management
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from solokit.core.command_runner import CommandRunner
|
|
22
|
+
from solokit.core.config import get_config_manager
|
|
23
|
+
from solokit.core.constants import (
|
|
24
|
+
GIT_LONG_TIMEOUT,
|
|
25
|
+
GIT_QUICK_TIMEOUT,
|
|
26
|
+
GIT_STANDARD_TIMEOUT,
|
|
27
|
+
get_config_file,
|
|
28
|
+
get_work_items_file,
|
|
29
|
+
)
|
|
30
|
+
from solokit.core.error_handlers import convert_subprocess_errors
|
|
31
|
+
from solokit.core.exceptions import (
|
|
32
|
+
CommandExecutionError,
|
|
33
|
+
ErrorCode,
|
|
34
|
+
FileOperationError,
|
|
35
|
+
GitError,
|
|
36
|
+
NotAGitRepoError,
|
|
37
|
+
WorkingDirNotCleanError,
|
|
38
|
+
)
|
|
39
|
+
from solokit.core.types import GitStatus, WorkItemStatus, WorkItemType
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GitWorkflow:
|
|
45
|
+
"""Manage git workflow for sessions."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, project_root: Path | None = None) -> None:
|
|
48
|
+
"""Initialize GitWorkflow with project root path."""
|
|
49
|
+
self.project_root = project_root or Path.cwd()
|
|
50
|
+
self.work_items_file = get_work_items_file(self.project_root)
|
|
51
|
+
self.config_file = get_config_file(self.project_root)
|
|
52
|
+
|
|
53
|
+
# Use ConfigManager for centralized config management
|
|
54
|
+
config_manager = get_config_manager()
|
|
55
|
+
config_manager.load_config(self.config_file)
|
|
56
|
+
self.config = config_manager.git_workflow
|
|
57
|
+
|
|
58
|
+
# Initialize command runner
|
|
59
|
+
self.runner = CommandRunner(default_timeout=GIT_LONG_TIMEOUT, working_dir=self.project_root)
|
|
60
|
+
|
|
61
|
+
@convert_subprocess_errors
|
|
62
|
+
def check_git_status(self) -> None:
|
|
63
|
+
"""Check if working directory is clean.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
NotAGitRepoError: If not in a git repository
|
|
67
|
+
WorkingDirNotCleanError: If working directory has uncommitted changes
|
|
68
|
+
CommandExecutionError: If git command times out or fails
|
|
69
|
+
"""
|
|
70
|
+
result = self.runner.run(["git", "status", "--porcelain"], timeout=GIT_QUICK_TIMEOUT)
|
|
71
|
+
|
|
72
|
+
if not result.success:
|
|
73
|
+
if result.timed_out:
|
|
74
|
+
raise CommandExecutionError("git status", -1, "Command timed out")
|
|
75
|
+
raise NotAGitRepoError("Not a git repository")
|
|
76
|
+
|
|
77
|
+
if result.stdout.strip():
|
|
78
|
+
changes = result.stdout.strip().split("\n")
|
|
79
|
+
raise WorkingDirNotCleanError(changes=changes)
|
|
80
|
+
|
|
81
|
+
def get_current_branch(self) -> str | None:
|
|
82
|
+
"""Get current git branch name."""
|
|
83
|
+
result = self.runner.run(["git", "branch", "--show-current"], timeout=GIT_QUICK_TIMEOUT)
|
|
84
|
+
return result.stdout.strip() if result.success else None
|
|
85
|
+
|
|
86
|
+
@convert_subprocess_errors
|
|
87
|
+
def create_branch(self, work_item_id: str, session_num: int) -> tuple[str, str | None]:
|
|
88
|
+
"""Create a new branch for work item.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
work_item_id: ID of the work item
|
|
92
|
+
session_num: Session number
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Tuple of (branch_name, parent_branch)
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
GitError: If branch creation fails
|
|
99
|
+
CommandExecutionError: If git command fails
|
|
100
|
+
"""
|
|
101
|
+
# Capture parent branch BEFORE creating new branch
|
|
102
|
+
parent_branch = self.get_current_branch()
|
|
103
|
+
branch_name = work_item_id
|
|
104
|
+
|
|
105
|
+
# Create and checkout branch
|
|
106
|
+
result = self.runner.run(["git", "checkout", "-b", branch_name], timeout=GIT_QUICK_TIMEOUT)
|
|
107
|
+
|
|
108
|
+
if not result.success:
|
|
109
|
+
raise GitError(
|
|
110
|
+
f"Failed to create branch: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return branch_name, parent_branch
|
|
114
|
+
|
|
115
|
+
@convert_subprocess_errors
|
|
116
|
+
def checkout_branch(self, branch_name: str) -> None:
|
|
117
|
+
"""Checkout existing branch.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
branch_name: Name of the branch to checkout
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
GitError: If checkout fails
|
|
124
|
+
CommandExecutionError: If git command fails
|
|
125
|
+
"""
|
|
126
|
+
result = self.runner.run(["git", "checkout", branch_name], timeout=GIT_QUICK_TIMEOUT)
|
|
127
|
+
|
|
128
|
+
if not result.success:
|
|
129
|
+
raise GitError(
|
|
130
|
+
f"Failed to checkout branch: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@convert_subprocess_errors
|
|
134
|
+
def commit_changes(self, message: str) -> str:
|
|
135
|
+
"""Stage all changes and commit.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
message: Commit message
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Short commit SHA (7 characters)
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
GitError: If staging or commit fails
|
|
145
|
+
CommandExecutionError: If git command fails
|
|
146
|
+
"""
|
|
147
|
+
# Stage all changes
|
|
148
|
+
stage_result = self.runner.run(["git", "add", "."], timeout=GIT_STANDARD_TIMEOUT)
|
|
149
|
+
if not stage_result.success:
|
|
150
|
+
raise GitError(f"Staging failed: {stage_result.stderr}", ErrorCode.GIT_COMMAND_FAILED)
|
|
151
|
+
|
|
152
|
+
# Commit
|
|
153
|
+
result = self.runner.run(["git", "commit", "-m", message], timeout=GIT_STANDARD_TIMEOUT)
|
|
154
|
+
|
|
155
|
+
if not result.success:
|
|
156
|
+
raise GitError(f"Commit failed: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED)
|
|
157
|
+
|
|
158
|
+
# Get commit SHA
|
|
159
|
+
sha_result = self.runner.run(["git", "rev-parse", "HEAD"], timeout=GIT_QUICK_TIMEOUT)
|
|
160
|
+
commit_sha = sha_result.stdout.strip()[:7] if sha_result.success else "unknown"
|
|
161
|
+
return commit_sha
|
|
162
|
+
|
|
163
|
+
@convert_subprocess_errors
|
|
164
|
+
def push_branch(self, branch_name: str) -> None:
|
|
165
|
+
"""Push branch to remote.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
branch_name: Name of the branch to push
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
GitError: If push fails (ignores no remote/upstream errors)
|
|
172
|
+
CommandExecutionError: If git command fails
|
|
173
|
+
"""
|
|
174
|
+
result = self.runner.run(
|
|
175
|
+
["git", "push", "-u", "origin", branch_name], timeout=GIT_LONG_TIMEOUT
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if not result.success:
|
|
179
|
+
# Check if it's just "no upstream" error - not a real error
|
|
180
|
+
if "no upstream" in result.stderr.lower() or "no remote" in result.stderr.lower():
|
|
181
|
+
logger.info("No remote configured (local only)")
|
|
182
|
+
return
|
|
183
|
+
raise GitError(f"Push failed: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED)
|
|
184
|
+
|
|
185
|
+
@convert_subprocess_errors
|
|
186
|
+
def delete_remote_branch(self, branch_name: str) -> None:
|
|
187
|
+
"""Delete branch from remote.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
branch_name: Name of the remote branch to delete
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
GitError: If deletion fails (ignores already deleted branches)
|
|
194
|
+
CommandExecutionError: If git command fails
|
|
195
|
+
"""
|
|
196
|
+
result = self.runner.run(
|
|
197
|
+
["git", "push", "origin", "--delete", branch_name], timeout=GIT_STANDARD_TIMEOUT
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not result.success:
|
|
201
|
+
# Not an error if branch doesn't exist on remote
|
|
202
|
+
if "remote ref does not exist" in result.stderr.lower():
|
|
203
|
+
logger.info(f"Remote branch {branch_name} doesn't exist (already deleted)")
|
|
204
|
+
return
|
|
205
|
+
raise GitError(
|
|
206
|
+
f"Failed to delete remote branch: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@convert_subprocess_errors
|
|
210
|
+
def push_main_to_remote(self, branch_name: str = "main") -> None:
|
|
211
|
+
"""Push main (or other parent branch) to remote after local merge.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
branch_name: Name of the branch to push (default: main)
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
GitError: If push fails
|
|
218
|
+
CommandExecutionError: If git command fails
|
|
219
|
+
"""
|
|
220
|
+
result = self.runner.run(["git", "push", "origin", branch_name], timeout=GIT_LONG_TIMEOUT)
|
|
221
|
+
|
|
222
|
+
if not result.success:
|
|
223
|
+
raise GitError(
|
|
224
|
+
f"Failed to push {branch_name}: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@convert_subprocess_errors
|
|
228
|
+
def create_pull_request(
|
|
229
|
+
self, work_item_id: str, branch_name: str, work_item: dict, session_num: int
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Create a pull request using gh CLI.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
work_item_id: ID of the work item
|
|
235
|
+
branch_name: Name of the branch
|
|
236
|
+
work_item: Work item data
|
|
237
|
+
session_num: Session number
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
PR URL
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
GitError: If PR creation fails or gh CLI not installed
|
|
244
|
+
CommandExecutionError: If gh command fails
|
|
245
|
+
"""
|
|
246
|
+
# Check if gh CLI is available
|
|
247
|
+
check_gh = self.runner.run(["gh", "--version"], timeout=GIT_QUICK_TIMEOUT)
|
|
248
|
+
|
|
249
|
+
if not check_gh.success:
|
|
250
|
+
raise GitError(
|
|
251
|
+
"gh CLI not installed. Install from: https://cli.github.com/",
|
|
252
|
+
ErrorCode.GIT_COMMAND_FAILED,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Generate PR title and body from templates
|
|
256
|
+
title = self._format_pr_title(work_item, session_num)
|
|
257
|
+
body = self._format_pr_body(work_item, work_item_id, session_num)
|
|
258
|
+
|
|
259
|
+
# Create PR using gh CLI
|
|
260
|
+
result = self.runner.run(
|
|
261
|
+
["gh", "pr", "create", "--title", title, "--body", body], timeout=GIT_LONG_TIMEOUT
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if not result.success:
|
|
265
|
+
raise GitError(f"Failed to create PR: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED)
|
|
266
|
+
|
|
267
|
+
return result.stdout.strip()
|
|
268
|
+
|
|
269
|
+
def _format_pr_title(self, work_item: dict, session_num: int) -> str:
|
|
270
|
+
"""Format PR title from template."""
|
|
271
|
+
template = self.config.pr_title_template
|
|
272
|
+
return template.format(
|
|
273
|
+
type=work_item.get("type", WorkItemType.FEATURE.value).title(),
|
|
274
|
+
title=work_item.get("title", "Work Item"),
|
|
275
|
+
work_item_id=work_item.get("id", "unknown"),
|
|
276
|
+
session_num=session_num,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _format_pr_body(self, work_item: dict, work_item_id: str, session_num: int) -> str:
|
|
280
|
+
"""Format PR body from template."""
|
|
281
|
+
template = self.config.pr_body_template
|
|
282
|
+
|
|
283
|
+
# Get recent commits for this work item
|
|
284
|
+
commit_messages = ""
|
|
285
|
+
if "git" in work_item and "commits" in work_item["git"]:
|
|
286
|
+
commits = work_item["git"]["commits"]
|
|
287
|
+
if commits:
|
|
288
|
+
commit_messages = "\n".join([f"- {c}" for c in commits])
|
|
289
|
+
|
|
290
|
+
return template.format(
|
|
291
|
+
work_item_id=work_item_id,
|
|
292
|
+
type=work_item.get("type", WorkItemType.FEATURE.value),
|
|
293
|
+
title=work_item.get("title", ""),
|
|
294
|
+
description=work_item.get("description", ""),
|
|
295
|
+
session_num=session_num,
|
|
296
|
+
commit_messages=commit_messages if commit_messages else "See commits for details",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@convert_subprocess_errors
|
|
300
|
+
def merge_to_parent(self, branch_name: str, parent_branch: str = "main") -> None:
|
|
301
|
+
"""Merge branch to parent branch and delete branch.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
branch_name: Name of the branch to merge
|
|
305
|
+
parent_branch: Name of the parent branch to merge into (default: main)
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
GitError: If checkout, merge, or delete fails
|
|
309
|
+
CommandExecutionError: If git command fails
|
|
310
|
+
"""
|
|
311
|
+
# Checkout parent branch (not hardcoded main)
|
|
312
|
+
checkout_result = self.runner.run(
|
|
313
|
+
["git", "checkout", parent_branch], timeout=GIT_QUICK_TIMEOUT
|
|
314
|
+
)
|
|
315
|
+
if not checkout_result.success:
|
|
316
|
+
raise GitError(
|
|
317
|
+
f"Failed to checkout {parent_branch}: {checkout_result.stderr}",
|
|
318
|
+
ErrorCode.GIT_COMMAND_FAILED,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Merge
|
|
322
|
+
result = self.runner.run(
|
|
323
|
+
["git", "merge", "--no-ff", branch_name], timeout=GIT_STANDARD_TIMEOUT
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if not result.success:
|
|
327
|
+
raise GitError(f"Merge failed: {result.stderr}", ErrorCode.GIT_COMMAND_FAILED)
|
|
328
|
+
|
|
329
|
+
# Delete branch
|
|
330
|
+
self.runner.run(["git", "branch", "-d", branch_name], timeout=GIT_QUICK_TIMEOUT)
|
|
331
|
+
|
|
332
|
+
def start_work_item(self, work_item_id: str, session_num: int) -> dict:
|
|
333
|
+
"""Start working on a work item (create or resume branch).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
work_item_id: ID of the work item
|
|
337
|
+
session_num: Session number
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Dictionary with action, branch, success, and message
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
FileOperationError: If work items file cannot be read/written
|
|
344
|
+
GitError: If branch operations fail
|
|
345
|
+
"""
|
|
346
|
+
# Load work items
|
|
347
|
+
try:
|
|
348
|
+
with open(self.work_items_file) as f:
|
|
349
|
+
data = json.load(f)
|
|
350
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
351
|
+
raise FileOperationError(
|
|
352
|
+
operation="read",
|
|
353
|
+
file_path=str(self.work_items_file),
|
|
354
|
+
details=f"Failed to load work items: {e}",
|
|
355
|
+
cause=e,
|
|
356
|
+
) from e
|
|
357
|
+
|
|
358
|
+
work_item = data["work_items"][work_item_id]
|
|
359
|
+
|
|
360
|
+
# Check if work item already has a branch
|
|
361
|
+
if "git" in work_item and work_item["git"].get("status") == GitStatus.IN_PROGRESS.value:
|
|
362
|
+
# Resume existing branch
|
|
363
|
+
branch_name = work_item["git"]["branch"]
|
|
364
|
+
try:
|
|
365
|
+
self.checkout_branch(branch_name)
|
|
366
|
+
return {
|
|
367
|
+
"action": "resumed",
|
|
368
|
+
"branch": branch_name,
|
|
369
|
+
"success": True,
|
|
370
|
+
"message": f"Switched to branch {branch_name}",
|
|
371
|
+
}
|
|
372
|
+
except GitError as e:
|
|
373
|
+
return {
|
|
374
|
+
"action": "resumed",
|
|
375
|
+
"branch": branch_name,
|
|
376
|
+
"success": False,
|
|
377
|
+
"message": str(e),
|
|
378
|
+
}
|
|
379
|
+
else:
|
|
380
|
+
# Create new branch
|
|
381
|
+
try:
|
|
382
|
+
branch_name, parent_branch = self.create_branch(work_item_id, session_num)
|
|
383
|
+
|
|
384
|
+
# Update work item with git info (including parent branch)
|
|
385
|
+
work_item["git"] = {
|
|
386
|
+
"branch": branch_name,
|
|
387
|
+
"parent_branch": parent_branch, # Store parent for merging
|
|
388
|
+
"created_at": datetime.now().isoformat(),
|
|
389
|
+
"status": GitStatus.IN_PROGRESS.value,
|
|
390
|
+
"commits": [],
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Save updated work items
|
|
394
|
+
try:
|
|
395
|
+
with open(self.work_items_file, "w") as f:
|
|
396
|
+
json.dump(data, f, indent=2)
|
|
397
|
+
except OSError as e:
|
|
398
|
+
raise FileOperationError(
|
|
399
|
+
operation="write",
|
|
400
|
+
file_path=str(self.work_items_file),
|
|
401
|
+
details=f"Failed to save work items: {e}",
|
|
402
|
+
cause=e,
|
|
403
|
+
) from e
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
"action": "created",
|
|
407
|
+
"branch": branch_name,
|
|
408
|
+
"success": True,
|
|
409
|
+
"message": branch_name,
|
|
410
|
+
}
|
|
411
|
+
except GitError as e:
|
|
412
|
+
return {
|
|
413
|
+
"action": "created",
|
|
414
|
+
"branch": "",
|
|
415
|
+
"success": False,
|
|
416
|
+
"message": str(e),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
def complete_work_item(
|
|
420
|
+
self, work_item_id: str, commit_message: str, merge: bool = False, session_num: int = 1
|
|
421
|
+
) -> dict:
|
|
422
|
+
"""Complete work on a work item (commit, push, optionally merge or create PR).
|
|
423
|
+
|
|
424
|
+
Behavior depends on git_workflow.mode config:
|
|
425
|
+
- "pr": Commit, push, create pull request (no local merge)
|
|
426
|
+
- "local": Commit, push, merge locally, push main, delete remote branch
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
work_item_id: ID of the work item
|
|
430
|
+
commit_message: Commit message
|
|
431
|
+
merge: Whether to merge/create PR
|
|
432
|
+
session_num: Session number
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Dictionary with success, commit, pushed, and message
|
|
436
|
+
|
|
437
|
+
Raises:
|
|
438
|
+
FileOperationError: If work items file cannot be read/written
|
|
439
|
+
"""
|
|
440
|
+
# Load work items
|
|
441
|
+
try:
|
|
442
|
+
with open(self.work_items_file) as f:
|
|
443
|
+
data = json.load(f)
|
|
444
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
445
|
+
raise FileOperationError(
|
|
446
|
+
operation="read",
|
|
447
|
+
file_path=str(self.work_items_file),
|
|
448
|
+
details=f"Failed to load work items: {e}",
|
|
449
|
+
cause=e,
|
|
450
|
+
) from e
|
|
451
|
+
|
|
452
|
+
work_item = data["work_items"][work_item_id]
|
|
453
|
+
|
|
454
|
+
if "git" not in work_item:
|
|
455
|
+
return {
|
|
456
|
+
"success": False,
|
|
457
|
+
"message": "Work item has no git tracking (may be single-session item)",
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
branch_name = work_item["git"]["branch"]
|
|
461
|
+
workflow_mode = self.config.mode
|
|
462
|
+
|
|
463
|
+
# Step 1: Commit changes (if there are any)
|
|
464
|
+
try:
|
|
465
|
+
commit_sha = self.commit_changes(commit_message)
|
|
466
|
+
# Update work item commits with the new commit
|
|
467
|
+
if commit_sha not in work_item["git"]["commits"]:
|
|
468
|
+
work_item["git"]["commits"].append(commit_sha)
|
|
469
|
+
except GitError as e:
|
|
470
|
+
# If commit fails because there's nothing to commit, extract existing commits
|
|
471
|
+
if "nothing to commit" in str(e).lower():
|
|
472
|
+
# No new changes to commit - extract existing commits from git log
|
|
473
|
+
result = self.runner.run(
|
|
474
|
+
[
|
|
475
|
+
"git",
|
|
476
|
+
"log",
|
|
477
|
+
"--format=%h",
|
|
478
|
+
branch_name,
|
|
479
|
+
f"^{work_item['git'].get('parent_branch', 'main')}",
|
|
480
|
+
],
|
|
481
|
+
timeout=GIT_STANDARD_TIMEOUT,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if result.success and result.stdout.strip():
|
|
485
|
+
existing_commits = result.stdout.strip().split("\n")
|
|
486
|
+
# Update commits array with any new commits not already tracked
|
|
487
|
+
for commit in reversed(existing_commits): # Oldest to newest
|
|
488
|
+
if commit not in work_item["git"]["commits"]:
|
|
489
|
+
work_item["git"]["commits"].append(commit)
|
|
490
|
+
|
|
491
|
+
# If we found commits, treat this as success
|
|
492
|
+
if existing_commits:
|
|
493
|
+
commit_sha = f"Found {len(existing_commits)} existing commit(s)"
|
|
494
|
+
else:
|
|
495
|
+
return {"success": False, "message": "No commits found for this work item"}
|
|
496
|
+
else:
|
|
497
|
+
return {
|
|
498
|
+
"success": False,
|
|
499
|
+
"message": f"Failed to retrieve commits: {str(e)}",
|
|
500
|
+
}
|
|
501
|
+
else:
|
|
502
|
+
return {"success": False, "message": f"Commit failed: {str(e)}"}
|
|
503
|
+
|
|
504
|
+
# Step 2: Push to remote (if enabled)
|
|
505
|
+
push_success = True
|
|
506
|
+
try:
|
|
507
|
+
self.push_branch(branch_name)
|
|
508
|
+
except GitError as e:
|
|
509
|
+
push_success = False
|
|
510
|
+
logger.warning(f"Push failed: {e}")
|
|
511
|
+
|
|
512
|
+
# Step 3: Handle completion based on workflow mode
|
|
513
|
+
if merge and work_item["status"] == WorkItemStatus.COMPLETED.value:
|
|
514
|
+
if workflow_mode == "pr":
|
|
515
|
+
# PR Mode: Create pull request (no local merge)
|
|
516
|
+
pr_success = False
|
|
517
|
+
pr_msg = "PR creation skipped (auto_create_pr disabled)"
|
|
518
|
+
|
|
519
|
+
if self.config.auto_create_pr:
|
|
520
|
+
try:
|
|
521
|
+
pr_url = self.create_pull_request(
|
|
522
|
+
work_item_id, branch_name, work_item, session_num
|
|
523
|
+
)
|
|
524
|
+
pr_success = True
|
|
525
|
+
pr_msg = f"PR created: {pr_url}"
|
|
526
|
+
except GitError as e:
|
|
527
|
+
pr_msg = str(e)
|
|
528
|
+
|
|
529
|
+
if pr_success:
|
|
530
|
+
work_item["git"]["status"] = GitStatus.PR_CREATED.value
|
|
531
|
+
work_item["git"]["pr_url"] = pr_msg.split(": ")[-1] if ": " in pr_msg else ""
|
|
532
|
+
else:
|
|
533
|
+
work_item["git"]["status"] = GitStatus.READY_FOR_PR.value
|
|
534
|
+
|
|
535
|
+
message = f"Committed {commit_sha}, Pushed to remote. {pr_msg}"
|
|
536
|
+
|
|
537
|
+
else:
|
|
538
|
+
# Local Mode: Merge locally, push main, delete remote branch
|
|
539
|
+
parent_branch = work_item["git"].get("parent_branch", "main")
|
|
540
|
+
|
|
541
|
+
# Merge locally
|
|
542
|
+
try:
|
|
543
|
+
self.merge_to_parent(branch_name, parent_branch)
|
|
544
|
+
merge_success = True
|
|
545
|
+
merge_msg = f"Merged to {parent_branch} and branch deleted"
|
|
546
|
+
except GitError as e:
|
|
547
|
+
merge_success = False
|
|
548
|
+
merge_msg = str(e)
|
|
549
|
+
|
|
550
|
+
if merge_success:
|
|
551
|
+
# Push merged main to remote
|
|
552
|
+
try:
|
|
553
|
+
self.push_main_to_remote(parent_branch)
|
|
554
|
+
push_main_msg = f"Pushed {parent_branch} to remote"
|
|
555
|
+
except GitError as e:
|
|
556
|
+
push_main_msg = f"Failed to push {parent_branch}: {e}"
|
|
557
|
+
|
|
558
|
+
# Delete remote branch if configured
|
|
559
|
+
if self.config.delete_branch_after_merge:
|
|
560
|
+
try:
|
|
561
|
+
self.delete_remote_branch(branch_name)
|
|
562
|
+
delete_msg = f"Deleted remote branch {branch_name}"
|
|
563
|
+
except GitError as e:
|
|
564
|
+
delete_msg = f"Failed to delete remote branch: {e}"
|
|
565
|
+
else:
|
|
566
|
+
delete_msg = "Remote branch kept (delete_branch_after_merge disabled)"
|
|
567
|
+
|
|
568
|
+
work_item["git"]["status"] = GitStatus.MERGED.value
|
|
569
|
+
message = f"Committed {commit_sha}, {merge_msg}, {push_main_msg}, {delete_msg}"
|
|
570
|
+
else:
|
|
571
|
+
work_item["git"]["status"] = GitStatus.READY_TO_MERGE.value
|
|
572
|
+
message = f"Committed {commit_sha}, {merge_msg} - Manual merge required"
|
|
573
|
+
else:
|
|
574
|
+
# Work not complete or merge not requested
|
|
575
|
+
work_item["git"]["status"] = (
|
|
576
|
+
GitStatus.READY_TO_MERGE.value
|
|
577
|
+
if work_item["status"] == WorkItemStatus.COMPLETED.value
|
|
578
|
+
else GitStatus.IN_PROGRESS.value
|
|
579
|
+
)
|
|
580
|
+
push_msg = "Pushed to remote" if push_success else "Push failed"
|
|
581
|
+
message = f"Committed {commit_sha}, {push_msg}"
|
|
582
|
+
|
|
583
|
+
# Save updated work items
|
|
584
|
+
try:
|
|
585
|
+
with open(self.work_items_file, "w") as f:
|
|
586
|
+
json.dump(data, f, indent=2)
|
|
587
|
+
except OSError as e:
|
|
588
|
+
raise FileOperationError(
|
|
589
|
+
operation="write",
|
|
590
|
+
file_path=str(self.work_items_file),
|
|
591
|
+
details=f"Failed to save work items: {e}",
|
|
592
|
+
cause=e,
|
|
593
|
+
) from e
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
"success": True,
|
|
597
|
+
"commit": commit_sha,
|
|
598
|
+
"pushed": push_success,
|
|
599
|
+
"message": message,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def main() -> None:
|
|
604
|
+
"""CLI entry point for testing."""
|
|
605
|
+
workflow = GitWorkflow()
|
|
606
|
+
|
|
607
|
+
# Check status
|
|
608
|
+
try:
|
|
609
|
+
workflow.check_git_status()
|
|
610
|
+
logger.info("Git status: Clean")
|
|
611
|
+
except (NotAGitRepoError, WorkingDirNotCleanError, CommandExecutionError) as e:
|
|
612
|
+
logger.error(f"Git status: {e}")
|
|
613
|
+
|
|
614
|
+
current_branch = workflow.get_current_branch()
|
|
615
|
+
logger.info(f"Current branch: {current_branch}")
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
if __name__ == "__main__":
|
|
619
|
+
main()
|
solokit/init/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solokit Init Module
|
|
3
|
+
|
|
4
|
+
This module contains deterministic scripts for template-based project initialization.
|
|
5
|
+
Each script handles a specific phase of the initialization process.
|
|
6
|
+
|
|
7
|
+
Module Organization:
|
|
8
|
+
- git_setup.py: Git initialization and verification
|
|
9
|
+
- environment_validator.py: Environment validation with auto-update (pyenv, nvm)
|
|
10
|
+
- template_installer.py: Template file copying and installation
|
|
11
|
+
- readme_generator.py: README.md generation
|
|
12
|
+
- config_generator.py: Config file generation (.eslintrc, .prettierrc, etc.)
|
|
13
|
+
- dependency_installer.py: Dependency installation (npm/pip)
|
|
14
|
+
- docs_structure.py: Documentation directory creation
|
|
15
|
+
- code_generator.py: Minimal code generation
|
|
16
|
+
- test_generator.py: Smoke test generation
|
|
17
|
+
- env_generator.py: Environment file generation (.env.example)
|
|
18
|
+
- session_structure.py: Session structure initialization
|
|
19
|
+
- initial_scans.py: Initial scans (stack.txt, tree.txt)
|
|
20
|
+
- git_hooks.py: Git hooks installation
|
|
21
|
+
- gitignore_updater.py: .gitignore updates
|
|
22
|
+
- initial_commit.py: Initial commit creation
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"git_setup",
|
|
27
|
+
"environment_validator",
|
|
28
|
+
"template_installer",
|
|
29
|
+
"readme_generator",
|
|
30
|
+
"config_generator",
|
|
31
|
+
"dependency_installer",
|
|
32
|
+
"docs_structure",
|
|
33
|
+
"code_generator",
|
|
34
|
+
"test_generator",
|
|
35
|
+
"env_generator",
|
|
36
|
+
"session_structure",
|
|
37
|
+
"initial_scans",
|
|
38
|
+
"git_hooks",
|
|
39
|
+
"gitignore_updater",
|
|
40
|
+
"initial_commit",
|
|
41
|
+
]
|