fancygit 1.0.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.
- fancygit-1.0.0.dist-info/METADATA +169 -0
- fancygit-1.0.0.dist-info/RECORD +29 -0
- fancygit-1.0.0.dist-info/WHEEL +5 -0
- fancygit-1.0.0.dist-info/entry_points.txt +2 -0
- fancygit-1.0.0.dist-info/top_level.txt +2 -0
- src/__init__.py +1 -0
- src/colors.py +260 -0
- src/git_error.py +55 -0
- src/git_error_parser.py +43 -0
- src/git_insights.py +304 -0
- src/git_runner.py +20 -0
- src/loading_animation.py +167 -0
- src/merge_conflict.py +27 -0
- src/mermaid_export.py +430 -0
- src/ollama_client.py +142 -0
- src/output_colorizer.py +358 -0
- src/repo_state.py +29 -0
- src/utils.py +0 -0
- tests/README.md +186 -0
- tests/__init__.py +0 -0
- tests/conftest.py +61 -0
- tests/test_conflict_parser_integration.py +65 -0
- tests/test_fancygit_advanced.py +504 -0
- tests/test_fancygit_commands.py +507 -0
- tests/test_fancygit_integration.py +158 -0
- tests/test_fancygit_workflows.py +441 -0
- tests/test_git_error.py +74 -0
- tests/test_git_error_parser.py +129 -0
- tests/test_git_runner.py +118 -0
src/git_insights.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from collections import defaultdict, Counter
|
|
6
|
+
from typing import Dict, List, Tuple, Any
|
|
7
|
+
|
|
8
|
+
class GitInsights:
|
|
9
|
+
def __init__(self, git_runner):
|
|
10
|
+
self.runner = git_runner
|
|
11
|
+
|
|
12
|
+
def get_commit_frequency(self, days: int = 30) -> Dict[str, int]:
|
|
13
|
+
"""Get commit frequency per contributor over specified days"""
|
|
14
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
15
|
+
returncode, stdout, stderr = self.runner.run_git_command([
|
|
16
|
+
'log', '--since', since_date, '--pretty=format:%an', '--date=short'
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
if returncode != 0:
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
authors = stdout.strip().split('\n') if stdout.strip() else []
|
|
23
|
+
return dict(Counter(authors))
|
|
24
|
+
|
|
25
|
+
def get_lines_changed_over_time(self, days: int = 30) -> List[Dict]:
|
|
26
|
+
"""Get lines added/removed over time"""
|
|
27
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
28
|
+
returncode, stdout, stderr = self.runner.run_git_command([
|
|
29
|
+
'log', '--since', since_date, '--pretty=format:%H|%ad|%an',
|
|
30
|
+
'--date=short', '--numstat'
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
if returncode != 0:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
commits = []
|
|
37
|
+
current_commit = {}
|
|
38
|
+
|
|
39
|
+
for line in stdout.strip().split('\n'):
|
|
40
|
+
if '|' in line and len(line.split('|')) == 3:
|
|
41
|
+
if current_commit:
|
|
42
|
+
commits.append(current_commit)
|
|
43
|
+
hash_val, date, author = line.split('|')
|
|
44
|
+
current_commit = {
|
|
45
|
+
'hash': hash_val,
|
|
46
|
+
'date': date,
|
|
47
|
+
'author': author,
|
|
48
|
+
'files_changed': 0,
|
|
49
|
+
'lines_added': 0,
|
|
50
|
+
'lines_removed': 0
|
|
51
|
+
}
|
|
52
|
+
elif line.strip() and '\t' in line and current_commit:
|
|
53
|
+
# Parse numstat line: "10 5 file.py"
|
|
54
|
+
parts = line.strip().split('\t')
|
|
55
|
+
if len(parts) >= 3:
|
|
56
|
+
try:
|
|
57
|
+
added = int(parts[0]) if parts[0] != '-' else 0
|
|
58
|
+
removed = int(parts[1]) if parts[1] != '-' else 0
|
|
59
|
+
current_commit['lines_added'] += added
|
|
60
|
+
current_commit['lines_removed'] += removed
|
|
61
|
+
current_commit['files_changed'] += 1
|
|
62
|
+
except ValueError:
|
|
63
|
+
# Skip binary files
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if current_commit:
|
|
67
|
+
commits.append(current_commit)
|
|
68
|
+
|
|
69
|
+
return commits
|
|
70
|
+
|
|
71
|
+
def get_branch_analysis(self) -> Dict[str, Any]:
|
|
72
|
+
"""Analyze branches - active vs stale, separating local and remote"""
|
|
73
|
+
branch_info = {}
|
|
74
|
+
now = datetime.now()
|
|
75
|
+
|
|
76
|
+
# First, fetch latest remote data to ensure we have up-to-date branch information
|
|
77
|
+
self.runner.run_git_command(['fetch', '--all'])
|
|
78
|
+
|
|
79
|
+
# Get local branches
|
|
80
|
+
returncode, stdout, stderr = self.runner.run_git_command(['branch'])
|
|
81
|
+
if returncode == 0:
|
|
82
|
+
for line in stdout.strip().split('\n'):
|
|
83
|
+
if line.strip():
|
|
84
|
+
branch = line.strip().replace('* ', '')
|
|
85
|
+
branch_name = f"local/{branch}"
|
|
86
|
+
|
|
87
|
+
# Get last commit date for branch
|
|
88
|
+
commit_returncode, commit_stdout, _ = self.runner.run_git_command([
|
|
89
|
+
'log', '-1', '--pretty=format:%ad', '--date=iso', branch
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
if commit_returncode == 0 and commit_stdout.strip():
|
|
93
|
+
try:
|
|
94
|
+
# Parse ISO date with timezone info
|
|
95
|
+
date_str = commit_stdout.strip()
|
|
96
|
+
if ' ' in date_str:
|
|
97
|
+
# Format like "2026-03-10 02:46:15 +0300"
|
|
98
|
+
last_commit_date = datetime.strptime(date_str.split()[0] + ' ' + date_str.split()[1], "%Y-%m-%d %H:%M:%S")
|
|
99
|
+
else:
|
|
100
|
+
# Format like "2026-03-10"
|
|
101
|
+
last_commit_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
102
|
+
except ValueError:
|
|
103
|
+
# Fallback to simple date parsing
|
|
104
|
+
last_commit_date = datetime.strptime(date_str.split()[0], "%Y-%m-%d")
|
|
105
|
+
|
|
106
|
+
days_inactive = (now - last_commit_date).days
|
|
107
|
+
|
|
108
|
+
branch_info[branch_name] = {
|
|
109
|
+
'last_commit': commit_stdout.strip(),
|
|
110
|
+
'days_inactive': days_inactive,
|
|
111
|
+
'status': 'active' if days_inactive <= 30 else 'stale',
|
|
112
|
+
'type': 'local'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Get remote branches
|
|
116
|
+
returncode, stdout, stderr = self.runner.run_git_command(['branch', '-r'])
|
|
117
|
+
if returncode == 0:
|
|
118
|
+
for line in stdout.strip().split('\n'):
|
|
119
|
+
if line.strip():
|
|
120
|
+
branch = line.strip().replace('* ', '').replace('remotes/', '')
|
|
121
|
+
if not branch.startswith('HEAD ->'):
|
|
122
|
+
branch_name = f"remote/{branch}"
|
|
123
|
+
|
|
124
|
+
# Get last commit date for branch
|
|
125
|
+
commit_returncode, commit_stdout, _ = self.runner.run_git_command([
|
|
126
|
+
'log', '-1', '--pretty=format:%ad', '--date=iso', branch
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
if commit_returncode == 0 and commit_stdout.strip():
|
|
130
|
+
try:
|
|
131
|
+
# Parse ISO date with timezone info
|
|
132
|
+
date_str = commit_stdout.strip()
|
|
133
|
+
if ' ' in date_str:
|
|
134
|
+
# Format like "2026-03-10 02:46:15 +0300"
|
|
135
|
+
last_commit_date = datetime.strptime(date_str.split()[0] + ' ' + date_str.split()[1], "%Y-%m-%d %H:%M:%S")
|
|
136
|
+
else:
|
|
137
|
+
# Format like "2026-03-10"
|
|
138
|
+
last_commit_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
139
|
+
except ValueError:
|
|
140
|
+
# Fallback to simple date parsing
|
|
141
|
+
last_commit_date = datetime.strptime(date_str.split()[0], "%Y-%m-%d")
|
|
142
|
+
|
|
143
|
+
days_inactive = (now - last_commit_date).days
|
|
144
|
+
|
|
145
|
+
branch_info[branch_name] = {
|
|
146
|
+
'last_commit': commit_stdout.strip(),
|
|
147
|
+
'days_inactive': days_inactive,
|
|
148
|
+
'status': 'active' if days_inactive <= 30 else 'stale',
|
|
149
|
+
'type': 'remote'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return branch_info
|
|
153
|
+
|
|
154
|
+
def get_file_hotspots(self, days: int = 90) -> Dict[str, int]:
|
|
155
|
+
"""Get files/directories with most changes"""
|
|
156
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
157
|
+
returncode, stdout, stderr = self.runner.run_git_command([
|
|
158
|
+
'log', '--since', since_date, '--name-only', '--pretty=format:'
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
if returncode != 0:
|
|
162
|
+
return {}
|
|
163
|
+
|
|
164
|
+
files = []
|
|
165
|
+
for line in stdout.split('\n'):
|
|
166
|
+
if line.strip():
|
|
167
|
+
files.append(line.strip())
|
|
168
|
+
|
|
169
|
+
return dict(Counter(files))
|
|
170
|
+
|
|
171
|
+
def get_merge_efficiency(self, days: int = 90) -> Dict[str, Any]:
|
|
172
|
+
"""Analyze merge/pull request efficiency"""
|
|
173
|
+
since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
174
|
+
|
|
175
|
+
# Get merge commits
|
|
176
|
+
returncode, stdout, stderr = self.runner.run_git_command([
|
|
177
|
+
'log', '--since', since_date, '--merges', '--pretty=format:%H|%ad|%s',
|
|
178
|
+
'--date=iso'
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
if returncode != 0:
|
|
182
|
+
return {}
|
|
183
|
+
|
|
184
|
+
merges = []
|
|
185
|
+
for line in stdout.strip().split('\n'):
|
|
186
|
+
if '|' in line:
|
|
187
|
+
hash_val, date, subject = line.split('|', 2)
|
|
188
|
+
merges.append({
|
|
189
|
+
'hash': hash_val,
|
|
190
|
+
'date': date,
|
|
191
|
+
'subject': subject
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
# Calculate average time between merge commits
|
|
195
|
+
if len(merges) < 2:
|
|
196
|
+
return {'total_merges': len(merges), 'avg_merge_interval_days': 0}
|
|
197
|
+
|
|
198
|
+
merge_dates = []
|
|
199
|
+
for merge in merges:
|
|
200
|
+
try:
|
|
201
|
+
date_str = merge['date']
|
|
202
|
+
if ' ' in date_str:
|
|
203
|
+
# Format like "2026-03-10 02:46:15 +0300"
|
|
204
|
+
merge_date = datetime.strptime(date_str.split()[0] + ' ' + date_str.split()[1], "%Y-%m-%d %H:%M:%S")
|
|
205
|
+
else:
|
|
206
|
+
# Format like "2026-03-10"
|
|
207
|
+
merge_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
208
|
+
merge_dates.append(merge_date)
|
|
209
|
+
except ValueError:
|
|
210
|
+
# Skip invalid dates
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if len(merge_dates) < 2:
|
|
214
|
+
return {'total_merges': len(merges), 'avg_merge_interval_days': 0}
|
|
215
|
+
|
|
216
|
+
merge_dates.sort()
|
|
217
|
+
|
|
218
|
+
intervals = []
|
|
219
|
+
for i in range(1, len(merge_dates)):
|
|
220
|
+
interval = (merge_dates[i] - merge_dates[i-1]).days
|
|
221
|
+
intervals.append(interval)
|
|
222
|
+
|
|
223
|
+
avg_interval = sum(intervals) / len(intervals) if intervals else 0
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
'total_merges': len(merges),
|
|
227
|
+
'avg_merge_interval_days': round(avg_interval, 1),
|
|
228
|
+
'recent_merges': merges[-5:] # Last 5 merges
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
def get_contributor_stats(self) -> Dict[str, Dict]:
|
|
232
|
+
"""Get comprehensive contributor statistics"""
|
|
233
|
+
returncode, stdout, stderr = self.runner.run_git_command([
|
|
234
|
+
'log', '--pretty=format:%an|%ad', '--date=iso', '--numstat'
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
if returncode != 0:
|
|
238
|
+
return {}
|
|
239
|
+
|
|
240
|
+
contributors = defaultdict(lambda: {
|
|
241
|
+
'commits': 0,
|
|
242
|
+
'lines_added': 0,
|
|
243
|
+
'lines_removed': 0,
|
|
244
|
+
'files_changed': 0,
|
|
245
|
+
'first_commit': None,
|
|
246
|
+
'last_commit': None
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
lines = stdout.strip().split('\n')
|
|
250
|
+
current_author = None
|
|
251
|
+
current_date = None
|
|
252
|
+
|
|
253
|
+
for line in lines:
|
|
254
|
+
if '|' in line and len(line.split('|')) == 2:
|
|
255
|
+
author, date = line.split('|')
|
|
256
|
+
current_author = author.strip()
|
|
257
|
+
current_date = date.strip()
|
|
258
|
+
|
|
259
|
+
if contributors[current_author]['first_commit'] is None:
|
|
260
|
+
contributors[current_author]['first_commit'] = current_date
|
|
261
|
+
contributors[current_author]['last_commit'] = current_date
|
|
262
|
+
contributors[current_author]['commits'] += 1
|
|
263
|
+
|
|
264
|
+
elif line.strip() and '\t' in line and current_author:
|
|
265
|
+
# Parse numstat line: "10 5 file.py"
|
|
266
|
+
parts = line.strip().split('\t')
|
|
267
|
+
if len(parts) >= 3:
|
|
268
|
+
try:
|
|
269
|
+
added = int(parts[0]) if parts[0] != '-' else 0
|
|
270
|
+
removed = int(parts[1]) if parts[1] != '-' else 0
|
|
271
|
+
contributors[current_author]['lines_added'] += added
|
|
272
|
+
contributors[current_author]['lines_removed'] += removed
|
|
273
|
+
contributors[current_author]['files_changed'] += 1
|
|
274
|
+
except ValueError:
|
|
275
|
+
# Skip binary files or invalid numbers
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
return dict(contributors)
|
|
279
|
+
|
|
280
|
+
def generate_insights_report(self, days: int = 30) -> Dict[str, Any]:
|
|
281
|
+
"""Generate comprehensive insights report"""
|
|
282
|
+
report = {
|
|
283
|
+
'generated_at': datetime.now().isoformat(),
|
|
284
|
+
'analysis_period_days': days,
|
|
285
|
+
'commit_frequency': self.get_commit_frequency(days),
|
|
286
|
+
'lines_changed': self.get_lines_changed_over_time(days),
|
|
287
|
+
'branch_analysis': self.get_branch_analysis(),
|
|
288
|
+
'file_hotspots': self.get_file_hotspots(days),
|
|
289
|
+
'contributor_stats': self.get_contributor_stats()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Add summary statistics
|
|
293
|
+
total_commits = sum(report['commit_frequency'].values())
|
|
294
|
+
active_branches = sum(1 for b in report['branch_analysis'].values() if b['status'] == 'active')
|
|
295
|
+
|
|
296
|
+
report['summary'] = {
|
|
297
|
+
'total_commits': total_commits,
|
|
298
|
+
'active_contributors': len(report['commit_frequency']),
|
|
299
|
+
'active_branches': active_branches,
|
|
300
|
+
'total_branches': len(report['branch_analysis']),
|
|
301
|
+
'total_files_changed': len(report['file_hotspots'])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return report
|
src/git_runner.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
class GitRunner:
|
|
4
|
+
def __init__(self) -> None:
|
|
5
|
+
self.git_cmd = "git"
|
|
6
|
+
|
|
7
|
+
def run_git_command(self, args):
|
|
8
|
+
"""Run git command and capture output"""
|
|
9
|
+
try:
|
|
10
|
+
cmd = [self.git_cmd] + args
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
cmd,
|
|
13
|
+
capture_output=True,
|
|
14
|
+
text=True,
|
|
15
|
+
check=False
|
|
16
|
+
)
|
|
17
|
+
return result.returncode, result.stdout, result.stderr
|
|
18
|
+
except Exception as e:
|
|
19
|
+
return -1, "", str(e)
|
|
20
|
+
|
src/loading_animation.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
import itertools
|
|
6
|
+
import random
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LoadingAnimation:
|
|
10
|
+
"""ASCII loading animations for long-running operations"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._stop_event = threading.Event()
|
|
14
|
+
self._thread = None
|
|
15
|
+
self._last_line_length = 0
|
|
16
|
+
|
|
17
|
+
def _clear_line(self):
|
|
18
|
+
"""Clear the current line"""
|
|
19
|
+
sys.stdout.write('\r' + ' ' * self._last_line_length + '\r')
|
|
20
|
+
sys.stdout.flush()
|
|
21
|
+
self._last_line_length = 0
|
|
22
|
+
|
|
23
|
+
def _man_running(self):
|
|
24
|
+
"""Man running animation"""
|
|
25
|
+
frames = [
|
|
26
|
+
"- - - - 🏃",
|
|
27
|
+
"- - - 🏃 -",
|
|
28
|
+
"- - 🏃 - -",
|
|
29
|
+
"- 🏃 - - -",
|
|
30
|
+
"🏃 - - - -"
|
|
31
|
+
]
|
|
32
|
+
message = "🤖 AI is thinking"
|
|
33
|
+
|
|
34
|
+
while not self._stop_event.is_set():
|
|
35
|
+
for frame in frames:
|
|
36
|
+
if self._stop_event.is_set():
|
|
37
|
+
break
|
|
38
|
+
self._clear_line()
|
|
39
|
+
text = f"{message} {frame}"
|
|
40
|
+
sys.stdout.write(text)
|
|
41
|
+
sys.stdout.flush()
|
|
42
|
+
self._last_line_length = len(text)
|
|
43
|
+
time.sleep(0.3)
|
|
44
|
+
|
|
45
|
+
def _loading_dots(self):
|
|
46
|
+
"""Simple loading dots animation"""
|
|
47
|
+
dots = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
48
|
+
message = "🤖 AI is analyzing"
|
|
49
|
+
|
|
50
|
+
while not self._stop_event.is_set():
|
|
51
|
+
for dot in dots:
|
|
52
|
+
if self._stop_event.is_set():
|
|
53
|
+
break
|
|
54
|
+
self._clear_line()
|
|
55
|
+
text = f"{message} {dot}"
|
|
56
|
+
sys.stdout.write(text)
|
|
57
|
+
sys.stdout.flush()
|
|
58
|
+
self._last_line_length = len(text)
|
|
59
|
+
time.sleep(0.1)
|
|
60
|
+
|
|
61
|
+
def _progress_bar(self):
|
|
62
|
+
"""Progress bar animation"""
|
|
63
|
+
bar_chars = "░▒▓█"
|
|
64
|
+
message = "🤖 AI processing"
|
|
65
|
+
|
|
66
|
+
while not self._stop_event.is_set():
|
|
67
|
+
for i in range(21): # 0 to 20
|
|
68
|
+
if self._stop_event.is_set():
|
|
69
|
+
break
|
|
70
|
+
filled = i // 5
|
|
71
|
+
partial = i % 5
|
|
72
|
+
bar = "█" * filled + (bar_chars[partial] if partial < 3 else "█")
|
|
73
|
+
bar = bar.ljust(4, "░")
|
|
74
|
+
self._clear_line()
|
|
75
|
+
text = f"{message} [{bar}] {i*5}%"
|
|
76
|
+
sys.stdout.write(text)
|
|
77
|
+
sys.stdout.flush()
|
|
78
|
+
self._last_line_length = len(text)
|
|
79
|
+
time.sleep(0.15)
|
|
80
|
+
|
|
81
|
+
def _matrix_rain(self):
|
|
82
|
+
"""Matrix-style falling characters"""
|
|
83
|
+
chars = "01"
|
|
84
|
+
message = "🤖 AI computing"
|
|
85
|
+
|
|
86
|
+
while not self._stop_event.is_set():
|
|
87
|
+
for _ in range(10):
|
|
88
|
+
if self._stop_event.is_set():
|
|
89
|
+
break
|
|
90
|
+
rain = ''.join(random.choice(chars) for _ in range(8))
|
|
91
|
+
self._clear_line()
|
|
92
|
+
text = f"{message} [{rain}]"
|
|
93
|
+
sys.stdout.write(text)
|
|
94
|
+
sys.stdout.flush()
|
|
95
|
+
self._last_line_length = len(text)
|
|
96
|
+
time.sleep(0.2)
|
|
97
|
+
|
|
98
|
+
def _brain_activity(self):
|
|
99
|
+
"""Brain activity animation"""
|
|
100
|
+
frames = [
|
|
101
|
+
"🧠💭",
|
|
102
|
+
"🧠✨",
|
|
103
|
+
"🧠⚡",
|
|
104
|
+
"🧠💡",
|
|
105
|
+
"🧠🔮"
|
|
106
|
+
]
|
|
107
|
+
message = "🤖 AI thinking"
|
|
108
|
+
|
|
109
|
+
while not self._stop_event.is_set():
|
|
110
|
+
for frame in frames:
|
|
111
|
+
if self._stop_event.is_set():
|
|
112
|
+
break
|
|
113
|
+
self._clear_line()
|
|
114
|
+
text = f"{message} {frame}"
|
|
115
|
+
sys.stdout.write(text)
|
|
116
|
+
sys.stdout.flush()
|
|
117
|
+
self._last_line_length = len(text)
|
|
118
|
+
time.sleep(0.4)
|
|
119
|
+
|
|
120
|
+
def start(self, animation_type="run"):
|
|
121
|
+
"""Start the loading animation
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
animation_type: Type of animation ("run", "dots", "progress", "matrix", "brain")
|
|
125
|
+
"""
|
|
126
|
+
if self._thread and self._thread.is_alive():
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self._stop_event.clear()
|
|
130
|
+
|
|
131
|
+
animation_map = {
|
|
132
|
+
"run": self._man_running,
|
|
133
|
+
"dots": self._loading_dots,
|
|
134
|
+
"progress": self._progress_bar,
|
|
135
|
+
"matrix": self._matrix_rain,
|
|
136
|
+
"brain": self._brain_activity
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
animation_func = animation_map.get(animation_type, self._man_running)
|
|
140
|
+
self._thread = threading.Thread(target=animation_func, daemon=True)
|
|
141
|
+
self._thread.start()
|
|
142
|
+
|
|
143
|
+
def stop(self):
|
|
144
|
+
"""Stop the loading animation and clear the line"""
|
|
145
|
+
if self._thread and self._thread.is_alive():
|
|
146
|
+
self._stop_event.set()
|
|
147
|
+
self._thread.join(timeout=0.5)
|
|
148
|
+
|
|
149
|
+
# Clear the entire line and move to beginning
|
|
150
|
+
sys.stdout.write('\r' + ' ' * 100 + '\r')
|
|
151
|
+
sys.stdout.flush()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Context manager for easy usage
|
|
155
|
+
class LoadingContext:
|
|
156
|
+
"""Context manager for loading animations"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, animation_type="dots"):
|
|
159
|
+
self.animation = LoadingAnimation()
|
|
160
|
+
self.animation_type = animation_type
|
|
161
|
+
|
|
162
|
+
def __enter__(self):
|
|
163
|
+
self.animation.start(self.animation_type)
|
|
164
|
+
return self.animation
|
|
165
|
+
|
|
166
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
167
|
+
self.animation.stop()
|
src/merge_conflict.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# example for this dataclass object
|
|
2
|
+
# MergeConflict(
|
|
3
|
+
# file="fancygit.py",
|
|
4
|
+
# base_branch="main",
|
|
5
|
+
# incoming_branch="refactor/fancygit.py",
|
|
6
|
+
# current_code="return a + b",
|
|
7
|
+
# incoming_code="return a - b",
|
|
8
|
+
# start_line=12,
|
|
9
|
+
# end_line=34
|
|
10
|
+
# )
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
@dataclass (frozen=True)
|
|
15
|
+
class MergeConflict:
|
|
16
|
+
file: str
|
|
17
|
+
|
|
18
|
+
# to get hold of which branches
|
|
19
|
+
current_branch: str
|
|
20
|
+
incoming_branch: str
|
|
21
|
+
|
|
22
|
+
# to get hold of the line of code itself
|
|
23
|
+
current_code: str
|
|
24
|
+
incoming_code: str
|
|
25
|
+
|
|
26
|
+
start_line: int
|
|
27
|
+
end_line: int
|