flutter-dev 0.1.0__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.
managers/git.py ADDED
@@ -0,0 +1,981 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git Manager - Git tag, commit, version functions
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import subprocess
9
+
10
+ from common_utils import (
11
+ RED, GREEN, YELLOW, BLUE, NC,
12
+ timer_decorator,
13
+ is_windows,
14
+ )
15
+ from core.constants import PATTERNS
16
+ from managers.build import run_flutter_command
17
+
18
+
19
+ def get_version_from_pubspec():
20
+ """Get the version from pubspec.yaml using regex"""
21
+ if os.path.isfile("pubspec.yaml"):
22
+ with open("pubspec.yaml", 'r', encoding='utf-8') as file:
23
+ try:
24
+ content = file.read()
25
+ # Use regex to find the version field in pubspec.yaml
26
+ version_match = PATTERNS['version'].search(content)
27
+ if version_match:
28
+ version = version_match.group(1).strip()
29
+ # Remove quotes if present and split by + to get only version number
30
+ version = version.strip('"\'').split('+')[0]
31
+ return version
32
+ else:
33
+ print(f"{RED}Error: Could not find 'version' field in pubspec.yaml.{NC}")
34
+ return None
35
+ except Exception as e:
36
+ print(f"{RED}Error: Could not read pubspec.yaml: {e}{NC}")
37
+ return None
38
+ else:
39
+ print(f"{RED}Error: pubspec.yaml not found in the current directory.{NC}")
40
+ print(f"{YELLOW}Please run this command from the root of a Flutter project.{NC}")
41
+ return None
42
+
43
+
44
+ def parse_version(version_str):
45
+ """Parse version string (e.g., 'v1.2.3' or '1.2.3') into tuple (1, 2, 3)"""
46
+ # Remove 'v' prefix if present
47
+ version_str = version_str.lstrip('v')
48
+ try:
49
+ parts = version_str.split('.')
50
+ return tuple(int(p) for p in parts[:3]) # Return only major.minor.patch
51
+ except:
52
+ return None
53
+
54
+
55
+ def get_all_tags():
56
+ """Get all tags from both local and remote repositories"""
57
+ all_tags = set()
58
+
59
+ # Get local tags
60
+ try:
61
+ result = subprocess.run(["git", "tag", "-l"],
62
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
63
+ if result.stdout.strip():
64
+ local_tags = result.stdout.strip().split('\n')
65
+ all_tags.update(local_tags)
66
+ except Exception as e:
67
+ print(f"{YELLOW}Warning: Could not get local tags: {e}{NC}")
68
+
69
+ # Get remote tags
70
+ try:
71
+ result = subprocess.run(["git", "ls-remote", "--tags", "origin"],
72
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
73
+ if result.stdout.strip():
74
+ for line in result.stdout.strip().split('\n'):
75
+ # Extract tag name from "hash refs/tags/v1.0.0"
76
+ if 'refs/tags/' in line:
77
+ tag = line.split('refs/tags/')[-1]
78
+ # Skip ^{} references
79
+ if not tag.endswith('^{}'):
80
+ all_tags.add(tag)
81
+ except Exception as e:
82
+ print(f"{YELLOW}Warning: Could not get remote tags: {e}{NC}")
83
+
84
+ return sorted(all_tags)
85
+
86
+
87
+ def increment_version(version_tuple):
88
+ """Increment patch version: (1, 2, 3) -> (1, 2, 4)"""
89
+ major, minor, patch = version_tuple
90
+ return (major, minor, patch + 1)
91
+
92
+
93
+ def get_build_number_from_pubspec():
94
+ """Get the build number from pubspec.yaml (e.g., from '1.0.0+5' returns 5)"""
95
+ if os.path.isfile("pubspec.yaml"):
96
+ with open("pubspec.yaml", 'r', encoding='utf-8') as file:
97
+ try:
98
+ content = file.read()
99
+ # Find version line with build number
100
+ version_match = PATTERNS['version_with_build'].search(content)
101
+ if version_match:
102
+ build_number = int(version_match.group(2))
103
+ return build_number
104
+ return None
105
+ except Exception:
106
+ return None
107
+ return None
108
+
109
+
110
+ def update_pubspec_version(new_version):
111
+ """Update version in pubspec.yaml file, preserving and incrementing build number
112
+ Returns: (success, build_number) tuple
113
+ """
114
+ if not os.path.isfile("pubspec.yaml"):
115
+ print(f"{RED}Error: pubspec.yaml not found in the current directory.{NC}")
116
+ return (False, None)
117
+
118
+ try:
119
+ with open("pubspec.yaml", 'r', encoding='utf-8') as file:
120
+ content = file.read()
121
+
122
+ # Get current build number
123
+ current_build = get_build_number_from_pubspec()
124
+
125
+ # Increment build number or start from 1
126
+ new_build = (current_build + 1) if current_build is not None else 1
127
+
128
+ # Create new version string with build number
129
+ new_version_with_build = f"{new_version}+{new_build}"
130
+
131
+ # Find the version line and replace it
132
+ new_content = PATTERNS['version_line'].sub(
133
+ f'version: {new_version_with_build}',
134
+ content
135
+ )
136
+
137
+ # Write back to file
138
+ with open("pubspec.yaml", 'w', encoding='utf-8') as file:
139
+ file.write(new_content)
140
+
141
+ print(f"{GREEN} Build number: {current_build or 0} → {new_build}{NC}")
142
+ return (True, new_build)
143
+ except Exception as e:
144
+ print(f"{RED}Error updating pubspec.yaml: {e}{NC}")
145
+ return (False, None)
146
+
147
+
148
+ def commit_version_change(version, build_number=None):
149
+ """Commit the pubspec.yaml version change"""
150
+ # Add pubspec.yaml to staging
151
+ result = subprocess.run(
152
+ ["git", "add", "pubspec.yaml"],
153
+ capture_output=True,
154
+ text=True,
155
+ encoding='utf-8',
156
+ errors='replace'
157
+ )
158
+
159
+ if result.returncode != 0:
160
+ print(f"{RED}Failed to stage pubspec.yaml{NC}")
161
+ return False
162
+
163
+ # Commit with version bump message
164
+ if build_number:
165
+ commit_message = f"chore: bump version to {version}+{build_number}"
166
+ else:
167
+ commit_message = f"chore: bump version to {version}"
168
+
169
+ result = subprocess.run(
170
+ ["git", "commit", "-m", commit_message],
171
+ capture_output=True,
172
+ text=True,
173
+ encoding='utf-8',
174
+ errors='replace'
175
+ )
176
+
177
+ if result.returncode != 0:
178
+ print(f"{RED}Failed to commit version change{NC}")
179
+ return False
180
+
181
+ return True
182
+
183
+
184
+ def create_and_push_tag():
185
+ """Create git tag by auto-incrementing the latest existing tag and push to remote"""
186
+ print(f"{YELLOW}Creating and pushing git tag...{NC}\n")
187
+
188
+ # Get all existing tags
189
+ print(f"{BLUE}Checking existing tags...{NC}")
190
+ all_tags = get_all_tags()
191
+
192
+ if all_tags:
193
+ print(f"{GREEN}Found {len(all_tags)} existing tag(s):{NC}")
194
+ for tag in all_tags[-5:]: # Show last 5 tags
195
+ print(f" • {tag}")
196
+ if len(all_tags) > 5:
197
+ print(f" ... and {len(all_tags) - 5} more")
198
+ print()
199
+
200
+ # Find the latest version
201
+ latest_version = None
202
+ latest_tag = None
203
+
204
+ for tag in all_tags:
205
+ parsed = parse_version(tag)
206
+ if parsed:
207
+ if latest_version is None or parsed > latest_version:
208
+ latest_version = parsed
209
+ latest_tag = tag
210
+
211
+ # Determine new version
212
+ if latest_version:
213
+ new_version = increment_version(latest_version)
214
+ new_tag = f"v{new_version[0]}.{new_version[1]}.{new_version[2]}"
215
+ new_version_str = f"{new_version[0]}.{new_version[1]}.{new_version[2]}"
216
+ print(f"{BLUE}Latest tag: {latest_tag} → New tag: {new_tag}{NC}\n")
217
+ else:
218
+ # No existing tags, use version from pubspec
219
+ version = get_version_from_pubspec()
220
+ if not version:
221
+ print(f"{YELLOW}No existing tags found and cannot read pubspec version.{NC}")
222
+ print(f"{YELLOW}Using default: v1.0.0{NC}\n")
223
+ new_tag = "v1.0.0"
224
+ new_version_str = "1.0.0"
225
+ else:
226
+ new_tag = f"v{version}"
227
+ new_version_str = version
228
+ print(f"{BLUE}No existing tags found. Creating first tag: {new_tag}{NC}\n")
229
+
230
+ # Confirm with user
231
+ user_input = input(f"Update pubspec.yaml, commit and create tag {GREEN}{new_tag}{NC}? (Y/n): ")
232
+ if user_input.lower() == 'n':
233
+ print(f"{YELLOW}Operation cancelled.{NC}")
234
+ return False
235
+
236
+ # Step 1: Update pubspec.yaml version
237
+ print(f"{BLUE}Updating pubspec.yaml version to {new_version_str}...{NC}")
238
+ success, build_number = update_pubspec_version(new_version_str)
239
+ if not success:
240
+ print(f"{RED}Failed to update pubspec.yaml{NC}")
241
+ return False
242
+ print(f"{GREEN}✓ pubspec.yaml updated to {new_version_str}+{build_number}{NC}")
243
+
244
+ # Step 2: Commit the version change
245
+ print(f"{BLUE}Committing version change...{NC}")
246
+ if not commit_version_change(new_version_str, build_number):
247
+ print(f"{RED}Failed to commit version change{NC}")
248
+ return False
249
+ print(f"{GREEN}✓ Version change committed{NC}")
250
+
251
+ # Step 3: Create git tag
252
+ success = run_flutter_command(["git", "tag", new_tag], f"Creating tag {new_tag}... ")
253
+ if not success:
254
+ print(f"{RED}Failed to create git tag.{NC}")
255
+ return False
256
+
257
+ # Step 4: Push commit to remote
258
+ print(f"{BLUE}Pushing commit to remote...{NC}")
259
+ success = run_flutter_command(["git", "push"], f"Pushing commit... ")
260
+ if not success:
261
+ print(f"{RED}Failed to push commit to remote.{NC}")
262
+ return False
263
+
264
+ # Step 5: Push tag to remote
265
+ success = run_flutter_command(["git", "push", "-u", "origin", new_tag], f"Pushing tag to remote... ")
266
+ if not success:
267
+ print(f"{RED}Failed to push tag to remote.{NC}")
268
+ return False
269
+
270
+ print(f"\n{GREEN}✓ Version {new_version_str}+{build_number} updated, committed, and git tag {new_tag} created and pushed successfully!{NC}")
271
+ return True
272
+
273
+
274
+ def discard_changes(discard_all=True):
275
+ """Discard all uncommitted changes in the current git repository
276
+
277
+ Args:
278
+ discard_all: If True, also removes untracked files (git clean -fd)
279
+ """
280
+ print(f"{YELLOW}Discarding uncommitted changes...{NC}\n")
281
+
282
+ # Check if git repository
283
+ try:
284
+ subprocess.run(["git", "status"], check=True, capture_output=True)
285
+ except subprocess.CalledProcessError:
286
+ print(f"{RED}Error: Not a git repository or git not available{NC}")
287
+ return False
288
+
289
+ # Check for changes
290
+ try:
291
+ # Get tracked file changes
292
+ result = subprocess.run(["git", "status", "--porcelain"],
293
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
294
+ changes = result.stdout.strip()
295
+
296
+ if not changes:
297
+ print(f"{GREEN}No uncommitted changes to discard{NC}")
298
+ return True
299
+
300
+ # Parse and display changes
301
+ modified_files = []
302
+ untracked_files = []
303
+
304
+ for line in changes.split('\n'):
305
+ if line:
306
+ status = line[:2]
307
+ filename = line[3:]
308
+ if status.strip() == '??':
309
+ untracked_files.append(filename)
310
+ else:
311
+ modified_files.append(filename)
312
+
313
+ # Show what will be discarded
314
+ if modified_files:
315
+ print(f"{BLUE}Modified/Staged files to reset ({len(modified_files)}):{NC}")
316
+ for f in modified_files[:10]:
317
+ print(f" {RED}✗{NC} {f}")
318
+ if len(modified_files) > 10:
319
+ print(f" ... and {len(modified_files) - 10} more")
320
+ print()
321
+
322
+ if untracked_files and discard_all:
323
+ print(f"{BLUE}Untracked files to delete ({len(untracked_files)}):{NC}")
324
+ for f in untracked_files[:10]:
325
+ print(f" {RED}✗{NC} {f}")
326
+ if len(untracked_files) > 10:
327
+ print(f" ... and {len(untracked_files) - 10} more")
328
+ print()
329
+
330
+ # Ask for confirmation
331
+ total = len(modified_files) + (len(untracked_files) if discard_all else 0)
332
+ print(f"{YELLOW}⚠ WARNING: This action cannot be undone!{NC}")
333
+ user_input = input(f"Discard {total} file(s)? (Y/n): ")
334
+
335
+ if user_input.lower() == 'n':
336
+ print(f"{YELLOW}Operation cancelled{NC}")
337
+ return False
338
+
339
+ # Reset tracked file changes
340
+ if modified_files:
341
+ print(f"{BLUE}Resetting tracked files...{NC}")
342
+ result = subprocess.run(["git", "checkout", "."],
343
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
344
+ if result.returncode != 0:
345
+ print(f"{RED}Error resetting tracked files: {result.stderr}{NC}")
346
+ return False
347
+
348
+ # Also reset staged changes
349
+ subprocess.run(["git", "reset", "HEAD"],
350
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
351
+ print(f"{GREEN}✓ Tracked files reset{NC}")
352
+
353
+ # Remove untracked files if requested
354
+ if untracked_files and discard_all:
355
+ print(f"{BLUE}Removing untracked files...{NC}")
356
+ result = subprocess.run(["git", "clean", "-fd"],
357
+ capture_output=True, text=True, encoding='utf-8', errors='replace')
358
+ if result.returncode != 0:
359
+ print(f"{RED}Error removing untracked files: {result.stderr}{NC}")
360
+ return False
361
+ print(f"{GREEN}✓ Untracked files removed{NC}")
362
+
363
+ print(f"\n{GREEN}✓ All uncommitted changes discarded successfully!{NC}")
364
+ return True
365
+
366
+ except subprocess.CalledProcessError as e:
367
+ print(f"{RED}Error checking git changes: {e}{NC}")
368
+ return False
369
+
370
+
371
+ @timer_decorator
372
+ def smart_commit():
373
+ """Generate git diff, create commit message using Gemini AI, and commit"""
374
+ print(f"{YELLOW}Smart Git Commit...{NC}\n")
375
+
376
+ current_dir = os.getcwd()
377
+
378
+ # Check if git repository
379
+ try:
380
+ subprocess.run(["git", "status"], check=True, capture_output=True)
381
+ except subprocess.CalledProcessError:
382
+ print(f"{RED}Error: Not a git repository or git not available{NC}")
383
+ return False
384
+
385
+ # Check for changes
386
+ try:
387
+ result = subprocess.run(["git", "diff", "--staged"], capture_output=True, text=True, encoding='utf-8', errors='replace')
388
+ staged_changes = result.stdout.strip()
389
+
390
+ result = subprocess.run(["git", "diff"], capture_output=True, text=True, encoding='utf-8', errors='replace')
391
+ unstaged_changes = result.stdout.strip()
392
+
393
+ if not staged_changes and not unstaged_changes:
394
+ print(f"{YELLOW}No changes detected to commit{NC}")
395
+ return False
396
+
397
+ except subprocess.CalledProcessError as e:
398
+ print(f"{RED}Error checking git changes: {e}{NC}")
399
+ return False
400
+
401
+ # Get all changes (staged + unstaged)
402
+ try:
403
+ result = subprocess.run(["git", "diff", "HEAD"], capture_output=True, text=True, encoding='utf-8', errors='replace')
404
+ all_changes = result.stdout.strip()
405
+
406
+ if not all_changes:
407
+ # If no changes from HEAD, get staged changes only
408
+ all_changes = staged_changes
409
+
410
+ except subprocess.CalledProcessError:
411
+ all_changes = staged_changes + "\n" + unstaged_changes
412
+
413
+ if not all_changes:
414
+ print(f"{YELLOW}No changes to analyze{NC}")
415
+ return False
416
+
417
+ # Import AI API helper
418
+ try:
419
+ from gemini_api import generate_commit_message
420
+
421
+ except ImportError as e:
422
+ print(f"{RED}Error importing AI API helper: {e}{NC}")
423
+ return False
424
+
425
+ # Generate commit message
426
+ commit_message = generate_commit_message(all_changes)
427
+
428
+ if not commit_message:
429
+ print(f"{RED}Failed to generate commit message{NC}")
430
+ return False
431
+
432
+ print(f"\n{BLUE}Generated commit message:{NC}")
433
+
434
+ # Process commit message to add "-" to description lines
435
+ lines = commit_message.split('\n')
436
+ processed_lines = []
437
+
438
+ for i, line in enumerate(lines):
439
+ # Skip the first line (title) and empty lines
440
+ if i == 0 or line.strip() == "":
441
+ processed_lines.append(line)
442
+ else:
443
+ # Add "-" to non-empty description lines
444
+ if line.strip():
445
+ processed_lines.append(f"- {line}")
446
+ else:
447
+ processed_lines.append(line)
448
+
449
+ formatted_commit_message = '\n'.join(processed_lines)
450
+ print(f"{GREEN}{formatted_commit_message}{NC}\n")
451
+
452
+ # Ask for confirmation
453
+ user_input = input(f"Proceed with this commit? (Y/n): ")
454
+ if user_input.lower() == 'n':
455
+ print(f"{YELLOW}Commit cancelled{NC}")
456
+ return False
457
+
458
+ # Stage all changes if there are unstaged changes
459
+ if unstaged_changes:
460
+ print(f"{YELLOW}Staging all changes...{NC}")
461
+ try:
462
+ subprocess.run(["git", "add", "."], check=True)
463
+ print(f"{GREEN}✓ Changes staged{NC}")
464
+ except subprocess.CalledProcessError as e:
465
+ print(f"{RED}Error staging changes: {e}{NC}")
466
+ return False
467
+
468
+ # Commit with generated message
469
+ try:
470
+ subprocess.run(["git", "commit", "-m", formatted_commit_message], check=True)
471
+ print(f"\n{GREEN}✓ Commit successful!{NC}")
472
+
473
+ # Wait 1.5 seconds to show success message
474
+ time.sleep(1.5)
475
+
476
+ # Clear terminal after successful commit
477
+ if is_windows():
478
+ os.system('cls')
479
+ else:
480
+ os.system('clear')
481
+
482
+ print(f"{GREEN}✓ Commit completed and terminal cleared!{NC}")
483
+ print(f"{BLUE}Ready for next commit 🚀{NC}\n")
484
+
485
+ return True
486
+
487
+ except subprocess.CalledProcessError as e:
488
+ print(f"{RED}Error creating commit: {e}{NC}")
489
+ return False
490
+
491
+
492
+ def sync_branches(branch_names):
493
+ """Merge current branch with specified branches bidirectionally, push to all branches automatically
494
+
495
+ This function:
496
+ 1. Fetches latest changes from remote
497
+ 2. Merges specified branches INTO current branch
498
+ 3. Pushes current branch to origin
499
+ 4. Merges current branch INTO each specified branch and pushes them
500
+
501
+ Result: All specified branches + current branch become fully synchronized
502
+
503
+ Args:
504
+ branch_names: List of branch names to sync with (e.g., ["dev-farhan", "dev-sufi"])
505
+ """
506
+ print(f"{YELLOW}Syncing with branches: {', '.join(branch_names)}...{NC}\n")
507
+
508
+ # Track if any actual changes were made
509
+ changes_made = False
510
+ actions_taken = []
511
+
512
+ # Check if git repository
513
+ try:
514
+ subprocess.run(["git", "status"], check=True, capture_output=True)
515
+ except subprocess.CalledProcessError:
516
+ print(f"{RED}Error: Not a git repository or git not available{NC}")
517
+ return False
518
+
519
+ # Get current branch name
520
+ try:
521
+ result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"],
522
+ capture_output=True, text=True, check=True,
523
+ encoding='utf-8', errors='replace')
524
+ current_branch = result.stdout.strip()
525
+ print(f"{BLUE}Current branch: {current_branch}{NC}")
526
+ except subprocess.CalledProcessError as e:
527
+ print(f"{RED}Error getting current branch: {e}{NC}")
528
+ return False
529
+
530
+ # Check for uncommitted changes
531
+ try:
532
+ result = subprocess.run(["git", "status", "--porcelain"],
533
+ capture_output=True, text=True, check=True,
534
+ encoding='utf-8', errors='replace')
535
+ if result.stdout.strip():
536
+ print(f"{RED}Error: You have uncommitted changes{NC}")
537
+ print(f"{YELLOW}Please commit or stash your changes first{NC}")
538
+ return False
539
+ except subprocess.CalledProcessError as e:
540
+ print(f"{RED}Error checking git status: {e}{NC}")
541
+ return False
542
+
543
+ # Step 1: Fetch latest changes (silent)
544
+ result = subprocess.run(
545
+ ["git", "fetch", "origin"],
546
+ capture_output=True, text=True,
547
+ encoding='utf-8', errors='replace'
548
+ )
549
+ if result.returncode != 0:
550
+ print(f"{RED}Failed to fetch latest changes{NC}")
551
+ return False
552
+
553
+ # Step 2: Check for unpushed commits and auto-push
554
+ had_unpushed = False
555
+ try:
556
+ result = subprocess.run(
557
+ ["git", "rev-list", f"origin/{current_branch}..{current_branch}"],
558
+ capture_output=True, text=True,
559
+ encoding='utf-8', errors='replace'
560
+ )
561
+ unpushed_commits = result.stdout.strip().split('\n') if result.stdout.strip() else []
562
+
563
+ if unpushed_commits and unpushed_commits[0]:
564
+ had_unpushed = True
565
+ changes_made = True
566
+ print(f"{YELLOW}Found {len(unpushed_commits)} unpushed commit(s) on {current_branch}{NC}")
567
+ success = run_flutter_command(
568
+ ["git", "push", "origin", current_branch],
569
+ f"Pushing {current_branch} to origin... "
570
+ )
571
+ if not success:
572
+ print(f"{RED}Failed to push {current_branch} to origin{NC}")
573
+ return False
574
+ actions_taken.append(f"Pushed {len(unpushed_commits)} commit(s) from {current_branch}")
575
+ except subprocess.CalledProcessError:
576
+ # Remote branch might not exist, try to push anyway
577
+ had_unpushed = True
578
+ changes_made = True
579
+ print(f"{YELLOW}Remote branch not found, pushing {current_branch}...{NC}")
580
+ success = run_flutter_command(
581
+ ["git", "push", "-u", "origin", current_branch],
582
+ f"Pushing {current_branch} to origin... "
583
+ )
584
+ if not success:
585
+ print(f"{RED}Failed to push {current_branch} to origin{NC}")
586
+ return False
587
+ actions_taken.append(f"Created and pushed {current_branch}")
588
+
589
+ all_success = True
590
+ successfully_merged_branches = []
591
+ branches_with_changes = []
592
+
593
+ # Step 3: Merge each branch into current
594
+ for branch_name in branch_names:
595
+ merge_process = subprocess.run(
596
+ ["git", "merge", branch_name],
597
+ capture_output=True,
598
+ text=True,
599
+ encoding='utf-8',
600
+ errors='replace'
601
+ )
602
+
603
+ if merge_process.returncode == 0:
604
+ if "Already up to date" in merge_process.stdout or "Already up-to-date" in merge_process.stdout:
605
+ pass # No changes, stay silent
606
+ else:
607
+ changes_made = True
608
+ branches_with_changes.append(branch_name)
609
+ print(f"{GREEN}✓ Merged {branch_name} → {current_branch}{NC}")
610
+ successfully_merged_branches.append(branch_name)
611
+ else:
612
+ # Check for merge conflicts
613
+ try:
614
+ result = subprocess.run(
615
+ ["git", "diff", "--name-only", "--diff-filter=U"],
616
+ capture_output=True,
617
+ text=True,
618
+ check=True,
619
+ encoding='utf-8',
620
+ errors='replace'
621
+ )
622
+ conflicted_files = result.stdout.strip().split('\n') if result.stdout.strip() else []
623
+
624
+ if conflicted_files:
625
+ print(f"{RED}✗ Merge conflict detected in {len(conflicted_files)} file(s):{NC}")
626
+ for file in conflicted_files:
627
+ print(f" • {file}")
628
+
629
+ print(f"\n{BLUE}Opening conflicted files in VSCode...{NC}")
630
+
631
+ valid_files = [f for f in conflicted_files if f]
632
+ if valid_files:
633
+ try:
634
+ subprocess.run(["code"] + valid_files, check=True)
635
+ print(f"{GREEN}✓ Opened all {len(valid_files)} file(s) in VSCode{NC}")
636
+ except subprocess.CalledProcessError:
637
+ print(f"{RED}✗ Failed to open files in VSCode{NC}")
638
+ except FileNotFoundError:
639
+ print(f"{RED}Error: VSCode (code) command not found{NC}")
640
+ print(f"{YELLOW}Please install VSCode CLI or resolve conflicts manually{NC}")
641
+
642
+ print(f"\n{YELLOW}Please resolve the conflicts in VSCode and then:{NC}")
643
+ print(f" 1. Stage the resolved files: {BLUE}git add <file>{NC}")
644
+ print(f" 2. Complete the merge: {BLUE}git commit{NC}")
645
+ print(f" 3. Run sync again to push changes{NC}")
646
+ print(f" Or abort the merge: {BLUE}git merge --abort{NC}")
647
+
648
+ all_success = False
649
+ break
650
+ else:
651
+ print(f"{RED}✗ Merge failed: {merge_process.stderr}{NC}")
652
+ all_success = False
653
+ break
654
+
655
+ except subprocess.CalledProcessError as e:
656
+ print(f"{RED}Error checking for conflicts: {e}{NC}")
657
+ all_success = False
658
+ break
659
+
660
+ if not all_success:
661
+ print(f"\n{RED}⚠ Sync stopped due to conflicts or errors{NC}")
662
+ return False
663
+
664
+ # Step 4: Push current branch (silent if no changes)
665
+ if branches_with_changes:
666
+ push_result = subprocess.run(
667
+ ["git", "push", "origin", current_branch],
668
+ capture_output=True, text=True,
669
+ encoding='utf-8', errors='replace'
670
+ )
671
+ if push_result.returncode != 0:
672
+ print(f"{RED}Failed to push {current_branch}{NC}")
673
+ return False
674
+ actions_taken.append(f"Pushed {current_branch} with merged changes")
675
+
676
+ # Step 5: Push merged changes to source branches
677
+ successfully_pushed_branches = []
678
+ failed_push_branches = []
679
+
680
+ for branch_name in successfully_merged_branches:
681
+ # Checkout the branch
682
+ checkout_result = subprocess.run(
683
+ ["git", "checkout", branch_name],
684
+ capture_output=True,
685
+ text=True,
686
+ encoding='utf-8',
687
+ errors='replace'
688
+ )
689
+
690
+ if checkout_result.returncode != 0:
691
+ print(f"{RED}✗ Failed to checkout {branch_name}: {checkout_result.stderr}{NC}")
692
+ failed_push_branches.append(branch_name)
693
+ continue
694
+
695
+ # Merge current branch into this branch
696
+ merge_result = subprocess.run(
697
+ ["git", "merge", current_branch],
698
+ capture_output=True,
699
+ text=True,
700
+ encoding='utf-8',
701
+ errors='replace'
702
+ )
703
+
704
+ if merge_result.returncode != 0:
705
+ print(f"{RED}✗ Failed to merge {current_branch} into {branch_name}{NC}")
706
+ subprocess.run(["git", "checkout", current_branch], capture_output=True)
707
+ failed_push_branches.append(branch_name)
708
+ continue
709
+
710
+ branch_had_changes = False
711
+ if "Already up to date" in merge_result.stdout or "Already up-to-date" in merge_result.stdout:
712
+ pass # Silent when up to date
713
+ else:
714
+ branch_had_changes = True
715
+ changes_made = True
716
+ print(f"{GREEN}✓ Merged {current_branch} → {branch_name}{NC}")
717
+
718
+ # Push the branch
719
+ push_result = subprocess.run(
720
+ ["git", "push", "origin", branch_name],
721
+ capture_output=True,
722
+ text=True,
723
+ encoding='utf-8',
724
+ errors='replace'
725
+ )
726
+
727
+ if push_result.returncode == 0:
728
+ if branch_had_changes:
729
+ print(f"{GREEN}✓ Pushed {branch_name} to origin{NC}")
730
+ actions_taken.append(f"Updated {branch_name}")
731
+ successfully_pushed_branches.append(branch_name)
732
+ else:
733
+ # Handle non-fast-forward push
734
+ if "non-fast-forward" in push_result.stderr or "rejected" in push_result.stderr:
735
+ print(f"{YELLOW}⚠ Remote {branch_name} has new changes, pulling and retrying...{NC}")
736
+ changes_made = True
737
+
738
+ pull_result = subprocess.run(
739
+ ["git", "pull", "origin", branch_name, "--no-rebase", "--no-edit"],
740
+ capture_output=True,
741
+ text=True,
742
+ encoding='utf-8',
743
+ errors='replace'
744
+ )
745
+
746
+ if pull_result.returncode != 0:
747
+ print(f"{RED}✗ Failed to pull {branch_name}: {pull_result.stderr}{NC}")
748
+ failed_push_branches.append(branch_name)
749
+ continue
750
+ print(f"{GREEN}✓ Pulled latest changes from {branch_name}{NC}")
751
+
752
+ push_retry = subprocess.run(
753
+ ["git", "push", "origin", branch_name],
754
+ capture_output=True,
755
+ text=True,
756
+ encoding='utf-8',
757
+ errors='replace'
758
+ )
759
+
760
+ if push_retry.returncode == 0:
761
+ print(f"{GREEN}✓ Pushed {branch_name} to origin{NC}")
762
+ successfully_pushed_branches.append(branch_name)
763
+ actions_taken.append(f"Synced {branch_name} with remote")
764
+ else:
765
+ print(f"{RED}✗ Failed to push {branch_name} after retry: {push_retry.stderr}{NC}")
766
+ failed_push_branches.append(branch_name)
767
+ else:
768
+ print(f"{RED}✗ Failed to push {branch_name}: {push_result.stderr}{NC}")
769
+ failed_push_branches.append(branch_name)
770
+
771
+ # Return to current branch (silent)
772
+ subprocess.run(["git", "checkout", current_branch], capture_output=True)
773
+
774
+ # Summary - compact if no changes, detailed if changes made
775
+ if not changes_made and not failed_push_branches:
776
+ print(f"\n{GREEN}✓ Everything already in sync - no changes needed{NC}")
777
+ return True
778
+
779
+ # Detailed summary when changes were made
780
+ print(f"\n{'='*55}")
781
+ if failed_push_branches:
782
+ print(f"{YELLOW}⚠ Sync completed with issues{NC}")
783
+ else:
784
+ print(f"{GREEN}✓ Sync complete!{NC}")
785
+ print(f"{'='*55}")
786
+
787
+ if actions_taken:
788
+ print(f"{BLUE}Actions taken:{NC}")
789
+ for action in actions_taken:
790
+ print(f" {GREEN}•{NC} {action}")
791
+
792
+ if failed_push_branches:
793
+ print(f" {RED}• Failed: {', '.join(failed_push_branches)}{NC}")
794
+ print(f"{YELLOW}Some branches failed to update. You may need to manually resolve.{NC}")
795
+
796
+ return len(failed_push_branches) == 0
797
+
798
+
799
+ def deploy_to_deployment():
800
+ """Merge current branch into 'deployment' branch and push (one-way deploy)
801
+
802
+ This function:
803
+ 1. Fetches latest changes from remote
804
+ 2. Auto-pushes current branch if unpushed commits exist
805
+ 3. Switches to 'deployment' branch
806
+ 4. Pulls latest 'deployment' from remote
807
+ 5. Merges current branch INTO 'deployment'
808
+ 6. Pushes 'deployment' to remote
809
+ 7. Switches back to original branch
810
+
811
+ Use case: Deploy feature branch to deployment testing branch
812
+ """
813
+ print(f"{YELLOW}Deploying to deployment...{NC}\n")
814
+
815
+ # Track if any actual changes were made
816
+ changes_made = False
817
+ actions_taken = []
818
+
819
+ # Check if git repository
820
+ try:
821
+ subprocess.run(["git", "status"], check=True, capture_output=True)
822
+ except subprocess.CalledProcessError:
823
+ print(f"{RED}Error: Not a git repository or git not available{NC}")
824
+ return False
825
+
826
+ # Get current branch name
827
+ try:
828
+ result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"],
829
+ capture_output=True, text=True, check=True,
830
+ encoding='utf-8', errors='replace')
831
+ current_branch = result.stdout.strip()
832
+ print(f"{BLUE}Current branch: {current_branch}{NC}")
833
+ except subprocess.CalledProcessError as e:
834
+ print(f"{RED}Error getting current branch: {e}{NC}")
835
+ return False
836
+
837
+ # Check if current branch is deployment
838
+ if current_branch == "deployment":
839
+ print(f"{RED}Error: Already on deployment branch{NC}")
840
+ print(f"{YELLOW}Please switch to a feature branch first{NC}")
841
+ return False
842
+
843
+ # Check for uncommitted changes
844
+ try:
845
+ result = subprocess.run(["git", "status", "--porcelain"],
846
+ capture_output=True, text=True, check=True,
847
+ encoding='utf-8', errors='replace')
848
+ if result.stdout.strip():
849
+ print(f"{RED}Error: You have uncommitted changes{NC}")
850
+ print(f"{YELLOW}Please commit or stash your changes first{NC}")
851
+ return False
852
+ except subprocess.CalledProcessError as e:
853
+ print(f"{RED}Error checking git status: {e}{NC}")
854
+ return False
855
+
856
+ # Fetch latest changes (silent)
857
+ result = subprocess.run(
858
+ ["git", "fetch", "origin"],
859
+ capture_output=True, text=True,
860
+ encoding='utf-8', errors='replace'
861
+ )
862
+ if result.returncode != 0:
863
+ print(f"{RED}Failed to fetch latest changes{NC}")
864
+ return False
865
+
866
+ # Check for unpushed commits and auto-push if needed
867
+ try:
868
+ result = subprocess.run(
869
+ ["git", "rev-list", f"origin/{current_branch}..{current_branch}"],
870
+ capture_output=True, text=True,
871
+ encoding='utf-8', errors='replace'
872
+ )
873
+ unpushed_commits = result.stdout.strip().split('\n') if result.stdout.strip() else []
874
+
875
+ if unpushed_commits and unpushed_commits[0]:
876
+ changes_made = True
877
+ print(f"{YELLOW}Found {len(unpushed_commits)} unpushed commit(s) on {current_branch}{NC}")
878
+ success = run_flutter_command(
879
+ ["git", "push", "origin", current_branch],
880
+ f"Pushing {current_branch} to origin... "
881
+ )
882
+ if not success:
883
+ print(f"{RED}Failed to push {current_branch} to origin{NC}")
884
+ return False
885
+ actions_taken.append(f"Pushed {len(unpushed_commits)} commit(s) from {current_branch}")
886
+ except subprocess.CalledProcessError:
887
+ # Remote branch might not exist, try to push anyway
888
+ changes_made = True
889
+ print(f"{YELLOW}Remote branch not found, pushing {current_branch}...{NC}")
890
+ success = run_flutter_command(
891
+ ["git", "push", "-u", "origin", current_branch],
892
+ f"Pushing {current_branch} to origin... "
893
+ )
894
+ if not success:
895
+ print(f"{RED}Failed to push {current_branch} to origin{NC}")
896
+ return False
897
+ actions_taken.append(f"Created and pushed {current_branch}")
898
+
899
+ # Checkout deployment branch (silent)
900
+ checkout_result = subprocess.run(
901
+ ["git", "checkout", "deployment"],
902
+ capture_output=True, text=True,
903
+ encoding='utf-8', errors='replace'
904
+ )
905
+ if checkout_result.returncode != 0:
906
+ print(f"{RED}Failed to checkout deployment{NC}")
907
+ return False
908
+
909
+ # Pull latest deployment (silent)
910
+ pull_result = subprocess.run(
911
+ ["git", "pull", "origin", "deployment"],
912
+ capture_output=True, text=True,
913
+ encoding='utf-8', errors='replace'
914
+ )
915
+ if pull_result.returncode != 0:
916
+ print(f"{RED}Failed to pull latest deployment{NC}")
917
+ subprocess.run(["git", "checkout", current_branch], capture_output=True)
918
+ return False
919
+
920
+ # Merge current branch into deployment
921
+ merge_result = subprocess.run(
922
+ ["git", "merge", current_branch],
923
+ capture_output=True,
924
+ text=True,
925
+ encoding='utf-8',
926
+ errors='replace'
927
+ )
928
+
929
+ if merge_result.returncode != 0:
930
+ print(f"{RED}Merge failed! There may be conflicts to resolve.{NC}")
931
+ print(f"{YELLOW}You are now on deployment branch{NC}")
932
+ print(f"\n{BLUE}To resolve:{NC}")
933
+ print(f" 1. Fix conflicts in VSCode")
934
+ print(f" 2. {BLUE}git add <file>{NC}")
935
+ print(f" 3. {BLUE}git commit{NC}")
936
+ print(f" 4. {BLUE}git push origin deployment{NC}")
937
+ print(f" 5. {BLUE}git checkout {current_branch}{NC}")
938
+ print(f"\n Or abort: {BLUE}git merge --abort && git checkout {current_branch}{NC}")
939
+ return False
940
+
941
+ merge_had_changes = False
942
+ if merge_result.returncode == 0:
943
+ if "Already up to date" in merge_result.stdout or "Already up-to-date" in merge_result.stdout:
944
+ pass # Silent when up to date
945
+ else:
946
+ merge_had_changes = True
947
+ changes_made = True
948
+ print(f"{GREEN}✓ Merged {current_branch} → deployment{NC}")
949
+ actions_taken.append(f"Merged {current_branch} into deployment")
950
+
951
+ # Push to remote (only if there were changes)
952
+ if merge_had_changes:
953
+ push_result = subprocess.run(
954
+ ["git", "push", "origin", "deployment"],
955
+ capture_output=True, text=True,
956
+ encoding='utf-8', errors='replace'
957
+ )
958
+ if push_result.returncode != 0:
959
+ print(f"{RED}Failed to push to remote{NC}")
960
+ print(f"{YELLOW}Merge completed locally but not pushed{NC}")
961
+ return False
962
+ print(f"{GREEN}✓ Pushed deployment to origin{NC}")
963
+
964
+ # Checkout back to original branch (silent)
965
+ subprocess.run(["git", "checkout", current_branch], capture_output=True)
966
+
967
+ # Summary - compact if no changes
968
+ if not changes_made:
969
+ print(f"\n{GREEN}✓ deployment already up to date with {current_branch} - no changes needed{NC}")
970
+ return True
971
+
972
+ # Detailed summary when changes were made
973
+ print(f"\n{'='*55}")
974
+ print(f"{GREEN}✓ Deploy complete!{NC}")
975
+ print(f"{'='*55}")
976
+ if actions_taken:
977
+ print(f"{BLUE}Actions taken:{NC}")
978
+ for action in actions_taken:
979
+ print(f" {GREEN}•{NC} {action}")
980
+
981
+ return True