pygitweb 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.
- pygitweb/__init__.py +22 -0
- pygitweb/__meta__.py +9 -0
- pygitweb/actions.py +1303 -0
- pygitweb/addproject_permissions_test.py +114 -0
- pygitweb/api/__init__.py +0 -0
- pygitweb/api/action.py +20 -0
- pygitweb/api/plugins/__init__.py +0 -0
- pygitweb/api/plugins/project_zip.py +71 -0
- pygitweb/api/project.py +67 -0
- pygitweb/api/subpage.py +22 -0
- pygitweb/auth.py +387 -0
- pygitweb/auth.schema.json +106 -0
- pygitweb/auth_config.py +243 -0
- pygitweb/auth_oauth.py +343 -0
- pygitweb/auth_test.py +390 -0
- pygitweb/boards_permissions_test.py +113 -0
- pygitweb/change_queue.py +110 -0
- pygitweb/change_queue_routes_test.py +146 -0
- pygitweb/change_queue_test.py +91 -0
- pygitweb/comments_permissions_test.py +140 -0
- pygitweb/config.py +185 -0
- pygitweb/conftest.py +43 -0
- pygitweb/dependencies.py +87 -0
- pygitweb/formatting.py +182 -0
- pygitweb/formatting_test.py +68 -0
- pygitweb/git_helpers.py +454 -0
- pygitweb/git_helpers_test.py +223 -0
- pygitweb/gravatar.py +16 -0
- pygitweb/gravatar_test.py +10 -0
- pygitweb/hooks_install.py +278 -0
- pygitweb/hooks_install_test.py +363 -0
- pygitweb/hooks_permissions_test.py +116 -0
- pygitweb/main.py +887 -0
- pygitweb/merge_requests.py +230 -0
- pygitweb/merge_requests_test.py +85 -0
- pygitweb/mr_permissions_test.py +148 -0
- pygitweb/password_hash.py +74 -0
- pygitweb/password_hash_test.py +20 -0
- pygitweb/permissions.py +209 -0
- pygitweb/permissions_test.py +86 -0
- pygitweb/plugin_loader.py +83 -0
- pygitweb/projects.py +213 -0
- pygitweb/projects_test.py +186 -0
- pygitweb/sessions.py +73 -0
- pygitweb/settings.py +412 -0
- pygitweb/settings.schema.json +73 -0
- pygitweb/settings_config.py +122 -0
- pygitweb/settings_config_test.py +101 -0
- pygitweb/settings_permissions_test.py +76 -0
- pygitweb/static/blob-view.js +133 -0
- pygitweb/static/board.js +839 -0
- pygitweb/static/hook-install.js +53 -0
- pygitweb/static/icons/cloud-down.svg +1 -0
- pygitweb/static/icons/cloud-exclamation.svg +1 -0
- pygitweb/static/icons/cloud-filled.svg +1 -0
- pygitweb/static/icons/cloud-storm.svg +1 -0
- pygitweb/static/icons/cloud-up.svg +1 -0
- pygitweb/static/icons/cloud.svg +1 -0
- pygitweb/static/logo.svg +44 -0
- pygitweb/static/main.css +1210 -0
- pygitweb/static/navbar-auth.js +82 -0
- pygitweb/static/readme-render.js +174 -0
- pygitweb/static/search-page.js +139 -0
- pygitweb/static/summary-ref-switcher.js +130 -0
- pygitweb/static/themes/solarized.css +34 -0
- pygitweb/tasks.py +595 -0
- pygitweb/tasks_permissions_test.py +137 -0
- pygitweb/tasks_routes_test.py +430 -0
- pygitweb/templates/addproject.html +174 -0
- pygitweb/templates/blobdiff.html +57 -0
- pygitweb/templates/board.html +31 -0
- pygitweb/templates/board_card.html +44 -0
- pygitweb/templates/commitdiff.html +57 -0
- pygitweb/templates/hooks_summary_cell.html +37 -0
- pygitweb/templates/login.html +37 -0
- pygitweb/templates/merge_request_summary_cell.html +18 -0
- pygitweb/templates/merge_request_tag.html +90 -0
- pygitweb/templates/pagination.html +44 -0
- pygitweb/templates/preamble.html +89 -0
- pygitweb/templates/project_settings.html +13 -0
- pygitweb/templates/pygit2_settings.html +12 -0
- pygitweb/templates/pygitweb_settings.html +12 -0
- pygitweb/templates/search.html +77 -0
- pygitweb/templates/settings_form_field.html +27 -0
- pygitweb/templates/table.html +15 -0
- pygitweb/templates/user_profile.html +10 -0
- pygitweb/templates_env.py +21 -0
- pygitweb/timeline_cache.py +243 -0
- pygitweb/validation.py +75 -0
- pygitweb/validation_test.py +167 -0
- pygitweb-0.1.0.dist-info/METADATA +165 -0
- pygitweb-0.1.0.dist-info/RECORD +95 -0
- pygitweb-0.1.0.dist-info/WHEEL +5 -0
- pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
- pygitweb-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<h1 class="page-title">Add project</h1>
|
|
2
|
+
{% if show_sign_in_notice %}
|
|
3
|
+
<div class="alert board-task-error-hint py-2 px-3 mb-3" role="alert">
|
|
4
|
+
You must be signed in to add a project (sign in via the sidebar or <a href="/login?next=/addproject">open the sign-in page</a>).
|
|
5
|
+
</div>
|
|
6
|
+
{% endif %}
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<form action="/addproject" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
|
|
10
|
+
<div class="mb-3" id="project_name_wrapper">
|
|
11
|
+
<label class="form-label required" for="project_name">Project Name</label>
|
|
12
|
+
<input type="text" class="form-control" id="project_name" name="project_name" required
|
|
13
|
+
placeholder="my-repo" autocomplete="off">
|
|
14
|
+
<small class="form-hint">Unique name for the project (no path segments).</small>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="mb-3">
|
|
18
|
+
<label class="form-label">Project Type</label>
|
|
19
|
+
<radio-picker aria-label="Project source" role="radiogroup">
|
|
20
|
+
<label><input type="radio" name="project_source_type" id="empty-repo" value="empty" checked>Empty Repo</label>
|
|
21
|
+
<label><input type="radio" name="project_source_type" id="upload-zip" value="zip">Upload Zip</label>
|
|
22
|
+
</radio-picker>
|
|
23
|
+
<radio-tabs>
|
|
24
|
+
<div id="tab-empty-repo" tabindex="0" class="mt-3">
|
|
25
|
+
{{ empty_repo_form_content | safe }}
|
|
26
|
+
</div>
|
|
27
|
+
<div id="tab-upload-zip" tabindex="0" class="mt-3 d-none">
|
|
28
|
+
{{ upload_zip_form_content | safe }}
|
|
29
|
+
</div>
|
|
30
|
+
</radio-tabs>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="mb-3">
|
|
34
|
+
<div class="form-check">
|
|
35
|
+
<input type="checkbox" class="form-check-input" id="perform_maintenance" name="perform_maintenance" value="on">
|
|
36
|
+
<label class="form-check-label" for="perform_maintenance">Perform maintenance</label>
|
|
37
|
+
</div>
|
|
38
|
+
<small class="form-hint">Run <code>git maintenance start</code> to schedule gc and other maintenance at a specified interval.</small>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="mb-3">
|
|
42
|
+
<div class="form-check">
|
|
43
|
+
<input type="checkbox" class="form-check-input" id="create_task_board" name="create_task_board" value="on">
|
|
44
|
+
<label class="form-check-label" for="create_task_board">Create task board</label>
|
|
45
|
+
</div>
|
|
46
|
+
<small class="form-hint">Create a task board for this project; tasks are stored in the object database.</small>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="form-footer">
|
|
50
|
+
<button type="submit" class="btn btn-primary">Add project</button>
|
|
51
|
+
<a href="/" class="btn btn-ghost-secondary">Cancel</a>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<script>
|
|
58
|
+
(function() {
|
|
59
|
+
var projectNameInput = document.getElementById('project_name');
|
|
60
|
+
var nameValidTimeout = null;
|
|
61
|
+
|
|
62
|
+
function setProjectNameBorder(valid) {
|
|
63
|
+
projectNameInput.classList.remove('border', 'border-success', 'border-danger');
|
|
64
|
+
projectNameInput.classList.add('border');
|
|
65
|
+
if (valid === true) {
|
|
66
|
+
projectNameInput.classList.add('border-success');
|
|
67
|
+
} else if (valid === false) {
|
|
68
|
+
projectNameInput.classList.add('border-danger');
|
|
69
|
+
} else {
|
|
70
|
+
projectNameInput.classList.remove('border');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkProjectName() {
|
|
75
|
+
var name = (projectNameInput.value || '').trim();
|
|
76
|
+
if (!name) {
|
|
77
|
+
setProjectNameBorder(null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
var url = '/projectnamevalid?name=' + encodeURIComponent(name);
|
|
81
|
+
fetch(url, { credentials: 'same-origin' })
|
|
82
|
+
.then(function(res) {
|
|
83
|
+
// 401/403 when not logged in → red outline; same as other validation failures.
|
|
84
|
+
setProjectNameBorder(res.ok);
|
|
85
|
+
})
|
|
86
|
+
.catch(function() {
|
|
87
|
+
setProjectNameBorder(null);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
projectNameInput.addEventListener('blur', function() {
|
|
92
|
+
checkProjectName();
|
|
93
|
+
});
|
|
94
|
+
projectNameInput.addEventListener('input', function() {
|
|
95
|
+
clearTimeout(nameValidTimeout);
|
|
96
|
+
var name = (projectNameInput.value || '').trim();
|
|
97
|
+
if (!name) {
|
|
98
|
+
setProjectNameBorder(null);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
nameValidTimeout = setTimeout(checkProjectName, 400);
|
|
102
|
+
});
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
(function() {
|
|
106
|
+
var sourceRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="project_source_type"]'));
|
|
107
|
+
var emptyTab = document.getElementById('tab-empty-repo');
|
|
108
|
+
var zipTab = document.getElementById('tab-upload-zip');
|
|
109
|
+
var remoteInput = document.getElementById('pull_from_remote');
|
|
110
|
+
var zipInput = document.getElementById('repo_zip');
|
|
111
|
+
|
|
112
|
+
function getSelectedSource() {
|
|
113
|
+
for (var i = 0; i < sourceRadios.length; i++) {
|
|
114
|
+
if (sourceRadios[i].checked) {
|
|
115
|
+
return sourceRadios[i].value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return 'empty';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function updateSourceTabs() {
|
|
122
|
+
var selected = getSelectedSource();
|
|
123
|
+
var isZip = selected === 'zip';
|
|
124
|
+
emptyTab.classList.toggle('d-none', isZip);
|
|
125
|
+
zipTab.classList.toggle('d-none', !isZip);
|
|
126
|
+
if (isZip) {
|
|
127
|
+
remoteInput.value = '';
|
|
128
|
+
} else {
|
|
129
|
+
zipInput.value = '';
|
|
130
|
+
zipInput.dispatchEvent(new Event('change'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
sourceRadios.forEach(function(radio) {
|
|
135
|
+
radio.addEventListener('change', updateSourceTabs);
|
|
136
|
+
});
|
|
137
|
+
updateSourceTabs();
|
|
138
|
+
})();
|
|
139
|
+
|
|
140
|
+
(function() {
|
|
141
|
+
var dropzone = document.getElementById('dropzone');
|
|
142
|
+
var input = document.getElementById('repo_zip');
|
|
143
|
+
var placeholder = dropzone.querySelector('[data-dropzone-placeholder]');
|
|
144
|
+
var filenameEl = dropzone.querySelector('[data-dropzone-filename]');
|
|
145
|
+
|
|
146
|
+
function showFile(name) {
|
|
147
|
+
if (name) {
|
|
148
|
+
placeholder.classList.add('d-none');
|
|
149
|
+
filenameEl.classList.remove('d-none');
|
|
150
|
+
filenameEl.textContent = 'Selected: ' + name;
|
|
151
|
+
} else {
|
|
152
|
+
placeholder.classList.remove('d-none');
|
|
153
|
+
filenameEl.classList.add('d-none');
|
|
154
|
+
filenameEl.textContent = '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
dropzone.addEventListener('click', function() { input.click(); });
|
|
159
|
+
dropzone.addEventListener('dragover', function(e) { e.preventDefault(); dropzone.classList.add('border-primary'); });
|
|
160
|
+
dropzone.addEventListener('dragleave', function() { dropzone.classList.remove('border-primary'); });
|
|
161
|
+
dropzone.addEventListener('drop', function(e) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
dropzone.classList.remove('border-primary');
|
|
164
|
+
var files = e.dataTransfer.files;
|
|
165
|
+
if (files.length && files[0].name.toLowerCase().endsWith('.zip')) {
|
|
166
|
+
input.files = files;
|
|
167
|
+
showFile(files[0].name);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
input.addEventListener('change', function() {
|
|
171
|
+
showFile(input.files.length ? input.files[0].name : null);
|
|
172
|
+
});
|
|
173
|
+
})();
|
|
174
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<h1>Blob diff {{ path_new }}</h1>
|
|
2
|
+
<p>Project: <a href="/project/{{ project }}">{{ project }}</a> · <a href="{{ blob_link }}">blob</a></p>
|
|
3
|
+
<div id="d2h-container" data-diff-b64="{{ diff_b64 }}" data-has-diff="{{ 'true' if diff_b64 else 'false' }}">
|
|
4
|
+
<div id="d2h-output"></div>
|
|
5
|
+
<p id="d2h-empty" class="text-muted" style="display: none;">No changes between these blobs.</p>
|
|
6
|
+
</div>
|
|
7
|
+
<script>
|
|
8
|
+
(function () {
|
|
9
|
+
var container = document.getElementById('d2h-container');
|
|
10
|
+
var output = document.getElementById('d2h-output');
|
|
11
|
+
var emptyEl = document.getElementById('d2h-empty');
|
|
12
|
+
var diffB64 = container.getAttribute('data-diff-b64');
|
|
13
|
+
var hasDiff = container.getAttribute('data-has-diff') === 'true';
|
|
14
|
+
|
|
15
|
+
function render() {
|
|
16
|
+
if (!hasDiff || !diffB64) {
|
|
17
|
+
emptyEl.style.display = 'block';
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
var raw = atob(diffB64);
|
|
22
|
+
if (!raw) {
|
|
23
|
+
emptyEl.style.display = 'block';
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
var diffStr = (typeof TextDecoder !== 'undefined')
|
|
27
|
+
? new TextDecoder('utf-8').decode(Uint8Array.from(raw, function (c) { return c.charCodeAt(0); }))
|
|
28
|
+
: raw;
|
|
29
|
+
if (typeof Diff2HtmlUI === 'undefined') {
|
|
30
|
+
output.innerHTML = '<p class="text-warning">Diff2HtmlUI not loaded.</p>';
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
var config = {
|
|
34
|
+
drawFileList: true,
|
|
35
|
+
matching: 'lines',
|
|
36
|
+
outputFormat: 'side-by-side',
|
|
37
|
+
synchronisedScroll: true,
|
|
38
|
+
highlight: true
|
|
39
|
+
};
|
|
40
|
+
var diff2htmlUi = new Diff2HtmlUI(output, diffStr, config);
|
|
41
|
+
diff2htmlUi.draw();
|
|
42
|
+
diff2htmlUi.highlightCode();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
output.innerHTML = '<p class="text-danger">Failed to render diff: ' + (e.message || e) + '</p>';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var script = document.getElementById('diff2html-script');
|
|
49
|
+
if (script && (typeof Diff2HtmlUI !== 'undefined')) {
|
|
50
|
+
render();
|
|
51
|
+
} else if (script) {
|
|
52
|
+
script.addEventListener('load', render);
|
|
53
|
+
} else {
|
|
54
|
+
render();
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{# Board view: columns (dropzones) and task cards. Expects: project, project_url, board_name, board_url, columns [ { status, label, tasks } ] #}
|
|
2
|
+
|
|
3
|
+
<template id="board-card-template">
|
|
4
|
+
{% include "board_card.html" without context %}
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<h1 class="page-title d-flex align-items-center gap-2">
|
|
8
|
+
<a href="{{ project_url }}" class="text-reset">{{ project }}</a>
|
|
9
|
+
<span class="text-muted">/</span>
|
|
10
|
+
<span>{{ board_name }} board</span>
|
|
11
|
+
</h1>
|
|
12
|
+
|
|
13
|
+
<div class="board-columns d-flex gap-3 overflow-auto pb-2" style="min-height: 70vh;" data-project="{{ project }}" data-board="{{ board_name }}">
|
|
14
|
+
{% for col in columns %}
|
|
15
|
+
<div class="board-column flex-shrink-0 rounded border p-2 dropzone"
|
|
16
|
+
data-status="{{ col.status }}"
|
|
17
|
+
style="min-width: 280px; width: 280px;">
|
|
18
|
+
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
19
|
+
<h3 class="h6 mb-0 text-muted">{{ col.label }}</h3>
|
|
20
|
+
<button type="button" class="badge bg-secondary border-0 board-column-add-card" aria-label="Add card">{{ col.tasks | length }}</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="board-column-cards d-flex flex-column">
|
|
23
|
+
{% for task in col.tasks %}
|
|
24
|
+
{% include "board_card.html" %}
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<script src="/static/board.js"></script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{# Static card structure. When `task` is provided and truthy, title and meta are filled (server); otherwise empty for template clone / JS fill. .board-card-details is hidden on the small card and shown in the popout. #}
|
|
2
|
+
<div class="card board-card mb-2 draggable" data-task-ref="{{ task.ref if task else '' }}" data-task-status="{{ (task.status or 'TODO') if task else 'TODO' }}" draggable="true">
|
|
3
|
+
<div class="card-body py-2 px-3 position-relative">
|
|
4
|
+
<span class="board-card-icon-wrap position-absolute top-0 end-0 mt-1 me-1" aria-hidden="true"><span class="board-card-icon" data-icon="cloud"></span></span>
|
|
5
|
+
<div class="board-card-title fw-medium text-truncate"{% if task %} title="{{ task.title }}"{% endif %}>{% if task %}{{ task.title or 'Untitled' }}{% endif %}</div>
|
|
6
|
+
<div class="board-card-description small board-card-description-preview mt-1 text-truncate">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div>
|
|
7
|
+
<div class="board-card-meta d-flex align-items-center gap-1 mt-1 small text-muted">
|
|
8
|
+
{% if task %}
|
|
9
|
+
{% if task.priority and task.priority != 'LOW' %}
|
|
10
|
+
<span class="badge badge-outline text-{{ 'danger' if task.priority == 'CRITICAL' else 'warning' if task.priority == 'HIGH' else 'secondary' }}">{{ task.priority }}</span>
|
|
11
|
+
{% else %}
|
|
12
|
+
<span class="badge badge-outline text-secondary">LOW</span>
|
|
13
|
+
{% endif %}
|
|
14
|
+
{% if task.assignee %}
|
|
15
|
+
<span class="text-truncate">{{ task.assignee }}</span>
|
|
16
|
+
{% else %}
|
|
17
|
+
<span class="text-truncate">None</span>
|
|
18
|
+
{% endif %}
|
|
19
|
+
<span class="ms-auto board-card-comments-count-meta">{{ task.comments_count | default(0) }} comment{{ 's' if (task.comments_count | default(0)) != 1 else '' }}</span>
|
|
20
|
+
{% else %}
|
|
21
|
+
<span class="badge badge-outline text-secondary">LOW</span>
|
|
22
|
+
<span class="text-truncate">None</span>
|
|
23
|
+
<span class="ms-auto">0 comments</span>
|
|
24
|
+
{% endif %}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="board-card-details d-none mt-2 pt-2 border-top small">
|
|
27
|
+
<div class="mb-2"><strong class="text-muted">Description</strong><div class="board-card-description-full board-card-editable board-card-markdown mt-1">{% if task and task.description %}{{ task.description }}{% else %}{% endif %}</div></div>
|
|
28
|
+
<div class="mb-2"><strong class="text-muted">Due date</strong><div class="board-card-due-date board-card-editable text-muted mt-1">{% if task and task.due_date %}{{ task.due_date }}{% else %}None{% endif %}</div></div>
|
|
29
|
+
<div class="mb-2"><strong class="text-muted">Last modified</strong><div class="board-card-updated-at text-muted mt-1">{% if task and task.updated_at %}{{ task.updated_at }}{% else %}None{% endif %}</div></div>
|
|
30
|
+
<div class="mb-2"><strong class="text-muted">Status</strong><div class="board-card-status board-card-editable text-muted mt-1">{% if task %}{{ task.status or 'TODO' }}{% else %}TODO{% endif %}</div></div>
|
|
31
|
+
<div class="mb-2"><strong class="text-muted">Priority</strong><div class="board-card-priority board-card-editable text-muted mt-1">{% if task and task.priority %}{{ task.priority }}{% else %}LOW{% endif %}</div></div>
|
|
32
|
+
<div class="mb-2"><strong class="text-muted">Assignee</strong><div class="board-card-assignee text-muted mt-1">{% if task and task.assignee %}{{ task.assignee }}{% else %}None{% endif %}</div></div>
|
|
33
|
+
<div class="mb-0 board-card-comments-section">
|
|
34
|
+
<strong class="text-muted">Comments</strong>
|
|
35
|
+
<div class="board-card-comments-list mt-2"></div>
|
|
36
|
+
<form class="board-card-comment-form d-none mt-2" action="#" method="post">
|
|
37
|
+
<textarea class="form-control form-control-sm board-card-comment-input" rows="2" placeholder="Add a comment…" aria-label="Comment"></textarea>
|
|
38
|
+
<button type="submit" class="btn btn-sm btn-primary mt-1">Post comment</button>
|
|
39
|
+
</form>
|
|
40
|
+
<p class="board-card-comment-signin-hint text-muted small mt-1 mb-0 d-none">Sign in to add a comment.</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<h1>Commit diff {{ oid_short }}</h1>
|
|
2
|
+
<p>Project: <a href="/project/{{ project }}">{{ project }}</a> · <a href="{{ commit_link }}">commit</a></p>
|
|
3
|
+
<div id="d2h-container" data-diff-b64="{{ diff_b64 }}" data-has-diff="{{ 'true' if diff_b64 else 'false' }}">
|
|
4
|
+
<div id="d2h-output"></div>
|
|
5
|
+
<p id="d2h-empty" class="text-muted" style="display: none;">No changes in this commit.</p>
|
|
6
|
+
</div>
|
|
7
|
+
<script>
|
|
8
|
+
(function () {
|
|
9
|
+
var container = document.getElementById('d2h-container');
|
|
10
|
+
var output = document.getElementById('d2h-output');
|
|
11
|
+
var emptyEl = document.getElementById('d2h-empty');
|
|
12
|
+
var diffB64 = container.getAttribute('data-diff-b64');
|
|
13
|
+
var hasDiff = container.getAttribute('data-has-diff') === 'true';
|
|
14
|
+
|
|
15
|
+
function render() {
|
|
16
|
+
if (!hasDiff || !diffB64) {
|
|
17
|
+
emptyEl.style.display = 'block';
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
var raw = atob(diffB64);
|
|
22
|
+
if (!raw) {
|
|
23
|
+
emptyEl.style.display = 'block';
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
var diffStr = (typeof TextDecoder !== 'undefined')
|
|
27
|
+
? new TextDecoder('utf-8').decode(Uint8Array.from(raw, function (c) { return c.charCodeAt(0); }))
|
|
28
|
+
: raw;
|
|
29
|
+
if (typeof Diff2HtmlUI === 'undefined') {
|
|
30
|
+
output.innerHTML = '<p class="text-warning">Diff2HtmlUI not loaded.</p>';
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
var config = {
|
|
34
|
+
drawFileList: true,
|
|
35
|
+
matching: 'lines',
|
|
36
|
+
outputFormat: 'side-by-side',
|
|
37
|
+
synchronisedScroll: true,
|
|
38
|
+
highlight: true
|
|
39
|
+
};
|
|
40
|
+
var diff2htmlUi = new Diff2HtmlUI(output, diffStr, config);
|
|
41
|
+
diff2htmlUi.draw();
|
|
42
|
+
diff2htmlUi.highlightCode();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
output.innerHTML = '<p class="text-danger">Failed to render diff: ' + (e.message || e) + '</p>';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var script = document.getElementById('diff2html-script');
|
|
49
|
+
if (script && (typeof Diff2HtmlUI !== 'undefined')) {
|
|
50
|
+
render();
|
|
51
|
+
} else if (script) {
|
|
52
|
+
script.addEventListener('load', render);
|
|
53
|
+
} else {
|
|
54
|
+
render();
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{# Project summary hooks row: installed hooks + install dropdown (hook-install.js). #}
|
|
2
|
+
{% if mode == "unavailable" %}
|
|
3
|
+
<span class="text-muted">unavailable</span>
|
|
4
|
+
{% else %}
|
|
5
|
+
{% if installed %}
|
|
6
|
+
<ul class="list-unstyled mb-2">
|
|
7
|
+
{% for hook in installed %}
|
|
8
|
+
<li class="d-flex flex-wrap align-items-center gap-2 mb-1">
|
|
9
|
+
<span>{{ hook.label }} (<code>{{ hook.target }}</code>)</span>
|
|
10
|
+
{% if hook.version %}
|
|
11
|
+
<span class="text-muted small">v{{ hook.version }}</span>
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if hook.status == "installed" %}
|
|
14
|
+
<button type="button" class="btn btn-sm btn-ghost-secondary hook-toggle" data-project="{{ project_enc }}" data-sample="{{ hook.name }}" data-op="remove">Uninstall</button>
|
|
15
|
+
{% elif hook.status == "different" %}
|
|
16
|
+
<span class="text-muted small">custom hook installed; refusing to manage</span>
|
|
17
|
+
{% endif %}
|
|
18
|
+
</li>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</ul>
|
|
21
|
+
{% else %}
|
|
22
|
+
<p class="text-muted mb-2">No hooks installed.</p>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% if available %}
|
|
25
|
+
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
26
|
+
<label for="hook-install-select" class="visually-hidden">Install hook</label>
|
|
27
|
+
<select id="hook-install-select" class="form-select form-select-sm" style="width: auto; min-width: 12rem;">
|
|
28
|
+
{% for hook in available %}
|
|
29
|
+
<option value="{{ hook.name }}">{{ hook.label }} ({{ hook.target }})</option>
|
|
30
|
+
{% endfor %}
|
|
31
|
+
</select>
|
|
32
|
+
<button type="button" class="btn btn-sm btn-primary hook-install-btn" data-project="{{ project_enc }}" data-op="add">Install</button>
|
|
33
|
+
</div>
|
|
34
|
+
{% else %}
|
|
35
|
+
<span class="text-muted">All available hooks are installed.</span>
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% endif %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<h1 class="page-title">Sign in</h1>
|
|
2
|
+
<div class="card">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
{% if error %}
|
|
5
|
+
<div class="alert alert-danger mb-3" role="alert">{{ error }}</div>
|
|
6
|
+
{% endif %}
|
|
7
|
+
{% if oauth_available %}
|
|
8
|
+
<p class="mb-3">
|
|
9
|
+
<a class="btn btn-outline-primary" href="/auth/oauth/start?next={{ next | urlencode }}">
|
|
10
|
+
Sign in with {{ oauth_provider | e }}
|
|
11
|
+
</a>
|
|
12
|
+
</p>
|
|
13
|
+
{% if local_available %}
|
|
14
|
+
<p class="text-muted small mb-3">or use a local account</p>
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% if local_available %}
|
|
18
|
+
<form method="post" action="/login" class="needs-validation" novalidate>
|
|
19
|
+
<input type="hidden" name="next" value="{{ next }}">
|
|
20
|
+
<div class="mb-3">
|
|
21
|
+
<label class="form-label required" for="login_username">Username</label>
|
|
22
|
+
<input type="text" class="form-control" id="login_username" name="username" required
|
|
23
|
+
autocomplete="username" autofocus>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="mb-3">
|
|
26
|
+
<label class="form-label required" for="login_password">Password</label>
|
|
27
|
+
<input type="password" class="form-control" id="login_password" name="password" required
|
|
28
|
+
autocomplete="current-password">
|
|
29
|
+
</div>
|
|
30
|
+
<button type="submit" class="btn btn-primary">Sign in</button>
|
|
31
|
+
</form>
|
|
32
|
+
<p class="mt-3 mb-0 text-muted small">API clients can use <code>POST /token</code> (OAuth2 password) instead.</p>
|
|
33
|
+
{% elif not oauth_available %}
|
|
34
|
+
<p class="text-muted mb-0">No sign-in methods are configured.</p>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{# Summary row: open MR from branch picks. Expects: project, branches [ { short, ref } ], theirs_default_ref, ours_default_ref #}
|
|
2
|
+
<form method="post" action="/mr/create" class="d-flex flex-wrap align-items-center gap-2">
|
|
3
|
+
<input type="hidden" name="project" value="{{ project }}" />
|
|
4
|
+
<label for="summary-mr-theirs" class="form-label mb-0 text-muted small">Theirs</label>
|
|
5
|
+
<select name="theirs" id="summary-mr-theirs" class="form-select form-select-sm" style="width: auto; min-width: 10rem;" required>
|
|
6
|
+
{% for b in branches %}
|
|
7
|
+
<option value="{{ b.ref }}" {% if b.ref == theirs_default_ref %}selected{% endif %}>{{ b.short }}</option>
|
|
8
|
+
{% endfor %}
|
|
9
|
+
</select>
|
|
10
|
+
<span class="text-muted" aria-hidden="true">→</span>
|
|
11
|
+
<label for="summary-mr-ours" class="form-label mb-0 text-muted small">Ours</label>
|
|
12
|
+
<select name="ours" id="summary-mr-ours" class="form-select form-select-sm" style="width: auto; min-width: 10rem;" required>
|
|
13
|
+
{% for b in branches %}
|
|
14
|
+
<option value="{{ b.ref }}" {% if b.ref == ours_default_ref %}selected{% endif %}>{{ b.short }}</option>
|
|
15
|
+
{% endfor %}
|
|
16
|
+
</select>
|
|
17
|
+
<button type="submit" class="btn btn-sm btn-primary">Open merge request</button>
|
|
18
|
+
</form>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{# Merge request view for annotated MR tags. Expects: project, project_url, tag_name, tag_oid, tag_view_url, mr_title, mr_description, ours_ref, ours_url, theirs_ref, theirs_url, head_short, head_url, head_full, tagger, tagger_date, opener, created_at, updated_at, comments [ (short, url) ], merge_action_url, status_code, status_label, status_detail, show_merge_button #}
|
|
2
|
+
|
|
3
|
+
<h1 class="page-title mb-3">Merge request</h1>
|
|
4
|
+
|
|
5
|
+
<p class="text-muted mb-4">
|
|
6
|
+
Project: <a href="{{ project_url }}" class="text-reset">{{ project }}</a>
|
|
7
|
+
· Tag: <a href="{{ tag_view_url }}" class="text-reset">{{ tag_name }}</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<div class="row row-cards mb-4">
|
|
11
|
+
<div class="col-12">
|
|
12
|
+
<div class="card">
|
|
13
|
+
<div class="card-header">
|
|
14
|
+
<h2 class="card-title mb-0">{{ mr_title }}</h2>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card-body">
|
|
17
|
+
{% if mr_description %}
|
|
18
|
+
<div class="markdown-body mb-3" style="white-space: pre-wrap;">{{ mr_description }}</div>
|
|
19
|
+
{% endif %}
|
|
20
|
+
<dl class="row mb-0">
|
|
21
|
+
<dt class="col-sm-3">Ours</dt>
|
|
22
|
+
<dd class="col-sm-9"><a href="{{ ours_url }}">{{ ours_ref }}</a></dd>
|
|
23
|
+
<dt class="col-sm-3">Theirs</dt>
|
|
24
|
+
<dd class="col-sm-9">
|
|
25
|
+
{% if theirs_ref %}
|
|
26
|
+
<a href="{{ theirs_url }}">{{ theirs_ref }}</a>
|
|
27
|
+
{% else %}
|
|
28
|
+
<span class="text-muted">(not tracked)</span>
|
|
29
|
+
{% endif %}
|
|
30
|
+
</dd>
|
|
31
|
+
<dt class="col-sm-3">Head</dt>
|
|
32
|
+
<dd class="col-sm-9">
|
|
33
|
+
{% if head_url %}<a href="{{ head_url }}">{{ head_short }}</a>{% else %}{{ head_short }}{% endif %}
|
|
34
|
+
<span class="text-muted small ms-1">{{ head_full }}</span>
|
|
35
|
+
</dd>
|
|
36
|
+
<dt class="col-sm-3">Tagger (annotated tag)</dt>
|
|
37
|
+
<dd class="col-sm-9">{{ tagger }} · {{ tagger_date }}</dd>
|
|
38
|
+
<dt class="col-sm-3">Opener (payload)</dt>
|
|
39
|
+
<dd class="col-sm-9">{{ opener }}</dd>
|
|
40
|
+
<dt class="col-sm-3">Created</dt>
|
|
41
|
+
<dd class="col-sm-9"><code class="small">{{ created_at }}</code></dd>
|
|
42
|
+
<dt class="col-sm-3">Updated</dt>
|
|
43
|
+
<dd class="col-sm-9"><code class="small">{{ updated_at }}</code></dd>
|
|
44
|
+
</dl>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="row row-cards mb-4">
|
|
51
|
+
<div class="col-12 col-lg-6">
|
|
52
|
+
<div class="card">
|
|
53
|
+
<div class="card-header">
|
|
54
|
+
<h3 class="card-title">Merge status</h3>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="card-body">
|
|
57
|
+
<p class="mb-1"><span class="badge bg-secondary">{{ status_code }}</span> {{ status_label }}</p>
|
|
58
|
+
{% if status_detail %}
|
|
59
|
+
<p class="text-danger small mb-0">{{ status_detail }}</p>
|
|
60
|
+
{% endif %}
|
|
61
|
+
<form method="post" action="{{ merge_action_url }}" class="mt-3">
|
|
62
|
+
<button type="submit" class="btn btn-primary" {% if not show_merge_button %}disabled{% endif %}>
|
|
63
|
+
Merge (fast-forward only)
|
|
64
|
+
</button>
|
|
65
|
+
{% if not show_merge_button %}
|
|
66
|
+
<span class="text-muted small ms-2">Only available when a fast-forward merge is possible.</span>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</form>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="col-12 col-lg-6">
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="card-header">
|
|
75
|
+
<h3 class="card-title">Comments</h3>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="card-body">
|
|
78
|
+
{% if comments %}
|
|
79
|
+
<ul class="list-unstyled mb-0">
|
|
80
|
+
{% for short_id, url in comments %}
|
|
81
|
+
<li class="mb-1"><a href="{{ url }}">{{ short_id }}</a></li>
|
|
82
|
+
{% endfor %}
|
|
83
|
+
</ul>
|
|
84
|
+
{% else %}
|
|
85
|
+
<p class="text-muted mb-0">No comment references.</p>
|
|
86
|
+
{% endif %}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{# Tabler-style pagination. Expects: current_page, pagecount, has_prev, has_next, prev_url, next_url; optional: total_pages, page_links (list of (page_num, url) for numbered links). #}
|
|
2
|
+
<nav aria-label="Pagination" class="d-flex justify-content-between align-items-center mt-3">
|
|
3
|
+
<div class="text-secondary small">
|
|
4
|
+
Page {{ current_page }}{% if total_pages is defined and total_pages is not none %} of {{ total_pages }}{% endif %}
|
|
5
|
+
<span class="ms-2">({{ pagecount }} per page)</span>
|
|
6
|
+
</div>
|
|
7
|
+
<ul class="pagination mb-0">
|
|
8
|
+
<li class="page-item{% if not has_prev %} disabled{% endif %}">
|
|
9
|
+
{% if has_prev %}
|
|
10
|
+
<a class="page-link" href="{{ prev_url }}" aria-label="Previous">
|
|
11
|
+
<span aria-hidden="true">«</span>
|
|
12
|
+
</a>
|
|
13
|
+
{% else %}
|
|
14
|
+
<span class="page-link" aria-label="Previous" aria-disabled="true">
|
|
15
|
+
<span aria-hidden="true">«</span>
|
|
16
|
+
</span>
|
|
17
|
+
{% endif %}
|
|
18
|
+
</li>
|
|
19
|
+
{% if page_links is defined and page_links %}
|
|
20
|
+
{% for p, u in page_links %}
|
|
21
|
+
<li class="page-item{% if p == current_page %} active{% endif %}{% if not u %} disabled{% endif %}">
|
|
22
|
+
{% if u %}
|
|
23
|
+
<a class="page-link" href="{{ u }}">{{ p }}</a>
|
|
24
|
+
{% else %}
|
|
25
|
+
<span class="page-link">…</span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</li>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
{% else %}
|
|
30
|
+
<li class="page-item active"><span class="page-link">{{ current_page }}</span></li>
|
|
31
|
+
{% endif %}
|
|
32
|
+
<li class="page-item{% if not has_next %} disabled{% endif %}">
|
|
33
|
+
{% if has_next %}
|
|
34
|
+
<a class="page-link" href="{{ next_url }}" aria-label="Next">
|
|
35
|
+
<span aria-hidden="true">»</span>
|
|
36
|
+
</a>
|
|
37
|
+
{% else %}
|
|
38
|
+
<span class="page-link" aria-label="Next" aria-disabled="true">
|
|
39
|
+
<span aria-hidden="true">»</span>
|
|
40
|
+
</span>
|
|
41
|
+
{% endif %}
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</nav>
|