worktree-for-agents 1.0.1 → 1.1.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/docs/mcp-contract.md +23 -0
- package/package.json +1 -1
- package/worktree_manager/__init__.py +1 -1
- package/worktree_manager/cli.py +3 -3
- package/worktree_manager/mcp_contract.py +5 -2
- package/worktree_manager/mcp_operations.py +4 -3
- package/worktree_manager/mcp_server.py +2 -1
- package/worktree_manager/recovery_manager.py +10 -4
- package/worktree_manager/worktree_lifecycle.py +57 -20
package/docs/mcp-contract.md
CHANGED
|
@@ -76,3 +76,26 @@ The dry run publishes the composed `commit_subject` and `commit_body`; execute p
|
|
|
76
76
|
to the lifecycle. If missing, the manager uses the neutral fallback
|
|
77
77
|
`Complete <work_id> (generic closure)` / `No delivery description was available
|
|
78
78
|
at closure time.`.
|
|
79
|
+
|
|
80
|
+
## Merge strategy
|
|
81
|
+
|
|
82
|
+
`worktree_complete` accepts the optional string `merge_strategy` with the enum
|
|
83
|
+
values `squash` (default) and `preserve-history`. Squash integrates the branch
|
|
84
|
+
as one commit; preserve-history records an explicit merge commit that keeps the
|
|
85
|
+
branch commits. Neither strategy uses a fast-forward.
|
|
86
|
+
|
|
87
|
+
The field is validated as a string and against the enum before any mutation; a
|
|
88
|
+
value outside the enum is rejected as `invalid_request` with exit code 3. It is
|
|
89
|
+
included in the plan hash only for `worktree_complete`, so a preflight for one
|
|
90
|
+
strategy cannot execute the other and reports `plan_mismatch` with exit code 5.
|
|
91
|
+
|
|
92
|
+
A dry run publishes `data.merge_strategy` and the planned `mutations`:
|
|
93
|
+
`squash_merge` for squash, or `merge_no_ff` for preserve-history, followed by
|
|
94
|
+
`remove_worktree` and `delete_branch` in both cases. Execute returns
|
|
95
|
+
`merge_strategy` and `integrated_commit`; `squash_commit` is present only for
|
|
96
|
+
squash and equals `integrated_commit`. The transaction records the strategy and
|
|
97
|
+
the integrated commit. Recovery validates the parent shape recorded by the
|
|
98
|
+
strategy, reads a transaction without the field as squash, and reports an
|
|
99
|
+
unknown value as `invalid_journal` with exit code 5 while retaining evidence.
|
|
100
|
+
With `preserve-history`, a branch with no commits ahead of the base fails as
|
|
101
|
+
`operation_failed` with exit code 5 without a transaction or mutation.
|
package/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Standalone Python worktree management backend."""
|
|
2
|
-
__version__ = '1.0
|
|
2
|
+
__version__ = '1.1.0'
|
package/worktree_manager/cli.py
CHANGED
|
@@ -6,7 +6,7 @@ from .configuration import Configuration
|
|
|
6
6
|
from .git_repository import GitRepository
|
|
7
7
|
from .models import ManagerError,OperationResult,ResultKind
|
|
8
8
|
from .output import emit
|
|
9
|
-
from .worktree_lifecycle import WorktreeLifecycle
|
|
9
|
+
from .worktree_lifecycle import WorktreeLifecycle, MERGE_STRATEGIES, DEFAULT_MERGE_STRATEGY
|
|
10
10
|
from .recovery_manager import RecoveryManager
|
|
11
11
|
from .target_direct_adapter import TargetDirectAdapter
|
|
12
12
|
from . import __version__ as VERSION
|
|
@@ -18,7 +18,7 @@ def _parser():
|
|
|
18
18
|
for name in ('create','complete','cleanup'):
|
|
19
19
|
p=sub.add_parser(name); p.add_argument('work_id'); p.add_argument('--base');
|
|
20
20
|
if name=='cleanup': p.add_argument('--discard',action='store_true')
|
|
21
|
-
if name=='complete': p.add_argument('--preserve-dirty',action='store_true'); p.add_argument('--title'); p.add_argument('--body')
|
|
21
|
+
if name=='complete': p.add_argument('--preserve-dirty',action='store_true'); p.add_argument('--title'); p.add_argument('--body'); p.add_argument('--merge-strategy',choices=MERGE_STRATEGIES,default=DEFAULT_MERGE_STRATEGY)
|
|
22
22
|
if name=='create':
|
|
23
23
|
p.add_argument('--init-submodules',action='store_true'); p.add_argument('--prepare',action='store_true'); p.add_argument('--verify',action='store_true')
|
|
24
24
|
p=sub.add_parser('recover'); p.add_argument('transaction_id',nargs='?')
|
|
@@ -120,7 +120,7 @@ def main(argv=None)->int:
|
|
|
120
120
|
result=OperationResult(operation,data=data)
|
|
121
121
|
elif operation=='cleanup': result=OperationResult(operation,data=lifecycle.cleanup(args.work_id,args.discard,args.base or 'main'))
|
|
122
122
|
elif operation=='complete':
|
|
123
|
-
data=lifecycle.complete(args.work_id,args.base or 'main',args.preserve_dirty,commit_title=args.title,commit_body=args.body)
|
|
123
|
+
data=lifecycle.complete(args.work_id,args.base or 'main',args.preserve_dirty,commit_title=args.title,commit_body=args.body,merge_strategy=args.merge_strategy)
|
|
124
124
|
data['merge']='success'
|
|
125
125
|
result=OperationResult(operation,data=data)
|
|
126
126
|
if operation=='list' and not args.json:
|
|
@@ -5,13 +5,14 @@ import pathlib
|
|
|
5
5
|
|
|
6
6
|
CONTRACT_VERSION = 1
|
|
7
7
|
from . import __version__ as TOOL_VERSION
|
|
8
|
+
from .worktree_lifecycle import MERGE_STRATEGIES
|
|
8
9
|
TOOL_NAMES = (
|
|
9
10
|
"worktree_create", "worktree_complete", "worktree_cleanup",
|
|
10
11
|
"worktree_recover", "worktree_list", "worktree_prune", "worktree_status",
|
|
11
12
|
)
|
|
12
13
|
FIELDS = {
|
|
13
14
|
"worktree_create": {"repo", "work_id", "base", "dry_run", "parent_repo", "submodule_path", "confirm", "plan_hash", "init_submodules", "prepare", "verify"},
|
|
14
|
-
"worktree_complete": {"repo", "work_id", "base", "dry_run", "authorization", "confirm", "plan_hash", "transaction_id", "preserve_dirty", "target_direct", "parent_repo", "commit_title", "commit_body"},
|
|
15
|
+
"worktree_complete": {"repo", "work_id", "base", "dry_run", "authorization", "confirm", "plan_hash", "transaction_id", "preserve_dirty", "target_direct", "parent_repo", "commit_title", "commit_body", "merge_strategy"},
|
|
15
16
|
"worktree_cleanup": {"repo", "work_id", "base", "dry_run", "authorization", "confirm", "plan_hash", "discard", "discard_consent"},
|
|
16
17
|
"worktree_recover": {"repo", "transaction_id", "dry_run", "authorization", "confirm", "target_direct", "parent_repo"},
|
|
17
18
|
"worktree_list": {"repo"}, "worktree_prune": {"repo", "dry_run", "authorization", "confirm", "plan_hash"},
|
|
@@ -40,9 +41,11 @@ def validate_request(tool: str, request: dict) -> dict:
|
|
|
40
41
|
for key in ("dry_run", "discard", "preserve_dirty", "discard_consent", "target_direct", "confirm", "init_submodules", "prepare", "verify"):
|
|
41
42
|
if key in request and type(request[key]) is not bool:
|
|
42
43
|
raise ValueError(f"{key}_must_be_boolean")
|
|
43
|
-
for key in ("work_id", "base", "transaction_id", "authorization", "parent_repo", "submodule_path", "commit_title", "commit_body"):
|
|
44
|
+
for key in ("work_id", "base", "transaction_id", "authorization", "parent_repo", "submodule_path", "commit_title", "commit_body", "merge_strategy"):
|
|
44
45
|
if key in request and not isinstance(request[key], str):
|
|
45
46
|
raise ValueError(f"{key}_must_be_string")
|
|
47
|
+
if tool == "worktree_complete" and "merge_strategy" in request and request["merge_strategy"] not in MERGE_STRATEGIES:
|
|
48
|
+
raise ValueError("merge_strategy_invalid")
|
|
46
49
|
if tool in {"worktree_create", "worktree_complete", "worktree_cleanup", "worktree_status"} and not request.get("work_id"):
|
|
47
50
|
raise ValueError("work_id_required")
|
|
48
51
|
if tool == "worktree_create" and bool(request.get("parent_repo")) != bool(request.get("submodule_path")):
|
|
@@ -32,7 +32,7 @@ def call(tool, request):
|
|
|
32
32
|
req["transaction_id"] = __import__("uuid").uuid4().hex
|
|
33
33
|
fields = ["operation", "repo", "work_id", "confirm", "transaction_id", "base", "preserve_dirty", "discard", "discard_consent", "target_direct", "parent_repo", "submodule_path", "state_digest"]
|
|
34
34
|
if tool == "worktree_complete":
|
|
35
|
-
fields = fields + ["commit_title", "commit_body"]
|
|
35
|
+
fields = fields + ["commit_title", "commit_body", "merge_strategy"]
|
|
36
36
|
def plan_for(digest):
|
|
37
37
|
return {key: (tool if key == "operation" else digest if key == "state_digest" else bool(req.get("authorization")) if key == "confirm" else req.get(key)) for key in fields}
|
|
38
38
|
if dry:
|
|
@@ -117,8 +117,9 @@ def call(tool, request):
|
|
|
117
117
|
if lifecycle.git.run(("-C", str(path), "status", "--porcelain", "--untracked-files=all")).stdout: raise ManagerError("intervention_required", "Worktree has uncommitted changes; it was retained", 5)
|
|
118
118
|
from .commit_message import compose_commit_message
|
|
119
119
|
_subject, _body = compose_commit_message(req["work_id"], req.get("commit_title"), req.get("commit_body"))
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
strategy = req.get("merge_strategy", "squash")
|
|
121
|
+
data = {"planned": True, "work_id": req["work_id"], "transaction_id":req.get("transaction_id"), "preflight": "passed", "merge_strategy": strategy, "mutations": ["squash_merge" if strategy == "squash" else "merge_no_ff", "remove_worktree", "delete_branch"], "plan_hash":req.get("_computed_plan_hash"), "state_digest":digest, "commit_subject": _subject, "commit_body": _body}
|
|
122
|
+
else: data = lifecycle.complete(req["work_id"], req.get("base", "main"), req.get("preserve_dirty", False), req.get("transaction_id"), req.get("plan_hash"), req, merge_strategy=req.get("merge_strategy", "squash"))
|
|
122
123
|
elif tool == "worktree_cleanup":
|
|
123
124
|
if req.get("discard") and req.get("discard_consent") is not True:
|
|
124
125
|
raise ManagerError("discard_consent_required", "Separate explicit discard consent is required", 3)
|
|
@@ -17,7 +17,8 @@ def schemas():
|
|
|
17
17
|
"init_submodules":{"type":"boolean"}, "prepare":{"type":"boolean"}, "verify":{"type":"boolean"},
|
|
18
18
|
"dry_run": {"type": "boolean"}, "discard": {"type": "boolean"},
|
|
19
19
|
"discard_consent": {"type": "boolean"}, "preserve_dirty": {"type": "boolean"},
|
|
20
|
-
"commit_title": {"type": "string"}, "commit_body": {"type": "string"}
|
|
20
|
+
"commit_title": {"type": "string"}, "commit_body": {"type": "string"},
|
|
21
|
+
"merge_strategy": {"type": "string", "enum": ["squash", "preserve-history"]}}
|
|
21
22
|
props.update({"parent_repo": {"type": "string"}, "submodule_path": {"type": "string"},
|
|
22
23
|
"target_direct": {"type": "boolean"}})
|
|
23
24
|
return [{"name": name, "description": "Manage repository worktrees", "inputSchema": {
|
|
@@ -6,6 +6,7 @@ from .models import ManagerError, ResultKind
|
|
|
6
6
|
from .transaction_journal import TransactionJournal
|
|
7
7
|
from .git_repository import GitRepository
|
|
8
8
|
from .lock_manager import LockManager
|
|
9
|
+
from .worktree_lifecycle import MERGE_STRATEGIES
|
|
9
10
|
import hashlib
|
|
10
11
|
class RecoveryManager:
|
|
11
12
|
def __init__(self,state_root: str|Path,repo_root: str|Path|None=None): self.journal=TransactionJournal(state_root); self.repo=GitRepository(repo_root) if repo_root else None
|
|
@@ -61,6 +62,8 @@ class RecoveryManager:
|
|
|
61
62
|
try: meta=json.loads((self.journal.directory(transaction_id)/'meta.json').read_text(encoding='utf-8'))
|
|
62
63
|
except (OSError,ValueError) as exc: raise ManagerError('invalid_journal','Recovery metadata is corrupt; backups were retained',5) from exc
|
|
63
64
|
if meta.get('base') is None or meta.get('branch') is None or meta.get('path') is None: raise ManagerError('invalid_journal','Transaction identity is incomplete',5)
|
|
65
|
+
merge_strategy=meta.get('merge_strategy','squash')
|
|
66
|
+
if merge_strategy not in MERGE_STRATEGIES: raise ManagerError('invalid_journal','Transaction merge strategy is not recognised; resources retained',5)
|
|
64
67
|
# Preserve-dirty captures are a separate recovery contract from a
|
|
65
68
|
# durable squash. A crash can leave captured parent state that
|
|
66
69
|
# must not be silently classified as a completed close, regardless
|
|
@@ -78,13 +81,16 @@ class RecoveryManager:
|
|
|
78
81
|
branch_tip=self.repo.run(('rev-parse',f'refs/heads/{branch}'),check=False)
|
|
79
82
|
parents=self.repo.run(('rev-list','--parents','-n','1',head),check=False).stdout.split()
|
|
80
83
|
registered_paths={str(Path(item).resolve()) for item in registered}
|
|
84
|
+
expected_parents=3 if merge_strategy=='preserve-history' else 2
|
|
85
|
+
valid_parents=len(parents)==expected_parents and parents[0]==head and parents[1]==before
|
|
86
|
+
if merge_strategy=='preserve-history' and valid_parents: valid_parents=parents[2]==meta.get('origin')
|
|
81
87
|
if (base_tip.returncode or base_tip.stdout.strip()!=head or
|
|
82
88
|
str(path.resolve()) not in registered_paths or
|
|
83
89
|
worktree_head.returncode or worktree_head.stdout.strip()!=meta.get('origin') or
|
|
84
90
|
symbolic.returncode or symbolic.stdout.strip()!=f'refs/heads/{branch}' or
|
|
85
91
|
branch_tip.returncode or branch_tip.stdout.strip()!=meta.get('origin') or
|
|
86
|
-
|
|
87
|
-
raise ManagerError('intervention_required','Durable
|
|
92
|
+
not valid_parents):
|
|
93
|
+
raise ManagerError('intervention_required','Durable integration evidence is incomplete or ambiguous; resources retained',5)
|
|
88
94
|
return ResultKind.NO_PENDING_WORK, None
|
|
89
95
|
if phase in {'captured','merge_started'} and head==before:
|
|
90
96
|
# The squash commit did not happen. Restore captured destination state once.
|
|
@@ -94,8 +100,8 @@ class RecoveryManager:
|
|
|
94
100
|
lifecycle._restore_saved(transaction_id,meta)
|
|
95
101
|
self.journal.write_phase(transaction_id,'restored')
|
|
96
102
|
return ResultKind.SUCCESS,{'transaction_id':transaction_id,'phase':'restored','integrated':False}
|
|
97
|
-
|
|
98
|
-
if phase in {'merged','remove_started','removed'} and (not
|
|
103
|
+
integrated=meta.get('integrated') or meta.get('squash')
|
|
104
|
+
if phase in {'merged','remove_started','removed'} and (not integrated or head!=integrated): raise ManagerError('intervention_required','Repository HEAD drifted from the recorded integrated commit; resources retained',5)
|
|
99
105
|
if path.exists() or str(path) in registered:
|
|
100
106
|
if phase not in {'merged','remove_started'}: raise ManagerError('intervention_required','Worktree state is ambiguous; resources retained',5)
|
|
101
107
|
if self.repo.run(('-C',str(path),'status','--porcelain')).stdout: raise ManagerError('intervention_required','Worktree is dirty; resources retained',5)
|
|
@@ -21,6 +21,13 @@ def validate_work_id(value: str) -> str:
|
|
|
21
21
|
raise ManagerError('invalid_identity','Invalid work identifier',3)
|
|
22
22
|
return value
|
|
23
23
|
|
|
24
|
+
MERGE_STRATEGIES = ('squash', 'preserve-history')
|
|
25
|
+
DEFAULT_MERGE_STRATEGY = 'squash'
|
|
26
|
+
def validate_merge_strategy(value: str) -> str:
|
|
27
|
+
if value not in MERGE_STRATEGIES:
|
|
28
|
+
raise ManagerError('invalid_merge_strategy','Invalid merge strategy',3)
|
|
29
|
+
return value
|
|
30
|
+
|
|
24
31
|
class WorktreeLifecycle:
|
|
25
32
|
def __init__(self, config: Configuration):
|
|
26
33
|
self.config=config
|
|
@@ -132,15 +139,18 @@ class WorktreeLifecycle:
|
|
|
132
139
|
self.git.run(('worktree','remove',*(('--force',) if discard else ()),str(path)))
|
|
133
140
|
self.git.run(('branch','-D' if discard else '-d',branch))
|
|
134
141
|
return {'cleaned':work_id,'branch':branch,'worktree':str(path)}
|
|
135
|
-
def complete(self,work_id: str,base: str,preserve_dirty: bool=False,transaction_id: str|None=None,plan_hash: str|None=None,request:dict|None=None,commit_title=None,commit_body=None)->dict[str,str]:
|
|
136
|
-
"""
|
|
142
|
+
def complete(self,work_id: str,base: str,preserve_dirty: bool=False,transaction_id: str|None=None,plan_hash: str|None=None,request:dict|None=None,commit_title=None,commit_body=None,merge_strategy: str|None=None)->dict[str,str]:
|
|
143
|
+
"""Integrate a worktree branch and retire it only after verified success.
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
Squash produces one commit on the base branch. Preserve-history records
|
|
146
|
+
an explicit merge commit that keeps the branch commits. Dirty-destination
|
|
147
|
+
state is captured and restored around the integration in both strategies.
|
|
140
148
|
"""
|
|
141
149
|
branch=self._branch(work_id); base=self._base(base); path=self._path(work_id)
|
|
142
150
|
if commit_title is None and isinstance(request,dict): commit_title=request.get('commit_title')
|
|
143
151
|
if commit_body is None and isinstance(request,dict): commit_body=request.get('commit_body')
|
|
152
|
+
if merge_strategy is None and isinstance(request,dict): merge_strategy=request.get('merge_strategy')
|
|
153
|
+
merge_strategy=validate_merge_strategy(merge_strategy or DEFAULT_MERGE_STRATEGY)
|
|
144
154
|
registered={item['path'] for item in self.list()}
|
|
145
155
|
if str(path) not in registered or not path.is_dir():
|
|
146
156
|
raise ManagerError('invalid_identity','Worktree identity does not match registration',3)
|
|
@@ -153,6 +163,12 @@ class WorktreeLifecycle:
|
|
|
153
163
|
status=self.git.run(('status','--porcelain','--untracked-files=all')).stdout
|
|
154
164
|
if status and not preserve_dirty:
|
|
155
165
|
raise ManagerError('intervention_required','Destination has uncommitted changes; use --preserve-dirty to retain them',5)
|
|
166
|
+
if merge_strategy=='preserve-history':
|
|
167
|
+
ahead=self.git.run(('rev-list','--count',f'{base}..{branch}'),check=False)
|
|
168
|
+
if ahead.returncode:
|
|
169
|
+
raise ManagerError('operation_failed','Could not inspect the branch ahead of the base; nothing was merged',5)
|
|
170
|
+
if (ahead.stdout.strip() or '0')=='0':
|
|
171
|
+
raise ManagerError('operation_failed','Branch has no commits ahead of the base; nothing was merged',5)
|
|
156
172
|
state=self.state_root
|
|
157
173
|
locks=LockManager(state/self.config.lock_dir)
|
|
158
174
|
tx=transaction_id or uuid.uuid4().hex
|
|
@@ -161,7 +177,7 @@ class WorktreeLifecycle:
|
|
|
161
177
|
raise ManagerError('intervention_required','Transaction binding already exists; execute is blocked',5)
|
|
162
178
|
before=self.git.head(); origin=self.git.run(('rev-parse',f'refs/heads/{branch}')).stdout.strip()
|
|
163
179
|
meta={'work_id':work_id,'base':base,'branch':branch,'path':str(path),'before':before,'origin':origin,
|
|
164
|
-
'preserve_dirty':bool(status),'tracked_ref':None,'untracked_ref':None,
|
|
180
|
+
'preserve_dirty':bool(status),'merge_strategy':merge_strategy,'tracked_ref':None,'untracked_ref':None,
|
|
165
181
|
'backup_ref_prefix':self.config.backup_ref_prefix}
|
|
166
182
|
meta_path=journal.directory(tx)/'meta.json'
|
|
167
183
|
def save_meta(phase):
|
|
@@ -182,6 +198,7 @@ class WorktreeLifecycle:
|
|
|
182
198
|
try:
|
|
183
199
|
journal.write_binding(tx, {'transaction_id':tx,'repo':str(self.config.repo_root.resolve()),'work_id':work_id,
|
|
184
200
|
'operation':'worktree_complete','request':request or {},'plan_hash':plan_hash,
|
|
201
|
+
'merge_strategy':merge_strategy,
|
|
185
202
|
'state_digest':__import__('worktree_manager.state_digest',fromlist=['state_digest']).state_digest(self.config.repo_root,state),
|
|
186
203
|
'created_at':__import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
|
|
187
204
|
'phase':'prepared','base':base,'before':before,'origin':origin})
|
|
@@ -215,19 +232,33 @@ class WorktreeLifecycle:
|
|
|
215
232
|
meta['commit_subject']=subject
|
|
216
233
|
meta['commit_body']=body
|
|
217
234
|
save_meta('merge_started')
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
self.git.run(('
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
if merge_strategy=='preserve-history':
|
|
236
|
+
message=subject+('\n\n'+body if body else '')
|
|
237
|
+
merged=self.git.run(('merge','--no-ff','-m',message,branch),check=False)
|
|
238
|
+
if merged.returncode:
|
|
239
|
+
self.git.run(('merge','--abort'),check=False)
|
|
240
|
+
save_meta('pending_manual')
|
|
241
|
+
raise ManagerError('operation_failed','Preserve-history merge failed; worktree and branch retained',5)
|
|
242
|
+
after=self.git.head()
|
|
243
|
+
parents=self.git.run(('rev-list','--parents','-n','1',after)).stdout.split()
|
|
244
|
+
if after==before or len(parents)!=3 or parents[0]!=after or parents[1]!=before or parents[2]!=origin:
|
|
245
|
+
save_meta('pending_manual'); raise ManagerError('operation_failed','Merge commit verification failed; resources retained',5)
|
|
246
|
+
else:
|
|
247
|
+
merged=self.git.run(('merge','--squash',branch),check=False)
|
|
248
|
+
if merged.returncode or self.git.run(('diff','--cached','--quiet'),check=False).returncode==0:
|
|
249
|
+
self.git.run(('reset','--merge'),check=False)
|
|
250
|
+
save_meta('pending_manual')
|
|
251
|
+
raise ManagerError('operation_failed','Squash merge failed or produced no changes; worktree and branch retained',5)
|
|
252
|
+
commit_args=['commit','-m',subject]
|
|
253
|
+
if body:
|
|
254
|
+
commit_args += ['-m',body]
|
|
255
|
+
self.git.run(tuple(commit_args))
|
|
256
|
+
after=self.git.head()
|
|
257
|
+
if after==before or len(self.git.run(('rev-list','--parents','-n','1',after)).stdout.split())!=2:
|
|
258
|
+
save_meta('pending_manual'); raise ManagerError('operation_failed','Squash commit verification failed; resources retained',5)
|
|
259
|
+
meta['integrated']=after
|
|
260
|
+
if merge_strategy=='squash': meta['squash']=after
|
|
261
|
+
save_meta('merged')
|
|
231
262
|
self.git.run(('worktree','remove','--force','--',str(path)))
|
|
232
263
|
if path.exists() or str(path) in {item['path'] for item in self.list()}:
|
|
233
264
|
save_meta('pending_manual'); raise ManagerError('operation_failed','Worktree removal could not be verified',5)
|
|
@@ -236,10 +267,16 @@ class WorktreeLifecycle:
|
|
|
236
267
|
save_meta('pending_manual'); raise ManagerError('operation_failed','Branch changed; it was retained',5)
|
|
237
268
|
self.git.run(('update-ref','-d',f'refs/heads/{branch}',origin)); save_meta('closed')
|
|
238
269
|
if status:
|
|
239
|
-
meta['
|
|
270
|
+
meta['integrated']=after
|
|
271
|
+
if merge_strategy=='squash': meta['squash']=after
|
|
272
|
+
save_meta('merged')
|
|
240
273
|
self._restore_saved(tx,meta)
|
|
241
274
|
save_meta('restored')
|
|
242
|
-
|
|
275
|
+
result={'completed':work_id,'branch':branch,'base_branch':base,'worktree':str(path),
|
|
276
|
+
'integrated_commit':after,'merge_strategy':merge_strategy,
|
|
277
|
+
'transaction_id':tx,'commit_subject':subject,'commit_body':body}
|
|
278
|
+
if merge_strategy=='squash': result['squash_commit']=after
|
|
279
|
+
return result
|
|
243
280
|
finally: locks.release()
|
|
244
281
|
|
|
245
282
|
def _restore_saved(self,tx,meta):
|