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.
- common_utils.py +261 -0
- core/__init__.py +7 -0
- core/constants.py +57 -0
- core/state.py +25 -0
- create_page.py +288 -0
- fdev.py +258 -0
- flutter_dev-0.1.0.dist-info/METADATA +411 -0
- flutter_dev-0.1.0.dist-info/RECORD +30 -0
- flutter_dev-0.1.0.dist-info/WHEEL +5 -0
- flutter_dev-0.1.0.dist-info/entry_points.txt +5 -0
- flutter_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_dev-0.1.0.dist-info/top_level.txt +9 -0
- gemini_api.py +395 -0
- git_diff_output_editor.py +34 -0
- install_legacy.py +467 -0
- managers/__init__.py +69 -0
- managers/ai.py +113 -0
- managers/app.py +541 -0
- managers/brew.py +477 -0
- managers/build.py +436 -0
- managers/datetime.py +49 -0
- managers/device.py +207 -0
- managers/doctor.py +286 -0
- managers/git.py +981 -0
- managers/git_account.py +542 -0
- managers/merge.py +165 -0
- managers/mirror.py +205 -0
- managers/project.py +138 -0
- managers/web_deploy.py +43 -0
- switch_ai.py +181 -0
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
|