lambda-watcher 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/PKG-INFO +1 -1
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/pyproject.toml +1 -1
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/__init__.py +1 -1
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/render_html.py +427 -97
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/PKG-INFO +1 -1
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_diff.py +2 -2
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_render_html.py +129 -1
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/LICENSE +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/README.md +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/setup.cfg +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/__main__.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/__init__.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/deps.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/envvars.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/handler.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/inventory.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/runtime.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/secrets.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/analysis/services.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/cli.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/config.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/db.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/__init__.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/build.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/compare.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/highlight.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/icons.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/intraline.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/diffing/render_text.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/extract.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/gitmirror.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/identify.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/ingest.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/notify.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/reindex.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/service.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/store.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/templates.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/utils.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher/watcher.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/SOURCES.txt +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/dependency_links.txt +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/entry_points.txt +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/requires.txt +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/top_level.txt +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_analysis.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_cli.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_docs.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_extract.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_identify.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_ingest.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_no_window.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_service.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_utils.py +0 -0
- {lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/tests/test_watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-watcher
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions.
|
|
5
5
|
Author: Lambda Watcher contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lambda-watcher"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -130,44 +130,52 @@ table.grid td.label { width: 210px; color: var(--muted); }
|
|
|
130
130
|
.tok.removed { background: var(--del-bg); border-color: var(--del-gutter); color: var(--del-fg); }
|
|
131
131
|
|
|
132
132
|
/* ---- toolbar --------------------------------------------------------- */
|
|
133
|
-
.toolbar { display: flex; gap:
|
|
134
|
-
position: sticky; top: 0; background: var(--bg); padding: 10px 0; z-index:
|
|
133
|
+
.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px;
|
|
134
|
+
position: sticky; top: 0; background: var(--bg); padding: 10px 0; z-index: 6;
|
|
135
135
|
box-shadow: 0 1px 0 var(--border); }
|
|
136
136
|
.toolbar #shown-count { margin-left: auto; white-space: nowrap; flex: 0 0 auto; color: var(--faint); }
|
|
137
|
-
.toolbar input[type=search] { flex: 1 1
|
|
137
|
+
.toolbar input[type=search] { flex: 1 1 240px; min-width: 150px; padding: 7px 11px; border-radius: 7px;
|
|
138
138
|
border: 1px solid var(--border); background: var(--panel); color: var(--text); font-size: 13px; }
|
|
139
|
-
.toolbar input[type=search]:focus { outline: none; border-color: var(--accent);
|
|
139
|
+
.toolbar input[type=search]:focus { outline: none; border-color: var(--accent);
|
|
140
|
+
box-shadow: 0 0 0 3px var(--accent-wash); }
|
|
140
141
|
.toolbar label { color: var(--muted); font-size: 13px; display: inline-flex; gap: 6px;
|
|
141
142
|
align-items: center; cursor: pointer; user-select: none; }
|
|
142
|
-
.toolbar button { padding: 6px 11px; border-radius: 6px; border: 1px solid var(--border);
|
|
143
|
-
background: var(--panel); color: var(--muted); font-size: 13px; cursor: pointer; }
|
|
144
|
-
.toolbar button:hover { border-color: var(--faint); color: var(--text); }
|
|
145
143
|
|
|
146
|
-
/* ----
|
|
147
|
-
|
|
144
|
+
/* ---- the file list --------------------------------------------------- */
|
|
145
|
+
/* One panel of rows rather than a stack of cards, because the list is an index
|
|
146
|
+
now: the diff it points at opens beside it instead of pushing the rest of
|
|
147
|
+
the list down the page. A row is a button, since that is what it does. */
|
|
148
|
+
.files { border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
|
|
148
149
|
background: var(--panel); }
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
.file { border-top: 1px solid var(--rule); }
|
|
151
|
+
/* Transparent rather than absent so every row is the same height, whichever
|
|
152
|
+
one the filter left at the top. */
|
|
153
|
+
.file:first-child, .file.first-shown { border-top-color: transparent; }
|
|
154
|
+
.row { display: flex; gap: 10px; align-items: center; width: 100%; padding: 9px 14px;
|
|
155
|
+
font: inherit; color: var(--text); text-align: left; background: none; border: 0;
|
|
156
|
+
cursor: pointer; }
|
|
157
|
+
.row:hover { background: var(--sunken); }
|
|
158
|
+
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
|
|
159
|
+
/* The chevron points where the diff will appear: to the side, not downwards.
|
|
160
|
+
Drawn in CSS because a vendored diff runs to thousands of rows, and each one
|
|
161
|
+
would otherwise carry its own copy of the glyph. */
|
|
162
|
+
.row::after { content: ""; flex: 0 0 auto; width: 6px; height: 6px; margin-left: 2px;
|
|
163
|
+
border: 1.6px solid var(--faint); border-left: 0; border-bottom: 0;
|
|
164
|
+
transform: rotate(45deg); transition: transform .15s ease, border-color .15s ease; }
|
|
165
|
+
.row:hover::after { border-color: var(--accent); transform: translateX(2px) rotate(45deg); }
|
|
166
|
+
.file.active .row { background: var(--accent-wash); box-shadow: inset 3px 0 0 var(--accent); }
|
|
167
|
+
.file.active .row::after { border-color: var(--accent); transform: translateX(2px) rotate(45deg); }
|
|
168
|
+
.row .path { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
|
169
|
+
.row .path .p { font-family: var(--mono); font-size: 12.5px; overflow-wrap: anywhere; }
|
|
161
170
|
/* The tint hugs the changed part exactly — padding here would open a gap in
|
|
162
171
|
the middle of a path and read as though the name contained a space. */
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
.path .ren { background: var(--sunken); border-radius: 3px; }
|
|
173
|
+
.path .was { color: var(--faint); }
|
|
174
|
+
.stat-line { font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap;
|
|
166
175
|
display: flex; gap: 8px; align-items: baseline; color: var(--faint); }
|
|
167
176
|
|
|
168
177
|
/* ---- the diff itself ------------------------------------------------- */
|
|
169
178
|
.diff { overflow-x: auto; border-top: 1px solid var(--border); }
|
|
170
|
-
details.file[open] > .diff { border-top: none; }
|
|
171
179
|
.diff table { border-collapse: collapse; width: 100%; font-family: var(--mono);
|
|
172
180
|
font-size: 12.5px; line-height: 1.5; }
|
|
173
181
|
.diff td { padding: 0 8px; white-space: pre; vertical-align: top; }
|
|
@@ -224,49 +232,273 @@ footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--border
|
|
|
224
232
|
.moved-list { margin: 0; padding: 10px 16px 12px 34px; list-style: disc;
|
|
225
233
|
color: var(--muted); font-size: 12px; line-height: 1.9; }
|
|
226
234
|
.moved-list .hint { color: var(--faint); }
|
|
235
|
+
/* ---- the sheet ------------------------------------------------------- */
|
|
236
|
+
/* Where the diff lands when a row is clicked. Code wants width, and a block
|
|
237
|
+
that opened downwards spent the page's widest dimension on the file list it
|
|
238
|
+
had just pushed out of view; this keeps the list where it was, so the next
|
|
239
|
+
file is one click away rather than one scroll back.
|
|
240
|
+
|
|
241
|
+
A wide window docks the sheet and the page makes room beside it. A narrow
|
|
242
|
+
one slides it over the page with a scrim, because a laptop in portrait has
|
|
243
|
+
no room to dock and half a diff is worse than a covered list. */
|
|
244
|
+
:root { --sheet-w: 760px; }
|
|
245
|
+
.scrim { position: fixed; inset: 0; z-index: 30; background: rgba(8, 13, 20, .44);
|
|
246
|
+
opacity: 0; visibility: hidden; transition: opacity .26s ease, visibility 0s linear .26s; }
|
|
247
|
+
body.sheet-open .scrim { opacity: 1; visibility: visible; transition: opacity .26s ease; }
|
|
248
|
+
.sheet { position: fixed; top: 0; right: 0; bottom: 0; z-index: 40;
|
|
249
|
+
width: min(100%, var(--sheet-w)); display: flex; flex-direction: column;
|
|
250
|
+
background: var(--bg); border-left: 1px solid var(--border);
|
|
251
|
+
box-shadow: -24px 0 60px -30px rgba(6, 11, 18, .5);
|
|
252
|
+
/* `visibility` rather than `display`, so the sheet can animate out and still
|
|
253
|
+
leave nothing behind for the keyboard to land on while it is shut. It turns
|
|
254
|
+
visible on the same frame it is asked to — a transition would leave it
|
|
255
|
+
unfocusable for the length of the slide, which is exactly when the script
|
|
256
|
+
is moving focus into it — and back to hidden only once the slide is over. */
|
|
257
|
+
transform: translateX(100%); visibility: hidden;
|
|
258
|
+
transition: transform .26s cubic-bezier(.22, .61, .36, 1), visibility 0s linear .26s; }
|
|
259
|
+
body.sheet-open .sheet { transform: none; visibility: visible;
|
|
260
|
+
transition: transform .26s cubic-bezier(.22, .61, .36, 1); }
|
|
261
|
+
.sheet-head { padding: 12px 14px 11px; border-bottom: 1px solid var(--border);
|
|
262
|
+
background: var(--panel); display: flex; flex-direction: column; gap: 7px; }
|
|
263
|
+
.sheet-bar { display: flex; align-items: center; gap: 10px; }
|
|
264
|
+
.sheet-title { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
265
|
+
.sheet-title .path { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
266
|
+
.sheet-title .p { font-family: var(--mono); font-size: 13px; font-weight: 600;
|
|
267
|
+
overflow-wrap: anywhere; }
|
|
268
|
+
/* Wrapping rather than clipping: a file whose counts are a sentence — "missing
|
|
269
|
+
from the archive" — is exactly the one whose reader needs to read them. */
|
|
270
|
+
.sheet-sub { display: flex; align-items: baseline; gap: 12px; min-height: 18px;
|
|
271
|
+
flex-wrap: wrap; color: var(--faint); font-size: 12px; }
|
|
272
|
+
.sheet-sub .ver { margin-left: auto; font-family: var(--mono); white-space: nowrap; }
|
|
273
|
+
.sheet-nav { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
|
|
274
|
+
.sheet-nav .pos { color: var(--faint); font-size: 12px; padding: 0 3px;
|
|
275
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
276
|
+
.iconbtn { display: inline-flex; align-items: center; justify-content: center;
|
|
277
|
+
width: 28px; height: 28px; padding: 0; border-radius: 7px; border: 1px solid transparent;
|
|
278
|
+
background: none; color: var(--muted); cursor: pointer; }
|
|
279
|
+
.iconbtn:hover:not(:disabled) { background: var(--sunken); border-color: var(--border);
|
|
280
|
+
color: var(--text); }
|
|
281
|
+
.iconbtn:disabled { opacity: .3; cursor: default; }
|
|
282
|
+
.iconbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
283
|
+
.iconbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor;
|
|
284
|
+
stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
285
|
+
.iconbtn.close { margin-left: 5px; }
|
|
286
|
+
.sheet-body { flex: 1; overflow: auto; overscroll-behavior: contain; }
|
|
287
|
+
.sheet-body:focus { outline: none; }
|
|
288
|
+
/* The panel's own top rule would double the header's. */
|
|
289
|
+
.sheet-body > .body > :first-child { border-top: none; }
|
|
290
|
+
/* One file wants sixty columns and the next wants two hundred, and which one
|
|
291
|
+
is on screen is not something the page can know — so the edge is draggable
|
|
292
|
+
wherever there is a pointer and room to dock. */
|
|
293
|
+
.grip { position: absolute; left: 0; top: 0; bottom: 0; width: 11px; display: none;
|
|
294
|
+
cursor: col-resize; }
|
|
295
|
+
.grip::before { content: ""; position: absolute; left: 4px; top: 50%; width: 3px; height: 44px;
|
|
296
|
+
margin-top: -22px; border-radius: 3px; background: var(--border); transition: background .15s; }
|
|
297
|
+
.grip:hover::before, body.dragging .grip::before { background: var(--accent); }
|
|
298
|
+
body.dragging { user-select: none; cursor: col-resize; }
|
|
299
|
+
body.dragging, body.dragging .sheet { transition: none; }
|
|
300
|
+
|
|
301
|
+
@media (min-width: 1280px) {
|
|
302
|
+
:root { --sheet-w: min(50vw, 960px); }
|
|
303
|
+
.scrim { display: none; }
|
|
304
|
+
.grip { display: block; }
|
|
305
|
+
body { transition: padding-right .26s cubic-bezier(.22, .61, .36, 1); }
|
|
306
|
+
body.sheet-open { padding-right: min(var(--sheet-w), 76vw); }
|
|
307
|
+
}
|
|
308
|
+
/* Nothing to dock into: the sheet is over the page, so the page stops scrolling
|
|
309
|
+
underneath it. */
|
|
310
|
+
@media (max-width: 1279.98px) {
|
|
311
|
+
body.sheet-open { overflow: hidden; }
|
|
312
|
+
}
|
|
313
|
+
@media (prefers-reduced-motion: reduce) {
|
|
314
|
+
body, .sheet, .scrim, .row::after { transition: none; }
|
|
315
|
+
}
|
|
316
|
+
/* On paper there is no clicking, so every diff is printed under its own row and
|
|
317
|
+
the chrome that only answers a pointer is left out. */
|
|
318
|
+
@media print {
|
|
319
|
+
.toolbar, .scrim, .sheet-nav, .grip { display: none !important; }
|
|
320
|
+
.sheet { position: static; transform: none; visibility: visible; width: auto;
|
|
321
|
+
border-left: 0; box-shadow: none; }
|
|
322
|
+
body.sheet-open { padding-right: 0; overflow: visible; }
|
|
323
|
+
.file .body[hidden] { display: block; }
|
|
324
|
+
}
|
|
227
325
|
.hidden { display: none !important; }
|
|
228
326
|
"""
|
|
229
327
|
|
|
328
|
+
#: What the page does once it is open: narrow the list, and move one file's diff
|
|
329
|
+
#: into the sheet and back out again. Moving rather than copying is the whole
|
|
330
|
+
#: design — the diff exists once, so the sheet cannot drift from the row, and
|
|
331
|
+
#: closing it leaves the page exactly as it was rendered. One ``<script>`` at the
|
|
332
|
+
#: end of the body, so everything it binds to is already parsed.
|
|
230
333
|
JS = """
|
|
231
334
|
(function () {
|
|
232
335
|
var search = document.getElementById('filter');
|
|
233
336
|
var vendorToggle = document.getElementById('vendor');
|
|
234
|
-
var
|
|
337
|
+
var counter = document.getElementById('shown-count');
|
|
338
|
+
var files = Array.prototype.slice.call(document.querySelectorAll('.files .file'));
|
|
339
|
+
var scrim = document.getElementById('scrim');
|
|
340
|
+
var host = document.getElementById('sheet-body');
|
|
341
|
+
var titleBox = document.getElementById('sheet-title');
|
|
342
|
+
var statBox = document.getElementById('sheet-stat');
|
|
343
|
+
var posBox = document.getElementById('sheet-pos');
|
|
344
|
+
var prevBtn = document.getElementById('sheet-prev');
|
|
345
|
+
var nextBtn = document.getElementById('sheet-next');
|
|
346
|
+
var closeBtn = document.getElementById('sheet-close');
|
|
347
|
+
var grip = document.getElementById('sheet-grip');
|
|
348
|
+
var openFile = null; // whose diff is sitting in the sheet right now
|
|
349
|
+
var opener = null; // the row that put it there, to hand focus back to
|
|
350
|
+
|
|
351
|
+
function listed() {
|
|
352
|
+
return files.filter(function (el) { return !el.classList.contains('hidden'); });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function empty(node) {
|
|
356
|
+
while (node && node.firstChild) { node.removeChild(node.firstChild); }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// The sheet header says the same things the row does, so it is built from a
|
|
360
|
+
// copy of the row rather than from a second set of markup on every file.
|
|
361
|
+
function copyInto(node, source) {
|
|
362
|
+
empty(node);
|
|
363
|
+
if (node && source) { node.appendChild(source.cloneNode(true)); }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function position() {
|
|
367
|
+
var rows = listed();
|
|
368
|
+
var at = openFile ? rows.indexOf(openFile) : -1;
|
|
369
|
+
if (posBox) { posBox.textContent = at < 0 ? '' : (at + 1) + ' / ' + rows.length; }
|
|
370
|
+
if (prevBtn) { prevBtn.disabled = at <= 0; }
|
|
371
|
+
if (nextBtn) { nextBtn.disabled = at < 0 || at >= rows.length - 1; }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The diff is moved into the sheet rather than copied, so this puts it back
|
|
375
|
+
// where it came from. One diff exists at a time, wherever it is showing.
|
|
376
|
+
function park() {
|
|
377
|
+
if (!openFile) { return; }
|
|
378
|
+
var body = host.firstElementChild;
|
|
379
|
+
if (body) { body.hidden = true; openFile.appendChild(body); }
|
|
380
|
+
openFile.classList.remove('active');
|
|
381
|
+
var row = openFile.querySelector('.row');
|
|
382
|
+
if (row) { row.setAttribute('aria-expanded', 'false'); }
|
|
383
|
+
openFile = null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function show(file, takeFocus) {
|
|
387
|
+
var row = file && file.querySelector('.row');
|
|
388
|
+
if (!row || !host) { return; }
|
|
389
|
+
park();
|
|
390
|
+
openFile = file;
|
|
391
|
+
file.classList.add('active');
|
|
392
|
+
row.setAttribute('aria-expanded', 'true');
|
|
393
|
+
var body = file.querySelector('.body');
|
|
394
|
+
if (body) { body.hidden = false; host.appendChild(body); }
|
|
395
|
+
host.scrollTop = 0;
|
|
396
|
+
empty(titleBox);
|
|
397
|
+
['.chip', '.path'].forEach(function (part) {
|
|
398
|
+
var found = row.querySelector(part);
|
|
399
|
+
if (found && titleBox) { titleBox.appendChild(found.cloneNode(true)); }
|
|
400
|
+
});
|
|
401
|
+
copyInto(statBox, row.querySelector('.stat-line'));
|
|
402
|
+
document.body.classList.add('sheet-open');
|
|
403
|
+
position();
|
|
404
|
+
if (takeFocus) { host.focus(); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function close() {
|
|
408
|
+
park();
|
|
409
|
+
document.body.classList.remove('sheet-open');
|
|
410
|
+
empty(titleBox);
|
|
411
|
+
empty(statBox);
|
|
412
|
+
position();
|
|
413
|
+
if (opener) { opener.focus(); opener = null; }
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function step(delta) {
|
|
417
|
+
var rows = listed();
|
|
418
|
+
var at = openFile ? rows.indexOf(openFile) : -1;
|
|
419
|
+
var next = at < 0 ? null : rows[at + delta];
|
|
420
|
+
if (next) {
|
|
421
|
+
// Focus stays on the button that is walking the list, so the next press
|
|
422
|
+
// lands on it too.
|
|
423
|
+
show(next, false);
|
|
424
|
+
opener = next.querySelector('.row');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
235
427
|
|
|
236
428
|
function apply() {
|
|
237
|
-
var term = (search.value
|
|
429
|
+
var term = (search ? search.value : '').toLowerCase();
|
|
238
430
|
var showVendor = vendorToggle ? vendorToggle.checked : true;
|
|
239
431
|
var shown = 0;
|
|
432
|
+
var total = 0;
|
|
433
|
+
var first = true;
|
|
240
434
|
files.forEach(function (el) {
|
|
241
435
|
var path = (el.getAttribute('data-path') || '').toLowerCase();
|
|
242
436
|
var isVendor = el.getAttribute('data-vendor') === '1';
|
|
243
437
|
var ok = (!term || path.indexOf(term) !== -1) && (showVendor || !isVendor);
|
|
438
|
+
// A move block stands for every file it folded up, so both halves of the
|
|
439
|
+
// count are sums of `data-files` rather than counts of rows.
|
|
440
|
+
var covers = parseInt(el.getAttribute('data-files') || '1', 10);
|
|
244
441
|
el.classList.toggle('hidden', !ok);
|
|
245
|
-
|
|
442
|
+
el.classList.toggle('first-shown', ok && first);
|
|
443
|
+
total += covers;
|
|
444
|
+
if (ok) { shown += covers; first = false; }
|
|
246
445
|
});
|
|
247
|
-
|
|
248
|
-
|
|
446
|
+
if (counter) { counter.textContent = shown + ' of ' + total + ' files shown'; }
|
|
447
|
+
// Leaving the sheet open on a file the filter just took off the list would
|
|
448
|
+
// show a diff with no row to close it from.
|
|
449
|
+
if (openFile && openFile.classList.contains('hidden')) { close(); } else { position(); }
|
|
249
450
|
}
|
|
250
451
|
|
|
251
|
-
|
|
252
|
-
|
|
452
|
+
files.forEach(function (file) {
|
|
453
|
+
var row = file.querySelector('.row');
|
|
454
|
+
if (!row) { return; }
|
|
455
|
+
row.addEventListener('click', function () {
|
|
456
|
+
if (file === openFile) { close(); return; }
|
|
457
|
+
opener = row;
|
|
458
|
+
show(file, true);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (search) { search.addEventListener('input', apply); }
|
|
463
|
+
if (vendorToggle) { vendorToggle.addEventListener('change', apply); }
|
|
464
|
+
if (closeBtn) { closeBtn.addEventListener('click', close); }
|
|
465
|
+
if (scrim) { scrim.addEventListener('click', close); }
|
|
466
|
+
if (prevBtn) { prevBtn.addEventListener('click', function () { step(-1); }); }
|
|
467
|
+
if (nextBtn) { nextBtn.addEventListener('click', function () { step(1); }); }
|
|
468
|
+
|
|
469
|
+
document.addEventListener('keydown', function (ev) {
|
|
470
|
+
if (!openFile || ev.ctrlKey || ev.metaKey || ev.altKey) { return; }
|
|
471
|
+
if (ev.key === 'Escape') { close(); return; }
|
|
472
|
+
var tag = (ev.target.tagName || '').toLowerCase();
|
|
473
|
+
if (tag === 'input' || tag === 'textarea') { return; }
|
|
474
|
+
if (ev.key === 'j') { step(1); ev.preventDefault(); }
|
|
475
|
+
if (ev.key === 'k') { step(-1); ev.preventDefault(); }
|
|
476
|
+
});
|
|
253
477
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
478
|
+
if (grip && window.PointerEvent) {
|
|
479
|
+
grip.addEventListener('pointerdown', function (down) {
|
|
480
|
+
down.preventDefault();
|
|
481
|
+
grip.setPointerCapture(down.pointerId);
|
|
482
|
+
document.body.classList.add('dragging');
|
|
483
|
+
function drag(move) {
|
|
484
|
+
// Clamped so a drag can neither shut the sheet nor bury the list
|
|
485
|
+
// behind it, whichever direction it is thrown in.
|
|
486
|
+
var width = Math.min(Math.max(window.innerWidth - move.clientX, 380),
|
|
487
|
+
Math.max(window.innerWidth - 320, 380));
|
|
488
|
+
document.documentElement.style.setProperty('--sheet-w', Math.round(width) + 'px');
|
|
489
|
+
}
|
|
490
|
+
function drop() {
|
|
491
|
+
document.body.classList.remove('dragging');
|
|
492
|
+
grip.removeEventListener('pointermove', drag);
|
|
493
|
+
grip.removeEventListener('pointerup', drop);
|
|
494
|
+
grip.removeEventListener('pointercancel', drop);
|
|
495
|
+
}
|
|
496
|
+
grip.addEventListener('pointermove', drag);
|
|
497
|
+
grip.addEventListener('pointerup', drop);
|
|
498
|
+
grip.addEventListener('pointercancel', drop);
|
|
499
|
+
});
|
|
258
500
|
}
|
|
259
|
-
window.addEventListener('resize', measure);
|
|
260
|
-
measure();
|
|
261
501
|
|
|
262
|
-
var expand = document.getElementById('expand-all');
|
|
263
|
-
var collapse = document.getElementById('collapse-all');
|
|
264
|
-
if (expand) expand.addEventListener('click', function () {
|
|
265
|
-
files.forEach(function (f) { if (!f.classList.contains('hidden')) f.open = true; });
|
|
266
|
-
});
|
|
267
|
-
if (collapse) collapse.addEventListener('click', function () {
|
|
268
|
-
files.forEach(function (f) { f.open = false; });
|
|
269
|
-
});
|
|
270
502
|
apply();
|
|
271
503
|
})();
|
|
272
504
|
"""
|
|
@@ -392,8 +624,46 @@ def _row_code(row: _Row, lang: str, old: _Painted, new: _Painted) -> str:
|
|
|
392
624
|
return highlight(row.text, lang)
|
|
393
625
|
|
|
394
626
|
|
|
627
|
+
def _file_block(
|
|
628
|
+
*,
|
|
629
|
+
path_key: str,
|
|
630
|
+
chip_class: str,
|
|
631
|
+
chip_text: str,
|
|
632
|
+
icon: str,
|
|
633
|
+
title: str,
|
|
634
|
+
stat: str,
|
|
635
|
+
body: str,
|
|
636
|
+
is_vendor: bool,
|
|
637
|
+
files: int | None = None,
|
|
638
|
+
) -> str:
|
|
639
|
+
"""One row of the file list, carrying the diff the sheet opens when it is clicked.
|
|
640
|
+
|
|
641
|
+
The row is what the reader scans — kind, icon, path, counts — and ``body``
|
|
642
|
+
is what opens beside it. The body ships inside the row's own element rather
|
|
643
|
+
than in a second list keyed by path, so a block stays one self-contained
|
|
644
|
+
thing to filter, hide or print; the script moves that element into the sheet
|
|
645
|
+
and back out again, which is why no diff is ever on the page twice.
|
|
646
|
+
|
|
647
|
+
``path_key`` is what the filter box matches on: a file's own path and the
|
|
648
|
+
one it was renamed from, or every member path of a move. ``files`` is how
|
|
649
|
+
many files the block is the only entry for, left off when that is one.
|
|
650
|
+
"""
|
|
651
|
+
extra = f' data-files="{files}"' if files is not None else ""
|
|
652
|
+
return (
|
|
653
|
+
f'<article class="file" data-path="{_esc(path_key)}" '
|
|
654
|
+
f'data-vendor="{1 if is_vendor else 0}"{extra}>'
|
|
655
|
+
'<button class="row" type="button" aria-expanded="false" aria-controls="sheet">'
|
|
656
|
+
f'<span class="chip {_esc(chip_class)}">{_esc(chip_text)}</span>'
|
|
657
|
+
f'<span class="path">{icon}<span class="p">{title}</span></span>'
|
|
658
|
+
f'<span class="stat-line">{stat}</span>'
|
|
659
|
+
"</button>"
|
|
660
|
+
f'<div class="body" hidden>{body}</div>'
|
|
661
|
+
"</article>"
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
|
|
395
665
|
def _render_file(change: FileChange, a_root: Path | None = None, b_root: Path | None = None) -> str:
|
|
396
|
-
"""Render one file's change as a
|
|
666
|
+
"""Render one file's change as a list row and the diff table behind it.
|
|
397
667
|
|
|
398
668
|
A rename is titled as one file with only the moved part written twice
|
|
399
669
|
(``boto3-{1.34.0 → 1.35.20}.dist-info/METADATA``), rather than as two
|
|
@@ -423,22 +693,12 @@ def _render_file(change: FileChange, a_root: Path | None = None, b_root: Path |
|
|
|
423
693
|
if change.size_delta:
|
|
424
694
|
stat += f"<span>{signed(change.size_delta)} B</span>"
|
|
425
695
|
if note := change.line_count_note:
|
|
426
|
-
# The
|
|
427
|
-
#
|
|
428
|
-
#
|
|
696
|
+
# The diff is behind a click, so a row with no ``+`` and no ``−`` is all
|
|
697
|
+
# most readers ever see of this file. The note is the difference between
|
|
698
|
+
# "measured differently" and "unchanged".
|
|
429
699
|
stat += f'<span class="skipped">{_esc(note)}</span>'
|
|
430
700
|
|
|
431
|
-
parts = [
|
|
432
|
-
f'<details class="file" data-path="{_esc(change.path)} {_esc(change.old_path or "")}" '
|
|
433
|
-
f'data-vendor="{1 if change.is_vendor else 0}">',
|
|
434
|
-
"<summary>",
|
|
435
|
-
f'<span class="chip {_esc(change.kind)}">{_esc(change.kind)}</span>',
|
|
436
|
-
f'<span class="path">{icons.file_icon(change.path, lang)}'
|
|
437
|
-
f'<span class="p">{title}</span></span>',
|
|
438
|
-
f'<span class="stat-line">{stat}</span>',
|
|
439
|
-
"</summary>",
|
|
440
|
-
]
|
|
441
|
-
|
|
701
|
+
parts: list[str] = []
|
|
442
702
|
if change.diff_lines:
|
|
443
703
|
old = _paint(a_root, change.old_path or change.path, lang) if change.old else None
|
|
444
704
|
new = _paint(b_root, change.path, lang) if change.new else None
|
|
@@ -450,7 +710,7 @@ def _render_file(change: FileChange, a_root: Path | None = None, b_root: Path |
|
|
|
450
710
|
parts.append(f'<tr class="hunk"><td colspan="4">{_esc(row.text)}</td></tr>')
|
|
451
711
|
continue
|
|
452
712
|
css = f' class="{row.css}"' if row.css in ("add", "del") else ""
|
|
453
|
-
mark = {"add": "+", "del": "
|
|
713
|
+
mark = {"add": "+", "del": "−"}.get(row.css, "")
|
|
454
714
|
code = intraline.mark(_row_code(row, lang, old, new), marks.get(index, []))
|
|
455
715
|
parts.append(
|
|
456
716
|
f"<tr{css}>"
|
|
@@ -490,8 +750,16 @@ def _render_file(change: FileChange, a_root: Path | None = None, b_root: Path |
|
|
|
490
750
|
'is on disk.')
|
|
491
751
|
parts.append(f'<div class="note">{note}</div>')
|
|
492
752
|
|
|
493
|
-
|
|
494
|
-
|
|
753
|
+
return _file_block(
|
|
754
|
+
path_key=f'{change.path} {change.old_path or ""}',
|
|
755
|
+
chip_class=change.kind,
|
|
756
|
+
chip_text=change.kind,
|
|
757
|
+
icon=icons.file_icon(change.path, lang),
|
|
758
|
+
title=title,
|
|
759
|
+
stat=stat,
|
|
760
|
+
body="\n".join(parts),
|
|
761
|
+
is_vendor=change.is_vendor,
|
|
762
|
+
)
|
|
495
763
|
|
|
496
764
|
|
|
497
765
|
def _render_word_edits(change: FileChange) -> str:
|
|
@@ -541,15 +809,14 @@ def _render_move(group: MoveGroup) -> str:
|
|
|
541
809
|
files, so nothing is hidden that expanding will not show. The alternative
|
|
542
810
|
is twenty blocks whose titles differ only in the filename at the end.
|
|
543
811
|
|
|
544
|
-
Kept searchable and filterable like any file block:
|
|
545
|
-
every member path
|
|
546
|
-
|
|
547
|
-
|
|
812
|
+
Kept searchable and filterable like any file block: the filter box matches
|
|
813
|
+
on every member path, and the block counts as vendored only when the whole
|
|
814
|
+
move is, which is the dependency-bump case
|
|
815
|
+
(``boto3-1.{34.0 → 35.20}.dist-info/``).
|
|
548
816
|
|
|
549
|
-
|
|
550
|
-
"N of M files shown" counter stays a count of files
|
|
551
|
-
|
|
552
|
-
otherwise be counted twice.
|
|
817
|
+
The file count it reports to :func:`_file_block` leaves out the edited
|
|
818
|
+
members, so the "N of M files shown" counter stays a count of files: those
|
|
819
|
+
members follow as blocks of their own and would otherwise be counted twice.
|
|
553
820
|
"""
|
|
554
821
|
head, was, now, tail = rename_label(*group.display_dirs)
|
|
555
822
|
title = (
|
|
@@ -579,18 +846,16 @@ def _render_move(group: MoveGroup) -> str:
|
|
|
579
846
|
+ "</li>"
|
|
580
847
|
for c in group.members
|
|
581
848
|
)
|
|
582
|
-
return (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
"
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
f'<
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
f'<ul class="moved-list">{rows}</ul>'
|
|
593
|
-
"</details>"
|
|
849
|
+
return _file_block(
|
|
850
|
+
path_key=searchable,
|
|
851
|
+
chip_class="renamed",
|
|
852
|
+
chip_text="moved",
|
|
853
|
+
icon=icons.file_icon(group.new_dir + "/", "text"),
|
|
854
|
+
title=title,
|
|
855
|
+
stat=stat,
|
|
856
|
+
body=f'<ul class="moved-list">{rows}</ul>',
|
|
857
|
+
is_vendor=group.is_vendor,
|
|
858
|
+
files=group.moved - group.edited,
|
|
594
859
|
)
|
|
595
860
|
|
|
596
861
|
|
|
@@ -773,8 +1038,68 @@ def _findings_section(diff: VersionDiff) -> str:
|
|
|
773
1038
|
return f"<h2>New findings</h2>{table}{resolved}"
|
|
774
1039
|
|
|
775
1040
|
|
|
1041
|
+
#: Without the script the sheet can never open, so the diffs sit under their own
|
|
1042
|
+
#: rows instead and the controls that would do nothing are taken off the page.
|
|
1043
|
+
NOSCRIPT = """
|
|
1044
|
+
.toolbar, .sheet, .scrim { display: none; }
|
|
1045
|
+
.row { cursor: default; }
|
|
1046
|
+
.row::after { display: none; }
|
|
1047
|
+
.file .body[hidden] { display: block; }
|
|
1048
|
+
"""
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _nav_button(ident: str, label: str, glyph: str, extra: str = "") -> str:
|
|
1052
|
+
"""One of the sheet's three controls: a 16×16 stroked glyph with a spoken name.
|
|
1053
|
+
|
|
1054
|
+
``label`` names the keystroke as well as the action (``Next file (j)``),
|
|
1055
|
+
because a shortcut on a page with no menu has nowhere else to be found.
|
|
1056
|
+
"""
|
|
1057
|
+
return (
|
|
1058
|
+
f'<button type="button" class="iconbtn{extra}" id="{ident}" '
|
|
1059
|
+
f'title="{_esc(label)}" aria-label="{_esc(label)}">'
|
|
1060
|
+
f'<svg viewBox="0 0 16 16" aria-hidden="true"><path d="{glyph}"/></svg></button>'
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _sheet(diff: VersionDiff) -> str:
|
|
1065
|
+
"""The panel a file's diff opens into, and the scrim behind it on a narrow window.
|
|
1066
|
+
|
|
1067
|
+
One frame per page rather than one per file: the script moves the clicked
|
|
1068
|
+
file's diff into it, so what is written here is everything around that diff
|
|
1069
|
+
— the path, its counts, which two versions are being compared, and the
|
|
1070
|
+
controls that walk the list without going back to it.
|
|
1071
|
+
|
|
1072
|
+
The heading is left empty because it is filled from the row that was
|
|
1073
|
+
clicked; see the ``show`` function in :data:`JS`.
|
|
1074
|
+
"""
|
|
1075
|
+
return (
|
|
1076
|
+
'<div class="scrim" id="scrim"></div>'
|
|
1077
|
+
'<aside class="sheet" id="sheet" role="dialog" aria-labelledby="sheet-title">'
|
|
1078
|
+
'<div class="grip" id="sheet-grip" title="Drag to resize"></div>'
|
|
1079
|
+
'<header class="sheet-head"><div class="sheet-bar">'
|
|
1080
|
+
'<div class="sheet-title" id="sheet-title"></div>'
|
|
1081
|
+
'<div class="sheet-nav">'
|
|
1082
|
+
+ _nav_button("sheet-prev", "Previous file (k)", "m4.5 9.75 3.5-3.5 3.5 3.5")
|
|
1083
|
+
+ '<span class="pos" id="sheet-pos"></span>'
|
|
1084
|
+
+ _nav_button("sheet-next", "Next file (j)", "m4.5 6.25 3.5 3.5 3.5-3.5")
|
|
1085
|
+
+ _nav_button("sheet-close", "Close (Esc)", "m4.5 4.5 7 7m0-7-7 7", " close")
|
|
1086
|
+
+ "</div></div>"
|
|
1087
|
+
'<div class="sheet-sub"><span id="sheet-stat"></span>'
|
|
1088
|
+
f'<span class="ver">v{diff.a_seq:04d} → v{diff.b_seq:04d}</span>'
|
|
1089
|
+
"</div></header>"
|
|
1090
|
+
'<div class="sheet-body" id="sheet-body" tabindex="-1"></div>'
|
|
1091
|
+
"</aside>"
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
|
|
776
1095
|
def render_html(diff: VersionDiff, generated_by: str = "lambda-watcher") -> str:
|
|
777
|
-
"""Render the full report as a single HTML document.
|
|
1096
|
+
"""Render the full report as a single HTML document.
|
|
1097
|
+
|
|
1098
|
+
What the reader gets is a summary they can take in without scrolling, then
|
|
1099
|
+
a list of every changed file. Clicking one opens its diff in the sheet
|
|
1100
|
+
beside the list rather than underneath it — see :func:`_sheet` — so the
|
|
1101
|
+
list keeps its place and the code gets the width it wants.
|
|
1102
|
+
"""
|
|
778
1103
|
title = f"{diff.function_name} · v{diff.a_seq:04d} → v{diff.b_seq:04d}"
|
|
779
1104
|
a_when = format_ts(diff.a_meta.get("ingested_at"))
|
|
780
1105
|
b_when = format_ts(diff.b_meta.get("ingested_at"))
|
|
@@ -790,15 +1115,24 @@ def render_html(diff: VersionDiff, generated_by: str = "lambda-watcher") -> str:
|
|
|
790
1115
|
blocks.extend(
|
|
791
1116
|
_render_file(c, diff.a_root, diff.b_root) for c in row.edited_members
|
|
792
1117
|
)
|
|
793
|
-
file_blocks = "\n".join(blocks)
|
|
794
|
-
if not diff.files:
|
|
795
|
-
file_blocks = '<div class="empty">No file-level changes between these versions.</div>'
|
|
796
|
-
|
|
797
1118
|
vendor_toggle = (
|
|
798
1119
|
'<label><input type="checkbox" id="vendor" checked> show vendored files</label>'
|
|
799
1120
|
if any(c.is_vendor for c in diff.files)
|
|
800
1121
|
else ""
|
|
801
1122
|
)
|
|
1123
|
+
# Two versions with the same tree have nothing to filter and nothing to
|
|
1124
|
+
# open, so that page is one sentence: a search box above it would only
|
|
1125
|
+
# offer to narrow an empty list.
|
|
1126
|
+
toolbar = listing = ""
|
|
1127
|
+
if diff.files:
|
|
1128
|
+
toolbar = (
|
|
1129
|
+
'<div class="toolbar">'
|
|
1130
|
+
'<input type="search" id="filter" placeholder="Filter by path…" autocomplete="off">'
|
|
1131
|
+
f'{vendor_toggle}<span class="sub" id="shown-count"></span></div>'
|
|
1132
|
+
)
|
|
1133
|
+
listing = '<div class="files">{}</div>'.format("\n".join(blocks))
|
|
1134
|
+
else:
|
|
1135
|
+
listing = '<div class="empty">No file-level changes between these versions.</div>'
|
|
802
1136
|
|
|
803
1137
|
return f"""<!DOCTYPE html>
|
|
804
1138
|
<html lang="en">
|
|
@@ -807,6 +1141,7 @@ def render_html(diff: VersionDiff, generated_by: str = "lambda-watcher") -> str:
|
|
|
807
1141
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
808
1142
|
<title>{_esc(title)}</title>
|
|
809
1143
|
<style>{CSS}{ICON_CSS}</style>
|
|
1144
|
+
<noscript><style>{NOSCRIPT}</style></noscript>
|
|
810
1145
|
</head>
|
|
811
1146
|
<body>
|
|
812
1147
|
{icons.sprite()}
|
|
@@ -826,20 +1161,15 @@ def render_html(diff: VersionDiff, generated_by: str = "lambda-watcher") -> str:
|
|
|
826
1161
|
{_findings_section(diff)}
|
|
827
1162
|
|
|
828
1163
|
<h2>File changes</h2>
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
{vendor_toggle}
|
|
832
|
-
<button type="button" id="expand-all">Expand all</button>
|
|
833
|
-
<button type="button" id="collapse-all">Collapse all</button>
|
|
834
|
-
<span class="sub" id="shown-count"></span>
|
|
835
|
-
</div>
|
|
836
|
-
{file_blocks}
|
|
1164
|
+
{toolbar}
|
|
1165
|
+
{listing}
|
|
837
1166
|
|
|
838
1167
|
<footer>
|
|
839
1168
|
Generated by {_esc(generated_by)} on {_esc(datetime.now().strftime('%Y-%m-%d %H:%M'))}.
|
|
840
1169
|
Content hashes ignore zip timestamps, so re-downloading unchanged code does not create a new version.
|
|
841
1170
|
</footer>
|
|
842
1171
|
</div>
|
|
1172
|
+
{_sheet(diff)}
|
|
843
1173
|
<script>{JS}</script>
|
|
844
1174
|
</body>
|
|
845
1175
|
</html>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lambda-watcher
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Watch your Downloads folder for AWS Lambda deployment zips, archive them as versions, analyse them, and diff any two versions.
|
|
5
5
|
Author: Lambda Watcher contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -526,8 +526,8 @@ def test_the_html_report_folds_the_move_but_keeps_the_edits(cfg, db, ingestor: I
|
|
|
526
526
|
ingestor.ingest(make_zip("fn.zip", after))
|
|
527
527
|
|
|
528
528
|
html = render_html(_diff(cfg, db, ingestor))
|
|
529
|
-
assert html.count('
|
|
530
|
-
assert html.count('<li class="mono">') == 20 # nothing hidden that
|
|
529
|
+
assert html.count('<article class="file"') == 3 # the move, plus the two edits
|
|
530
|
+
assert html.count('<li class="mono">') == 20 # nothing hidden that opening it won't show
|
|
531
531
|
assert html.count('<div class="diff">') == 2 # both edits keep their patch
|
|
532
532
|
# The counter stays a count of files, not of blocks.
|
|
533
533
|
assert 'data-files="18"' in html
|
|
@@ -23,7 +23,14 @@ from lambda_watcher.diffing.highlight import (
|
|
|
23
23
|
highlight_lines,
|
|
24
24
|
language_of,
|
|
25
25
|
)
|
|
26
|
-
from lambda_watcher.diffing.render_html import
|
|
26
|
+
from lambda_watcher.diffing.render_html import (
|
|
27
|
+
CSS,
|
|
28
|
+
JS,
|
|
29
|
+
NOSCRIPT,
|
|
30
|
+
_Row,
|
|
31
|
+
_row_code,
|
|
32
|
+
render_html,
|
|
33
|
+
)
|
|
27
34
|
from lambda_watcher.ingest import Ingestor
|
|
28
35
|
from lambda_watcher.utils import LANG_BY_EXT
|
|
29
36
|
|
|
@@ -371,3 +378,124 @@ def test_the_report_paints_the_code_and_carries_one_sprite(cfg, db, ingestor: In
|
|
|
371
378
|
assert 'colspan="4"' in page and 'colspan="3"' not in page
|
|
372
379
|
# Still one file, no network.
|
|
373
380
|
assert "http://" not in page and "https://" not in page
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# --------------------------------------------------- the list and the sheet
|
|
384
|
+
# A file's diff opens beside the list rather than under it, which makes the
|
|
385
|
+
# page three things that have to agree: markup the script can find, a script
|
|
386
|
+
# that moves exactly one diff into the sheet, and a stylesheet that draws the
|
|
387
|
+
# states the script sets. Nothing here can see a browser, so these pin down the
|
|
388
|
+
# contract between the three — the failures that reach a reader as a row that
|
|
389
|
+
# does nothing when clicked.
|
|
390
|
+
def _report(cfg, db, ingestor, make_zip) -> str:
|
|
391
|
+
"""A rendered report listing a first-party file and a vendored one.
|
|
392
|
+
|
|
393
|
+
Listing the vendored file is what puts the "show vendored files" box on the
|
|
394
|
+
page, so this is the page that carries every control the script binds to.
|
|
395
|
+
"""
|
|
396
|
+
ingestor.ingest(make_zip("fn.zip", {
|
|
397
|
+
"lambda_function.py": PY_V1, "site-packages/boto3/__init__.py": "V = '1.34.0'\n"}))
|
|
398
|
+
ingestor.ingest(make_zip("fn.zip", {
|
|
399
|
+
"lambda_function.py": PY_V2, "site-packages/boto3/__init__.py": "V = '1.35.20'\n"}))
|
|
400
|
+
return render_html(_diff(cfg, db, ingestor, include_vendor=True))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_a_file_is_a_row_with_its_diff_parked_behind_it(cfg, db, ingestor: Ingestor, make_zip):
|
|
404
|
+
"""The row is what the reader scans; the diff travels with it, hidden until asked for.
|
|
405
|
+
|
|
406
|
+
Hidden in the markup rather than absent from it is what keeps the page one
|
|
407
|
+
file: there is nothing to fetch when a row is clicked, and a browser with
|
|
408
|
+
no script still has every diff on the page.
|
|
409
|
+
"""
|
|
410
|
+
page = _report(cfg, db, ingestor, make_zip)
|
|
411
|
+
|
|
412
|
+
assert '<article class="file"' in page
|
|
413
|
+
assert '<button class="row" type="button" aria-expanded="false" aria-controls="sheet">' in page
|
|
414
|
+
assert '<div class="body" hidden>' in page
|
|
415
|
+
# One body per block, each holding that block's diff and nothing else, so
|
|
416
|
+
# no diff is on the page twice for the script to pick the wrong copy of.
|
|
417
|
+
assert page.count('<div class="body" hidden>') == page.count('<article class="file"') == 2
|
|
418
|
+
assert page.count('<div class="body" hidden><div class="diff">') == 2
|
|
419
|
+
# The block that opened downwards is gone, markers and all.
|
|
420
|
+
assert "<details" not in page and "<summary" not in page
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_the_page_carries_exactly_one_sheet(cfg, db, ingestor: Ingestor, make_zip):
|
|
424
|
+
"""One frame for every row to open into, outside the list the script moves diffs out of.
|
|
425
|
+
|
|
426
|
+
A sheet per file would repeat the diff, and a sheet nested in the list
|
|
427
|
+
would be hidden by the filter that hides its row.
|
|
428
|
+
"""
|
|
429
|
+
page = _report(cfg, db, ingestor, make_zip)
|
|
430
|
+
|
|
431
|
+
assert page.count('id="sheet"') == 1
|
|
432
|
+
assert page.count('id="sheet-body"') == 1
|
|
433
|
+
assert page.index('class="files"') < page.index('class="sheet"')
|
|
434
|
+
# The versions being compared are named in the sheet too: it covers the
|
|
435
|
+
# page header outright on a window too narrow to dock it.
|
|
436
|
+
assert 'class="ver">v0001 → v0002</span>' in page
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_every_handle_the_script_reaches_for_is_on_the_page(cfg, db, ingestor: Ingestor,
|
|
440
|
+
make_zip):
|
|
441
|
+
"""The script finds the page by id, so a rename on either side is a dead row.
|
|
442
|
+
|
|
443
|
+
Nothing raises when it happens — ``getElementById`` answers null and the
|
|
444
|
+
listener is never attached — so clicking a file would quietly do nothing.
|
|
445
|
+
"""
|
|
446
|
+
page = _report(cfg, db, ingestor, make_zip)
|
|
447
|
+
|
|
448
|
+
wanted = set(re.findall(r"getElementById\('([^']+)'\)", JS))
|
|
449
|
+
assert wanted, "no ids parsed out of the script"
|
|
450
|
+
for ident in sorted(wanted):
|
|
451
|
+
assert f'id="{ident}"' in page, f"the script looks up #{ident}, which nothing renders"
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_every_state_the_script_sets_is_drawn(cfg, db, ingestor: Ingestor, make_zip):
|
|
455
|
+
"""A class the stylesheet has no rule for is a state the reader cannot see."""
|
|
456
|
+
for css_class in ("hidden", "first-shown", "active", "sheet-open", "dragging"):
|
|
457
|
+
assert f"classList.toggle('{css_class}'" in JS or f"classList.add('{css_class}'" in JS
|
|
458
|
+
assert f".{css_class}" in CSS, f"the script sets .{css_class} and nothing draws it"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def test_the_sheet_says_which_key_does_what(cfg, db, ingestor: Ingestor, make_zip):
|
|
462
|
+
"""A shortcut on a page with no menu is only discoverable from the control it doubles."""
|
|
463
|
+
page = _report(cfg, db, ingestor, make_zip)
|
|
464
|
+
|
|
465
|
+
for key, action in (("j", "Next file"), ("k", "Previous file"), ("Esc", "Close")):
|
|
466
|
+
assert f'title="{action} ({key})"' in page
|
|
467
|
+
assert f'aria-label="{action} ({key})"' in page
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_without_a_script_the_diffs_are_still_readable(cfg, db, ingestor: Ingestor, make_zip):
|
|
471
|
+
"""No script means no sheet, so the diffs go back to sitting under their own rows.
|
|
472
|
+
|
|
473
|
+
The alternative is a page of rows that do nothing when clicked, with every
|
|
474
|
+
diff on it and no way to reach one.
|
|
475
|
+
"""
|
|
476
|
+
page = _report(cfg, db, ingestor, make_zip)
|
|
477
|
+
|
|
478
|
+
assert "<noscript><style>" in page
|
|
479
|
+
assert ".file .body[hidden] { display: block; }" in NOSCRIPT
|
|
480
|
+
# ...and the controls that only a script can answer are taken off the page.
|
|
481
|
+
assert ".toolbar, .sheet, .scrim { display: none; }" in NOSCRIPT
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def test_a_report_with_nothing_to_list_offers_nothing_to_filter(cfg, db, ingestor: Ingestor,
|
|
485
|
+
make_zip):
|
|
486
|
+
"""Two versions differing only outside the file tree still land on a page that reads.
|
|
487
|
+
|
|
488
|
+
A search box over an empty list invites the reader to narrow it further,
|
|
489
|
+
which is the opposite of what the sentence under it is telling them.
|
|
490
|
+
"""
|
|
491
|
+
# A dependency bump and nothing else: the vendored files that changed are
|
|
492
|
+
# hidden by default, and the dependency table above has already explained
|
|
493
|
+
# them, so the list itself has nothing left to show.
|
|
494
|
+
ingestor.ingest(make_zip("fn.zip", {
|
|
495
|
+
"lambda_function.py": PY_V1, "site-packages/boto3/__init__.py": "V = '1.34.0'\n"}))
|
|
496
|
+
ingestor.ingest(make_zip("fn.zip", {
|
|
497
|
+
"lambda_function.py": PY_V1, "site-packages/boto3/__init__.py": "V = '1.35.20'\n"}))
|
|
498
|
+
page = render_html(_diff(cfg, db, ingestor))
|
|
499
|
+
|
|
500
|
+
assert '<div class="empty">' in page
|
|
501
|
+
assert 'id="filter"' not in page and 'class="files"' not in page
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lambda_watcher-0.2.1 → lambda_watcher-0.3.0}/src/lambda_watcher.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|