aline-ai 0.2.3__py3-none-any.whl → 0.2.5__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.2.3.dist-info → aline_ai-0.2.5.dist-info}/METADATA +2 -1
- {aline_ai-0.2.3.dist-info → aline_ai-0.2.5.dist-info}/RECORD +14 -11
- realign/__init__.py +1 -1
- realign/cli.py +47 -1
- realign/commands/__init__.py +4 -0
- realign/commands/hide.py +1024 -0
- realign/commands/push.py +290 -0
- realign/commands/review.py +492 -0
- realign/mcp_server.py +133 -0
- realign/redactor.py +48 -10
- {aline_ai-0.2.3.dist-info → aline_ai-0.2.5.dist-info}/WHEEL +0 -0
- {aline_ai-0.2.3.dist-info → aline_ai-0.2.5.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.2.3.dist-info → aline_ai-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.2.3.dist-info → aline_ai-0.2.5.dist-info}/top_level.txt +0 -0
realign/commands/push.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Push command - Interactive workflow for reviewing and pushing commits.
|
|
4
|
+
|
|
5
|
+
This command provides an interactive workflow:
|
|
6
|
+
1. Run aline review to show unpushed commits
|
|
7
|
+
2. Ask user if they want to hide any commits
|
|
8
|
+
3. If yes, allow interactive hiding
|
|
9
|
+
4. Ask for final confirmation before pushing
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from .review import review_command, get_unpushed_commits
|
|
18
|
+
from .hide import hide_command, parse_commit_indices
|
|
19
|
+
from ..logging_config import setup_logger
|
|
20
|
+
|
|
21
|
+
logger = setup_logger('realign.commands.push', 'push.log')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def prompt_yes_no(question: str, default: bool = False) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Prompt user for yes/no confirmation.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
question: Question to ask
|
|
30
|
+
default: Default answer if user just presses Enter
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True for yes, False for no
|
|
34
|
+
"""
|
|
35
|
+
default_str = "Y/n" if default else "y/N"
|
|
36
|
+
prompt = f"{question} [{default_str}] "
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
response = input(prompt).strip().lower()
|
|
40
|
+
|
|
41
|
+
if not response:
|
|
42
|
+
return default
|
|
43
|
+
|
|
44
|
+
if response in ('y', 'yes'):
|
|
45
|
+
return True
|
|
46
|
+
elif response in ('n', 'no'):
|
|
47
|
+
return False
|
|
48
|
+
else:
|
|
49
|
+
print("Please answer 'y' or 'n'")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def push_command(
|
|
53
|
+
repo_root: Optional[Path] = None,
|
|
54
|
+
remote: str = "origin",
|
|
55
|
+
branch: Optional[str] = None,
|
|
56
|
+
force: bool = False,
|
|
57
|
+
dry_run: bool = False
|
|
58
|
+
) -> int:
|
|
59
|
+
"""
|
|
60
|
+
Interactive push workflow with review and hide options.
|
|
61
|
+
|
|
62
|
+
Workflow:
|
|
63
|
+
1. Review unpushed commits
|
|
64
|
+
2. Ask if user wants to hide any commits
|
|
65
|
+
3. Interactive hiding loop (if requested)
|
|
66
|
+
4. Final confirmation
|
|
67
|
+
5. Execute git push
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
repo_root: Path to repository root (auto-detected if None)
|
|
71
|
+
remote: Git remote name (default: origin)
|
|
72
|
+
branch: Branch to push (default: current branch)
|
|
73
|
+
force: Skip all confirmation prompts
|
|
74
|
+
dry_run: Don't actually push, just show what would happen
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
0 on success, 1 on error, 2 if cancelled by user
|
|
78
|
+
"""
|
|
79
|
+
logger.info("======== Push command started ========")
|
|
80
|
+
|
|
81
|
+
# Auto-detect repo root if not provided
|
|
82
|
+
if repo_root is None:
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
check=True
|
|
89
|
+
)
|
|
90
|
+
repo_root = Path(result.stdout.strip())
|
|
91
|
+
logger.debug(f"Detected repo root: {repo_root}")
|
|
92
|
+
except subprocess.CalledProcessError:
|
|
93
|
+
print("Error: Not in a git repository", file=sys.stderr)
|
|
94
|
+
logger.error("Not in a git repository")
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
# Detect current branch if not provided
|
|
98
|
+
if branch is None:
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
102
|
+
cwd=repo_root,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
check=True
|
|
106
|
+
)
|
|
107
|
+
branch = result.stdout.strip()
|
|
108
|
+
logger.debug(f"Detected current branch: {branch}")
|
|
109
|
+
except subprocess.CalledProcessError:
|
|
110
|
+
print("Error: Failed to detect current branch", file=sys.stderr)
|
|
111
|
+
logger.error("Failed to detect current branch")
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
# Step 1: Review unpushed commits
|
|
115
|
+
print("\n" + "=" * 60)
|
|
116
|
+
print("STEP 1: Review unpushed commits")
|
|
117
|
+
print("=" * 60 + "\n")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
commits = get_unpushed_commits(repo_root)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Error: Failed to get unpushed commits: {e}", file=sys.stderr)
|
|
123
|
+
logger.error(f"Failed to get unpushed commits: {e}", exc_info=True)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
if not commits:
|
|
127
|
+
print("✓ No unpushed commits found. Nothing to push.\n")
|
|
128
|
+
logger.info("No unpushed commits found")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
# Run review command to display commits
|
|
132
|
+
exit_code = review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
|
|
133
|
+
if exit_code != 0:
|
|
134
|
+
logger.error("Review command failed")
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
# Step 2: Ask if user wants to hide any commits
|
|
138
|
+
if not force:
|
|
139
|
+
print("\n" + "=" * 60)
|
|
140
|
+
print("STEP 2: Hide sensitive commits (optional)")
|
|
141
|
+
print("=" * 60 + "\n")
|
|
142
|
+
|
|
143
|
+
want_to_hide = prompt_yes_no(
|
|
144
|
+
"Do you want to hide any commits before pushing?",
|
|
145
|
+
default=False
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if want_to_hide:
|
|
149
|
+
# Interactive hiding loop
|
|
150
|
+
while True:
|
|
151
|
+
print("\nEnter commit indices to hide (e.g., '1', '1,3,5', '2-4', or 'done'):")
|
|
152
|
+
print(" Type 'done' when finished hiding commits")
|
|
153
|
+
print(" Type 'review' to see the current list of commits")
|
|
154
|
+
print(" Type 'cancel' to abort the push operation")
|
|
155
|
+
|
|
156
|
+
user_input = input("\n> ").strip()
|
|
157
|
+
|
|
158
|
+
if not user_input:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
if user_input.lower() == 'done':
|
|
162
|
+
break
|
|
163
|
+
elif user_input.lower() == 'cancel':
|
|
164
|
+
print("\n❌ Push cancelled by user.\n")
|
|
165
|
+
logger.info("Push cancelled by user during hide phase")
|
|
166
|
+
return 2
|
|
167
|
+
elif user_input.lower() == 'review':
|
|
168
|
+
print("\n" + "-" * 60)
|
|
169
|
+
review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
|
|
170
|
+
print("-" * 60)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Try to parse and hide the commits
|
|
174
|
+
try:
|
|
175
|
+
# Validate indices first
|
|
176
|
+
indices_list = parse_commit_indices(user_input)
|
|
177
|
+
|
|
178
|
+
# Refresh commit list
|
|
179
|
+
commits = get_unpushed_commits(repo_root)
|
|
180
|
+
if not commits:
|
|
181
|
+
print("\n✓ No more unpushed commits to hide.\n")
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
max_index = len(commits)
|
|
185
|
+
invalid_indices = [i for i in indices_list if i < 1 or i > max_index]
|
|
186
|
+
if invalid_indices:
|
|
187
|
+
print(f"❌ Invalid indices (out of range 1-{max_index}): {invalid_indices}")
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Execute hide command
|
|
191
|
+
print()
|
|
192
|
+
exit_code = hide_command(
|
|
193
|
+
indices=user_input,
|
|
194
|
+
repo_root=repo_root,
|
|
195
|
+
force=False # Always ask for confirmation
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if exit_code == 0:
|
|
199
|
+
print("\n✅ Commits hidden successfully.\n")
|
|
200
|
+
logger.info(f"Successfully hidden commits: {user_input}")
|
|
201
|
+
|
|
202
|
+
# Refresh and show updated commit list
|
|
203
|
+
commits = get_unpushed_commits(repo_root)
|
|
204
|
+
if not commits:
|
|
205
|
+
print("✓ No more unpushed commits.\n")
|
|
206
|
+
logger.info("No commits left after hiding")
|
|
207
|
+
return 0
|
|
208
|
+
else:
|
|
209
|
+
print("\n📋 Updated commit list:\n")
|
|
210
|
+
review_command(repo_root=repo_root, verbose=False, detect_secrets=False)
|
|
211
|
+
else:
|
|
212
|
+
print(f"\n❌ Failed to hide commits.\n")
|
|
213
|
+
logger.error(f"Hide command failed for indices: {user_input}")
|
|
214
|
+
|
|
215
|
+
# Ask if user wants to continue
|
|
216
|
+
if not prompt_yes_no("Continue with push workflow?", default=True):
|
|
217
|
+
print("\n❌ Push cancelled.\n")
|
|
218
|
+
return 2
|
|
219
|
+
|
|
220
|
+
except ValueError as e:
|
|
221
|
+
print(f"❌ Invalid input: {e}")
|
|
222
|
+
print(" Examples: '1', '1,3,5', '2-4'")
|
|
223
|
+
continue
|
|
224
|
+
except Exception as e:
|
|
225
|
+
print(f"❌ Error during hide: {e}", file=sys.stderr)
|
|
226
|
+
logger.error(f"Error during hide: {e}", exc_info=True)
|
|
227
|
+
|
|
228
|
+
# Ask if user wants to continue
|
|
229
|
+
if not prompt_yes_no("Continue with push workflow?", default=True):
|
|
230
|
+
print("\n❌ Push cancelled.\n")
|
|
231
|
+
return 2
|
|
232
|
+
|
|
233
|
+
# Step 3: Final confirmation
|
|
234
|
+
if not force:
|
|
235
|
+
print("\n" + "=" * 60)
|
|
236
|
+
print("STEP 3: Final confirmation")
|
|
237
|
+
print("=" * 60 + "\n")
|
|
238
|
+
|
|
239
|
+
# Refresh commit list one more time
|
|
240
|
+
try:
|
|
241
|
+
commits = get_unpushed_commits(repo_root)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
print(f"Error: Failed to refresh commits: {e}", file=sys.stderr)
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
if not commits:
|
|
247
|
+
print("✓ No unpushed commits. Nothing to push.\n")
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
print(f"Ready to push {len(commits)} commit(s) to {remote}/{branch}\n")
|
|
251
|
+
|
|
252
|
+
if not prompt_yes_no("Proceed with git push?", default=True):
|
|
253
|
+
print("\n❌ Push cancelled by user.\n")
|
|
254
|
+
logger.info("Push cancelled by user at final confirmation")
|
|
255
|
+
return 2
|
|
256
|
+
|
|
257
|
+
# Step 4: Execute git push
|
|
258
|
+
print("\n" + "=" * 60)
|
|
259
|
+
print("STEP 4: Pushing to remote")
|
|
260
|
+
print("=" * 60 + "\n")
|
|
261
|
+
|
|
262
|
+
if dry_run:
|
|
263
|
+
print(f"🔍 DRY RUN: Would execute: git push {remote} {branch}\n")
|
|
264
|
+
logger.info(f"Dry run: would push to {remote}/{branch}")
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
print(f"🚀 Pushing to {remote}/{branch}...")
|
|
269
|
+
|
|
270
|
+
result = subprocess.run(
|
|
271
|
+
["git", "push", remote, branch],
|
|
272
|
+
cwd=repo_root,
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if result.returncode == 0:
|
|
278
|
+
print("\n✅ Successfully pushed to remote!\n")
|
|
279
|
+
print(result.stdout)
|
|
280
|
+
logger.info(f"Successfully pushed to {remote}/{branch}")
|
|
281
|
+
return 0
|
|
282
|
+
else:
|
|
283
|
+
print(f"\n❌ Push failed:\n{result.stderr}\n", file=sys.stderr)
|
|
284
|
+
logger.error(f"Git push failed: {result.stderr}")
|
|
285
|
+
return 1
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print(f"\n❌ Error during push: {e}\n", file=sys.stderr)
|
|
289
|
+
logger.error(f"Error during push: {e}", exc_info=True)
|
|
290
|
+
return 1
|