aline-ai 0.1.5__py3-none-any.whl → 0.1.7__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.
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/METADATA +1 -1
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/RECORD +9 -9
- realign/__init__.py +1 -1
- realign/mcp_server.py +30 -14
- realign/mcp_watcher.py +78 -25
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.1.5.dist-info → aline_ai-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
aline_ai-0.1.
|
|
2
|
-
realign/__init__.py,sha256=
|
|
1
|
+
aline_ai-0.1.7.dist-info/licenses/LICENSE,sha256=H8wTqV5IF1oHw_HbBtS1PSDU8G_q81yblEIL_JfV8Vo,1077
|
|
2
|
+
realign/__init__.py,sha256=GAkpHLIQWfGF0lu7nkgDnXCA2GC4tJrr6dGTk8GMQB4,68
|
|
3
3
|
realign/claude_detector.py,sha256=NLxI0zJWcqNxNha9jAy9AslTMwHKakCc9yPGdkrbiFE,3028
|
|
4
4
|
realign/cli.py,sha256=bkwS329jMDEkrUEihXRN2DDyeTKE6HbAysoDxxskZ8g,941
|
|
5
5
|
realign/codex_detector.py,sha256=RI3JbZgebrhoqpRfTBMfclYCAISN7hZAHVW3bgftJpU,4428
|
|
6
6
|
realign/config.py,sha256=jarinbr0mA6e5DmgY19b_VpMnxk6SOYTwyvB9luq0ww,7207
|
|
7
7
|
realign/hooks.py,sha256=qhAeuln_62OgTq0vboZcUAuP2apOrNn58vSZqKwNmWQ,36456
|
|
8
8
|
realign/logging_config.py,sha256=KvkKktF-bkUu031y9vgUoHpsbnOw7ud25jhpzliNZwA,4929
|
|
9
|
-
realign/mcp_server.py,sha256
|
|
10
|
-
realign/mcp_watcher.py,sha256=
|
|
9
|
+
realign/mcp_server.py,sha256=srR1leOYbPyi6L0KALkOs94_djELi-SRLFQgrubQuhc,17760
|
|
10
|
+
realign/mcp_watcher.py,sha256=xL35Nz35YF6Mc9yu-nCbDzKzTFHQaYmtPNfyBzKKOhc,16038
|
|
11
11
|
realign/redactor.py,sha256=uZvLKKGrRGJm-qM8S4XJyJK6i0CSSby_wbKiay7VGJw,8148
|
|
12
12
|
realign/commands/__init__.py,sha256=GG6IMw6fUBQAXGJDFJvOOQgv6pkiRSfMh8z3AYXTyRM,31
|
|
13
13
|
realign/commands/auto_commit.py,sha256=_DOw7nt9q3tD_Y3qDL9IFKAUG1hM4qH_xZ-9nyBc2Bc,7451
|
|
@@ -16,8 +16,8 @@ realign/commands/config.py,sha256=oarvn6UuGT8svd2h5_8M_ueV5QWOCUOn8SYoa4XYjs8,65
|
|
|
16
16
|
realign/commands/init.py,sha256=EpSzh2Dd2EmEQ_wo3vAsg6Uq7_YOlQWIpzIkZa_2y0A,11863
|
|
17
17
|
realign/commands/search.py,sha256=0CZaXll99wtd01MRiZk5NAblxgogc4RUAzMyJunvckE,18044
|
|
18
18
|
realign/commands/show.py,sha256=P1waa94-AKJr9XjagkE40OHMXzE6IwC74DpeDKqwsqw,16693
|
|
19
|
-
aline_ai-0.1.
|
|
20
|
-
aline_ai-0.1.
|
|
21
|
-
aline_ai-0.1.
|
|
22
|
-
aline_ai-0.1.
|
|
23
|
-
aline_ai-0.1.
|
|
19
|
+
aline_ai-0.1.7.dist-info/METADATA,sha256=lgNJRQ4b1lDkDVvchqzD9VbvXB7bTkxgy9LgmmNaq34,1398
|
|
20
|
+
aline_ai-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
aline_ai-0.1.7.dist-info/entry_points.txt,sha256=h-NocHDzSueXfsepHTIdRPNQzhNZQPAztJfldd-mQTE,202
|
|
22
|
+
aline_ai-0.1.7.dist-info/top_level.txt,sha256=yIL3s2xv9nf1GwD5n71Aq_JEIV4AfzCIDNKBzewuRm4,8
|
|
23
|
+
aline_ai-0.1.7.dist-info/RECORD,,
|
realign/__init__.py
CHANGED
realign/mcp_server.py
CHANGED
|
@@ -395,10 +395,26 @@ async def handle_version(args: dict) -> list[TextContent]:
|
|
|
395
395
|
)]
|
|
396
396
|
|
|
397
397
|
|
|
398
|
+
def _server_log(msg: str):
|
|
399
|
+
"""Log MCP server messages to both stderr and file."""
|
|
400
|
+
from datetime import datetime
|
|
401
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
402
|
+
print(f"[MCP Server] {msg}", file=sys.stderr)
|
|
403
|
+
|
|
404
|
+
# Also write to the watcher log file for consistency
|
|
405
|
+
log_path = Path.home() / ".aline_watcher.log"
|
|
406
|
+
try:
|
|
407
|
+
with open(log_path, "a") as f:
|
|
408
|
+
f.write(f"[{timestamp}] [MCP Server] {msg}\n")
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
398
413
|
async def async_main():
|
|
399
414
|
"""Run the MCP server (async)."""
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
_server_log("Starting Aline MCP server...")
|
|
416
|
+
_server_log(f"Current working directory: {Path.cwd()}")
|
|
417
|
+
_server_log(f"Home directory: {Path.home()}")
|
|
402
418
|
|
|
403
419
|
# Detect workspace path and start the watcher
|
|
404
420
|
# Try multiple methods since MCP server may run in different context
|
|
@@ -414,52 +430,52 @@ async def async_main():
|
|
|
414
430
|
)
|
|
415
431
|
if result.returncode == 0:
|
|
416
432
|
repo_path = Path(result.stdout.strip())
|
|
417
|
-
|
|
433
|
+
_server_log(f"Detected workspace from git: {repo_path}")
|
|
418
434
|
else:
|
|
419
|
-
|
|
435
|
+
_server_log(f"Not in git repo (cwd: {Path.cwd()})")
|
|
420
436
|
|
|
421
437
|
# Method 2: If not in git repo, try to find from Claude session files
|
|
422
438
|
if not repo_path:
|
|
423
439
|
claude_projects = Path.home() / ".claude" / "projects"
|
|
424
|
-
|
|
440
|
+
_server_log(f"Checking Claude projects at: {claude_projects}")
|
|
425
441
|
if claude_projects.exists():
|
|
426
442
|
# Find most recently modified session file
|
|
427
443
|
session_files = list(claude_projects.glob("*/*.jsonl"))
|
|
428
|
-
|
|
444
|
+
_server_log(f"Found {len(session_files)} Claude session files")
|
|
429
445
|
if session_files:
|
|
430
446
|
# Sort by modification time, newest first
|
|
431
447
|
session_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
432
|
-
|
|
448
|
+
_server_log(f"Most recent session: {session_files[0]}")
|
|
433
449
|
# Extract project path from directory name
|
|
434
450
|
# Format: -Users-jundewu-Downloads-code-noclue -> /Users/jundewu/Downloads/code/noclue
|
|
435
451
|
project_dir_name = session_files[0].parent.name
|
|
436
|
-
|
|
452
|
+
_server_log(f"Project dir name: {project_dir_name}")
|
|
437
453
|
if project_dir_name.startswith('-'):
|
|
438
454
|
# Convert back to path: -Users-foo-bar -> /Users/foo/bar
|
|
439
455
|
# Note: underscores were also replaced with dashes, but we can't distinguish
|
|
440
456
|
# So we just replace dashes with slashes
|
|
441
457
|
path_str = '/' + project_dir_name[1:].replace('-', '/')
|
|
442
458
|
candidate_path = Path(path_str)
|
|
443
|
-
|
|
459
|
+
_server_log(f"Candidate path: {candidate_path}, exists: {candidate_path.exists()}")
|
|
444
460
|
if candidate_path.exists():
|
|
445
461
|
repo_path = candidate_path
|
|
446
|
-
|
|
462
|
+
_server_log(f"Detected workspace from Claude session: {repo_path}")
|
|
447
463
|
|
|
448
464
|
# Method 3: Fallback to current directory
|
|
449
465
|
if not repo_path:
|
|
450
466
|
repo_path = Path.cwd()
|
|
451
|
-
|
|
467
|
+
_server_log(f"Using current directory: {repo_path}")
|
|
452
468
|
|
|
453
469
|
# Start the watcher
|
|
454
|
-
|
|
470
|
+
_server_log(f"Starting watcher for: {repo_path}")
|
|
455
471
|
await start_watcher(repo_path)
|
|
456
472
|
|
|
457
473
|
except Exception as e:
|
|
458
|
-
|
|
474
|
+
_server_log(f"Warning: Could not start watcher: {e}")
|
|
459
475
|
import traceback
|
|
460
476
|
traceback.print_exc(file=sys.stderr)
|
|
461
477
|
|
|
462
|
-
|
|
478
|
+
_server_log("MCP server ready")
|
|
463
479
|
|
|
464
480
|
async with stdio_server() as (read_stream, write_stream):
|
|
465
481
|
await app.run(
|
realign/mcp_watcher.py
CHANGED
|
@@ -13,6 +13,35 @@ from .config import ReAlignConfig
|
|
|
13
13
|
from .hooks import find_all_active_sessions
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
# File-based logger for debugging when stderr is not visible
|
|
17
|
+
_log_file = None
|
|
18
|
+
|
|
19
|
+
def _log(msg: str):
|
|
20
|
+
"""Log to both stderr and a debug file."""
|
|
21
|
+
global _log_file
|
|
22
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
23
|
+
full_msg = f"[{timestamp}] {msg}"
|
|
24
|
+
print(f"[MCP Watcher] {msg}", file=sys.stderr)
|
|
25
|
+
|
|
26
|
+
# Also write to file for debugging
|
|
27
|
+
if _log_file is None:
|
|
28
|
+
log_path = Path.home() / ".aline_watcher.log"
|
|
29
|
+
try:
|
|
30
|
+
_log_file = open(log_path, "a", buffering=1) # Line buffered
|
|
31
|
+
_log_file.write(f"\n{'='*60}\n")
|
|
32
|
+
_log_file.write(f"[{timestamp}] MCP Watcher Started\n")
|
|
33
|
+
_log_file.write(f"{'='*60}\n")
|
|
34
|
+
except Exception:
|
|
35
|
+
_log_file = False # Mark as failed so we don't retry
|
|
36
|
+
|
|
37
|
+
if _log_file and _log_file is not False:
|
|
38
|
+
try:
|
|
39
|
+
_log_file.write(full_msg + "\n")
|
|
40
|
+
_log_file.flush()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
16
45
|
class DialogueWatcher:
|
|
17
46
|
"""Watch session files and auto-commit immediately after each user request completes."""
|
|
18
47
|
|
|
@@ -30,25 +59,38 @@ class DialogueWatcher:
|
|
|
30
59
|
async def start(self):
|
|
31
60
|
"""Start watching session files."""
|
|
32
61
|
if not self.config.mcp_auto_commit:
|
|
33
|
-
|
|
62
|
+
_log("Auto-commit disabled in config")
|
|
34
63
|
return
|
|
35
64
|
|
|
36
65
|
self.running = True
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
_log("Started watching for dialogue completion")
|
|
67
|
+
_log(f"Mode: Per-request (triggers at end of each Claude response)")
|
|
68
|
+
_log(f"Debounce: {self.debounce_delay}s, Cooldown: {self.min_commit_interval}s")
|
|
69
|
+
_log(f"Home directory: {Path.home()}")
|
|
70
|
+
_log(f"Current working directory: {Path.cwd()}")
|
|
40
71
|
|
|
41
72
|
# Initialize baseline sizes and stop_reason counts
|
|
42
73
|
self.last_session_sizes = self._get_session_sizes()
|
|
43
74
|
self.last_stop_reason_counts = self._get_stop_reason_counts()
|
|
44
75
|
|
|
76
|
+
# Log initial session files being monitored
|
|
77
|
+
if self.last_session_sizes:
|
|
78
|
+
_log(f"Monitoring {len(self.last_session_sizes)} session file(s):")
|
|
79
|
+
for session_path in list(self.last_session_sizes.keys())[:5]: # Show first 5
|
|
80
|
+
_log(f" - {session_path}")
|
|
81
|
+
if len(self.last_session_sizes) > 5:
|
|
82
|
+
_log(f" ... and {len(self.last_session_sizes) - 5} more")
|
|
83
|
+
else:
|
|
84
|
+
claude_projects = Path.home() / ".claude" / "projects"
|
|
85
|
+
_log(f"WARNING: No session files found in {claude_projects}")
|
|
86
|
+
|
|
45
87
|
# Poll for file changes more frequently
|
|
46
88
|
while self.running:
|
|
47
89
|
try:
|
|
48
90
|
await self.check_for_changes()
|
|
49
91
|
await asyncio.sleep(0.5) # Check every 0.5 seconds for responsiveness
|
|
50
92
|
except Exception as e:
|
|
51
|
-
|
|
93
|
+
_log(f"Error: {e}")
|
|
52
94
|
await asyncio.sleep(1.0)
|
|
53
95
|
|
|
54
96
|
async def stop(self):
|
|
@@ -56,7 +98,7 @@ class DialogueWatcher:
|
|
|
56
98
|
self.running = False
|
|
57
99
|
if self.pending_commit_task:
|
|
58
100
|
self.pending_commit_task.cancel()
|
|
59
|
-
|
|
101
|
+
_log("Stopped")
|
|
60
102
|
|
|
61
103
|
def _get_session_sizes(self) -> Dict[str, int]:
|
|
62
104
|
"""Get current sizes of all active session files (from all Claude projects)."""
|
|
@@ -69,7 +111,7 @@ class DialogueWatcher:
|
|
|
69
111
|
if session_file.exists():
|
|
70
112
|
sizes[str(session_file)] = session_file.stat().st_size
|
|
71
113
|
except Exception as e:
|
|
72
|
-
|
|
114
|
+
_log(f"Error getting session sizes: {e}")
|
|
73
115
|
return sizes
|
|
74
116
|
|
|
75
117
|
def _get_stop_reason_counts(self) -> Dict[str, int]:
|
|
@@ -83,7 +125,7 @@ class DialogueWatcher:
|
|
|
83
125
|
if session_file.exists():
|
|
84
126
|
counts[str(session_file)] = self._count_stop_reasons(session_file)
|
|
85
127
|
except Exception as e:
|
|
86
|
-
|
|
128
|
+
_log(f"Error getting stop_reason counts: {e}")
|
|
87
129
|
return counts
|
|
88
130
|
|
|
89
131
|
def _count_stop_reasons(self, session_file: Path) -> int:
|
|
@@ -115,7 +157,7 @@ class DialogueWatcher:
|
|
|
115
157
|
except json.JSONDecodeError:
|
|
116
158
|
continue
|
|
117
159
|
except Exception as e:
|
|
118
|
-
|
|
160
|
+
_log(f"Error counting stop_reasons in {session_file}: {e}")
|
|
119
161
|
return len(unique_message_ids)
|
|
120
162
|
|
|
121
163
|
async def check_for_changes(self):
|
|
@@ -134,7 +176,7 @@ class DialogueWatcher:
|
|
|
134
176
|
old_size = self.last_session_sizes.get(path, 0)
|
|
135
177
|
if size > old_size:
|
|
136
178
|
changed_files.append(Path(path))
|
|
137
|
-
|
|
179
|
+
_log(f"Session file changed: {Path(path).name} ({old_size} -> {size} bytes)")
|
|
138
180
|
|
|
139
181
|
if changed_files:
|
|
140
182
|
# File changed - cancel any pending commit and schedule a new one
|
|
@@ -150,7 +192,7 @@ class DialogueWatcher:
|
|
|
150
192
|
self.last_session_sizes = current_sizes
|
|
151
193
|
|
|
152
194
|
except Exception as e:
|
|
153
|
-
|
|
195
|
+
_log(f"Error checking for changes: {e}")
|
|
154
196
|
|
|
155
197
|
async def _debounced_commit(self, changed_files: list):
|
|
156
198
|
"""Wait for debounce period, then check if dialogue is complete and commit."""
|
|
@@ -161,7 +203,7 @@ class DialogueWatcher:
|
|
|
161
203
|
# Check if any of the changed files contains a complete dialogue turn
|
|
162
204
|
for session_file in changed_files:
|
|
163
205
|
if await self._check_if_turn_complete(session_file):
|
|
164
|
-
|
|
206
|
+
_log(f"Complete turn detected in {session_file.name}")
|
|
165
207
|
|
|
166
208
|
# Extract project path from session file's parent directory
|
|
167
209
|
project_path = self._get_project_path_from_session(session_file)
|
|
@@ -170,16 +212,18 @@ class DialogueWatcher:
|
|
|
170
212
|
current_time = time.time()
|
|
171
213
|
last_time = self.last_commit_time.get(str(project_path), 0)
|
|
172
214
|
if current_time - last_time < self.min_commit_interval:
|
|
173
|
-
|
|
215
|
+
_log(f"Skipping commit for {project_path} (cooldown)")
|
|
174
216
|
continue
|
|
175
217
|
|
|
176
218
|
await self._do_commit(project_path)
|
|
219
|
+
else:
|
|
220
|
+
_log(f"WARNING: Could not extract project path from {session_file}, skipping commit")
|
|
177
221
|
|
|
178
222
|
except asyncio.CancelledError:
|
|
179
223
|
# Task was cancelled because a newer change was detected
|
|
180
224
|
pass
|
|
181
225
|
except Exception as e:
|
|
182
|
-
|
|
226
|
+
_log(f"Error in debounced commit: {e}")
|
|
183
227
|
|
|
184
228
|
async def _check_if_turn_complete(self, session_file: Path) -> bool:
|
|
185
229
|
"""
|
|
@@ -201,7 +245,7 @@ class DialogueWatcher:
|
|
|
201
245
|
|
|
202
246
|
# Commit after each complete assistant response (1 new end_turn)
|
|
203
247
|
if new_stop_reasons >= 1:
|
|
204
|
-
|
|
248
|
+
_log(f"Detected {new_stop_reasons} new end_turn entry(ies) in {session_file.name}")
|
|
205
249
|
# Update baseline immediately to avoid double-counting
|
|
206
250
|
self.last_stop_reason_counts[session_path] = current_count
|
|
207
251
|
return True
|
|
@@ -209,7 +253,7 @@ class DialogueWatcher:
|
|
|
209
253
|
return False
|
|
210
254
|
|
|
211
255
|
except Exception as e:
|
|
212
|
-
|
|
256
|
+
_log(f"Error checking turn completion: {e}")
|
|
213
257
|
return False
|
|
214
258
|
|
|
215
259
|
def _get_project_path_from_session(self, session_file: Path) -> Optional[Path]:
|
|
@@ -221,14 +265,23 @@ class DialogueWatcher:
|
|
|
221
265
|
"""
|
|
222
266
|
try:
|
|
223
267
|
project_dir_name = session_file.parent.name
|
|
268
|
+
_log(f"Extracting project path from: {project_dir_name}")
|
|
269
|
+
|
|
224
270
|
if project_dir_name.startswith('-'):
|
|
225
271
|
# Convert back to path: -Users-foo-bar -> /Users/foo/bar
|
|
226
272
|
path_str = '/' + project_dir_name[1:].replace('-', '/')
|
|
227
273
|
candidate_path = Path(path_str)
|
|
274
|
+
_log(f"Candidate project path: {candidate_path}")
|
|
275
|
+
|
|
228
276
|
if candidate_path.exists():
|
|
277
|
+
_log(f"Project path exists: {candidate_path}")
|
|
229
278
|
return candidate_path
|
|
279
|
+
else:
|
|
280
|
+
_log(f"WARNING: Project path does not exist: {candidate_path}")
|
|
281
|
+
else:
|
|
282
|
+
_log(f"WARNING: Directory name doesn't start with '-': {project_dir_name}")
|
|
230
283
|
except Exception as e:
|
|
231
|
-
|
|
284
|
+
_log(f"Error extracting project path: {e}")
|
|
232
285
|
return None
|
|
233
286
|
|
|
234
287
|
async def _do_commit(self, project_path: Path):
|
|
@@ -238,7 +291,7 @@ class DialogueWatcher:
|
|
|
238
291
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
239
292
|
message = f"chore: Auto-commit MCP session ({timestamp})"
|
|
240
293
|
|
|
241
|
-
|
|
294
|
+
_log(f"Attempting commit in {project_path} with message: {message}")
|
|
242
295
|
|
|
243
296
|
# Use realign commit command with the specific project path
|
|
244
297
|
result = await asyncio.get_event_loop().run_in_executor(
|
|
@@ -249,12 +302,12 @@ class DialogueWatcher:
|
|
|
249
302
|
)
|
|
250
303
|
|
|
251
304
|
if result:
|
|
252
|
-
|
|
305
|
+
_log(f"✓ Committed in {project_path}: {message}")
|
|
253
306
|
self.last_commit_time[str(project_path)] = time.time()
|
|
254
307
|
# Baseline counts already updated in _check_if_turn_complete()
|
|
255
308
|
|
|
256
309
|
except Exception as e:
|
|
257
|
-
|
|
310
|
+
_log(f"Error during commit: {e}")
|
|
258
311
|
|
|
259
312
|
def _run_realign_commit(self, message: str, project_path: Path) -> bool:
|
|
260
313
|
"""
|
|
@@ -274,7 +327,7 @@ class DialogueWatcher:
|
|
|
274
327
|
realign_dir = project_path / ".realign"
|
|
275
328
|
|
|
276
329
|
if not realign_dir.exists():
|
|
277
|
-
|
|
330
|
+
_log(f"Aline not initialized in {project_path}, initializing...")
|
|
278
331
|
|
|
279
332
|
# Auto-initialize Aline (which also inits git if needed)
|
|
280
333
|
init_result = init_repository(
|
|
@@ -284,10 +337,10 @@ class DialogueWatcher:
|
|
|
284
337
|
)
|
|
285
338
|
|
|
286
339
|
if not init_result.get("success"):
|
|
287
|
-
|
|
340
|
+
_log(f"Failed to initialize Aline: {init_result.get('message', 'Unknown error')}")
|
|
288
341
|
return False
|
|
289
342
|
|
|
290
|
-
|
|
343
|
+
_log("✓ Aline initialized successfully")
|
|
291
344
|
|
|
292
345
|
# Now run the commit with stage_all=True
|
|
293
346
|
result = smart_commit(
|
|
@@ -307,11 +360,11 @@ class DialogueWatcher:
|
|
|
307
360
|
else:
|
|
308
361
|
# Log the error for debugging
|
|
309
362
|
error_msg = result.get("message", "Unknown error")
|
|
310
|
-
|
|
363
|
+
_log(f"Commit failed: {error_msg}")
|
|
311
364
|
return False
|
|
312
365
|
|
|
313
366
|
except Exception as e:
|
|
314
|
-
|
|
367
|
+
_log(f"Commit error: {e}")
|
|
315
368
|
return False
|
|
316
369
|
|
|
317
370
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|