draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Simple git merge operations without state machine coupling.
|
|
2
|
+
|
|
3
|
+
This module provides straightforward git merge operations that can be called
|
|
4
|
+
from any context without requiring specific ticket states or validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SimpleMergeResult:
|
|
17
|
+
"""Result of a simple git merge operation."""
|
|
18
|
+
|
|
19
|
+
success: bool
|
|
20
|
+
message: str
|
|
21
|
+
merged_branch: str | None = None
|
|
22
|
+
target_branch: str | None = None
|
|
23
|
+
merge_commit: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GitMergeError(Exception):
|
|
27
|
+
"""Raised when git merge operations fail."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def git_merge_worktree_branch(
|
|
33
|
+
repo_path: Path,
|
|
34
|
+
branch_name: str,
|
|
35
|
+
target_branch: str = "main",
|
|
36
|
+
delete_branch_after: bool = True,
|
|
37
|
+
push_to_remote: bool = False,
|
|
38
|
+
squash: bool = False,
|
|
39
|
+
check_divergence: bool = True,
|
|
40
|
+
) -> SimpleMergeResult:
|
|
41
|
+
"""Merge a worktree branch into the target branch.
|
|
42
|
+
|
|
43
|
+
This is a simple, synchronous git merge operation with no state validation
|
|
44
|
+
or database coupling. It just runs git commands.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
repo_path: Path to the main git repository (not the worktree)
|
|
48
|
+
branch_name: Name of the branch to merge (e.g., "goal/xxx/ticket/yyy")
|
|
49
|
+
target_branch: Target branch to merge into (default: "main")
|
|
50
|
+
delete_branch_after: Whether to delete the branch after merge
|
|
51
|
+
push_to_remote: Whether to push to remote after merge
|
|
52
|
+
squash: Whether to squash commits (single commit per task)
|
|
53
|
+
check_divergence: Whether to check if base branch moved ahead
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
SimpleMergeResult with success status and details
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
GitMergeError: If git operations fail
|
|
60
|
+
"""
|
|
61
|
+
logger.info(
|
|
62
|
+
f"Starting simple merge: {branch_name} -> {target_branch} (squash={squash})"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# 1. Ensure we're in the repo (not a worktree)
|
|
67
|
+
if not (repo_path / ".git").exists():
|
|
68
|
+
raise GitMergeError(f"Not a git repository: {repo_path}")
|
|
69
|
+
|
|
70
|
+
# 2. Fetch latest (if remote exists)
|
|
71
|
+
logger.info("Fetching latest from remote...")
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["git", "fetch", "origin"],
|
|
74
|
+
cwd=repo_path,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
timeout=30,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
logger.warning(f"Git fetch failed (may not have remote): {result.stderr}")
|
|
81
|
+
|
|
82
|
+
# 3. Divergence check (copied from Vibe Kanban!)
|
|
83
|
+
if check_divergence:
|
|
84
|
+
logger.info("Checking for divergence...")
|
|
85
|
+
# Count commits in target_branch that are not in branch_name
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["git", "rev-list", "--count", f"{branch_name}..{target_branch}"],
|
|
88
|
+
cwd=repo_path,
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=10,
|
|
92
|
+
)
|
|
93
|
+
if result.returncode == 0:
|
|
94
|
+
commits_behind = int(result.stdout.strip())
|
|
95
|
+
if commits_behind > 0:
|
|
96
|
+
raise GitMergeError(
|
|
97
|
+
f"Cannot merge: {target_branch} is {commits_behind} commits ahead of {branch_name}. "
|
|
98
|
+
f"The base branch has moved forward since the task was created. "
|
|
99
|
+
f"Rebase the task branch onto {target_branch} first."
|
|
100
|
+
)
|
|
101
|
+
logger.info("✓ No divergence detected")
|
|
102
|
+
else:
|
|
103
|
+
logger.warning(f"Divergence check failed: {result.stderr}")
|
|
104
|
+
|
|
105
|
+
# 4. Checkout target branch
|
|
106
|
+
logger.info(f"Checking out {target_branch}...")
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
["git", "checkout", target_branch],
|
|
109
|
+
cwd=repo_path,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
timeout=10,
|
|
113
|
+
)
|
|
114
|
+
if result.returncode != 0:
|
|
115
|
+
raise GitMergeError(f"Failed to checkout {target_branch}: {result.stderr}")
|
|
116
|
+
|
|
117
|
+
# 4. Pull latest from target branch
|
|
118
|
+
logger.info(f"Pulling latest {target_branch}...")
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
["git", "pull", "origin", target_branch],
|
|
121
|
+
cwd=repo_path,
|
|
122
|
+
capture_output=True,
|
|
123
|
+
text=True,
|
|
124
|
+
timeout=30,
|
|
125
|
+
)
|
|
126
|
+
if result.returncode != 0:
|
|
127
|
+
logger.warning(f"Git pull failed (may not have remote): {result.stderr}")
|
|
128
|
+
|
|
129
|
+
# 5. Merge the branch (squash or regular)
|
|
130
|
+
if squash:
|
|
131
|
+
# Squash merge - all commits become one (like Vibe Kanban!)
|
|
132
|
+
logger.info(f"Squash merging {branch_name} into {target_branch}...")
|
|
133
|
+
|
|
134
|
+
# Check if branches have diverged (are there actual changes?)
|
|
135
|
+
diff_check = subprocess.run(
|
|
136
|
+
["git", "diff", f"{target_branch}...{branch_name}", "--stat"],
|
|
137
|
+
cwd=repo_path,
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
timeout=10,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not diff_check.stdout.strip():
|
|
144
|
+
logger.info(
|
|
145
|
+
f"No changes between {target_branch} and {branch_name} - branches are identical"
|
|
146
|
+
)
|
|
147
|
+
# No actual changes, but this is considered success
|
|
148
|
+
# Skip the merge since branches are already in sync
|
|
149
|
+
merge_commit = None # No new commit created
|
|
150
|
+
else:
|
|
151
|
+
logger.info(f"Changes detected:\n{diff_check.stdout}")
|
|
152
|
+
|
|
153
|
+
# Stage all changes from branch
|
|
154
|
+
result = subprocess.run(
|
|
155
|
+
["git", "merge", "--squash", branch_name],
|
|
156
|
+
cwd=repo_path,
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
timeout=30,
|
|
160
|
+
)
|
|
161
|
+
if result.returncode != 0:
|
|
162
|
+
if "CONFLICT" in result.stdout or "CONFLICT" in result.stderr:
|
|
163
|
+
raise GitMergeError(
|
|
164
|
+
f"Merge conflict during squash: {result.stderr}"
|
|
165
|
+
)
|
|
166
|
+
raise GitMergeError(f"Squash merge failed: {result.stderr}")
|
|
167
|
+
|
|
168
|
+
# Check if there are changes to commit
|
|
169
|
+
status_result = subprocess.run(
|
|
170
|
+
["git", "status", "--porcelain"],
|
|
171
|
+
cwd=repo_path,
|
|
172
|
+
capture_output=True,
|
|
173
|
+
text=True,
|
|
174
|
+
timeout=5,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if not status_result.stdout.strip():
|
|
178
|
+
# No changes to commit - this shouldn't happen after squash
|
|
179
|
+
logger.warning(
|
|
180
|
+
"No changes staged after squash merge - unexpected state"
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
# Create single commit
|
|
184
|
+
result = subprocess.run(
|
|
185
|
+
[
|
|
186
|
+
"git",
|
|
187
|
+
"commit",
|
|
188
|
+
"-m",
|
|
189
|
+
f"Merge {branch_name} into {target_branch}",
|
|
190
|
+
],
|
|
191
|
+
cwd=repo_path,
|
|
192
|
+
capture_output=True,
|
|
193
|
+
text=True,
|
|
194
|
+
timeout=10,
|
|
195
|
+
)
|
|
196
|
+
if result.returncode != 0:
|
|
197
|
+
# Log both stdout and stderr for debugging
|
|
198
|
+
logger.error(
|
|
199
|
+
f"Git commit failed. Stdout: {result.stdout}, Stderr: {result.stderr}"
|
|
200
|
+
)
|
|
201
|
+
raise GitMergeError(
|
|
202
|
+
f"Commit after squash failed: {result.stderr}\n"
|
|
203
|
+
f"Stdout: {result.stdout}\n"
|
|
204
|
+
f"This might be due to git user config not being set."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
logger.info("Squash merge successful")
|
|
208
|
+
else:
|
|
209
|
+
# Regular merge with --no-ff
|
|
210
|
+
logger.info(f"Merging {branch_name} into {target_branch}...")
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
[
|
|
213
|
+
"git",
|
|
214
|
+
"merge",
|
|
215
|
+
"--no-ff",
|
|
216
|
+
branch_name,
|
|
217
|
+
"-m",
|
|
218
|
+
f"Merge {branch_name} into {target_branch}",
|
|
219
|
+
],
|
|
220
|
+
cwd=repo_path,
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
timeout=30,
|
|
224
|
+
)
|
|
225
|
+
if result.returncode != 0:
|
|
226
|
+
# Check if it's a conflict
|
|
227
|
+
if "CONFLICT" in result.stdout or "CONFLICT" in result.stderr:
|
|
228
|
+
raise GitMergeError(f"Merge conflict: {result.stderr}")
|
|
229
|
+
raise GitMergeError(f"Merge failed: {result.stderr}")
|
|
230
|
+
|
|
231
|
+
logger.info("Merge successful")
|
|
232
|
+
|
|
233
|
+
# 6. Get merge commit hash (only if we didn't already set it)
|
|
234
|
+
if "merge_commit" not in locals():
|
|
235
|
+
result = subprocess.run(
|
|
236
|
+
["git", "rev-parse", "HEAD"],
|
|
237
|
+
cwd=repo_path,
|
|
238
|
+
capture_output=True,
|
|
239
|
+
text=True,
|
|
240
|
+
timeout=5,
|
|
241
|
+
)
|
|
242
|
+
merge_commit = result.stdout.strip() if result.returncode == 0 else None
|
|
243
|
+
|
|
244
|
+
# 7. Push to remote (if requested)
|
|
245
|
+
if push_to_remote:
|
|
246
|
+
logger.info(f"Pushing {target_branch} to remote...")
|
|
247
|
+
result = subprocess.run(
|
|
248
|
+
["git", "push", "origin", target_branch],
|
|
249
|
+
cwd=repo_path,
|
|
250
|
+
capture_output=True,
|
|
251
|
+
text=True,
|
|
252
|
+
timeout=30,
|
|
253
|
+
)
|
|
254
|
+
if result.returncode != 0:
|
|
255
|
+
logger.warning(f"Push failed: {result.stderr}")
|
|
256
|
+
# Don't fail the merge if push fails
|
|
257
|
+
|
|
258
|
+
# 8. Delete branch (if requested)
|
|
259
|
+
if delete_branch_after:
|
|
260
|
+
logger.info(f"Deleting branch {branch_name}...")
|
|
261
|
+
result = subprocess.run(
|
|
262
|
+
["git", "branch", "-d", branch_name],
|
|
263
|
+
cwd=repo_path,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
text=True,
|
|
266
|
+
timeout=5,
|
|
267
|
+
)
|
|
268
|
+
if result.returncode != 0:
|
|
269
|
+
# Try force delete
|
|
270
|
+
result = subprocess.run(
|
|
271
|
+
["git", "branch", "-D", branch_name],
|
|
272
|
+
cwd=repo_path,
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True,
|
|
275
|
+
timeout=5,
|
|
276
|
+
)
|
|
277
|
+
if result.returncode != 0:
|
|
278
|
+
logger.warning(f"Failed to delete branch: {result.stderr}")
|
|
279
|
+
|
|
280
|
+
# Prepare success message based on whether changes were merged
|
|
281
|
+
if merge_commit:
|
|
282
|
+
message = f"Successfully merged {branch_name} into {target_branch}"
|
|
283
|
+
else:
|
|
284
|
+
message = f"Branches {branch_name} and {target_branch} are already in sync (no changes to merge)"
|
|
285
|
+
|
|
286
|
+
return SimpleMergeResult(
|
|
287
|
+
success=True,
|
|
288
|
+
message=message,
|
|
289
|
+
merged_branch=branch_name,
|
|
290
|
+
target_branch=target_branch,
|
|
291
|
+
merge_commit=merge_commit,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
except subprocess.TimeoutExpired as e:
|
|
295
|
+
raise GitMergeError(f"Git operation timed out: {e}")
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Merge failed: {e}")
|
|
298
|
+
raise GitMergeError(str(e))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def cleanup_worktree(repo_path: Path, worktree_path: Path) -> bool:
|
|
302
|
+
"""Remove a git worktree directory.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
repo_path: Path to the main git repository
|
|
306
|
+
worktree_path: Path to the worktree to remove
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if cleanup succeeded, False otherwise
|
|
310
|
+
"""
|
|
311
|
+
logger.info(f"Cleaning up worktree: {worktree_path}")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Remove worktree using git
|
|
315
|
+
result = subprocess.run(
|
|
316
|
+
["git", "worktree", "remove", "--force", str(worktree_path)],
|
|
317
|
+
cwd=repo_path,
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
timeout=10,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if result.returncode != 0:
|
|
324
|
+
logger.warning(f"Git worktree remove failed: {result.stderr}")
|
|
325
|
+
# Try to remove directory manually
|
|
326
|
+
import shutil
|
|
327
|
+
|
|
328
|
+
if worktree_path.exists():
|
|
329
|
+
shutil.rmtree(worktree_path)
|
|
330
|
+
logger.info("Manually removed worktree directory")
|
|
331
|
+
|
|
332
|
+
# Prune stale worktree references
|
|
333
|
+
subprocess.run(
|
|
334
|
+
["git", "worktree", "prune"],
|
|
335
|
+
cwd=repo_path,
|
|
336
|
+
capture_output=True,
|
|
337
|
+
text=True,
|
|
338
|
+
timeout=5,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
logger.info("Worktree cleanup complete")
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.error(f"Worktree cleanup failed: {e}")
|
|
346
|
+
return False
|