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.
@@ -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