difflicious 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.
- difflicious/__init__.py +6 -0
- difflicious/app.py +505 -0
- difflicious/cli.py +77 -0
- difflicious/diff_parser.py +525 -0
- difflicious/dummy_data.json +44 -0
- difflicious/git_operations.py +1005 -0
- difflicious/services/__init__.py +1 -0
- difflicious/services/base_service.py +32 -0
- difflicious/services/diff_service.py +403 -0
- difflicious/services/exceptions.py +19 -0
- difflicious/services/git_service.py +135 -0
- difflicious/services/syntax_service.py +162 -0
- difflicious/services/template_service.py +382 -0
- difflicious/static/css/styles.css +885 -0
- difflicious/static/css/tailwind.css +1 -0
- difflicious/static/css/tailwind.input.css +5 -0
- difflicious/static/js/app.js +1002 -0
- difflicious/static/js/diff-interactions.js +1617 -0
- difflicious/templates/base.html +54 -0
- difflicious/templates/diff_file.html +90 -0
- difflicious/templates/diff_groups.html +29 -0
- difflicious/templates/diff_hunk.html +170 -0
- difflicious/templates/index.html +54 -0
- difflicious/templates/partials/empty_state.html +29 -0
- difflicious/templates/partials/global_controls.html +23 -0
- difflicious/templates/partials/loading_state.html +7 -0
- difflicious/templates/partials/toolbar.html +165 -0
- difflicious-0.1.0.dist-info/METADATA +190 -0
- difflicious-0.1.0.dist-info/RECORD +32 -0
- difflicious-0.1.0.dist-info/WHEEL +4 -0
- difflicious-0.1.0.dist-info/entry_points.txt +2 -0
- difflicious-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>{% block title %}Difflicious - Git Diff Visualization{% endblock %}</title>
|
|
8
|
+
|
|
9
|
+
<!-- Tailwind CSS (local build) -->
|
|
10
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
|
|
11
|
+
|
|
12
|
+
<!-- Google Fonts -->
|
|
13
|
+
{% if font_config.google_fonts_enabled %}
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link href="{{ font_config.selected_font.google_fonts_url }}" rel="stylesheet">
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<!-- Favicon -->
|
|
20
|
+
<link rel="icon" href="/favicon.ico" type="image/png">
|
|
21
|
+
|
|
22
|
+
<!-- Custom CSS -->
|
|
23
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
|
24
|
+
|
|
25
|
+
<!-- Syntax highlighting CSS -->
|
|
26
|
+
<style>
|
|
27
|
+
{
|
|
28
|
+
{
|
|
29
|
+
syntax_css|safe
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
|
|
34
|
+
<!-- Dynamic Font CSS -->
|
|
35
|
+
<style>
|
|
36
|
+
:root {
|
|
37
|
+
--font-family-mono: {{ font_config.selected_font.css_family|safe }}, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Courier New', monospace;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
40
|
+
|
|
41
|
+
{% block extra_head %}{% endblock %}
|
|
42
|
+
</head>
|
|
43
|
+
|
|
44
|
+
<body class="min-h-screen">
|
|
45
|
+
<div class="flex flex-col min-h-screen">
|
|
46
|
+
{% block content %}{% endblock %}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Minimal JavaScript for interactions -->
|
|
50
|
+
<script src="{{ url_for('static', filename='js/diff-interactions.js') }}"></script>
|
|
51
|
+
{% block extra_scripts %}{% endblock %}
|
|
52
|
+
</body>
|
|
53
|
+
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<!-- Single File Diff -->
|
|
2
|
+
{% set file_id = group_name + ":" + file.path %}
|
|
3
|
+
<div class="file-diff bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-sm"
|
|
4
|
+
data-file="{{ file_id }}">
|
|
5
|
+
<!-- File Header -->
|
|
6
|
+
<div class="file-header flex items-center justify-between p-4 cursor-pointer hover:bg-neutral-50 transition-colors space-x-2"
|
|
7
|
+
onclick="toggleFile('{{ file_id }}')">
|
|
8
|
+
<!-- Left side: toggle, expand, filename, status - all flush left -->
|
|
9
|
+
<div class="flex items-center space-x-3 flex-shrink min-w-0">
|
|
10
|
+
<span class="toggle-icon text-neutral-400 transition-transform duration-200 flex-shrink-0"
|
|
11
|
+
data-expanded="{{ 'true' if file.expanded else 'false' }}">
|
|
12
|
+
{{ '▼' if file.expanded else '▶' }}
|
|
13
|
+
</span>
|
|
14
|
+
<span
|
|
15
|
+
class="expand-icon text-neutral-400 hover:text-neutral-600 cursor-pointer transition-colors flex-shrink-0"
|
|
16
|
+
onclick="loadFullDiff('{{ file.path }}', '{{ file_id }}'); event.stopPropagation()"
|
|
17
|
+
title="Show complete file diff with unlimited context" id="expand-icon-{{ file_id }}">
|
|
18
|
+
↕
|
|
19
|
+
</span>
|
|
20
|
+
<div class="flex-1 min-w-0 break-words">
|
|
21
|
+
<span class="font-mono text-sm text-neutral-900" title="{{ file.path }}">
|
|
22
|
+
{%- if file.status == 'renamed' and file.old_path -%}
|
|
23
|
+
{{ file.old_path }} → {{ file.path }}
|
|
24
|
+
{%- else -%}
|
|
25
|
+
{{ file.path }}
|
|
26
|
+
{%- endif -%}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Status label positioned on the right when filename wraps -->
|
|
31
|
+
<div class="flex items-center ml-4">
|
|
32
|
+
<span class="text-xs px-2 py-1 rounded flex-shrink-0
|
|
33
|
+
{%- if file.status == 'added' %} bg-success-bg-100 text-success-text-800
|
|
34
|
+
{%- elif file.status == 'deleted' %} bg-danger-bg-100 text-danger-text-800
|
|
35
|
+
{%- elif file.status == 'renamed' %} bg-warning-bg-100 text-warning-text-800
|
|
36
|
+
{%- else %} bg-neutral-100 text-neutral-500
|
|
37
|
+
{%- endif %}">
|
|
38
|
+
{%- if file.status == 'renamed' %}moved{%- else %}{{ file.status }}{%- endif %}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Right side: diff counts and navigation -->
|
|
44
|
+
<div class="flex items-center space-x-2 text-xs flex-shrink-0 ml-6">
|
|
45
|
+
<!-- File Stats -->
|
|
46
|
+
{% if file.additions > 0 %}
|
|
47
|
+
<span class="bg-success-bg-100 text-success-text-800 px-2 py-1 rounded">
|
|
48
|
+
+{{ file.additions }}
|
|
49
|
+
</span>
|
|
50
|
+
{% endif %}
|
|
51
|
+
{% if file.deletions > 0 %}
|
|
52
|
+
<span class="bg-danger-bg-100 text-danger-text-800 px-2 py-1 rounded">
|
|
53
|
+
-{{ file.deletions }}
|
|
54
|
+
</span>
|
|
55
|
+
{% endif %}
|
|
56
|
+
|
|
57
|
+
<!-- File Navigation -->
|
|
58
|
+
<div class="flex items-center space-x-1" onclick="event.stopPropagation()">
|
|
59
|
+
<button onclick="navigateToPreviousFile('{{ file_id }}')"
|
|
60
|
+
class="p-1 text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 rounded transition-colors"
|
|
61
|
+
title="Previous file">
|
|
62
|
+
<span class="text-sm">↑</span>
|
|
63
|
+
</button>
|
|
64
|
+
<button onclick="navigateToNextFile('{{ file_id }}')"
|
|
65
|
+
class="p-1 text-neutral-400 hover:text-neutral-600 hover:bg-neutral-100 rounded transition-colors"
|
|
66
|
+
title="Next file">
|
|
67
|
+
<span class="text-sm">↓</span>
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- File Content -->
|
|
74
|
+
<div class="file-content border-t border-neutral-200" data-file-content="{{ file_id }}"
|
|
75
|
+
style="display: {{ 'block' if file.expanded else 'none' }};">
|
|
76
|
+
{% if not file.hunks or file.hunks|length == 0 %}
|
|
77
|
+
<!-- No content available -->
|
|
78
|
+
<div class="p-8 text-center text-neutral-500">
|
|
79
|
+
<div class="text-4xl mb-2">📄</div>
|
|
80
|
+
<p>No diff content available</p>
|
|
81
|
+
<p class="text-sm">(Binary file, untracked, or no changes)</p>
|
|
82
|
+
</div>
|
|
83
|
+
{% else %}
|
|
84
|
+
<!-- Render hunks -->
|
|
85
|
+
{% for hunk in file.hunks %}
|
|
86
|
+
{% include "diff_hunk.html" with context %}
|
|
87
|
+
{% endfor %}
|
|
88
|
+
{% endif %}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- Diff Groups -->
|
|
2
|
+
{% for group_key, group in groups.items() %}
|
|
3
|
+
{% if group.count > 0 %}
|
|
4
|
+
<div class="diff-group space-y-2" data-group="{{ group_key }}">
|
|
5
|
+
<!-- Group Header (hidden for staged-only view or ungrouped 'changes' view) -->
|
|
6
|
+
{% set hide_header = (group_key == 'staged' and not unstaged and not untracked) or (group_key == 'changes' and not untracked) %}
|
|
7
|
+
{% if not hide_header %}
|
|
8
|
+
<div class="group-header flex items-center space-x-3 px-2 py-2 cursor-pointer hover:bg-neutral-50 transition-colors rounded"
|
|
9
|
+
onclick="toggleGroup('{{ group_key }}')">
|
|
10
|
+
<span class="toggle-icon text-neutral-400 transition-transform duration-200"
|
|
11
|
+
data-expanded="true">▼</span>
|
|
12
|
+
<h3 class="text-neutral-700">{{ group_key|title }}</h3>
|
|
13
|
+
<span class="text-xs text-neutral-500 bg-neutral-100 px-2 py-1 rounded">
|
|
14
|
+
{{ group.count }} files
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<!-- Group Files -->
|
|
20
|
+
<div class="group-content space-y-4 {% if not hide_header %}ml-4{% endif %}"
|
|
21
|
+
data-group-content="{{ group_key }}">
|
|
22
|
+
{% for file in group.files %}
|
|
23
|
+
{% set group_name = group_key %}
|
|
24
|
+
{% include "diff_file.html" with context %}
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endfor %}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<!-- Single Hunk -->
|
|
2
|
+
<div class="hunk border-b border-neutral-100 last:border-b-0" data-line-start="{{ hunk.line_start }}"
|
|
3
|
+
data-line-end="{{ hunk.line_end }}" data-left-line-start="{{ hunk.left_start or 0 }}"
|
|
4
|
+
data-left-line-end="{{ hunk.left_end or 0 }}">
|
|
5
|
+
|
|
6
|
+
<!-- Up Expansion Controls (before hunk) -->
|
|
7
|
+
{% if hunk.can_expand_before %}
|
|
8
|
+
<div class="hunk-expansion bg-info-bg-50 text-xs font-mono text-info-text-800 border-b border-info-bg-100 flex">
|
|
9
|
+
<!-- Left Side Controls -->
|
|
10
|
+
<div class="flex-1 border-r border-info-bg-200">
|
|
11
|
+
<div class="flex">
|
|
12
|
+
<div class="w-12 bg-info-bg-100 border-r border-info-bg-200 select-none flex flex-col">
|
|
13
|
+
<button onclick="expandContext(this, '{{ file.path }}', {{ hunk.index }}, 'before', 10, 'pygments')"
|
|
14
|
+
class="expansion-btn w-full px-2 py-1 text-xs bg-info-bg-200 hover:bg-info-bg-300 text-info-text-800 transition-colors flex items-center justify-center h-full"
|
|
15
|
+
data-target-start="{{ hunk.expand_before_start }}"
|
|
16
|
+
data-target-end="{{ hunk.expand_before_end }}" data-direction="before"
|
|
17
|
+
title="Expand 10 lines up ({{ hunk.expand_before_start }}-{{ hunk.expand_before_end }})">
|
|
18
|
+
▲
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex-1 px-2 py-2 overflow-hidden flex items-center min-h-[2rem] min-w-0">
|
|
22
|
+
{% if hunk.section_header %}
|
|
23
|
+
<div class="w-full overflow-hidden">
|
|
24
|
+
<span class="truncate block">{{ hunk.section_header }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
{% else %}
|
|
27
|
+
<span class="text-transparent"> </span>
|
|
28
|
+
{% endif %}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Right Side Controls -->
|
|
34
|
+
<div class="flex-1">
|
|
35
|
+
<div class="flex">
|
|
36
|
+
<div class="w-12 bg-info-bg-100 border-r border-info-bg-200 select-none flex flex-col">
|
|
37
|
+
<button onclick="expandContext(this, '{{ file.path }}', {{ hunk.index }}, 'before', 10, 'pygments')"
|
|
38
|
+
class="expansion-btn w-full px-2 py-1 text-xs bg-info-bg-200 hover:bg-info-bg-300 text-info-text-800 transition-colors flex items-center justify-center h-full"
|
|
39
|
+
data-target-start="{{ hunk.expand_before_start }}"
|
|
40
|
+
data-target-end="{{ hunk.expand_before_end }}" data-direction="before"
|
|
41
|
+
title="Expand 10 lines up ({{ hunk.expand_before_start }}-{{ hunk.expand_before_end }})">
|
|
42
|
+
▲
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex-1 px-2 py-2 overflow-hidden min-h-[2rem] flex items-center">
|
|
46
|
+
<span class="text-transparent"> </span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
<!-- Hunk Lines -->
|
|
54
|
+
<div class="hunk-lines font-mono text-xs" data-left-start-line="{{ hunk.old_start }}"
|
|
55
|
+
data-left-end-line="{{ hunk.old_start + hunk.old_count - 1 }}" data-right-start-line="{{ hunk.new_start }}"
|
|
56
|
+
data-right-end-line="{{ hunk.new_start + hunk.new_count - 1 }}">
|
|
57
|
+
{% for line in hunk.lines %}
|
|
58
|
+
<div class="diff-line flex line-{{ line.type }}">
|
|
59
|
+
<!-- Left Side (Before) -->
|
|
60
|
+
<div class="line-left flex-1 border-r border-neutral-200 dark:border-neutral-600">
|
|
61
|
+
<div class="flex">
|
|
62
|
+
<div
|
|
63
|
+
class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 dark:border-neutral-600 select-none {% if line.left and line.left.type == 'deletion' %}bg-red-50 dark:bg-red-900/20{% endif %}">
|
|
64
|
+
{% if line.left and line.left.line_num %}
|
|
65
|
+
<span>{{ line.left.line_num }}</span>
|
|
66
|
+
{% endif %}
|
|
67
|
+
</div>
|
|
68
|
+
<div
|
|
69
|
+
class="line-content flex-1 px-2 py-1 overflow-x-auto {% if line.left and line.left.type == 'deletion' %}bg-red-50 dark:bg-red-900/20{% endif %}">
|
|
70
|
+
{% if line.left and line.left.type == 'deletion' %}
|
|
71
|
+
<span class="text-danger-text-600">-</span>
|
|
72
|
+
{% elif line.type == 'context' %}
|
|
73
|
+
<span class="text-neutral-400"> </span>
|
|
74
|
+
{% endif %}
|
|
75
|
+
{% if line.left and (line.left.highlighted_content or line.left.content) %}
|
|
76
|
+
{% if line.left.highlighted_content %}
|
|
77
|
+
<span>{{ line.left.highlighted_content|safe }}</span>
|
|
78
|
+
{% elif line.left.content %}
|
|
79
|
+
<span>{{ line.left.content }}</span>
|
|
80
|
+
{% endif %}
|
|
81
|
+
{% if line.left.missing_newline %}
|
|
82
|
+
<span class="no-newline-indicator text-danger-text-500"
|
|
83
|
+
aria-label="No newline at end of file">↩</span>
|
|
84
|
+
{% endif %}
|
|
85
|
+
{% else %}
|
|
86
|
+
<!-- Ensure empty content areas still take up space -->
|
|
87
|
+
<span> </span>
|
|
88
|
+
{% endif %}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- Right Side (After) -->
|
|
94
|
+
<div class="line-right flex-1">
|
|
95
|
+
<div class="flex">
|
|
96
|
+
<div
|
|
97
|
+
class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 dark:border-neutral-600 select-none {% if line.right and line.right.type == 'addition' %}bg-green-50 dark:bg-green-900/20{% endif %}">
|
|
98
|
+
{% if line.right and line.right.line_num %}
|
|
99
|
+
<span>{{ line.right.line_num }}</span>
|
|
100
|
+
{% endif %}
|
|
101
|
+
</div>
|
|
102
|
+
<div
|
|
103
|
+
class="line-content flex-1 px-2 py-1 overflow-x-auto {% if line.right and line.right.type == 'addition' %}bg-green-50 dark:bg-green-900/20{% endif %}">
|
|
104
|
+
{% if line.right and line.right.type == 'addition' %}
|
|
105
|
+
<span class="text-success-text-600">+</span>
|
|
106
|
+
{% elif line.type == 'context' %}
|
|
107
|
+
<span class="text-neutral-400"> </span>
|
|
108
|
+
{% endif %}
|
|
109
|
+
{% if line.right and (line.right.highlighted_content or line.right.content) %}
|
|
110
|
+
{% if line.right.highlighted_content %}
|
|
111
|
+
<span>{{ line.right.highlighted_content|safe }}</span>
|
|
112
|
+
{% elif line.right.content %}
|
|
113
|
+
<span>{{ line.right.content }}</span>
|
|
114
|
+
{% endif %}
|
|
115
|
+
{% if line.right.missing_newline %}
|
|
116
|
+
<span class="no-newline-indicator text-danger-text-500"
|
|
117
|
+
aria-label="No newline at end of file">↩</span>
|
|
118
|
+
{% endif %}
|
|
119
|
+
{% else %}
|
|
120
|
+
<!-- Ensure empty content areas still take up space -->
|
|
121
|
+
<span> </span>
|
|
122
|
+
{% endif %}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{% endfor %}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- Down Expansion Controls (after hunk) -->
|
|
131
|
+
{% if hunk.can_expand_after %}
|
|
132
|
+
<div class="hunk-expansion bg-info-bg-50 text-xs font-mono text-info-text-800 border-t border-info-bg-100 flex">
|
|
133
|
+
<!-- Left Side Controls -->
|
|
134
|
+
<div class="flex-1 border-r border-info-bg-200">
|
|
135
|
+
<div class="flex">
|
|
136
|
+
<div class="w-12 bg-info-bg-100 border-r border-info-bg-200 select-none flex flex-col">
|
|
137
|
+
<button onclick="expandContext(this, '{{ file.path }}', {{ hunk.index }}, 'after', 10, 'pygments')"
|
|
138
|
+
class="expansion-btn w-full px-2 py-1 text-xs bg-info-bg-200 hover:bg-info-bg-300 text-info-text-800 transition-colors flex items-center justify-center h-full"
|
|
139
|
+
data-target-start="{{ hunk.expand_after_start }}" data-target-end="{{ hunk.expand_after_end }}"
|
|
140
|
+
data-direction="after"
|
|
141
|
+
title="Expand 10 lines down ({{ hunk.expand_after_start }}-{{ hunk.expand_after_end }})">
|
|
142
|
+
▼
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="flex-1 px-2 py-2 overflow-hidden flex items-center min-h-[2rem] min-w-0">
|
|
146
|
+
<span class="text-transparent"> </span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Right Side Controls -->
|
|
152
|
+
<div class="flex-1">
|
|
153
|
+
<div class="flex">
|
|
154
|
+
<div class="w-12 bg-info-bg-100 border-r border-info-bg-200 select-none flex flex-col">
|
|
155
|
+
<button onclick="expandContext(this, '{{ file.path }}', {{ hunk.index }}, 'after', 10, 'pygments')"
|
|
156
|
+
class="expansion-btn w-full px-2 py-1 text-xs bg-info-bg-200 hover:bg-info-bg-300 text-info-text-800 transition-colors flex items-center justify-center h-full"
|
|
157
|
+
data-target-start="{{ hunk.expand_after_start }}" data-target-end="{{ hunk.expand_after_end }}"
|
|
158
|
+
data-direction="after"
|
|
159
|
+
title="Expand 10 lines down ({{ hunk.expand_after_start }}-{{ hunk.expand_after_end }})">
|
|
160
|
+
▼
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="flex-1 px-2 py-2 overflow-hidden min-h-[2rem] flex items-center">
|
|
164
|
+
<span class="text-transparent"> </span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
{% endif %}
|
|
170
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
{{ render_partial('partials/toolbar.html',
|
|
5
|
+
branches=branches,
|
|
6
|
+
current_base_ref=current_base_ref,
|
|
7
|
+
unstaged=unstaged,
|
|
8
|
+
staged=staged,
|
|
9
|
+
untracked=untracked,
|
|
10
|
+
search_filter=search_filter,
|
|
11
|
+
total_files=total_files,
|
|
12
|
+
is_head_comparison=is_head_comparison) }}
|
|
13
|
+
|
|
14
|
+
<!-- Main Content -->
|
|
15
|
+
<main class="flex-1 overflow-hidden">
|
|
16
|
+
{% if loading %}
|
|
17
|
+
{{ render_partial('partials/loading_state.html') }}
|
|
18
|
+
{% elif not groups or not total_files %}
|
|
19
|
+
{{ render_partial('partials/empty_state.html',
|
|
20
|
+
error=error,
|
|
21
|
+
unstaged=unstaged,
|
|
22
|
+
untracked=untracked,
|
|
23
|
+
current_base_ref=current_base_ref) }}
|
|
24
|
+
{% else %}
|
|
25
|
+
<!-- Diff Content -->
|
|
26
|
+
<div class="diff-container h-full overflow-y-auto">
|
|
27
|
+
<div class="p-4 space-y-6">
|
|
28
|
+
{{ render_partial('partials/global_controls.html') }}
|
|
29
|
+
|
|
30
|
+
<!-- Render Diff Groups -->
|
|
31
|
+
{% if not is_head_comparison and not untracked and groups.get('changes') %}
|
|
32
|
+
<!-- Branch comparison with no untracked files: show all changes at top level -->
|
|
33
|
+
<div class="group-content space-y-4" data-group-content="ungrouped">
|
|
34
|
+
{% for file in groups['changes'].files %}
|
|
35
|
+
{% set group_name = 'changes' %}
|
|
36
|
+
{% include 'diff_file.html' with context %}
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</div>
|
|
39
|
+
{% elif not untracked and groups.get('changes') %}
|
|
40
|
+
<!-- HEAD comparison with no untracked files: show ungrouped -->
|
|
41
|
+
<div class="group-content space-y-4" data-group-content="ungrouped">
|
|
42
|
+
{% for file in groups['changes'].files %}
|
|
43
|
+
{% set group_name = 'changes' %}
|
|
44
|
+
{% include 'diff_file.html' with context %}
|
|
45
|
+
{% endfor %}
|
|
46
|
+
</div>
|
|
47
|
+
{% else %}
|
|
48
|
+
{% include "diff_groups.html" %}
|
|
49
|
+
{% endif %}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{% endif %}
|
|
53
|
+
</main>
|
|
54
|
+
{% endblock %}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- Empty State -->
|
|
2
|
+
<div class="flex items-center justify-center h-full">
|
|
3
|
+
<div class="text-center">
|
|
4
|
+
{% if error %}
|
|
5
|
+
<div class="text-4xl mb-4">⚠️</div>
|
|
6
|
+
<h2 class="text-xl font-semibold text-danger-text-600 mb-2">Error</h2>
|
|
7
|
+
<p class="text-danger-text-500 mb-4">{{ error }}</p>
|
|
8
|
+
{% else %}
|
|
9
|
+
<div class="text-6xl mb-4">✨</div>
|
|
10
|
+
<h2 class="text-xl font-semibold text-neutral-900 mb-2">No changes found</h2>
|
|
11
|
+
{% endif %}
|
|
12
|
+
<div class="text-neutral-600 space-y-2">
|
|
13
|
+
{% if not unstaged and not untracked %}
|
|
14
|
+
<p>Enable "Unstaged" or "Untracked" to see changes.</p>
|
|
15
|
+
{% elif unstaged and not untracked %}
|
|
16
|
+
<p>No unstaged changes in your working directory.</p>
|
|
17
|
+
{% elif not unstaged and untracked %}
|
|
18
|
+
<p>No untracked files found.</p>
|
|
19
|
+
{% else %}
|
|
20
|
+
<p>Your working directory is clean - no unstaged or untracked files.</p>
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if current_base_ref != 'main' %}
|
|
23
|
+
<p class="text-sm text-neutral-500 mt-3">
|
|
24
|
+
Comparing against: <span class="font-mono">{{ current_base_ref }}</span>
|
|
25
|
+
</p>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!-- Global Controls -->
|
|
2
|
+
<div class="flex items-center justify-between mb-4">
|
|
3
|
+
<div class="flex items-center space-x-2">
|
|
4
|
+
<button id="expandAll" onclick="expandAllFiles()"
|
|
5
|
+
class="bg-neutral-100 text-neutral-700 hover:bg-neutral-200 px-3 py-1.5 rounded text-sm focus:outline-none focus:ring-2 focus:ring-neutral-400 transition-colors flex items-center space-x-2" title="Expand All">
|
|
6
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-label="Expand All">
|
|
7
|
+
<path d="M9 9H4v1h5V9z"/>
|
|
8
|
+
<path d="M7 12V7H6v5h1z"/>
|
|
9
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z"/>
|
|
10
|
+
</svg>
|
|
11
|
+
<span>Expand All</span>
|
|
12
|
+
</button>
|
|
13
|
+
<button id="collapseAll" onclick="collapseAllFiles()"
|
|
14
|
+
class="bg-neutral-100 text-neutral-700 hover:bg-neutral-200 px-3 py-1.5 rounded text-sm focus:outline-none focus:ring-2 focus:ring-neutral-400 transition-colors flex items-center space-x-2" title="Collapse All">
|
|
15
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-label="Collapse All">
|
|
16
|
+
<path d="M9 9H4v1h5V9z"/>
|
|
17
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z"/>
|
|
18
|
+
</svg>
|
|
19
|
+
<span>Collapse All</span>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div id="hidden-files-banner" class="text-xs text-neutral-600 italic" style="display: none;"></div>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!-- Loading State -->
|
|
2
|
+
<div class="flex items-center justify-center h-full">
|
|
3
|
+
<div class="text-center">
|
|
4
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-info-text-600 mx-auto"></div>
|
|
5
|
+
<p class="mt-2 text-neutral-600">Loading git diff data...</p>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<!-- Toolbar -->
|
|
2
|
+
<header class="bg-neutral-50 dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-700 px-4 py-3">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div class="flex items-center space-x-4 flex-1">
|
|
5
|
+
<h1 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
|
6
|
+
<a href="/" class="hover:underline">Difflicious</a>
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
<!-- Base Branch Dropdown -->
|
|
10
|
+
<form method="GET" action="/" class="flex items-center space-x-2 flex-1"
|
|
11
|
+
onsubmit="return scrubEmptyInputs(this)">
|
|
12
|
+
<label class="text-sm font-medium text-neutral-700">
|
|
13
|
+
<svg width="20" height="20" viewBox="0 0 512 512" fill="currentColor" class="inline" title="Branch"
|
|
14
|
+
aria-label="Select base branch">
|
|
15
|
+
<path
|
|
16
|
+
d="M416 160c0-35.3-28.7-64-64-64s-64 28.7-64 64c0 23.7 12.9 44.3 32 55.4v8.6c0 19.9-7.8 33.7-25.3 44.9-15.4 9.8-38.1 17.1-67.5 21.5-14 2.1-25.7 6-35.2 10.7V151.4c19.1-11.1 32-31.7 32-55.4 0-35.3-28.7-64-64-64S96 60.7 96 96c0 23.7 12.9 44.3 32 55.4v209.2c-19.1 11.1-32 31.7-32 55.4 0 35.3 28.7 64 64 64s64-28.7 64-64c0-16.6-6.3-31.7-16.7-43.1 1.9-4.9 9.7-16.3 29.4-19.3 38.8-5.8 68.9-15.9 92.3-30.8 36-22.8 55-57 55-98.8v-8.6c19.1-11.1 32-31.7 32-55.4zM160 56c22.1 0 40 17.9 40 40s-17.9 40-40 40-40-17.9-40-40 17.9-40 40-40zm0 400c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm192-256c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40z" />
|
|
17
|
+
</svg>
|
|
18
|
+
</label>
|
|
19
|
+
<select name="base_ref" onchange="this.form.submit()"
|
|
20
|
+
class="bg-neutral-50 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-info-text-600 focus:border-info-text-600 dark:text-neutral-100">
|
|
21
|
+
{% for branch in branches.all %}
|
|
22
|
+
<option value="{{ branch }}" {% if branch==current_base_ref %}selected{% endif %}>
|
|
23
|
+
{{ branch }}
|
|
24
|
+
</option>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</select>
|
|
27
|
+
|
|
28
|
+
<!-- Diff Options -->
|
|
29
|
+
<div class="flex items-center space-x-4 flex-1">
|
|
30
|
+
<!-- DEBUG: is_head_comparison = {{ is_head_comparison }}, base_ref = {{ current_base_ref }} -->
|
|
31
|
+
{% if is_head_comparison %}
|
|
32
|
+
<!-- HEAD comparison: show "Untracked" and "Unstaged" (staged always visible) -->
|
|
33
|
+
<label class="flex items-center">
|
|
34
|
+
<input type="checkbox" name="untracked" value="true" {% if untracked %}checked{% endif %}
|
|
35
|
+
class="h-4 w-4 text-info-text-600 focus:ring-info-text-600 border-neutral-300 rounded checkbox-control">
|
|
36
|
+
<svg width="20" height="20" viewBox="0 0 1024 1024" fill="currentColor"
|
|
37
|
+
class="ml-1 text-neutral-700" title="Untracked" aria-label="Untracked">
|
|
38
|
+
<path
|
|
39
|
+
d="M854.6 288.7L639.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.6-9.4-22.6zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0 0 42 42h216v494zM402 549c0 5.4 4.4 9.5 9.8 9.5h32.4c5.4 0 9.8-4.2 9.8-9.4 0-28.2 25.8-51.6 58-51.6s58 23.4 58 51.5c0 25.3-21 47.2-49.3 50.9-19.3 2.8-34.5 20.3-34.7 40.1v32c0 5.5 4.5 10 10 10h32c5.5 0 10-4.5 10-10v-12.2c0-6 4-11.5 9.7-13.3 44.6-14.4 75-54 74.3-98.9-.8-55.5-49.2-100.8-108.5-101.6-61.4-.7-111.5 45.6-111.5 103zm78 195a32 32 0 1 0 64 0 32 32 0 1 0-64 0z" />
|
|
40
|
+
</svg>
|
|
41
|
+
<span class="ml-1 text-sm text-neutral-700">Untracked</span>
|
|
42
|
+
</label>
|
|
43
|
+
|
|
44
|
+
<label class="flex items-center">
|
|
45
|
+
<input type="checkbox" name="unstaged" value="true" {% if unstaged %}checked{% endif %}
|
|
46
|
+
class="h-4 w-4 text-info-text-600 focus:ring-info-text-600 border-neutral-300 rounded checkbox-control">
|
|
47
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
48
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
49
|
+
class="ml-1 text-neutral-700" title="Unstaged" aria-label="Unstaged">
|
|
50
|
+
<circle cx="6" cy="6" r="3" />
|
|
51
|
+
<path d="M6 9v12" />
|
|
52
|
+
<path d="m21 3-6 6" />
|
|
53
|
+
<path d="m21 9-6-6" />
|
|
54
|
+
<path d="M18 11.5V15" />
|
|
55
|
+
<circle cx="18" cy="18" r="3" />
|
|
56
|
+
</svg>
|
|
57
|
+
<span class="ml-1 text-sm text-neutral-700">Unstaged</span>
|
|
58
|
+
</label>
|
|
59
|
+
{% else %}
|
|
60
|
+
<!-- Branch comparison: only show "Untracked" (changes are always visible) -->
|
|
61
|
+
<label class="flex items-center">
|
|
62
|
+
<input type="checkbox" name="untracked" value="true" {% if untracked %}checked{% endif %}
|
|
63
|
+
class="h-4 w-4 text-info-text-600 focus:ring-info-text-600 border-neutral-300 rounded checkbox-control">
|
|
64
|
+
<svg width="20" height="20" viewBox="0 0 1024 1024" fill="currentColor"
|
|
65
|
+
class="ml-1 text-neutral-700" title="Untracked" aria-label="Untracked">
|
|
66
|
+
<path
|
|
67
|
+
d="M854.6 288.7L639.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.6-9.4-22.6zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0 0 42 42h216v494zM402 549c0 5.4 4.4 9.5 9.8 9.5h32.4c5.4 0 9.8-4.2 9.8-9.4 0-28.2 25.8-51.6 58-51.6s58 23.4 58 51.5c0 25.3-21 47.2-49.3 50.9-19.3 2.8-34.5 20.3-34.7 40.1v32c0 5.5 4.5 10 10 10h32c5.5 0 10-4.5 10-10v-12.2c0-6 4-11.5 9.7-13.3 44.6-14.4 75-54 74.3-98.9-.8-55.5-49.2-100.8-108.5-101.6-61.4-.7-111.5 45.6-111.5 103zm78 195a32 32 0 1 0 64 0 32 32 0 1 0-64 0z" />
|
|
68
|
+
</svg>
|
|
69
|
+
<span class="ml-1 text-sm text-neutral-700">Untracked</span>
|
|
70
|
+
</label>
|
|
71
|
+
{% endif %}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Theme Toggle -->
|
|
75
|
+
<div class="flex items-center">
|
|
76
|
+
<button type="button" onclick="toggleTheme(); event.preventDefault(); event.stopPropagation();"
|
|
77
|
+
class="p-2 rounded-md hover:bg-neutral-100 text-neutral-600 hover:text-neutral-900 transition-colors"
|
|
78
|
+
title="Toggle dark/light theme">
|
|
79
|
+
<span id="theme-icon">🌙</span>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Search Filter -->
|
|
84
|
+
<div class="ml-auto relative flex items-center">
|
|
85
|
+
<svg width="22" height="22" viewBox="0 0 1024 1024" fill="currentColor"
|
|
86
|
+
class="text-neutral-400 mr-3" title="Filter" aria-label="Filter files">
|
|
87
|
+
<path
|
|
88
|
+
d="M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z" />
|
|
89
|
+
</svg>
|
|
90
|
+
<input id="diff-search-input" type="text" name="search" value="{{ search_filter or '' }}"
|
|
91
|
+
placeholder="Filter files..."
|
|
92
|
+
class="bg-neutral-50 dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-md pl-3 pr-8 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-info-text-600 focus:border-info-text-600 dark:text-neutral-100">
|
|
93
|
+
<button id="diff-search-clear" type="button"
|
|
94
|
+
class="hidden absolute inset-y-0 right-2 my-auto text-neutral-400 hover:text-neutral-600"
|
|
95
|
+
aria-label="Clear search">
|
|
96
|
+
×
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</header>
|
|
103
|
+
|
|
104
|
+
<script>
|
|
105
|
+
function updateCheckboxAndSubmit(checkbox) {
|
|
106
|
+
const form = checkbox.form;
|
|
107
|
+
const formData = new FormData(form);
|
|
108
|
+
const params = new URLSearchParams();
|
|
109
|
+
|
|
110
|
+
// Get all form data except checkboxes (we'll handle those explicitly)
|
|
111
|
+
for (const [key, value] of formData.entries()) {
|
|
112
|
+
if (key !== 'unstaged' && key !== 'staged' && key !== 'untracked') {
|
|
113
|
+
// Skip empty-ish search values
|
|
114
|
+
if (key === 'search' && (!value || value.trim() === '')) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
params.set(key, value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Explicitly set checkbox states based on their current checked state
|
|
122
|
+
const unstagedCheckbox = form.querySelector('input[name="unstaged"]');
|
|
123
|
+
const stagedCheckbox = form.querySelector('input[name="staged"]');
|
|
124
|
+
const untrackedCheckbox = form.querySelector('input[name="untracked"]');
|
|
125
|
+
|
|
126
|
+
if (unstagedCheckbox) {
|
|
127
|
+
params.set('unstaged', unstagedCheckbox.checked ? 'true' : 'false');
|
|
128
|
+
}
|
|
129
|
+
if (stagedCheckbox) {
|
|
130
|
+
// Only set staged parameter if staged checkbox exists (not in HEAD comparisons)
|
|
131
|
+
params.set('staged', stagedCheckbox.checked ? 'true' : 'false');
|
|
132
|
+
}
|
|
133
|
+
if (untrackedCheckbox) {
|
|
134
|
+
params.set('untracked', untrackedCheckbox.checked ? 'true' : 'false');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Navigate to the updated URL
|
|
138
|
+
window.location.href = '?' + params.toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function scrubEmptyInputs(form) {
|
|
142
|
+
// Prevent submitting empty search to avoid blank search param
|
|
143
|
+
const searchInput = form.querySelector('input[name="search"]');
|
|
144
|
+
if (searchInput && (!searchInput.value || searchInput.value.trim() === '')) {
|
|
145
|
+
searchInput.name = '';
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Set up event listeners when DOM is ready
|
|
151
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
152
|
+
const checkboxes = document.querySelectorAll('.checkbox-control');
|
|
153
|
+
|
|
154
|
+
checkboxes.forEach(function (checkbox) {
|
|
155
|
+
checkbox.addEventListener('change', function (event) {
|
|
156
|
+
// Prevent the default form submission behavior
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
event.stopImmediatePropagation();
|
|
160
|
+
|
|
161
|
+
updateCheckboxAndSubmit(this);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
</script>
|