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.
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FancyGit Output Colorizer
4
+ Intelligently colors git command outputs based on content and context
5
+ """
6
+
7
+ import re
8
+ from typing import List, Tuple
9
+ from src.colors import Colors, color_success, color_error, color_warning, color_info, color_file, color_branch, color_header
10
+
11
+ class OutputColorizer:
12
+ """Intelligent output coloring for git commands"""
13
+
14
+ def __init__(self):
15
+ # Git status patterns
16
+ self.status_patterns = {
17
+ 'modified': r'^\s*modified:',
18
+ 'new_file': r'^\s*new file:',
19
+ 'deleted': r'^\s*deleted:',
20
+ 'renamed': r'^\s*renamed:',
21
+ 'copied': r'^\s*copied:',
22
+ 'untracked': r'^\s*untracked:',
23
+ 'branch': r'^On branch |^Your branch is|^Your branch and |^nothing to commit',
24
+ 'changes': r'^Changes not staged for commit|^Changes to be committed',
25
+ 'conflict': r'^\s*both |^\s*<<<<<<< |^\s======= |^\s>>>>>>> ',
26
+ }
27
+
28
+ # Git log patterns
29
+ self.log_patterns = {
30
+ 'commit_hash': r'^commit [a-f0-9]{40}',
31
+ 'author': r'^Author:',
32
+ 'date': r'^Date:',
33
+ 'merge': r'^Merge:',
34
+ }
35
+
36
+ # Git diff patterns
37
+ self.diff_patterns = {
38
+ 'addition': r'^\+',
39
+ 'deletion': r'^-',
40
+ 'file_header': r'^diff --git|^index |^@@ ',
41
+ 'context': r'^ ',
42
+ }
43
+
44
+ # Git branch patterns
45
+ self.branch_patterns = {
46
+ 'current': r'^\* ',
47
+ 'remote': r'^ remotes/',
48
+ 'local': r'^ ',
49
+ }
50
+
51
+ # General patterns
52
+ self.general_patterns = {
53
+ 'error': r'^error:|^fatal:|Failed|Cannot|Unable',
54
+ 'warning': r'^warning:|^hint:',
55
+ 'success': r'^Successfully|^Already up-to-date|^Everything up-to-date',
56
+ 'info': r'^Note:|^INFO:|^hint:',
57
+ 'file_path': r'[/\\][\w\-./\\]+\.?\w*',
58
+ }
59
+
60
+ def colorize_output(self, command: str, stdout: str, stderr: str) -> Tuple[str, str]:
61
+ """Colorize command output based on command type and content"""
62
+
63
+ if stdout:
64
+ stdout = self._colorize_by_command(command, stdout)
65
+
66
+ if stderr:
67
+ stderr = self._colorize_stderr(stderr)
68
+
69
+ return stdout, stderr
70
+
71
+ def _colorize_by_command(self, command: str, output: str) -> str:
72
+ """Route to appropriate colorizer based on command"""
73
+
74
+ if command == 'status':
75
+ return self._colorize_status(output)
76
+ elif command == 'log':
77
+ return self._colorize_log(output)
78
+ elif command in ['diff', 'show']:
79
+ return self._colorize_diff(output)
80
+ elif command == 'branch':
81
+ return self._colorize_branch(output)
82
+ elif command in ['add', 'rm', 'mv']:
83
+ return self._colorize_file_operations(output)
84
+ elif command == 'commit':
85
+ return self._colorize_commit(output)
86
+ elif command in ['push', 'pull', 'fetch']:
87
+ return self._colorize_remote(output)
88
+ else:
89
+ return self._colorize_generic(output)
90
+
91
+ def _colorize_status(self, output: str) -> str:
92
+ """Colorize git status output"""
93
+ lines = output.split('\n')
94
+ colored_lines = []
95
+ in_untracked_section = False
96
+
97
+ for line in lines:
98
+ colored_line = line
99
+
100
+ # Branch information
101
+ if re.search(r'^On branch ', line):
102
+ branch_name = line.replace('On branch ', '').strip()
103
+ colored_line = f"On branch {color_branch(branch_name, True)}"
104
+ elif re.search(r'^Your branch is ahead', line):
105
+ colored_line = color_info(line)
106
+ elif re.search(r'^Your branch is behind', line):
107
+ colored_line = color_warning(line)
108
+ elif re.search(r'^Your branch and .* have diverged', line):
109
+ colored_line = color_warning(line)
110
+ elif re.search(r'^nothing to commit', line):
111
+ colored_line = color_success(line)
112
+
113
+ # Section headers
114
+ elif re.search(r'^Changes to be committed', line):
115
+ colored_line = color_header(line)
116
+ elif re.search(r'^Changes not staged for commit', line):
117
+ colored_line = color_warning(line)
118
+ elif re.search(r'^Untracked files', line):
119
+ colored_line = color_info(line)
120
+ in_untracked_section = True
121
+
122
+ # File status - untracked files come FIRST (before general file pattern)
123
+ elif in_untracked_section and re.search(r'^\t(.+)$', line):
124
+ # File paths under Untracked files section
125
+ file_path = line.strip() # Remove tab and get just the filename
126
+ colored_line = f"\t{color_file(file_path)}"
127
+ elif in_untracked_section and re.search(r'^(\s{8}|\t)untracked:', line):
128
+ match = re.search(r'untracked:\s*(.+)', line)
129
+ if match:
130
+ file_path = match.group(1)
131
+ # Preserve original indentation
132
+ indent = line[:line.index('untracked:')]
133
+ colored_line = f"{indent}untracked: {color_file(file_path)}"
134
+ else:
135
+ colored_line = Colors.muted(line)
136
+
137
+ # General file pattern - LAST RESORT
138
+ elif re.search(r'^(\s{8}|\t)(new file: |modified: |deleted: |renamed: )(.+)$', line):
139
+ # Any other file-like line with indentation
140
+ match = re.search(r'^(\s{8}|\t)(new file: |modified: |deleted: |renamed: )(.+)$', line)
141
+ if match:
142
+ file_path = match.group(3)
143
+ # Preserve original indentation and file status
144
+ indent = line[:line.index(match.group(2))]
145
+ colored_line = f"{indent}{match.group(2)}{color_file(file_path)}"
146
+
147
+ colored_lines.append(colored_line)
148
+
149
+ return '\n'.join(colored_lines)
150
+
151
+ def _colorize_log(self, output: str) -> str:
152
+ """Colorize git log output"""
153
+ lines = output.split('\n')
154
+ colored_lines = []
155
+
156
+ for line in lines:
157
+ colored_line = line
158
+
159
+ if re.search(r'^commit [a-f0-9]{40}', line):
160
+ # Color commit hash
161
+ hash_match = re.search(r'(commit )([a-f0-9]{40})', line)
162
+ if hash_match:
163
+ colored_line = f"{hash_match.group(1)}{color_info(hash_match.group(2))}"
164
+ elif re.search(r'^Author:', line):
165
+ colored_line = color_info(line)
166
+ elif re.search(r'^Date:', line):
167
+ colored_line = Colors.muted(line)
168
+ elif re.search(r'^Merge:', line):
169
+ colored_line = color_header(line)
170
+
171
+ colored_lines.append(colored_line)
172
+
173
+ return '\n'.join(colored_lines)
174
+
175
+ def _colorize_diff(self, output: str) -> str:
176
+ """Colorize git diff output"""
177
+ lines = output.split('\n')
178
+ colored_lines = []
179
+
180
+ for line in lines:
181
+ colored_line = line
182
+
183
+ if line.startswith('+++') or line.startswith('---'):
184
+ # File paths
185
+ colored_line = color_file(line)
186
+ elif line.startswith('+') and not line.startswith('+++'):
187
+ # Added lines
188
+ colored_line = color_success(line)
189
+ elif line.startswith('-') and not line.startswith('---'):
190
+ # Deleted lines
191
+ colored_line = color_error(line)
192
+ elif line.startswith('@@'):
193
+ # Line numbers
194
+ colored_line = color_info(line)
195
+ elif line.startswith('diff --git'):
196
+ colored_line = color_header(line)
197
+ elif line.startswith('index '):
198
+ colored_line = Colors.muted(line)
199
+
200
+ colored_lines.append(colored_line)
201
+
202
+ return '\n'.join(colored_lines)
203
+
204
+ def _colorize_branch(self, output: str) -> str:
205
+ """Colorize git branch output"""
206
+ lines = output.split('\n')
207
+ colored_lines = []
208
+
209
+ for line in lines:
210
+ colored_line = line
211
+
212
+ if line.startswith('* '):
213
+ # Current branch
214
+ branch_name = line[2:].strip()
215
+ colored_line = f"* {color_branch(branch_name, True)}"
216
+ elif line.startswith(' remotes/'):
217
+ # Remote branches
218
+ branch_name = line.replace(' remotes/', '').strip()
219
+ colored_line = f" {color_info(branch_name)} (remote)"
220
+ elif line.startswith(' '):
221
+ # Local branches
222
+ branch_name = line[2:].strip()
223
+ colored_line = f" {color_branch(branch_name, False)}"
224
+
225
+ colored_lines.append(colored_line)
226
+
227
+ return '\n'.join(colored_lines)
228
+
229
+ def _colorize_file_operations(self, output: str) -> str:
230
+ """Colorize file operation outputs (add, rm, mv)"""
231
+ lines = output.split('\n')
232
+ colored_lines = []
233
+
234
+ for line in lines:
235
+ colored_line = line
236
+
237
+ # Look for file paths in the output
238
+ file_matches = re.findall(r'[/\\][\w\-./\\]+\.?\w*', line)
239
+ for match in file_matches:
240
+ colored_line = colored_line.replace(match, color_file(match))
241
+
242
+ # Color success messages
243
+ if re.search(r'added|removed|renamed', line, re.IGNORECASE):
244
+ colored_line = color_success(line)
245
+
246
+ colored_lines.append(colored_line)
247
+
248
+ return '\n'.join(colored_lines)
249
+
250
+ def _colorize_commit(self, output: str) -> str:
251
+ """Colorize git commit output"""
252
+ lines = output.split('\n')
253
+ colored_lines = []
254
+
255
+ for line in lines:
256
+ colored_line = line
257
+
258
+ if re.search(r'\[master [a-f0-9]+\]|\[main [a-f0-9]+\]|\[develop [a-f0-9]+\]', line):
259
+ # Branch and commit info
260
+ colored_line = color_success(line)
261
+ elif re.search(r'files? changed', line, re.IGNORECASE):
262
+ colored_line = color_info(line)
263
+ elif re.search(r'insertion|deletion', line, re.IGNORECASE):
264
+ colored_line = Colors.muted(line)
265
+
266
+ colored_lines.append(colored_line)
267
+
268
+ return '\n'.join(colored_lines)
269
+
270
+ def _colorize_remote(self, output: str) -> str:
271
+ """Colorize remote operation outputs (push, pull, fetch)"""
272
+ lines = output.split('\n')
273
+ colored_lines = []
274
+
275
+ for line in lines:
276
+ colored_line = line
277
+
278
+ if re.search(r'From |To |->', line):
279
+ colored_line = color_info(line)
280
+ elif re.search(r'Fast-forward|Already up-to-date', line):
281
+ colored_line = color_success(line)
282
+ elif re.search(r'Fetching |Enumerating objects', line):
283
+ colored_line = Colors.muted(line)
284
+ elif re.search(r'Receiving objects|Resolving deltas', line):
285
+ colored_line = color_info(line)
286
+ elif re.search(r'error:|fatal:', line):
287
+ colored_line = color_error(line)
288
+ elif re.search(r'warning:', line):
289
+ colored_line = color_warning(line)
290
+
291
+ # Color branch names
292
+ branch_matches = re.findall(r'[\w\-/]+->[\w\-/]+', line)
293
+ for match in branch_matches:
294
+ parts = match.split('->')
295
+ if len(parts) == 2:
296
+ colored_line = colored_line.replace(
297
+ match,
298
+ f"{color_branch(parts[0])} -> {color_branch(parts[1])}"
299
+ )
300
+
301
+ colored_lines.append(colored_line)
302
+
303
+ return '\n'.join(colored_lines)
304
+
305
+ def _colorize_generic(self, output: str) -> str:
306
+ """Generic coloring for other git commands"""
307
+ lines = output.split('\n')
308
+ colored_lines = []
309
+
310
+ for line in lines:
311
+ colored_line = line
312
+
313
+ # Error patterns
314
+ if re.search(r'^error:|^fatal:', line):
315
+ colored_line = color_error(line)
316
+ # Warning patterns
317
+ elif re.search(r'^warning:|^hint:', line):
318
+ colored_line = color_warning(line)
319
+ # Success patterns
320
+ elif re.search(r'^Successfully|^Already up-to-date', line):
321
+ colored_line = color_success(line)
322
+ # Info patterns
323
+ elif re.search(r'^Note:|^INFO:', line):
324
+ colored_line = color_info(line)
325
+ # File paths
326
+ else:
327
+ file_matches = re.findall(r'[/\\][\w\-./\\]+\.?\w*', line)
328
+ for match in file_matches:
329
+ colored_line = colored_line.replace(match, color_file(match))
330
+
331
+ colored_lines.append(colored_line)
332
+
333
+ return '\n'.join(colored_lines)
334
+
335
+ def _colorize_stderr(self, stderr: str) -> str:
336
+ """Colorize stderr output"""
337
+ lines = stderr.split('\n')
338
+ colored_lines = []
339
+
340
+ for line in lines:
341
+ colored_line = line
342
+
343
+ if re.search(r'^error:|^fatal:', line):
344
+ colored_line = color_error(line)
345
+ elif re.search(r'^warning:|^hint:', line):
346
+ colored_line = color_warning(line)
347
+ else:
348
+ colored_line = Colors.muted(line)
349
+
350
+ colored_lines.append(colored_line)
351
+
352
+ return '\n'.join(colored_lines)
353
+
354
+ # Convenience function
355
+ def colorize_output(command: str, stdout: str, stderr: str) -> Tuple[str, str]:
356
+ """Convenience function to colorize output"""
357
+ colorizer = OutputColorizer()
358
+ return colorizer.colorize_output(command, stdout, stderr)
src/repo_state.py ADDED
@@ -0,0 +1,29 @@
1
+ # example for this dataclass object
2
+ # RepoState(
3
+ # branch="main",
4
+ # ahead_by=2,
5
+ # behind_by=1,
6
+ # staged_files=["fancygit.py"],
7
+ # unstaged_files=["git_runner.py"],
8
+ # untracked_files=["test.py"],
9
+ # has_conflicts=True
10
+ # is_merging = True,
11
+ # conflicts = [MergeConflict, .....]etc
12
+ # )
13
+
14
+ from dataclasses import dataclass
15
+ from src.merge_conflict import MergeConflict
16
+ @dataclass (frozen = True)
17
+ class RepoState:
18
+ branch: str
19
+ ahead_by: int
20
+ behind_by: int
21
+
22
+ staged_files: list[str]
23
+ unstaged_files: list[str]
24
+ untracked_files: list[str]
25
+
26
+ has_conflicts: bool
27
+ is_merging: bool
28
+
29
+ conflicts: list[MergeConflict] # list of merge conflicts
src/utils.py ADDED
File without changes
tests/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # Testing Guide for FancyGit
2
+
3
+ This directory contains the test suite for the FancyGit project.
4
+
5
+ ## Test Structure
6
+
7
+ ```
8
+ tests/
9
+ ├── __init__.py # Package initialization
10
+ ├── conftest.py # Pytest fixtures and configuration
11
+ ├── test_git_error.py # Unit tests for GitError dataclass
12
+ ├── test_git_error_parser.py # Unit tests for GitErrorParser
13
+ ├── test_git_runner.py # Unit tests for GitRunner
14
+ ├── test_fancygit_integration.py # Integration tests for FancyGit
15
+ ├── test_conflict_parser_integration.py # Conflict parser integration tests
16
+ └── README.md # This file
17
+ ```
18
+
19
+ ## Test Categories
20
+
21
+ ### Unit Tests (`@pytest.mark.unit`)
22
+ - Test individual components in isolation
23
+ - Fast and focused
24
+ - Use mocks to avoid external dependencies
25
+
26
+ ### Integration Tests (`@pytest.mark.integration`)
27
+ - Test multiple components working together
28
+ - Slower but more comprehensive
29
+ - Test real workflows
30
+
31
+ ### Slow Tests (`@pytest.mark.slow`)
32
+ - Tests that take significant time to run
33
+ - Often involve real git operations
34
+ - Not run by default in quick test runs
35
+
36
+ ## Running Tests
37
+
38
+ ### Quick Test Run (Recommended for Development)
39
+ ```bash
40
+ # Run unit and integration tests (excludes slow tests)
41
+ make test
42
+ # or
43
+ pytest -m "unit or integration" -v
44
+ ```
45
+
46
+ ### Run Specific Test Categories
47
+ ```bash
48
+ # Only unit tests
49
+ make test-unit
50
+ # or
51
+ pytest -m "unit" -v
52
+
53
+ # Only integration tests
54
+ make test-integration
55
+ # or
56
+ pytest -m "integration" -v
57
+
58
+ # Only slow tests
59
+ make test-slow
60
+ # or
61
+ pytest -m "slow" -v
62
+ ```
63
+
64
+ ### Run All Tests
65
+ ```bash
66
+ make test-all
67
+ # or
68
+ pytest -v --cov=src --cov-report=html --cov-report=term-missing
69
+ ```
70
+
71
+ ### Individual Test Files
72
+ ```bash
73
+ pytest tests/test_git_error.py -v
74
+ pytest tests/test_git_error_parser.py -v
75
+ ```
76
+
77
+ ## Coverage
78
+
79
+ Tests generate coverage reports to show how much of the codebase is tested:
80
+
81
+ ```bash
82
+ # Generate HTML coverage report
83
+ make test-unit
84
+
85
+ # View the report
86
+ open htmlcov/index.html # macOS
87
+ xdg-open htmlcov/index.html # Linux
88
+ ```
89
+
90
+ ## Fixtures
91
+
92
+ ### `temp_git_repo`
93
+ Creates a temporary git repository for testing:
94
+ ```python
95
+ def test_something(temp_git_repo):
96
+ # temp_git_repo is the path to a clean git repo
97
+ # Automatically cleaned up after the test
98
+ pass
99
+ ```
100
+
101
+ ### `sample_git_outputs`
102
+ Provides sample git command outputs:
103
+ ```python
104
+ def test_parser(sample_git_outputs):
105
+ clean_output = sample_git_outputs["clean_output"]
106
+ error_output = sample_git_outputs["error_output"]
107
+ # ... test with sample data
108
+ ```
109
+
110
+ ## Writing New Tests
111
+
112
+ ### Unit Test Example
113
+ ```python
114
+ import pytest
115
+ from src.module import ClassToTest
116
+
117
+ @pytest.mark.unit
118
+ class TestClassToTest:
119
+ def test_method_should_return_expected_result(self):
120
+ # Arrange
121
+ obj = ClassToTest()
122
+
123
+ # Act
124
+ result = obj.method()
125
+
126
+ # Assert
127
+ assert result == expected_value
128
+ ```
129
+
130
+ ### Integration Test Example
131
+ ```python
132
+ import pytest
133
+ from unittest.mock import patch
134
+
135
+ @pytest.mark.integration
136
+ class TestWorkflow:
137
+ @patch('src.git_runner.GitRunner.run_git_command')
138
+ def test_complete_workflow(self, mock_run_git):
139
+ # Setup mock
140
+ mock_run_git.return_value = (0, "success", "")
141
+
142
+ # Test workflow
143
+ # ...
144
+ ```
145
+
146
+ ## Best Practices
147
+
148
+ 1. **Use descriptive test names** that explain what is being tested
149
+ 2. **Follow Arrange-Act-Assert pattern** for clear test structure
150
+ 3. **Mock external dependencies** to keep tests fast and reliable
151
+ 4. **Use fixtures** for common setup code
152
+ 5. **Mark tests appropriately** with `@pytest.mark.unit`, `@pytest.mark.integration`, or `@pytest.mark.slow`
153
+ 6. **Test both happy path and error cases**
154
+ 7. **Keep tests focused** - one assertion per test when possible
155
+
156
+ ## CI/CD
157
+
158
+ Tests run automatically on:
159
+ - Push to `main` or `develop` branches
160
+ - Pull requests to `main` branch
161
+
162
+ The CI pipeline runs:
163
+ 1. Unit tests with coverage on all Python versions (3.8-3.11) and OS platforms
164
+ 2. Integration tests
165
+ 3. Coverage reporting to Codecov
166
+
167
+ ## Troubleshooting
168
+
169
+ ### Tests Fail with Import Errors
170
+ Make sure you're running from the project root:
171
+ ```bash
172
+ cd /path/to/Fancy_Git
173
+ pytest tests/
174
+ ```
175
+
176
+ ### Git Command Tests Fail
177
+ Ensure git is installed and available in your PATH:
178
+ ```bash
179
+ git --version
180
+ ```
181
+
182
+ ### Slow Tests Taking Too Long
183
+ Skip slow tests during development:
184
+ ```bash
185
+ pytest -m "not slow" -v
186
+ ```
tests/__init__.py ADDED
File without changes
tests/conftest.py ADDED
@@ -0,0 +1,61 @@
1
+ import pytest
2
+ import tempfile
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ @pytest.fixture
8
+ def temp_git_repo():
9
+ """Create a temporary git repository for testing"""
10
+ temp_dir = tempfile.mkdtemp(prefix="fancygit_test_")
11
+ original_cwd = os.getcwd()
12
+
13
+ try:
14
+ os.chdir(temp_dir)
15
+
16
+ # Initialize git repo with explicit commands for better Windows compatibility
17
+ import subprocess
18
+ subprocess.run(["git", "init"], check=True, capture_output=True)
19
+ subprocess.run(["git", "config", "user.name", "Test User"], check=True, capture_output=True)
20
+ subprocess.run(["git", "config", "user.email", "test@example.com"], check=True, capture_output=True)
21
+
22
+ # Create initial commit
23
+ with open("README.md", "w") as f:
24
+ f.write("# Test Repository\n")
25
+ subprocess.run(["git", "add", "README.md"], check=True, capture_output=True)
26
+ subprocess.run(["git", "commit", "-m", "Initial commit"], check=True, capture_output=True)
27
+
28
+ # Ensure working directory is clean
29
+ subprocess.run(["git", "status"], check=True, capture_output=True)
30
+
31
+ yield temp_dir
32
+
33
+ finally:
34
+ os.chdir(original_cwd)
35
+ shutil.rmtree(temp_dir, ignore_errors=True)
36
+
37
+ @pytest.fixture
38
+ def sample_git_outputs():
39
+ """Sample git command outputs for testing"""
40
+ return {
41
+ "clean_output": {
42
+ "stdout": "On branch main\nnothing to commit, working tree clean\n",
43
+ "stderr": ""
44
+ },
45
+ "error_output": {
46
+ "stdout": "",
47
+ "stderr": "error: pathspec 'nonexistent.txt' did not match any files\n"
48
+ },
49
+ "conflict_output": {
50
+ "stdout": "",
51
+ "stderr": "error: Merge conflict in README.md\n"
52
+ },
53
+ "warning_output": {
54
+ "stdout": "warning: LF will be replaced by CRLF in README.md\n",
55
+ "stderr": ""
56
+ },
57
+ "ahead_behind_output": {
58
+ "stdout": "Your branch is ahead of 'origin/main' by 1 commit\n",
59
+ "stderr": ""
60
+ }
61
+ }