feat(web): add the merge request pages, compare and the owner's writes
- commit
- a889e05916334bb8b55156d0640b20d38d944076
- author
- brice · · 2026-10-10 18:29 UTC
- parent
- 951a93a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
55 files · +5887 -255
M cmd/ermite/testdata/web/blob-binary.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -168,11 +171,24 @@ | |||
| 168 | 171 | </div> | |
| 169 | 172 | <div> | |
| 170 | 173 | <dt> | |
| 174 | + | <kbd>m</kbd> | |
| 175 | + | <kbd>g m</kbd> | |
| 176 | + | </dt> | |
| 177 | + | <dd>merge requests (when shown)</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 171 | 181 | <kbd>d</kbd> | |
| 172 | 182 | <kbd>u</kbd> | |
| 173 | 183 | <kbd>x</kbd> | |
| 174 | 184 | </dt> | |
| 175 | - | <dd>commit: split / unified / collapse all</dd> | |
| 185 | + | <dd>diff: split / unified / collapse all</dd> | |
| 186 | + | </div> | |
| 187 | + | <div> | |
| 188 | + | <dt> | |
| 189 | + | <kbd>n</kbd> | |
| 190 | + | </dt> | |
| 191 | + | <dd>merge request: next note in the diff</dd> | |
| 176 | 192 | </div> | |
| 177 | 193 | <div> | |
| 178 | 194 | <dt> | |
| @@ -248,6 +264,10 @@ | |||
| 248 | 264 | <a href="/files/refs" tabindex="-1"> | |
| 249 | 265 | <span class="k">section</span>refs</a> | |
| 250 | 266 | </li> | |
| 267 | + | <li data-k="section merge requests"> | |
| 268 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 269 | + | <span class="k">section</span>merge requests</a> | |
| 270 | + | </li> | |
| 251 | 271 | <li data-k="branch feature/x"> | |
| 252 | 272 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 253 | 273 | <span class="k">branch</span>feature/x</a> | |
| @@ -308,6 +328,13 @@ | |||
| 308 | 328 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 309 | 329 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 310 | 330 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 333 | + | k-next from:body -> { | |
| 334 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 336 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 337 | + | }; | |
| 311 | 338 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 312 | 339 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 313 | 340 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-code.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -238,11 +241,24 @@ | |||
| 238 | 241 | </div> | |
| 239 | 242 | <div> | |
| 240 | 243 | <dt> | |
| 244 | + | <kbd>m</kbd> | |
| 245 | + | <kbd>g m</kbd> | |
| 246 | + | </dt> | |
| 247 | + | <dd>merge requests (when shown)</dd> | |
| 248 | + | </div> | |
| 249 | + | <div> | |
| 250 | + | <dt> | |
| 241 | 251 | <kbd>d</kbd> | |
| 242 | 252 | <kbd>u</kbd> | |
| 243 | 253 | <kbd>x</kbd> | |
| 244 | 254 | </dt> | |
| 245 | - | <dd>commit: split / unified / collapse all</dd> | |
| 255 | + | <dd>diff: split / unified / collapse all</dd> | |
| 256 | + | </div> | |
| 257 | + | <div> | |
| 258 | + | <dt> | |
| 259 | + | <kbd>n</kbd> | |
| 260 | + | </dt> | |
| 261 | + | <dd>merge request: next note in the diff</dd> | |
| 246 | 262 | </div> | |
| 247 | 263 | <div> | |
| 248 | 264 | <dt> | |
| @@ -318,6 +334,10 @@ | |||
| 318 | 334 | <a href="/files/refs" tabindex="-1"> | |
| 319 | 335 | <span class="k">section</span>refs</a> | |
| 320 | 336 | </li> | |
| 337 | + | <li data-k="section merge requests"> | |
| 338 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 339 | + | <span class="k">section</span>merge requests</a> | |
| 340 | + | </li> | |
| 321 | 341 | <li data-k="branch feature/x"> | |
| 322 | 342 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 323 | 343 | <span class="k">branch</span>feature/x</a> | |
| @@ -379,6 +399,13 @@ | |||
| 379 | 399 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 380 | 400 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 381 | 401 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 402 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 403 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 404 | + | k-next from:body -> { | |
| 405 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 406 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 407 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 408 | + | }; | |
| 382 | 409 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 383 | 410 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 384 | 411 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-empty.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -168,11 +171,24 @@ | |||
| 168 | 171 | </div> | |
| 169 | 172 | <div> | |
| 170 | 173 | <dt> | |
| 174 | + | <kbd>m</kbd> | |
| 175 | + | <kbd>g m</kbd> | |
| 176 | + | </dt> | |
| 177 | + | <dd>merge requests (when shown)</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 171 | 181 | <kbd>d</kbd> | |
| 172 | 182 | <kbd>u</kbd> | |
| 173 | 183 | <kbd>x</kbd> | |
| 174 | 184 | </dt> | |
| 175 | - | <dd>commit: split / unified / collapse all</dd> | |
| 185 | + | <dd>diff: split / unified / collapse all</dd> | |
| 186 | + | </div> | |
| 187 | + | <div> | |
| 188 | + | <dt> | |
| 189 | + | <kbd>n</kbd> | |
| 190 | + | </dt> | |
| 191 | + | <dd>merge request: next note in the diff</dd> | |
| 176 | 192 | </div> | |
| 177 | 193 | <div> | |
| 178 | 194 | <dt> | |
| @@ -248,6 +264,10 @@ | |||
| 248 | 264 | <a href="/files/refs" tabindex="-1"> | |
| 249 | 265 | <span class="k">section</span>refs</a> | |
| 250 | 266 | </li> | |
| 267 | + | <li data-k="section merge requests"> | |
| 268 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 269 | + | <span class="k">section</span>merge requests</a> | |
| 270 | + | </li> | |
| 251 | 271 | <li data-k="branch feature/x"> | |
| 252 | 272 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 253 | 273 | <span class="k">branch</span>feature/x</a> | |
| @@ -308,6 +328,13 @@ | |||
| 308 | 328 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 309 | 329 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 310 | 330 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 333 | + | k-next from:body -> { | |
| 334 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 336 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 337 | + | }; | |
| 311 | 338 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 312 | 339 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 313 | 340 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-image.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -170,11 +173,24 @@ | |||
| 170 | 173 | </div> | |
| 171 | 174 | <div> | |
| 172 | 175 | <dt> | |
| 176 | + | <kbd>m</kbd> | |
| 177 | + | <kbd>g m</kbd> | |
| 178 | + | </dt> | |
| 179 | + | <dd>merge requests (when shown)</dd> | |
| 180 | + | </div> | |
| 181 | + | <div> | |
| 182 | + | <dt> | |
| 173 | 183 | <kbd>d</kbd> | |
| 174 | 184 | <kbd>u</kbd> | |
| 175 | 185 | <kbd>x</kbd> | |
| 176 | 186 | </dt> | |
| 177 | - | <dd>commit: split / unified / collapse all</dd> | |
| 187 | + | <dd>diff: split / unified / collapse all</dd> | |
| 188 | + | </div> | |
| 189 | + | <div> | |
| 190 | + | <dt> | |
| 191 | + | <kbd>n</kbd> | |
| 192 | + | </dt> | |
| 193 | + | <dd>merge request: next note in the diff</dd> | |
| 178 | 194 | </div> | |
| 179 | 195 | <div> | |
| 180 | 196 | <dt> | |
| @@ -250,6 +266,10 @@ | |||
| 250 | 266 | <a href="/files/refs" tabindex="-1"> | |
| 251 | 267 | <span class="k">section</span>refs</a> | |
| 252 | 268 | </li> | |
| 269 | + | <li data-k="section merge requests"> | |
| 270 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 271 | + | <span class="k">section</span>merge requests</a> | |
| 272 | + | </li> | |
| 253 | 273 | <li data-k="branch feature/x"> | |
| 254 | 274 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 255 | 275 | <span class="k">branch</span>feature/x</a> | |
| @@ -310,6 +330,13 @@ | |||
| 310 | 330 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 311 | 331 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 312 | 332 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 333 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 334 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 335 | + | k-next from:body -> { | |
| 336 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 337 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 338 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 339 | + | }; | |
| 313 | 340 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 314 | 341 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 315 | 342 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-markdown.html +28 -1
| @@ -55,6 +55,9 @@ | |||
| 55 | 55 | <li> | |
| 56 | 56 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 57 | 57 | </li> | |
| 58 | + | <li> | |
| 59 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 60 | + | </li> | |
| 58 | 61 | </ul> | |
| 59 | 62 | </nav> | |
| 60 | 63 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -235,11 +238,24 @@ | |||
| 235 | 238 | </div> | |
| 236 | 239 | <div> | |
| 237 | 240 | <dt> | |
| 241 | + | <kbd>m</kbd> | |
| 242 | + | <kbd>g m</kbd> | |
| 243 | + | </dt> | |
| 244 | + | <dd>merge requests (when shown)</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 238 | 248 | <kbd>d</kbd> | |
| 239 | 249 | <kbd>u</kbd> | |
| 240 | 250 | <kbd>x</kbd> | |
| 241 | 251 | </dt> | |
| 242 | - | <dd>commit: split / unified / collapse all</dd> | |
| 252 | + | <dd>diff: split / unified / collapse all</dd> | |
| 253 | + | </div> | |
| 254 | + | <div> | |
| 255 | + | <dt> | |
| 256 | + | <kbd>n</kbd> | |
| 257 | + | </dt> | |
| 258 | + | <dd>merge request: next note in the diff</dd> | |
| 243 | 259 | </div> | |
| 244 | 260 | <div> | |
| 245 | 261 | <dt> | |
| @@ -315,6 +331,10 @@ | |||
| 315 | 331 | <a href="/files/refs" tabindex="-1"> | |
| 316 | 332 | <span class="k">section</span>refs</a> | |
| 317 | 333 | </li> | |
| 334 | + | <li data-k="section merge requests"> | |
| 335 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 336 | + | <span class="k">section</span>merge requests</a> | |
| 337 | + | </li> | |
| 318 | 338 | <li data-k="branch feature/x"> | |
| 319 | 339 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 320 | 340 | <span class="k">branch</span>feature/x</a> | |
| @@ -376,6 +396,13 @@ | |||
| 376 | 396 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 377 | 397 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 378 | 398 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 399 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 400 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 401 | + | k-next from:body -> { | |
| 402 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 403 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 404 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 405 | + | }; | |
| 379 | 406 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 380 | 407 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 381 | 408 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-svg.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -193,11 +196,24 @@ | |||
| 193 | 196 | </div> | |
| 194 | 197 | <div> | |
| 195 | 198 | <dt> | |
| 199 | + | <kbd>m</kbd> | |
| 200 | + | <kbd>g m</kbd> | |
| 201 | + | </dt> | |
| 202 | + | <dd>merge requests (when shown)</dd> | |
| 203 | + | </div> | |
| 204 | + | <div> | |
| 205 | + | <dt> | |
| 196 | 206 | <kbd>d</kbd> | |
| 197 | 207 | <kbd>u</kbd> | |
| 198 | 208 | <kbd>x</kbd> | |
| 199 | 209 | </dt> | |
| 200 | - | <dd>commit: split / unified / collapse all</dd> | |
| 210 | + | <dd>diff: split / unified / collapse all</dd> | |
| 211 | + | </div> | |
| 212 | + | <div> | |
| 213 | + | <dt> | |
| 214 | + | <kbd>n</kbd> | |
| 215 | + | </dt> | |
| 216 | + | <dd>merge request: next note in the diff</dd> | |
| 201 | 217 | </div> | |
| 202 | 218 | <div> | |
| 203 | 219 | <dt> | |
| @@ -273,6 +289,10 @@ | |||
| 273 | 289 | <a href="/files/refs" tabindex="-1"> | |
| 274 | 290 | <span class="k">section</span>refs</a> | |
| 275 | 291 | </li> | |
| 292 | + | <li data-k="section merge requests"> | |
| 293 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 294 | + | <span class="k">section</span>merge requests</a> | |
| 295 | + | </li> | |
| 276 | 296 | <li data-k="branch feature/x"> | |
| 277 | 297 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 278 | 298 | <span class="k">branch</span>feature/x</a> | |
| @@ -334,6 +354,13 @@ | |||
| 334 | 354 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 335 | 355 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 336 | 356 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 357 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 358 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 359 | + | k-next from:body -> { | |
| 360 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 361 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 362 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 363 | + | }; | |
| 337 | 364 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 338 | 365 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 339 | 366 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-text.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -188,11 +191,24 @@ | |||
| 188 | 191 | </div> | |
| 189 | 192 | <div> | |
| 190 | 193 | <dt> | |
| 194 | + | <kbd>m</kbd> | |
| 195 | + | <kbd>g m</kbd> | |
| 196 | + | </dt> | |
| 197 | + | <dd>merge requests (when shown)</dd> | |
| 198 | + | </div> | |
| 199 | + | <div> | |
| 200 | + | <dt> | |
| 191 | 201 | <kbd>d</kbd> | |
| 192 | 202 | <kbd>u</kbd> | |
| 193 | 203 | <kbd>x</kbd> | |
| 194 | 204 | </dt> | |
| 195 | - | <dd>commit: split / unified / collapse all</dd> | |
| 205 | + | <dd>diff: split / unified / collapse all</dd> | |
| 206 | + | </div> | |
| 207 | + | <div> | |
| 208 | + | <dt> | |
| 209 | + | <kbd>n</kbd> | |
| 210 | + | </dt> | |
| 211 | + | <dd>merge request: next note in the diff</dd> | |
| 196 | 212 | </div> | |
| 197 | 213 | <div> | |
| 198 | 214 | <dt> | |
| @@ -268,6 +284,10 @@ | |||
| 268 | 284 | <a href="/files/refs" tabindex="-1"> | |
| 269 | 285 | <span class="k">section</span>refs</a> | |
| 270 | 286 | </li> | |
| 287 | + | <li data-k="section merge requests"> | |
| 288 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 289 | + | <span class="k">section</span>merge requests</a> | |
| 290 | + | </li> | |
| 271 | 291 | <li data-k="branch feature/x"> | |
| 272 | 292 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 273 | 293 | <span class="k">branch</span>feature/x</a> | |
| @@ -329,6 +349,13 @@ | |||
| 329 | 349 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 330 | 350 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 331 | 351 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 352 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 353 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 354 | + | k-next from:body -> { | |
| 355 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 356 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 357 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 358 | + | }; | |
| 332 | 359 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 333 | 360 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 334 | 361 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-too-large.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -168,11 +171,24 @@ | |||
| 168 | 171 | </div> | |
| 169 | 172 | <div> | |
| 170 | 173 | <dt> | |
| 174 | + | <kbd>m</kbd> | |
| 175 | + | <kbd>g m</kbd> | |
| 176 | + | </dt> | |
| 177 | + | <dd>merge requests (when shown)</dd> | |
| 178 | + | </div> | |
| 179 | + | <div> | |
| 180 | + | <dt> | |
| 171 | 181 | <kbd>d</kbd> | |
| 172 | 182 | <kbd>u</kbd> | |
| 173 | 183 | <kbd>x</kbd> | |
| 174 | 184 | </dt> | |
| 175 | - | <dd>commit: split / unified / collapse all</dd> | |
| 185 | + | <dd>diff: split / unified / collapse all</dd> | |
| 186 | + | </div> | |
| 187 | + | <div> | |
| 188 | + | <dt> | |
| 189 | + | <kbd>n</kbd> | |
| 190 | + | </dt> | |
| 191 | + | <dd>merge request: next note in the diff</dd> | |
| 176 | 192 | </div> | |
| 177 | 193 | <div> | |
| 178 | 194 | <dt> | |
| @@ -248,6 +264,10 @@ | |||
| 248 | 264 | <a href="/files/refs" tabindex="-1"> | |
| 249 | 265 | <span class="k">section</span>refs</a> | |
| 250 | 266 | </li> | |
| 267 | + | <li data-k="section merge requests"> | |
| 268 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 269 | + | <span class="k">section</span>merge requests</a> | |
| 270 | + | </li> | |
| 251 | 271 | <li data-k="branch feature/x"> | |
| 252 | 272 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 253 | 273 | <span class="k">branch</span>feature/x</a> | |
| @@ -308,6 +328,13 @@ | |||
| 308 | 328 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 309 | 329 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 310 | 330 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 331 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 332 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 333 | + | k-next from:body -> { | |
| 334 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 335 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 336 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 337 | + | }; | |
| 311 | 338 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 312 | 339 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 313 | 340 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/blob-trojan.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -235,11 +238,24 @@ | |||
| 235 | 238 | </div> | |
| 236 | 239 | <div> | |
| 237 | 240 | <dt> | |
| 241 | + | <kbd>m</kbd> | |
| 242 | + | <kbd>g m</kbd> | |
| 243 | + | </dt> | |
| 244 | + | <dd>merge requests (when shown)</dd> | |
| 245 | + | </div> | |
| 246 | + | <div> | |
| 247 | + | <dt> | |
| 238 | 248 | <kbd>d</kbd> | |
| 239 | 249 | <kbd>u</kbd> | |
| 240 | 250 | <kbd>x</kbd> | |
| 241 | 251 | </dt> | |
| 242 | - | <dd>commit: split / unified / collapse all</dd> | |
| 252 | + | <dd>diff: split / unified / collapse all</dd> | |
| 253 | + | </div> | |
| 254 | + | <div> | |
| 255 | + | <dt> | |
| 256 | + | <kbd>n</kbd> | |
| 257 | + | </dt> | |
| 258 | + | <dd>merge request: next note in the diff</dd> | |
| 243 | 259 | </div> | |
| 244 | 260 | <div> | |
| 245 | 261 | <dt> | |
| @@ -315,6 +331,10 @@ | |||
| 315 | 331 | <a href="/files/refs" tabindex="-1"> | |
| 316 | 332 | <span class="k">section</span>refs</a> | |
| 317 | 333 | </li> | |
| 334 | + | <li data-k="section merge requests"> | |
| 335 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 336 | + | <span class="k">section</span>merge requests</a> | |
| 337 | + | </li> | |
| 318 | 338 | <li data-k="branch feature/x"> | |
| 319 | 339 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 320 | 340 | <span class="k">branch</span>feature/x</a> | |
| @@ -376,6 +396,13 @@ | |||
| 376 | 396 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 377 | 397 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 378 | 398 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 399 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 400 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 401 | + | k-next from:body -> { | |
| 402 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 403 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 404 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 405 | + | }; | |
| 379 | 406 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 380 | 407 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 381 | 408 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-big.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -198,11 +201,24 @@ | |||
| 198 | 201 | </div> | |
| 199 | 202 | <div> | |
| 200 | 203 | <dt> | |
| 204 | + | <kbd>m</kbd> | |
| 205 | + | <kbd>g m</kbd> | |
| 206 | + | </dt> | |
| 207 | + | <dd>merge requests (when shown)</dd> | |
| 208 | + | </div> | |
| 209 | + | <div> | |
| 210 | + | <dt> | |
| 201 | 211 | <kbd>d</kbd> | |
| 202 | 212 | <kbd>u</kbd> | |
| 203 | 213 | <kbd>x</kbd> | |
| 204 | 214 | </dt> | |
| 205 | - | <dd>commit: split / unified / collapse all</dd> | |
| 215 | + | <dd>diff: split / unified / collapse all</dd> | |
| 216 | + | </div> | |
| 217 | + | <div> | |
| 218 | + | <dt> | |
| 219 | + | <kbd>n</kbd> | |
| 220 | + | </dt> | |
| 221 | + | <dd>merge request: next note in the diff</dd> | |
| 206 | 222 | </div> | |
| 207 | 223 | <div> | |
| 208 | 224 | <dt> | |
| @@ -278,6 +294,10 @@ | |||
| 278 | 294 | <a href="/diffs/refs" tabindex="-1"> | |
| 279 | 295 | <span class="k">section</span>refs</a> | |
| 280 | 296 | </li> | |
| 297 | + | <li data-k="section merge requests"> | |
| 298 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 299 | + | <span class="k">section</span>merge requests</a> | |
| 300 | + | </li> | |
| 281 | 301 | <li data-k="branch main"> | |
| 282 | 302 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 283 | 303 | <span class="k">branch</span>main</a> | |
| @@ -338,6 +358,13 @@ | |||
| 338 | 358 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 339 | 359 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 340 | 360 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 361 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 362 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 363 | + | k-next from:body -> { | |
| 364 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 365 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 366 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 367 | + | }; | |
| 341 | 368 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 342 | 369 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 343 | 370 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-binary.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -195,11 +198,24 @@ | |||
| 195 | 198 | </div> | |
| 196 | 199 | <div> | |
| 197 | 200 | <dt> | |
| 201 | + | <kbd>m</kbd> | |
| 202 | + | <kbd>g m</kbd> | |
| 203 | + | </dt> | |
| 204 | + | <dd>merge requests (when shown)</dd> | |
| 205 | + | </div> | |
| 206 | + | <div> | |
| 207 | + | <dt> | |
| 198 | 208 | <kbd>d</kbd> | |
| 199 | 209 | <kbd>u</kbd> | |
| 200 | 210 | <kbd>x</kbd> | |
| 201 | 211 | </dt> | |
| 202 | - | <dd>commit: split / unified / collapse all</dd> | |
| 212 | + | <dd>diff: split / unified / collapse all</dd> | |
| 213 | + | </div> | |
| 214 | + | <div> | |
| 215 | + | <dt> | |
| 216 | + | <kbd>n</kbd> | |
| 217 | + | </dt> | |
| 218 | + | <dd>merge request: next note in the diff</dd> | |
| 203 | 219 | </div> | |
| 204 | 220 | <div> | |
| 205 | 221 | <dt> | |
| @@ -275,6 +291,10 @@ | |||
| 275 | 291 | <a href="/diffs/refs" tabindex="-1"> | |
| 276 | 292 | <span class="k">section</span>refs</a> | |
| 277 | 293 | </li> | |
| 294 | + | <li data-k="section merge requests"> | |
| 295 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 296 | + | <span class="k">section</span>merge requests</a> | |
| 297 | + | </li> | |
| 278 | 298 | <li data-k="branch main"> | |
| 279 | 299 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 280 | 300 | <span class="k">branch</span>main</a> | |
| @@ -335,6 +355,13 @@ | |||
| 335 | 355 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 336 | 356 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 337 | 357 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 358 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 359 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 360 | + | k-next from:body -> { | |
| 361 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 362 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 363 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 364 | + | }; | |
| 338 | 365 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 339 | 366 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 340 | 367 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-merge.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -226,11 +229,24 @@ | |||
| 226 | 229 | </div> | |
| 227 | 230 | <div> | |
| 228 | 231 | <dt> | |
| 232 | + | <kbd>m</kbd> | |
| 233 | + | <kbd>g m</kbd> | |
| 234 | + | </dt> | |
| 235 | + | <dd>merge requests (when shown)</dd> | |
| 236 | + | </div> | |
| 237 | + | <div> | |
| 238 | + | <dt> | |
| 229 | 239 | <kbd>d</kbd> | |
| 230 | 240 | <kbd>u</kbd> | |
| 231 | 241 | <kbd>x</kbd> | |
| 232 | 242 | </dt> | |
| 233 | - | <dd>commit: split / unified / collapse all</dd> | |
| 243 | + | <dd>diff: split / unified / collapse all</dd> | |
| 244 | + | </div> | |
| 245 | + | <div> | |
| 246 | + | <dt> | |
| 247 | + | <kbd>n</kbd> | |
| 248 | + | </dt> | |
| 249 | + | <dd>merge request: next note in the diff</dd> | |
| 234 | 250 | </div> | |
| 235 | 251 | <div> | |
| 236 | 252 | <dt> | |
| @@ -306,6 +322,10 @@ | |||
| 306 | 322 | <a href="/diffs/refs" tabindex="-1"> | |
| 307 | 323 | <span class="k">section</span>refs</a> | |
| 308 | 324 | </li> | |
| 325 | + | <li data-k="section merge requests"> | |
| 326 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 327 | + | <span class="k">section</span>merge requests</a> | |
| 328 | + | </li> | |
| 309 | 329 | <li data-k="branch main"> | |
| 310 | 330 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 311 | 331 | <span class="k">branch</span>main</a> | |
| @@ -366,6 +386,13 @@ | |||
| 366 | 386 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 367 | 387 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 368 | 388 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 389 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 390 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 391 | + | k-next from:body -> { | |
| 392 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 393 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 394 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 395 | + | }; | |
| 369 | 396 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 370 | 397 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 371 | 398 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-modify-split.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -345,11 +348,24 @@ And <b>html</b> & "quotes".</pre> | |||
| 345 | 348 | </div> | |
| 346 | 349 | <div> | |
| 347 | 350 | <dt> | |
| 351 | + | <kbd>m</kbd> | |
| 352 | + | <kbd>g m</kbd> | |
| 353 | + | </dt> | |
| 354 | + | <dd>merge requests (when shown)</dd> | |
| 355 | + | </div> | |
| 356 | + | <div> | |
| 357 | + | <dt> | |
| 348 | 358 | <kbd>d</kbd> | |
| 349 | 359 | <kbd>u</kbd> | |
| 350 | 360 | <kbd>x</kbd> | |
| 351 | 361 | </dt> | |
| 352 | - | <dd>commit: split / unified / collapse all</dd> | |
| 362 | + | <dd>diff: split / unified / collapse all</dd> | |
| 363 | + | </div> | |
| 364 | + | <div> | |
| 365 | + | <dt> | |
| 366 | + | <kbd>n</kbd> | |
| 367 | + | </dt> | |
| 368 | + | <dd>merge request: next note in the diff</dd> | |
| 353 | 369 | </div> | |
| 354 | 370 | <div> | |
| 355 | 371 | <dt> | |
| @@ -425,6 +441,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 425 | 441 | <a href="/diffs/refs" tabindex="-1"> | |
| 426 | 442 | <span class="k">section</span>refs</a> | |
| 427 | 443 | </li> | |
| 444 | + | <li data-k="section merge requests"> | |
| 445 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 446 | + | <span class="k">section</span>merge requests</a> | |
| 447 | + | </li> | |
| 428 | 448 | <li data-k="branch main"> | |
| 429 | 449 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 430 | 450 | <span class="k">branch</span>main</a> | |
| @@ -485,6 +505,13 @@ And <b>html</b> & "quotes".</pre> | |||
| 485 | 505 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 486 | 506 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 487 | 507 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 508 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 509 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 510 | + | k-next from:body -> { | |
| 511 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 512 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 513 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 514 | + | }; | |
| 488 | 515 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 489 | 516 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 490 | 517 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-modify.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -345,11 +348,24 @@ And <b>html</b> & "quotes".</pre> | |||
| 345 | 348 | </div> | |
| 346 | 349 | <div> | |
| 347 | 350 | <dt> | |
| 351 | + | <kbd>m</kbd> | |
| 352 | + | <kbd>g m</kbd> | |
| 353 | + | </dt> | |
| 354 | + | <dd>merge requests (when shown)</dd> | |
| 355 | + | </div> | |
| 356 | + | <div> | |
| 357 | + | <dt> | |
| 348 | 358 | <kbd>d</kbd> | |
| 349 | 359 | <kbd>u</kbd> | |
| 350 | 360 | <kbd>x</kbd> | |
| 351 | 361 | </dt> | |
| 352 | - | <dd>commit: split / unified / collapse all</dd> | |
| 362 | + | <dd>diff: split / unified / collapse all</dd> | |
| 363 | + | </div> | |
| 364 | + | <div> | |
| 365 | + | <dt> | |
| 366 | + | <kbd>n</kbd> | |
| 367 | + | </dt> | |
| 368 | + | <dd>merge request: next note in the diff</dd> | |
| 353 | 369 | </div> | |
| 354 | 370 | <div> | |
| 355 | 371 | <dt> | |
| @@ -425,6 +441,10 @@ And <b>html</b> & "quotes".</pre> | |||
| 425 | 441 | <a href="/diffs/refs" tabindex="-1"> | |
| 426 | 442 | <span class="k">section</span>refs</a> | |
| 427 | 443 | </li> | |
| 444 | + | <li data-k="section merge requests"> | |
| 445 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 446 | + | <span class="k">section</span>merge requests</a> | |
| 447 | + | </li> | |
| 428 | 448 | <li data-k="branch main"> | |
| 429 | 449 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 430 | 450 | <span class="k">branch</span>main</a> | |
| @@ -485,6 +505,13 @@ And <b>html</b> & "quotes".</pre> | |||
| 485 | 505 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 486 | 506 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 487 | 507 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 508 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 509 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 510 | + | k-next from:body -> { | |
| 511 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 512 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 513 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 514 | + | }; | |
| 488 | 515 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 489 | 516 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 490 | 517 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-rename.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -252,11 +255,24 @@ | |||
| 252 | 255 | </div> | |
| 253 | 256 | <div> | |
| 254 | 257 | <dt> | |
| 258 | + | <kbd>m</kbd> | |
| 259 | + | <kbd>g m</kbd> | |
| 260 | + | </dt> | |
| 261 | + | <dd>merge requests (when shown)</dd> | |
| 262 | + | </div> | |
| 263 | + | <div> | |
| 264 | + | <dt> | |
| 255 | 265 | <kbd>d</kbd> | |
| 256 | 266 | <kbd>u</kbd> | |
| 257 | 267 | <kbd>x</kbd> | |
| 258 | 268 | </dt> | |
| 259 | - | <dd>commit: split / unified / collapse all</dd> | |
| 269 | + | <dd>diff: split / unified / collapse all</dd> | |
| 270 | + | </div> | |
| 271 | + | <div> | |
| 272 | + | <dt> | |
| 273 | + | <kbd>n</kbd> | |
| 274 | + | </dt> | |
| 275 | + | <dd>merge request: next note in the diff</dd> | |
| 260 | 276 | </div> | |
| 261 | 277 | <div> | |
| 262 | 278 | <dt> | |
| @@ -332,6 +348,10 @@ | |||
| 332 | 348 | <a href="/diffs/refs" tabindex="-1"> | |
| 333 | 349 | <span class="k">section</span>refs</a> | |
| 334 | 350 | </li> | |
| 351 | + | <li data-k="section merge requests"> | |
| 352 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 353 | + | <span class="k">section</span>merge requests</a> | |
| 354 | + | </li> | |
| 335 | 355 | <li data-k="branch main"> | |
| 336 | 356 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 337 | 357 | <span class="k">branch</span>main</a> | |
| @@ -392,6 +412,13 @@ | |||
| 392 | 412 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 393 | 413 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 394 | 414 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 415 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 416 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 417 | + | k-next from:body -> { | |
| 418 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 419 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 420 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 421 | + | }; | |
| 395 | 422 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 396 | 423 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 397 | 424 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-root.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -544,11 +547,24 @@ | |||
| 544 | 547 | </div> | |
| 545 | 548 | <div> | |
| 546 | 549 | <dt> | |
| 550 | + | <kbd>m</kbd> | |
| 551 | + | <kbd>g m</kbd> | |
| 552 | + | </dt> | |
| 553 | + | <dd>merge requests (when shown)</dd> | |
| 554 | + | </div> | |
| 555 | + | <div> | |
| 556 | + | <dt> | |
| 547 | 557 | <kbd>d</kbd> | |
| 548 | 558 | <kbd>u</kbd> | |
| 549 | 559 | <kbd>x</kbd> | |
| 550 | 560 | </dt> | |
| 551 | - | <dd>commit: split / unified / collapse all</dd> | |
| 561 | + | <dd>diff: split / unified / collapse all</dd> | |
| 562 | + | </div> | |
| 563 | + | <div> | |
| 564 | + | <dt> | |
| 565 | + | <kbd>n</kbd> | |
| 566 | + | </dt> | |
| 567 | + | <dd>merge request: next note in the diff</dd> | |
| 552 | 568 | </div> | |
| 553 | 569 | <div> | |
| 554 | 570 | <dt> | |
| @@ -624,6 +640,10 @@ | |||
| 624 | 640 | <a href="/diffs/refs" tabindex="-1"> | |
| 625 | 641 | <span class="k">section</span>refs</a> | |
| 626 | 642 | </li> | |
| 643 | + | <li data-k="section merge requests"> | |
| 644 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 645 | + | <span class="k">section</span>merge requests</a> | |
| 646 | + | </li> | |
| 627 | 647 | <li data-k="branch main"> | |
| 628 | 648 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 629 | 649 | <span class="k">branch</span>main</a> | |
| @@ -684,6 +704,13 @@ | |||
| 684 | 704 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 685 | 705 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 686 | 706 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 707 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 708 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 709 | + | k-next from:body -> { | |
| 710 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 711 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 712 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 713 | + | }; | |
| 687 | 714 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 688 | 715 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 689 | 716 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commit-trojan.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">detached<span class="sha">@SHA7</span> | |
| @@ -361,11 +364,24 @@ | |||
| 361 | 364 | </div> | |
| 362 | 365 | <div> | |
| 363 | 366 | <dt> | |
| 367 | + | <kbd>m</kbd> | |
| 368 | + | <kbd>g m</kbd> | |
| 369 | + | </dt> | |
| 370 | + | <dd>merge requests (when shown)</dd> | |
| 371 | + | </div> | |
| 372 | + | <div> | |
| 373 | + | <dt> | |
| 364 | 374 | <kbd>d</kbd> | |
| 365 | 375 | <kbd>u</kbd> | |
| 366 | 376 | <kbd>x</kbd> | |
| 367 | 377 | </dt> | |
| 368 | - | <dd>commit: split / unified / collapse all</dd> | |
| 378 | + | <dd>diff: split / unified / collapse all</dd> | |
| 379 | + | </div> | |
| 380 | + | <div> | |
| 381 | + | <dt> | |
| 382 | + | <kbd>n</kbd> | |
| 383 | + | </dt> | |
| 384 | + | <dd>merge request: next note in the diff</dd> | |
| 369 | 385 | </div> | |
| 370 | 386 | <div> | |
| 371 | 387 | <dt> | |
| @@ -441,6 +457,10 @@ | |||
| 441 | 457 | <a href="/diffs/refs" tabindex="-1"> | |
| 442 | 458 | <span class="k">section</span>refs</a> | |
| 443 | 459 | </li> | |
| 460 | + | <li data-k="section merge requests"> | |
| 461 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 462 | + | <span class="k">section</span>merge requests</a> | |
| 463 | + | </li> | |
| 444 | 464 | <li data-k="branch main"> | |
| 445 | 465 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 446 | 466 | <span class="k">branch</span>main</a> | |
| @@ -501,6 +521,13 @@ | |||
| 501 | 521 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 502 | 522 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 503 | 523 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 524 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 525 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 526 | + | k-next from:body -> { | |
| 527 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 528 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 529 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 530 | + | }; | |
| 504 | 531 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 505 | 532 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 506 | 533 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commits-page2.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -197,11 +200,24 @@ | |||
| 197 | 200 | </div> | |
| 198 | 201 | <div> | |
| 199 | 202 | <dt> | |
| 203 | + | <kbd>m</kbd> | |
| 204 | + | <kbd>g m</kbd> | |
| 205 | + | </dt> | |
| 206 | + | <dd>merge requests (when shown)</dd> | |
| 207 | + | </div> | |
| 208 | + | <div> | |
| 209 | + | <dt> | |
| 200 | 210 | <kbd>d</kbd> | |
| 201 | 211 | <kbd>u</kbd> | |
| 202 | 212 | <kbd>x</kbd> | |
| 203 | 213 | </dt> | |
| 204 | - | <dd>commit: split / unified / collapse all</dd> | |
| 214 | + | <dd>diff: split / unified / collapse all</dd> | |
| 215 | + | </div> | |
| 216 | + | <div> | |
| 217 | + | <dt> | |
| 218 | + | <kbd>n</kbd> | |
| 219 | + | </dt> | |
| 220 | + | <dd>merge request: next note in the diff</dd> | |
| 205 | 221 | </div> | |
| 206 | 222 | <div> | |
| 207 | 223 | <dt> | |
| @@ -269,6 +285,10 @@ | |||
| 269 | 285 | <a href="/long/refs" tabindex="-1"> | |
| 270 | 286 | <span class="k">section</span>refs</a> | |
| 271 | 287 | </li> | |
| 288 | + | <li data-k="section merge requests"> | |
| 289 | + | <a href="/long/merge-requests" tabindex="-1"> | |
| 290 | + | <span class="k">section</span>merge requests</a> | |
| 291 | + | </li> | |
| 272 | 292 | <li data-k="branch main"> | |
| 273 | 293 | <a href="/long/tree/main" tabindex="-1"> | |
| 274 | 294 | <span class="k">branch</span>main</a> | |
| @@ -326,6 +346,13 @@ | |||
| 326 | 346 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 327 | 347 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 328 | 348 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 349 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 350 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 351 | + | k-next from:body -> { | |
| 352 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 353 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 354 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 355 | + | }; | |
| 329 | 356 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 330 | 357 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 331 | 358 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commits-path.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -212,11 +215,24 @@ | |||
| 212 | 215 | </div> | |
| 213 | 216 | <div> | |
| 214 | 217 | <dt> | |
| 218 | + | <kbd>m</kbd> | |
| 219 | + | <kbd>g m</kbd> | |
| 220 | + | </dt> | |
| 221 | + | <dd>merge requests (when shown)</dd> | |
| 222 | + | </div> | |
| 223 | + | <div> | |
| 224 | + | <dt> | |
| 215 | 225 | <kbd>d</kbd> | |
| 216 | 226 | <kbd>u</kbd> | |
| 217 | 227 | <kbd>x</kbd> | |
| 218 | 228 | </dt> | |
| 219 | - | <dd>commit: split / unified / collapse all</dd> | |
| 229 | + | <dd>diff: split / unified / collapse all</dd> | |
| 230 | + | </div> | |
| 231 | + | <div> | |
| 232 | + | <dt> | |
| 233 | + | <kbd>n</kbd> | |
| 234 | + | </dt> | |
| 235 | + | <dd>merge request: next note in the diff</dd> | |
| 220 | 236 | </div> | |
| 221 | 237 | <div> | |
| 222 | 238 | <dt> | |
| @@ -284,6 +300,10 @@ | |||
| 284 | 300 | <a href="/long/refs" tabindex="-1"> | |
| 285 | 301 | <span class="k">section</span>refs</a> | |
| 286 | 302 | </li> | |
| 303 | + | <li data-k="section merge requests"> | |
| 304 | + | <a href="/long/merge-requests" tabindex="-1"> | |
| 305 | + | <span class="k">section</span>merge requests</a> | |
| 306 | + | </li> | |
| 287 | 307 | <li data-k="branch main"> | |
| 288 | 308 | <a href="/long/tree/main" tabindex="-1"> | |
| 289 | 309 | <span class="k">branch</span>main</a> | |
| @@ -341,6 +361,13 @@ | |||
| 341 | 361 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 342 | 362 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 343 | 363 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 364 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 365 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 366 | + | k-next from:body -> { | |
| 367 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 368 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 369 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 370 | + | }; | |
| 344 | 371 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 345 | 372 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 346 | 373 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commits-root.html +29 -2
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/long/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/long/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -994,11 +997,24 @@ | |||
| 994 | 997 | </div> | |
| 995 | 998 | <div> | |
| 996 | 999 | <dt> | |
| 1000 | + | <kbd>m</kbd> | |
| 1001 | + | <kbd>g m</kbd> | |
| 1002 | + | </dt> | |
| 1003 | + | <dd>merge requests (when shown)</dd> | |
| 1004 | + | </div> | |
| 1005 | + | <div> | |
| 1006 | + | <dt> | |
| 997 | 1007 | <kbd>d</kbd> | |
| 998 | 1008 | <kbd>u</kbd> | |
| 999 | 1009 | <kbd>x</kbd> | |
| 1000 | 1010 | </dt> | |
| 1001 | - | <dd>commit: split / unified / collapse all</dd> | |
| 1011 | + | <dd>diff: split / unified / collapse all</dd> | |
| 1012 | + | </div> | |
| 1013 | + | <div> | |
| 1014 | + | <dt> | |
| 1015 | + | <kbd>n</kbd> | |
| 1016 | + | </dt> | |
| 1017 | + | <dd>merge request: next note in the diff</dd> | |
| 1002 | 1018 | </div> | |
| 1003 | 1019 | <div> | |
| 1004 | 1020 | <dt> | |
| @@ -1066,6 +1082,10 @@ | |||
| 1066 | 1082 | <a href="/long/refs" tabindex="-1"> | |
| 1067 | 1083 | <span class="k">section</span>refs</a> | |
| 1068 | 1084 | </li> | |
| 1085 | + | <li data-k="section merge requests"> | |
| 1086 | + | <a href="/long/merge-requests" tabindex="-1"> | |
| 1087 | + | <span class="k">section</span>merge requests</a> | |
| 1088 | + | </li> | |
| 1069 | 1089 | <li data-k="branch main"> | |
| 1070 | 1090 | <a href="/long/tree/main" tabindex="-1"> | |
| 1071 | 1091 | <span class="k">branch</span>main</a> | |
| @@ -1090,7 +1110,7 @@ | |||
| 1090 | 1110 | </span> | |
| 1091 | 1111 | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Along&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 1092 | 1112 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 1093 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 1113 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 1094 | 1114 | gap -> ermiteLive.reload()">offline</span> | |
| 1095 | 1115 | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/50</span> | |
| 1096 | 1116 | </footer> | |
| @@ -1126,6 +1146,13 @@ | |||
| 1126 | 1146 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 1127 | 1147 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 1128 | 1148 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 1149 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 1150 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 1151 | + | k-next from:body -> { | |
| 1152 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 1153 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 1154 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 1155 | + | }; | |
| 1129 | 1156 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 1130 | 1157 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 1131 | 1158 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/commits-small.html +29 -2
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/diffs/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/diffs/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -338,11 +341,24 @@ | |||
| 338 | 341 | </div> | |
| 339 | 342 | <div> | |
| 340 | 343 | <dt> | |
| 344 | + | <kbd>m</kbd> | |
| 345 | + | <kbd>g m</kbd> | |
| 346 | + | </dt> | |
| 347 | + | <dd>merge requests (when shown)</dd> | |
| 348 | + | </div> | |
| 349 | + | <div> | |
| 350 | + | <dt> | |
| 341 | 351 | <kbd>d</kbd> | |
| 342 | 352 | <kbd>u</kbd> | |
| 343 | 353 | <kbd>x</kbd> | |
| 344 | 354 | </dt> | |
| 345 | - | <dd>commit: split / unified / collapse all</dd> | |
| 355 | + | <dd>diff: split / unified / collapse all</dd> | |
| 356 | + | </div> | |
| 357 | + | <div> | |
| 358 | + | <dt> | |
| 359 | + | <kbd>n</kbd> | |
| 360 | + | </dt> | |
| 361 | + | <dd>merge request: next note in the diff</dd> | |
| 346 | 362 | </div> | |
| 347 | 363 | <div> | |
| 348 | 364 | <dt> | |
| @@ -418,6 +434,10 @@ | |||
| 418 | 434 | <a href="/diffs/refs" tabindex="-1"> | |
| 419 | 435 | <span class="k">section</span>refs</a> | |
| 420 | 436 | </li> | |
| 437 | + | <li data-k="section merge requests"> | |
| 438 | + | <a href="/diffs/merge-requests" tabindex="-1"> | |
| 439 | + | <span class="k">section</span>merge requests</a> | |
| 440 | + | </li> | |
| 421 | 441 | <li data-k="branch main"> | |
| 422 | 442 | <a href="/diffs/tree/main" tabindex="-1"> | |
| 423 | 443 | <span class="k">branch</span>main</a> | |
| @@ -450,7 +470,7 @@ | |||
| 450 | 470 | </span> | |
| 451 | 471 | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Adiffs&view=commits&ref=main&head=SHA40" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 452 | 472 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 453 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 473 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 454 | 474 | gap -> ermiteLive.reload()">offline</span> | |
| 455 | 475 | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/8</span> | |
| 456 | 476 | </footer> | |
| @@ -486,6 +506,13 @@ | |||
| 486 | 506 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 487 | 507 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 488 | 508 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 509 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 510 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 511 | + | k-next from:body -> { | |
| 512 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 513 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 514 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 515 | + | }; | |
| 489 | 516 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 490 | 517 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 491 | 518 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/empty.html +21 -1
| @@ -130,11 +130,24 @@ | |||
| 130 | 130 | </div> | |
| 131 | 131 | <div> | |
| 132 | 132 | <dt> | |
| 133 | + | <kbd>m</kbd> | |
| 134 | + | <kbd>g m</kbd> | |
| 135 | + | </dt> | |
| 136 | + | <dd>merge requests (when shown)</dd> | |
| 137 | + | </div> | |
| 138 | + | <div> | |
| 139 | + | <dt> | |
| 133 | 140 | <kbd>d</kbd> | |
| 134 | 141 | <kbd>u</kbd> | |
| 135 | 142 | <kbd>x</kbd> | |
| 136 | 143 | </dt> | |
| 137 | - | <dd>commit: split / unified / collapse all</dd> | |
| 144 | + | <dd>diff: split / unified / collapse all</dd> | |
| 145 | + | </div> | |
| 146 | + | <div> | |
| 147 | + | <dt> | |
| 148 | + | <kbd>n</kbd> | |
| 149 | + | </dt> | |
| 150 | + | <dd>merge request: next note in the diff</dd> | |
| 138 | 151 | </div> | |
| 139 | 152 | <div> | |
| 140 | 153 | <dt> | |
| @@ -213,6 +226,13 @@ | |||
| 213 | 226 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 214 | 227 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 215 | 228 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 229 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 230 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 231 | + | k-next from:body -> { | |
| 232 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 233 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 234 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 235 | + | }; | |
| 216 | 236 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 217 | 237 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 218 | 238 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/home.html +22 -2
| @@ -166,11 +166,24 @@ | |||
| 166 | 166 | </div> | |
| 167 | 167 | <div> | |
| 168 | 168 | <dt> | |
| 169 | + | <kbd>m</kbd> | |
| 170 | + | <kbd>g m</kbd> | |
| 171 | + | </dt> | |
| 172 | + | <dd>merge requests (when shown)</dd> | |
| 173 | + | </div> | |
| 174 | + | <div> | |
| 175 | + | <dt> | |
| 169 | 176 | <kbd>d</kbd> | |
| 170 | 177 | <kbd>u</kbd> | |
| 171 | 178 | <kbd>x</kbd> | |
| 172 | 179 | </dt> | |
| 173 | - | <dd>commit: split / unified / collapse all</dd> | |
| 180 | + | <dd>diff: split / unified / collapse all</dd> | |
| 181 | + | </div> | |
| 182 | + | <div> | |
| 183 | + | <dt> | |
| 184 | + | <kbd>n</kbd> | |
| 185 | + | </dt> | |
| 186 | + | <dd>merge request: next note in the diff</dd> | |
| 174 | 187 | </div> | |
| 175 | 188 | <div> | |
| 176 | 189 | <dt> | |
| @@ -221,7 +234,7 @@ | |||
| 221 | 234 | </span> | |
| 222 | 235 | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=home" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 223 | 236 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 224 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 237 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 225 | 238 | gap -> ermiteLive.reload()">offline</span> | |
| 226 | 239 | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |
| 227 | 240 | </footer> | |
| @@ -257,6 +270,13 @@ | |||
| 257 | 270 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 258 | 271 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 259 | 272 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 273 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 274 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 275 | + | k-next from:body -> { | |
| 276 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 277 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 278 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 279 | + | }; | |
| 260 | 280 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 261 | 281 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 262 | 282 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/not-found-page.html +21 -1
| @@ -129,11 +129,24 @@ | |||
| 129 | 129 | </div> | |
| 130 | 130 | <div> | |
| 131 | 131 | <dt> | |
| 132 | + | <kbd>m</kbd> | |
| 133 | + | <kbd>g m</kbd> | |
| 134 | + | </dt> | |
| 135 | + | <dd>merge requests (when shown)</dd> | |
| 136 | + | </div> | |
| 137 | + | <div> | |
| 138 | + | <dt> | |
| 132 | 139 | <kbd>d</kbd> | |
| 133 | 140 | <kbd>u</kbd> | |
| 134 | 141 | <kbd>x</kbd> | |
| 135 | 142 | </dt> | |
| 136 | - | <dd>commit: split / unified / collapse all</dd> | |
| 143 | + | <dd>diff: split / unified / collapse all</dd> | |
| 144 | + | </div> | |
| 145 | + | <div> | |
| 146 | + | <dt> | |
| 147 | + | <kbd>n</kbd> | |
| 148 | + | </dt> | |
| 149 | + | <dd>merge request: next note in the diff</dd> | |
| 137 | 150 | </div> | |
| 138 | 151 | <div> | |
| 139 | 152 | <dt> | |
| @@ -208,6 +221,13 @@ | |||
| 208 | 221 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 209 | 222 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 210 | 223 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 224 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 225 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 226 | + | k-next from:body -> { | |
| 227 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 228 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 229 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 230 | + | }; | |
| 211 | 231 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 212 | 232 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 213 | 233 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/not-found.html +21 -1
| @@ -129,11 +129,24 @@ | |||
| 129 | 129 | </div> | |
| 130 | 130 | <div> | |
| 131 | 131 | <dt> | |
| 132 | + | <kbd>m</kbd> | |
| 133 | + | <kbd>g m</kbd> | |
| 134 | + | </dt> | |
| 135 | + | <dd>merge requests (when shown)</dd> | |
| 136 | + | </div> | |
| 137 | + | <div> | |
| 138 | + | <dt> | |
| 132 | 139 | <kbd>d</kbd> | |
| 133 | 140 | <kbd>u</kbd> | |
| 134 | 141 | <kbd>x</kbd> | |
| 135 | 142 | </dt> | |
| 136 | - | <dd>commit: split / unified / collapse all</dd> | |
| 143 | + | <dd>diff: split / unified / collapse all</dd> | |
| 144 | + | </div> | |
| 145 | + | <div> | |
| 146 | + | <dt> | |
| 147 | + | <kbd>n</kbd> | |
| 148 | + | </dt> | |
| 149 | + | <dd>merge request: next note in the diff</dd> | |
| 137 | 150 | </div> | |
| 138 | 151 | <div> | |
| 139 | 152 | <dt> | |
| @@ -208,6 +221,13 @@ | |||
| 208 | 221 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 209 | 222 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 210 | 223 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 224 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 225 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 226 | + | k-next from:body -> { | |
| 227 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 228 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 229 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 230 | + | }; | |
| 211 | 231 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 212 | 232 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 213 | 233 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/refs.html +29 -2
| @@ -46,6 +46,9 @@ | |||
| 46 | 46 | <li> | |
| 47 | 47 | <a href="/demo/refs" data-key="r" aria-keyshortcuts="g r" aria-current="page">refs</a> | |
| 48 | 48 | </li> | |
| 49 | + | <li> | |
| 50 | + | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 51 | + | </li> | |
| 49 | 52 | </ul> | |
| 50 | 53 | </nav> | |
| 51 | 54 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -237,11 +240,24 @@ | |||
| 237 | 240 | </div> | |
| 238 | 241 | <div> | |
| 239 | 242 | <dt> | |
| 243 | + | <kbd>m</kbd> | |
| 244 | + | <kbd>g m</kbd> | |
| 245 | + | </dt> | |
| 246 | + | <dd>merge requests (when shown)</dd> | |
| 247 | + | </div> | |
| 248 | + | <div> | |
| 249 | + | <dt> | |
| 240 | 250 | <kbd>d</kbd> | |
| 241 | 251 | <kbd>u</kbd> | |
| 242 | 252 | <kbd>x</kbd> | |
| 243 | 253 | </dt> | |
| 244 | - | <dd>commit: split / unified / collapse all</dd> | |
| 254 | + | <dd>diff: split / unified / collapse all</dd> | |
| 255 | + | </div> | |
| 256 | + | <div> | |
| 257 | + | <dt> | |
| 258 | + | <kbd>n</kbd> | |
| 259 | + | </dt> | |
| 260 | + | <dd>merge request: next note in the diff</dd> | |
| 245 | 261 | </div> | |
| 246 | 262 | <div> | |
| 247 | 263 | <dt> | |
| @@ -321,6 +337,10 @@ | |||
| 321 | 337 | <a href="/demo/refs" tabindex="-1"> | |
| 322 | 338 | <span class="k">section</span>refs</a> | |
| 323 | 339 | </li> | |
| 340 | + | <li data-k="section merge requests"> | |
| 341 | + | <a href="/demo/merge-requests" tabindex="-1"> | |
| 342 | + | <span class="k">section</span>merge requests</a> | |
| 343 | + | </li> | |
| 324 | 344 | <li data-k="branch feature/x"> | |
| 325 | 345 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 326 | 346 | <span class="k">branch</span>feature/x</a> | |
| @@ -357,7 +377,7 @@ | |||
| 357 | 377 | </span> | |
| 358 | 378 | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Ademo&view=refs" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 359 | 379 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 360 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 380 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 361 | 381 | gap -> ermiteLive.reload()">offline</span> | |
| 362 | 382 | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/4</span> | |
| 363 | 383 | </footer> | |
| @@ -393,6 +413,13 @@ | |||
| 393 | 413 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 394 | 414 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 395 | 415 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 416 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 417 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 418 | + | k-next from:body -> { | |
| 419 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 420 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 421 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 422 | + | }; | |
| 396 | 423 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 397 | 424 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 398 | 425 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/summary.html +29 -2
| @@ -43,6 +43,9 @@ | |||
| 43 | 43 | <li> | |
| 44 | 44 | <a href="/demo/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 45 | 45 | </li> | |
| 46 | + | <li> | |
| 47 | + | <a href="/demo/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 48 | + | </li> | |
| 46 | 49 | </ul> | |
| 47 | 50 | </nav> | |
| 48 | 51 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -231,11 +234,24 @@ | |||
| 231 | 234 | </div> | |
| 232 | 235 | <div> | |
| 233 | 236 | <dt> | |
| 237 | + | <kbd>m</kbd> | |
| 238 | + | <kbd>g m</kbd> | |
| 239 | + | </dt> | |
| 240 | + | <dd>merge requests (when shown)</dd> | |
| 241 | + | </div> | |
| 242 | + | <div> | |
| 243 | + | <dt> | |
| 234 | 244 | <kbd>d</kbd> | |
| 235 | 245 | <kbd>u</kbd> | |
| 236 | 246 | <kbd>x</kbd> | |
| 237 | 247 | </dt> | |
| 238 | - | <dd>commit: split / unified / collapse all</dd> | |
| 248 | + | <dd>diff: split / unified / collapse all</dd> | |
| 249 | + | </div> | |
| 250 | + | <div> | |
| 251 | + | <dt> | |
| 252 | + | <kbd>n</kbd> | |
| 253 | + | </dt> | |
| 254 | + | <dd>merge request: next note in the diff</dd> | |
| 239 | 255 | </div> | |
| 240 | 256 | <div> | |
| 241 | 257 | <dt> | |
| @@ -315,6 +331,10 @@ | |||
| 315 | 331 | <a href="/demo/refs" tabindex="-1"> | |
| 316 | 332 | <span class="k">section</span>refs</a> | |
| 317 | 333 | </li> | |
| 334 | + | <li data-k="section merge requests"> | |
| 335 | + | <a href="/demo/merge-requests" tabindex="-1"> | |
| 336 | + | <span class="k">section</span>merge requests</a> | |
| 337 | + | </li> | |
| 318 | 338 | <li data-k="branch feature/x"> | |
| 319 | 339 | <a href="/demo/tree/feature/x" tabindex="-1"> | |
| 320 | 340 | <span class="k">branch</span>feature/x</a> | |
| @@ -351,7 +371,7 @@ | |||
| 351 | 371 | </span> | |
| 352 | 372 | <span class="st-live" data-state="offline" role="status" hx-sse:connect="/-/events?topic=repo%3Ademo&view=summary" hx-swap="none" :text="data.state" :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 353 | 373 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 354 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 374 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 355 | 375 | gap -> ermiteLive.reload()">offline</span> | |
| 356 | 376 | <span class="st-pos num" title="cursor position" :text="(() => { let all = q('[data-rows] > :not([hidden])').arr(); let i = all.findIndex(r => r.hasAttribute('data-cursor')); return (i + 1) + '/' + all.length })()">1/2</span> | |
| 357 | 377 | </footer> | |
| @@ -387,6 +407,13 @@ | |||
| 387 | 407 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 388 | 408 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 389 | 409 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 410 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 411 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 412 | + | k-next from:body -> { | |
| 413 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 414 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 415 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 416 | + | }; | |
| 390 | 417 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 391 | 418 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 392 | 419 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/tree-nested.html +28 -1
| @@ -55,6 +55,9 @@ | |||
| 55 | 55 | <li> | |
| 56 | 56 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 57 | 57 | </li> | |
| 58 | + | <li> | |
| 59 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 60 | + | </li> | |
| 58 | 61 | </ul> | |
| 59 | 62 | </nav> | |
| 60 | 63 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -177,11 +180,24 @@ | |||
| 177 | 180 | </div> | |
| 178 | 181 | <div> | |
| 179 | 182 | <dt> | |
| 183 | + | <kbd>m</kbd> | |
| 184 | + | <kbd>g m</kbd> | |
| 185 | + | </dt> | |
| 186 | + | <dd>merge requests (when shown)</dd> | |
| 187 | + | </div> | |
| 188 | + | <div> | |
| 189 | + | <dt> | |
| 180 | 190 | <kbd>d</kbd> | |
| 181 | 191 | <kbd>u</kbd> | |
| 182 | 192 | <kbd>x</kbd> | |
| 183 | 193 | </dt> | |
| 184 | - | <dd>commit: split / unified / collapse all</dd> | |
| 194 | + | <dd>diff: split / unified / collapse all</dd> | |
| 195 | + | </div> | |
| 196 | + | <div> | |
| 197 | + | <dt> | |
| 198 | + | <kbd>n</kbd> | |
| 199 | + | </dt> | |
| 200 | + | <dd>merge request: next note in the diff</dd> | |
| 185 | 201 | </div> | |
| 186 | 202 | <div> | |
| 187 | 203 | <dt> | |
| @@ -257,6 +273,10 @@ | |||
| 257 | 273 | <a href="/files/refs" tabindex="-1"> | |
| 258 | 274 | <span class="k">section</span>refs</a> | |
| 259 | 275 | </li> | |
| 276 | + | <li data-k="section merge requests"> | |
| 277 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 278 | + | <span class="k">section</span>merge requests</a> | |
| 279 | + | </li> | |
| 260 | 280 | <li data-k="branch feature/x"> | |
| 261 | 281 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 262 | 282 | <span class="k">branch</span>feature/x</a> | |
| @@ -322,6 +342,13 @@ | |||
| 322 | 342 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 323 | 343 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 324 | 344 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 345 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 346 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 347 | + | k-next from:body -> { | |
| 348 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 349 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 350 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 351 | + | }; | |
| 325 | 352 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 326 | 353 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 327 | 354 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/tree-root.html +28 -1
| @@ -49,6 +49,9 @@ | |||
| 49 | 49 | <li> | |
| 50 | 50 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 51 | 51 | </li> | |
| 52 | + | <li> | |
| 53 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 54 | + | </li> | |
| 52 | 55 | </ul> | |
| 53 | 56 | </nav> | |
| 54 | 57 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -402,11 +405,24 @@ | |||
| 402 | 405 | </div> | |
| 403 | 406 | <div> | |
| 404 | 407 | <dt> | |
| 408 | + | <kbd>m</kbd> | |
| 409 | + | <kbd>g m</kbd> | |
| 410 | + | </dt> | |
| 411 | + | <dd>merge requests (when shown)</dd> | |
| 412 | + | </div> | |
| 413 | + | <div> | |
| 414 | + | <dt> | |
| 405 | 415 | <kbd>d</kbd> | |
| 406 | 416 | <kbd>u</kbd> | |
| 407 | 417 | <kbd>x</kbd> | |
| 408 | 418 | </dt> | |
| 409 | - | <dd>commit: split / unified / collapse all</dd> | |
| 419 | + | <dd>diff: split / unified / collapse all</dd> | |
| 420 | + | </div> | |
| 421 | + | <div> | |
| 422 | + | <dt> | |
| 423 | + | <kbd>n</kbd> | |
| 424 | + | </dt> | |
| 425 | + | <dd>merge request: next note in the diff</dd> | |
| 410 | 426 | </div> | |
| 411 | 427 | <div> | |
| 412 | 428 | <dt> | |
| @@ -482,6 +498,10 @@ | |||
| 482 | 498 | <a href="/files/refs" tabindex="-1"> | |
| 483 | 499 | <span class="k">section</span>refs</a> | |
| 484 | 500 | </li> | |
| 501 | + | <li data-k="section merge requests"> | |
| 502 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 503 | + | <span class="k">section</span>merge requests</a> | |
| 504 | + | </li> | |
| 485 | 505 | <li data-k="branch feature/x"> | |
| 486 | 506 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 487 | 507 | <span class="k">branch</span>feature/x</a> | |
| @@ -595,6 +615,13 @@ | |||
| 595 | 615 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 596 | 616 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 597 | 617 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 618 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 619 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 620 | + | k-next from:body -> { | |
| 621 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 622 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 623 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 624 | + | }; | |
| 598 | 625 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 599 | 626 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 600 | 627 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/tree-slash-ref.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">feature/x<span class="sha">@SHA7</span> | |
| @@ -174,11 +177,24 @@ | |||
| 174 | 177 | </div> | |
| 175 | 178 | <div> | |
| 176 | 179 | <dt> | |
| 180 | + | <kbd>m</kbd> | |
| 181 | + | <kbd>g m</kbd> | |
| 182 | + | </dt> | |
| 183 | + | <dd>merge requests (when shown)</dd> | |
| 184 | + | </div> | |
| 185 | + | <div> | |
| 186 | + | <dt> | |
| 177 | 187 | <kbd>d</kbd> | |
| 178 | 188 | <kbd>u</kbd> | |
| 179 | 189 | <kbd>x</kbd> | |
| 180 | 190 | </dt> | |
| 181 | - | <dd>commit: split / unified / collapse all</dd> | |
| 191 | + | <dd>diff: split / unified / collapse all</dd> | |
| 192 | + | </div> | |
| 193 | + | <div> | |
| 194 | + | <dt> | |
| 195 | + | <kbd>n</kbd> | |
| 196 | + | </dt> | |
| 197 | + | <dd>merge request: next note in the diff</dd> | |
| 182 | 198 | </div> | |
| 183 | 199 | <div> | |
| 184 | 200 | <dt> | |
| @@ -254,6 +270,10 @@ | |||
| 254 | 270 | <a href="/files/refs" tabindex="-1"> | |
| 255 | 271 | <span class="k">section</span>refs</a> | |
| 256 | 272 | </li> | |
| 273 | + | <li data-k="section merge requests"> | |
| 274 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 275 | + | <span class="k">section</span>merge requests</a> | |
| 276 | + | </li> | |
| 257 | 277 | <li data-k="branch feature/x"> | |
| 258 | 278 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 259 | 279 | <span class="k">branch</span>feature/x</a> | |
| @@ -319,6 +339,13 @@ | |||
| 319 | 339 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 320 | 340 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 321 | 341 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 342 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 343 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 344 | + | k-next from:body -> { | |
| 345 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 346 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 347 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 348 | + | }; | |
| 322 | 349 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 323 | 350 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 324 | 351 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/testdata/web/tree-subdir.html +28 -1
| @@ -52,6 +52,9 @@ | |||
| 52 | 52 | <li> | |
| 53 | 53 | <a href="/files/refs" data-key="r" aria-keyshortcuts="g r">refs</a> | |
| 54 | 54 | </li> | |
| 55 | + | <li> | |
| 56 | + | <a href="/files/merge-requests" data-key="m" aria-keyshortcuts="g m">merge requests</a> | |
| 57 | + | </li> | |
| 55 | 58 | </ul> | |
| 56 | 59 | </nav> | |
| 57 | 60 | <button type="button" class="ref" aria-haspopup="dialog" aria-controls="panel-refs" aria-keyshortcuts="b" title="switch ref (b)" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-refs'))">main<span class="sha">@SHA7</span> | |
| @@ -197,11 +200,24 @@ | |||
| 197 | 200 | </div> | |
| 198 | 201 | <div> | |
| 199 | 202 | <dt> | |
| 203 | + | <kbd>m</kbd> | |
| 204 | + | <kbd>g m</kbd> | |
| 205 | + | </dt> | |
| 206 | + | <dd>merge requests (when shown)</dd> | |
| 207 | + | </div> | |
| 208 | + | <div> | |
| 209 | + | <dt> | |
| 200 | 210 | <kbd>d</kbd> | |
| 201 | 211 | <kbd>u</kbd> | |
| 202 | 212 | <kbd>x</kbd> | |
| 203 | 213 | </dt> | |
| 204 | - | <dd>commit: split / unified / collapse all</dd> | |
| 214 | + | <dd>diff: split / unified / collapse all</dd> | |
| 215 | + | </div> | |
| 216 | + | <div> | |
| 217 | + | <dt> | |
| 218 | + | <kbd>n</kbd> | |
| 219 | + | </dt> | |
| 220 | + | <dd>merge request: next note in the diff</dd> | |
| 205 | 221 | </div> | |
| 206 | 222 | <div> | |
| 207 | 223 | <dt> | |
| @@ -277,6 +293,10 @@ | |||
| 277 | 293 | <a href="/files/refs" tabindex="-1"> | |
| 278 | 294 | <span class="k">section</span>refs</a> | |
| 279 | 295 | </li> | |
| 296 | + | <li data-k="section merge requests"> | |
| 297 | + | <a href="/files/merge-requests" tabindex="-1"> | |
| 298 | + | <span class="k">section</span>merge requests</a> | |
| 299 | + | </li> | |
| 280 | 300 | <li data-k="branch feature/x"> | |
| 281 | 301 | <a href="/files/tree/feature/x" tabindex="-1"> | |
| 282 | 302 | <span class="k">branch</span>feature/x</a> | |
| @@ -346,6 +366,13 @@ | |||
| 346 | 366 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 347 | 367 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 348 | 368 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 369 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 370 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 371 | + | k-next from:body -> { | |
| 372 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 373 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 374 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 375 | + | }; | |
| 349 | 376 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 350 | 377 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 351 | 378 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M cmd/ermite/web.go +7 -6
| @@ -27,12 +27,13 @@ import ( | |||
| 27 | 27 | func newWeb(cfg config.Config, api *rpc.API, sessions *app.Sessions, limiter *clientlimit.Limiter) (http.Handler, error) { | |
| 28 | 28 | hc := api.InProcessClient() | |
| 29 | 29 | srv, err := web.New(web.Config{ | |
| 30 | - | Repos: ermiteconnect.NewRepoServiceClient(hc, rpc.InProcessURL), | |
| 31 | - | Git: ermiteconnect.NewGitServiceClient(hc, rpc.InProcessURL), | |
| 32 | - | Events: ermiteconnect.NewEventServiceClient(hc, rpc.InProcessURL), | |
| 33 | - | PublicURL: cfg.PublicURL, | |
| 34 | - | SourceURL: cfg.SourceURL, | |
| 35 | - | Version: serverVersion(), | |
| 30 | + | Repos: ermiteconnect.NewRepoServiceClient(hc, rpc.InProcessURL), | |
| 31 | + | Git: ermiteconnect.NewGitServiceClient(hc, rpc.InProcessURL), | |
| 32 | + | MergeRequests: ermiteconnect.NewMergeRequestServiceClient(hc, rpc.InProcessURL), | |
| 33 | + | Events: ermiteconnect.NewEventServiceClient(hc, rpc.InProcessURL), | |
| 34 | + | PublicURL: cfg.PublicURL, | |
| 35 | + | SourceURL: cfg.SourceURL, | |
| 36 | + | Version: serverVersion(), | |
| 36 | 37 | ||
| 37 | 38 | Sessions: webSessions{sessions}, | |
| 38 | 39 | Limiter: limiter, | |
A internal/adapters/web/compare.go +221 -0
| @@ -0,0 +1,221 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "net/http" | |
| 8 | + | "slices" | |
| 9 | + | ||
| 10 | + | "connectrpc.com/connect" | |
| 11 | + | "github.com/a-h/templ" | |
| 12 | + | ||
| 13 | + | pb "bnema.dev/ermite-api/gen/ermite" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // compareDepth bounds the commits read on each side to find where a branch | |
| 17 | + | // left its target. | |
| 18 | + | const compareDepth = maxComparePages * pageSize | |
| 19 | + | ||
| 20 | + | // compare shows a branch against the default branch of the repository: the | |
| 21 | + | // commits it is ahead by and, for the owner, the form that opens a merge | |
| 22 | + | // request. | |
| 23 | + | // | |
| 24 | + | // The API has no "diff between two branches" call outside a merge request, so | |
| 25 | + | // the page shows the diff only when it is exact without one: the branch is | |
| 26 | + | // ahead by a single commit that sits on the tip of the target (that commit's | |
| 27 | + | // own diff is then the whole change). Otherwise it lists the commits, which | |
| 28 | + | // link to their own diffs, and says the whole diff comes with the merge | |
| 29 | + | // request. | |
| 30 | + | func (s *Server) compare(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 31 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 32 | + | if err != nil { | |
| 33 | + | return nil, 0, err | |
| 34 | + | } | |
| 35 | + | src, rest, ok := resolveRef(d.branches, r.PathValue("rest")) | |
| 36 | + | if !ok || rest != "" || d.head == nil { | |
| 37 | + | return nil, 0, errNotFound | |
| 38 | + | } | |
| 39 | + | tgt := d.head | |
| 40 | + | name := d.name() | |
| 41 | + | ||
| 42 | + | p := ComparePage{ | |
| 43 | + | Source: visibleText(src.GetShortName()), Target: visibleText(tgt.GetShortName()), | |
| 44 | + | SourceHref: repoHref(name, "tree", src.GetShortName()), TargetHref: repoHref(name, "tree", tgt.GetShortName()), | |
| 45 | + | CLI: Clone{ID: "cli-create", Lines: []string{ | |
| 46 | + | "ermite mr create " + shellWord(name) + " --source " + shellWord(src.GetShortName()) + " --title <title>", | |
| 47 | + | }}, | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | same := src.GetShortName() == tgt.GetShortName() | |
| 51 | + | var ahead []*pb.Commit | |
| 52 | + | exact, behind := false, false | |
| 53 | + | if same { | |
| 54 | + | p.Note = "this is the default branch: there is nothing to compare it with." | |
| 55 | + | p.AheadText = "0 commits" | |
| 56 | + | } else { | |
| 57 | + | ahead, exact, behind, err = s.commitsAhead(ctx, name, src.GetCommitId(), tgt.GetCommitId()) | |
| 58 | + | if err != nil { | |
| 59 | + | return nil, 0, err | |
| 60 | + | } | |
| 61 | + | now := s.cfg.Now() | |
| 62 | + | for _, c := range ahead { | |
| 63 | + | p.Ahead = append(p.Ahead, commitRow(name, c, now)) | |
| 64 | + | } | |
| 65 | + | p.AheadText = plural(len(ahead), "commit", "commits") | |
| 66 | + | if !exact { | |
| 67 | + | p.AheadText += " or more" | |
| 68 | + | } | |
| 69 | + | switch { | |
| 70 | + | case !exact: | |
| 71 | + | p.Note = "the branch left " + p.Target + " too long ago to tell where: only the newest commits are listed." | |
| 72 | + | case len(ahead) == 0: | |
| 73 | + | p.Note = "nothing to merge: " + p.Target + " already has every commit of " + p.Source + "." | |
| 74 | + | case behind: | |
| 75 | + | p.Note = p.Target + " has moved on since the branch started: a merge request can only be merged after " + | |
| 76 | + | p.Source + " is rebased on " + p.Target + " and pushed again." | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // The exact diff: one commit on top of the tip of the target. | |
| 81 | + | if exact && !behind && len(ahead) == 1 { | |
| 82 | + | res, err := s.cfg.Git.GetCommit(ctx, connect.NewRequest(&pb.GetCommitRequest{Repo: name, Revision: ahead[0].GetId()})) | |
| 83 | + | if err != nil { | |
| 84 | + | return nil, 0, err | |
| 85 | + | } | |
| 86 | + | p.HasDiff = true | |
| 87 | + | p.Files = diffFiles(res.Msg.GetDiff().GetFiles()) | |
| 88 | + | p.NFiles, p.Adds, p.Dels = int(res.Msg.GetStats().GetFilesChanged()), int(res.Msg.GetStats().GetAdditions()), int(res.Msg.GetStats().GetDeletions()) | |
| 89 | + | if n := diffNote(res.Msg.GetDiff(), len(p.Files), p.NFiles); n != "" { | |
| 90 | + | p.Note = n | |
| 91 | + | } | |
| 92 | + | lines, invisible := diffLines(p.Files) | |
| 93 | + | p.Invisible = invisible | |
| 94 | + | self := repoHref(name, "compare", src.GetShortName()) | |
| 95 | + | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | |
| 96 | + | } else if len(ahead) > 1 && exact { | |
| 97 | + | if p.Note == "" { | |
| 98 | + | p.Note = "the whole diff of the branch is on its merge request; each commit links to its own." | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // An open request for the same pair, or the form to open one. | |
| 103 | + | var existing *pb.MergeRequest | |
| 104 | + | if !same { | |
| 105 | + | existing, err = s.openRequestFor(ctx, name, src.GetShortName(), tgt.GetShortName()) | |
| 106 | + | if err != nil { | |
| 107 | + | return nil, 0, err | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | switch { | |
| 111 | + | case existing != nil: | |
| 112 | + | p.Existing = &Link{Label: "!" + itoa(int(existing.GetNumber())) + " " + visibleText(existing.GetTitle()), Href: mrHref(name, existing.GetNumber())} | |
| 113 | + | case SignedIn(ctx) && !same && len(ahead) > 0: | |
| 114 | + | title := "" | |
| 115 | + | if len(ahead) == 1 { | |
| 116 | + | title = ahead[0].GetSummary() | |
| 117 | + | } else { | |
| 118 | + | title = ahead[len(ahead)-1].GetSummary() | |
| 119 | + | } | |
| 120 | + | p.Create = &CreateForm{ | |
| 121 | + | Action: mrBase(name), Source: src.GetShortName(), Target: tgt.GetShortName(), | |
| 122 | + | Title: cutRunes(title, 120), | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | l := s.repoLayout(ctx, d, "merge-requests") | |
| 127 | + | l.Title = "compare " + p.Source + " · " + name + " · " + s.instance | |
| 128 | + | pinRef(&l, target{rev: src.GetCommitId(), name: src.GetShortName()}) | |
| 129 | + | l.Crumbs[0].Current = false | |
| 130 | + | l.Crumbs = append(l.Crumbs, | |
| 131 | + | Crumb{Label: "merge requests", Href: mrBase(name)}, | |
| 132 | + | Crumb{Label: "compare", Current: false}, | |
| 133 | + | Crumb{Label: p.Source, Current: true}, | |
| 134 | + | ) | |
| 135 | + | l.Status = Status{ | |
| 136 | + | Mode: "compare", Context: p.Source + " → " + p.Target + " · " + p.AheadText, | |
| 137 | + | Hints: "d split · u unified · x collapse all · ? help", | |
| 138 | + | } | |
| 139 | + | l.HasRail, l.RailSum = true, "commits · files" | |
| 140 | + | return Compare(l, p), http.StatusOK, nil | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | // commitsAhead lists the commits of the source that the target lacks, newest | |
| 144 | + | // first. exact is false when the walk gave up before finding where the source | |
| 145 | + | // meets the target. behind is true when the target has commits the source | |
| 146 | + | // lacks (the tip of the target is not an ancestor of the source). | |
| 147 | + | func (s *Server) commitsAhead(ctx context.Context, repo, srcTip, tgtTip string) (ahead []*pb.Commit, exact, behind bool, err error) { | |
| 148 | + | if srcTip == tgtTip { | |
| 149 | + | return nil, true, false, nil | |
| 150 | + | } | |
| 151 | + | onTarget := map[string]bool{} | |
| 152 | + | tokenT := "" | |
| 153 | + | for len(onTarget) < compareDepth { | |
| 154 | + | res, err := s.cfg.Git.ListCommits(ctx, connect.NewRequest(&pb.ListCommitsRequest{ | |
| 155 | + | Repo: repo, Revision: tgtTip, PageSize: pageSize, PageToken: tokenT, | |
| 156 | + | })) | |
| 157 | + | if err != nil { | |
| 158 | + | return nil, false, false, err | |
| 159 | + | } | |
| 160 | + | for _, c := range res.Msg.GetCommits() { | |
| 161 | + | onTarget[c.GetId()] = true | |
| 162 | + | } | |
| 163 | + | if tokenT = res.Msg.GetNextPageToken(); tokenT == "" { | |
| 164 | + | break | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | token := "" | |
| 168 | + | for n := 0; n < compareDepth; { | |
| 169 | + | res, err := s.cfg.Git.ListCommits(ctx, connect.NewRequest(&pb.ListCommitsRequest{ | |
| 170 | + | Repo: repo, Revision: srcTip, PageSize: pageSize, PageToken: token, | |
| 171 | + | })) | |
| 172 | + | if err != nil { | |
| 173 | + | return nil, false, false, err | |
| 174 | + | } | |
| 175 | + | for _, c := range res.Msg.GetCommits() { | |
| 176 | + | n++ | |
| 177 | + | if onTarget[c.GetId()] { | |
| 178 | + | // The first commit of the source that the target has: where the | |
| 179 | + | // branch left it. The target moved on when this is not its tip. | |
| 180 | + | return ahead, true, c.GetId() != tgtTip, nil | |
| 181 | + | } | |
| 182 | + | ahead = append(ahead, c) | |
| 183 | + | } | |
| 184 | + | if token = res.Msg.GetNextPageToken(); token == "" { | |
| 185 | + | // The whole history of the source is ahead: no common history. | |
| 186 | + | return ahead, true, true, nil | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | return ahead, false, !slices.ContainsFunc(ahead, func(c *pb.Commit) bool { return c.GetId() == tgtTip }), nil | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | // openRequestFor finds the open merge request with this source and target. | |
| 193 | + | func (s *Server) openRequestFor(ctx context.Context, repo, source, target string) (*pb.MergeRequest, error) { | |
| 194 | + | token := "" | |
| 195 | + | for range maxOpenPages { | |
| 196 | + | res, err := s.cfg.MergeRequests.ListMergeRequests(ctx, connect.NewRequest(&pb.ListMergeRequestsRequest{ | |
| 197 | + | Repo: repo, State: pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN, PageSize: pageSize, PageToken: token, | |
| 198 | + | })) | |
| 199 | + | if err != nil { | |
| 200 | + | return nil, err | |
| 201 | + | } | |
| 202 | + | for _, m := range res.Msg.GetMergeRequests() { | |
| 203 | + | if m.GetSourceBranch() == source && m.GetTargetBranch() == target { | |
| 204 | + | return m, nil | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | if token = res.Msg.GetNextPageToken(); token == "" { | |
| 208 | + | break | |
| 209 | + | } | |
| 210 | + | } | |
| 211 | + | return nil, nil | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // cutRunes shortens s to at most n characters. | |
| 215 | + | func cutRunes(s string, n int) string { | |
| 216 | + | r := []rune(s) | |
| 217 | + | if len(r) <= n { | |
| 218 | + | return s | |
| 219 | + | } | |
| 220 | + | return string(r[:n]) | |
| 221 | + | } | |
M internal/adapters/web/diff.go +44 -3
| @@ -31,12 +31,48 @@ type DiffLine struct { | |||
| 31 | 31 | Old, New string // line numbers, "" when the side has none | |
| 32 | 32 | Segs []Seg | |
| 33 | 33 | NoEOL bool // the side ends without a line break | |
| 34 | + | // OldHref and NewHref link the line number of a side to the comment form | |
| 35 | + | // of that side; they are set on a merge request that can be commented on. | |
| 36 | + | OldHref, NewHref string | |
| 37 | + | // Notes are the comments of a merge request on this line, and Form the | |
| 38 | + | // form to write one, when the reader asked for it. | |
| 39 | + | Notes []CommentView | |
| 40 | + | Form *CommentForm | |
| 41 | + | ||
| 42 | + | // oldN and newN are the line numbers of the sides, 0 for none. | |
| 43 | + | oldN, newN int | |
| 34 | 44 | } | |
| 35 | 45 | ||
| 36 | 46 | // SplitRow pairs a deleted line (left) with an added one (right); a context | |
| 37 | 47 | // line fills both sides. | |
| 38 | 48 | type SplitRow struct{ L, R *DiffLine } | |
| 39 | 49 | ||
| 50 | + | // Notes are the comments under a row of the split view: those of its left | |
| 51 | + | // line, then those of its right line; a context line, on both sides, counts | |
| 52 | + | // once. | |
| 53 | + | func (r SplitRow) Notes() []CommentView { | |
| 54 | + | switch { | |
| 55 | + | case r.L == nil && r.R == nil: | |
| 56 | + | return nil | |
| 57 | + | case r.L == nil: | |
| 58 | + | return r.R.Notes | |
| 59 | + | case r.R == nil || r.L == r.R: | |
| 60 | + | return r.L.Notes | |
| 61 | + | } | |
| 62 | + | return append(append([]CommentView(nil), r.L.Notes...), r.R.Notes...) | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // Form is the comment form of the row, nil when there is none. | |
| 66 | + | func (r SplitRow) Form() *CommentForm { | |
| 67 | + | switch { | |
| 68 | + | case r.R != nil && r.R.Form != nil: | |
| 69 | + | return r.R.Form | |
| 70 | + | case r.L != nil: | |
| 71 | + | return r.L.Form | |
| 72 | + | } | |
| 73 | + | return nil | |
| 74 | + | } | |
| 75 | + | ||
| 40 | 76 | // DiffHunk is a changed region. | |
| 41 | 77 | type DiffHunk struct { | |
| 42 | 78 | Header string | |
| @@ -50,8 +86,11 @@ type Bar struct{ Add, Del, Rest string } | |||
| 50 | 86 | ||
| 51 | 87 | // DiffFile is one changed file. | |
| 52 | 88 | type DiffFile struct { | |
| 53 | - | ID string | |
| 54 | - | Status string // A D M R C T | |
| 89 | + | ID string | |
| 90 | + | Status string // A D M R C T | |
| 91 | + | // Key is the path comments name: the new path, the old path of a deleted | |
| 92 | + | // file. It is not fit to display (Path is). | |
| 93 | + | Key string | |
| 55 | 94 | Path string // display path, "old → new" for a rename | |
| 56 | 95 | Add, Del int | |
| 57 | 96 | Bar Bar | |
| @@ -185,7 +224,7 @@ func hunkHeader(h *pb.Hunk) string { | |||
| 185 | 224 | ||
| 186 | 225 | // diffLine converts a line of the API. | |
| 187 | 226 | func diffLine(l *pb.Line) DiffLine { | |
| 188 | - | dl := DiffLine{Segs: segments(l.GetText()), NoEOL: l.GetNoNewlineAtEof()} | |
| 227 | + | dl := DiffLine{Segs: segments(l.GetText()), NoEOL: l.GetNoNewlineAtEof(), oldN: int(l.GetOldNumber()), newN: int(l.GetNewNumber())} | |
| 189 | 228 | switch l.GetKind() { | |
| 190 | 229 | case pb.LineKind_LINE_KIND_ADDITION: | |
| 191 | 230 | dl.Kind, dl.Mark, dl.New = "add", "+", lineNumber(l.GetNewNumber()) | |
| @@ -214,8 +253,10 @@ func diffFiles(files []*pb.FileDiff) []DiffFile { | |||
| 214 | 253 | // (binary, truncated) stays closed under its note. | |
| 215 | 254 | Open: len(f.GetHunks()) > 0 && !f.GetTruncated(), | |
| 216 | 255 | } | |
| 256 | + | df.Key = f.GetNewPath() | |
| 217 | 257 | switch f.GetStatus() { | |
| 218 | 258 | case pb.FileStatus_FILE_STATUS_DELETED: | |
| 259 | + | df.Key = f.GetOldPath() | |
| 219 | 260 | df.Path = visibleText(f.GetOldPath()) | |
| 220 | 261 | case pb.FileStatus_FILE_STATUS_RENAMED, pb.FileStatus_FILE_STATUS_COPIED: | |
| 221 | 262 | df.Path = visibleText(f.GetOldPath()) + " → " + visibleText(f.GetNewPath()) | |
M internal/adapters/web/diff.templ +25 -4
| @@ -175,11 +175,17 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 175 | 175 | <tr class="hunk"><td colspan="4">{ h.Header }</td></tr> | |
| 176 | 176 | for _, ln := range h.Lines { | |
| 177 | 177 | <tr class={ ln.Kind }> | |
| 178 | - | <td class="ln">{ ln.Old }</td> | |
| 179 | - | <td class="ln">{ ln.New }</td> | |
| 178 | + | <td class="ln">@gutter(ln.Old, ln.OldHref)</td> | |
| 179 | + | <td class="ln">@gutter(ln.New, ln.NewHref)</td> | |
| 180 | 180 | <td class="m">{ ln.Mark }</td> | |
| 181 | 181 | <td class="tx">@codeLine(ln)</td> | |
| 182 | 182 | </tr> | |
| 183 | + | if len(ln.Notes) > 0 || ln.Form != nil { | |
| 184 | + | <tr class="fnote"> | |
| 185 | + | <td colspan="3"></td> | |
| 186 | + | <td>@lineNotes(ln.Notes, ln.Form, "")</td> | |
| 187 | + | </tr> | |
| 188 | + | } | |
| 183 | 189 | } | |
| 184 | 190 | </tbody> | |
| 185 | 191 | } | |
| @@ -195,6 +201,11 @@ templ DiffFileView(f DiffFile, p DiffView) { | |||
| 195 | 201 | @splitSide(row.L, true) | |
| 196 | 202 | @splitSide(row.R, false) | |
| 197 | 203 | </tr> | |
| 204 | + | if notes, form := row.Notes(), row.Form(); len(notes) > 0 || form != nil { | |
| 205 | + | <tr class="fnote"> | |
| 206 | + | <td colspan="6">@lineNotes(notes, form, "-s")</td> | |
| 207 | + | </tr> | |
| 208 | + | } | |
| 198 | 209 | } | |
| 199 | 210 | </tbody> | |
| 200 | 211 | } | |
| @@ -223,12 +234,22 @@ templ splitSide(ln *DiffLine, old bool) { | |||
| 223 | 234 | } else { | |
| 224 | 235 | <td class={ "ln", ln.Kind }> | |
| 225 | 236 | if old { | |
| 226 | - | { ln.Old } | |
| 237 | + | @gutter(ln.Old, ln.OldHref) | |
| 227 | 238 | } else { | |
| 228 | - | { ln.New } | |
| 239 | + | @gutter(ln.New, ln.NewHref) | |
| 229 | 240 | } | |
| 230 | 241 | </td> | |
| 231 | 242 | <td class={ "m", ln.Kind }>{ ln.Mark }</td> | |
| 232 | 243 | <td class={ "tx", ln.Kind }>@codeLine(*ln)</td> | |
| 233 | 244 | } | |
| 234 | 245 | } | |
| 246 | + | ||
| 247 | + | // gutter is a line number of the gutter. On a merge request that can be | |
| 248 | + | // commented on, it links to the form that comments on that line. | |
| 249 | + | templ gutter(n, href string) { | |
| 250 | + | if href != "" { | |
| 251 | + | <a href={ templ.SafeURL(href) } title="comment on this line">{ n }</a> | |
| 252 | + | } else { | |
| 253 | + | { n } | |
| 254 | + | } | |
| 255 | + | } | |
M internal/adapters/web/diff_templ.go +163 -86 generated file: collapsed
M internal/adapters/web/diff_test.go +2 -2
| @@ -78,8 +78,8 @@ func TestPairLines(t *testing.T) { | |||
| 78 | 78 | assert.Equal(t, []pair{ | |
| 79 | 79 | {" c", " c"}, {"-d1", "+a1"}, {"-d2", ""}, {"-d3", ""}, {" c2", " c2"}, {"", "+a2"}, {"", "+a3"}, | |
| 80 | 80 | }, got) | |
| 81 | - | assert.Equal(t, DiffLine{Kind: "del", Mark: "-", Old: "2", Segs: []Seg{{Text: "d1"}}}, ls[1]) | |
| 82 | - | assert.Equal(t, DiffLine{Kind: "add", Mark: "+", New: "2", Segs: []Seg{{Text: "a1"}}}, ls[4]) | |
| 81 | + | assert.Equal(t, DiffLine{Kind: "del", Mark: "-", Old: "2", Segs: []Seg{{Text: "d1"}}, oldN: 2}, ls[1]) | |
| 82 | + | assert.Equal(t, DiffLine{Kind: "add", Mark: "+", New: "2", Segs: []Seg{{Text: "a1"}}, newN: 2}, ls[4]) | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | 85 | func TestChangeBar(t *testing.T) { | |
M internal/adapters/web/events.go +5 -1
| @@ -26,6 +26,8 @@ import ( | |||
| 26 | 26 | // GET /-/events?topic=home | |
| 27 | 27 | // GET /-/events?topic=repo:<name>&view=summary|refs | |
| 28 | 28 | // GET /-/events?topic=repo:<name>&view=commits&ref=<branch>&head=<sha> | |
| 29 | + | // GET /-/events?topic=repo:<name>&view=mrs | |
| 30 | + | // GET /-/events?topic=repo:<name>&view=mr&n=<number>&ref=<source branch> | |
| 29 | 31 | // | |
| 30 | 32 | // topic is the Watch topic. view says which fragments of a repository the | |
| 31 | 33 | // page shows, so that the server renders only those (summary when absent). | |
| @@ -43,6 +45,8 @@ import ( | |||
| 43 | 45 | // refs-list data: <tbody id="ref-rows">…</tbody>, and again with | |
| 44 | 46 | // <dl id="refs-kv">…</dl>, the facts of the rail | |
| 45 | 47 | // commits-head data: <div id="commits-head">N new commits — reload</div> | |
| 48 | + | // mr-notice data: <div id="mr-notice">…</div>, on the merge request pages: | |
| 49 | + | // something changed, with the link that reloads the page | |
| 46 | 50 | // gap data: gap; events were lost, or what the page shows is gone or | |
| 47 | 51 | // changed: the page fetches itself again (ermiteLive in ermite.js) | |
| 48 | 52 | // | |
| @@ -172,7 +176,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) { | |||
| 172 | 176 | return | |
| 173 | 177 | } | |
| 174 | 178 | ||
| 175 | - | st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head")} | |
| 179 | + | st := &liveStream{s: s, repo: repo, view: q.Get("view"), ref: q.Get("ref"), head: q.Get("head"), n: q.Get("n")} | |
| 176 | 180 | handle := func(m recvd) bool { | |
| 177 | 181 | if m.err != nil { | |
| 178 | 182 | if ctx.Err() == nil && connect.CodeOf(m.err) != connect.CodeCanceled { | |
M internal/adapters/web/fragments.go +54 -0
| @@ -20,6 +20,7 @@ type liveStream struct { | |||
| 20 | 20 | view string // "summary", "refs" or "commits" | |
| 21 | 21 | ref string // commits: the branch listed | |
| 22 | 22 | head string // commits: the commit listed first | |
| 23 | + | n string // mr: the number of the merge request shown | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 25 | 26 | // renderFragment renders a component to the data of an event. | |
| @@ -103,7 +104,21 @@ func (l *liveStream) repoPage(ctx context.Context, ev *pb.Event) ([]fragment, er | |||
| 103 | 104 | return gap, nil // the description, the default branch | |
| 104 | 105 | } | |
| 105 | 106 | return nil, nil | |
| 107 | + | case *pb.Event_MergeRequestOpened: | |
| 108 | + | return l.mrNotice(ctx, k.MergeRequestOpened.GetMergeRequest().GetNumber(), "a merge request was opened — reload") | |
| 109 | + | case *pb.Event_MergeRequestMerged: | |
| 110 | + | return l.mrNotice(ctx, k.MergeRequestMerged.GetMergeRequest().GetNumber(), "this merge request was merged — reload") | |
| 111 | + | case *pb.Event_MergeRequestClosed: | |
| 112 | + | return l.mrNotice(ctx, k.MergeRequestClosed.GetMergeRequest().GetNumber(), "this merge request was closed — reload") | |
| 113 | + | case *pb.Event_CommentAdded: | |
| 114 | + | return l.mrNotice(ctx, k.CommentAdded.GetNumber(), "a comment was added — reload") | |
| 106 | 115 | case *pb.Event_Pushed: | |
| 116 | + | switch l.view { | |
| 117 | + | case "mr": | |
| 118 | + | return l.mrPushed(ctx, k.Pushed) | |
| 119 | + | case "mrs": | |
| 120 | + | return nil, nil | |
| 121 | + | } | |
| 107 | 122 | switch l.view { | |
| 108 | 123 | case "refs": | |
| 109 | 124 | return l.refs(ctx) | |
| @@ -206,3 +221,42 @@ func newCommits(count string, n int) string { | |||
| 206 | 221 | ||
| 207 | 222 | // isFullID reports whether s is a full commit id. | |
| 208 | 223 | func isFullID(s string) bool { return len(s) == 40 && isCommitID(s) } | |
| 224 | + | ||
| 225 | + | // mrNotice tells a merge request page that something happened to the request | |
| 226 | + | // it shows, or the list that its content changed. Only a fixed sentence is sent: | |
| 227 | + | // the text of a comment or a title never leaves through the stream. | |
| 228 | + | func (l *liveStream) mrNotice(ctx context.Context, number int64, text string) ([]fragment, error) { | |
| 229 | + | switch l.view { | |
| 230 | + | case "mrs": | |
| 231 | + | text = "merge requests changed — reload" | |
| 232 | + | return l.notice(ctx, text, mrBase(l.repo)) | |
| 233 | + | case "mr": | |
| 234 | + | if strconv.FormatInt(number, 10) != l.n { | |
| 235 | + | return nil, nil | |
| 236 | + | } | |
| 237 | + | return l.notice(ctx, text, mrHref(l.repo, number)) | |
| 238 | + | } | |
| 239 | + | return nil, nil | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // mrPushed announces a push to the source branch of the merge request shown. | |
| 243 | + | func (l *liveStream) mrPushed(ctx context.Context, p *pb.Pushed) ([]fragment, error) { | |
| 244 | + | if l.ref == "" { | |
| 245 | + | return nil, nil | |
| 246 | + | } | |
| 247 | + | for _, u := range p.GetRefUpdates() { | |
| 248 | + | if u.GetRef() == "refs/heads/"+l.ref { | |
| 249 | + | n, err := strconv.ParseInt(l.n, 10, 64) | |
| 250 | + | if err != nil { | |
| 251 | + | return nil, nil | |
| 252 | + | } | |
| 253 | + | return l.notice(ctx, "the branch changed — reload", mrHref(l.repo, n)) | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | return nil, nil | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | func (l *liveStream) notice(ctx context.Context, text, href string) ([]fragment, error) { | |
| 260 | + | f, err := renderFragment(ctx, "mr-notice", MRNotice(text, href)) | |
| 261 | + | return []fragment{f}, err | |
| 262 | + | } | |
M internal/adapters/web/fragments.templ +9 -1
| @@ -68,6 +68,14 @@ templ CommitsBanner(text, href string) { | |||
| 68 | 68 | </div> | |
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | + | // MRNotice tells a merge request page that it is out of date. The empty slot | |
| 72 | + | // of the page has the same id, so the first notice replaces it. | |
| 73 | + | templ MRNotice(text, href string) { | |
| 74 | + | <div id="mr-notice" class="live-banner" role="status"> | |
| 75 | + | <a href={ templ.SafeURL(href) }>{ text }</a> | |
| 76 | + | </div> | |
| 77 | + | } | |
| 78 | + | ||
| 71 | 79 | // Live is the connection state of the event stream, in the status bar. | |
| 72 | 80 | // Without a stream it reads "offline" and does nothing. The events of the | |
| 73 | 81 | // stream are fragments that ermiteLive (ermite.js) swaps in the page. | |
| @@ -83,7 +91,7 @@ templ Live(l Layout) { | |||
| 83 | 91 | :title="data.state === 'live' ? 'receiving updates over SSE' : 'not connected to the event stream'" | |
| 84 | 92 | hx-on="htmx:sse:after:connection -> data.state = 'live'; | |
| 85 | 93 | htmx:sse:close, htmx:sse:error -> data.state = 'offline'; | |
| 86 | - | home-list, repo-last-push, refs-list, commits-head -> ermiteLive.swap(event.detail.data); | |
| 94 | + | home-list, repo-last-push, refs-list, commits-head, mr-notice -> ermiteLive.swap(event.detail.data); | |
| 87 | 95 | gap -> ermiteLive.reload()" | |
| 88 | 96 | >offline</span> | |
| 89 | 97 | } else { | |
M internal/adapters/web/fragments_templ.go +67 -10 generated file: collapsed
M internal/adapters/web/keys.templ +7 -0
| @@ -41,6 +41,13 @@ templ Keys() { | |||
| 41 | 41 | k-t from:body -> document.querySelector('.secs [data-key=t]')?.click(); | |
| 42 | 42 | k-c from:body -> document.querySelector('.secs [data-key=c]')?.click(); | |
| 43 | 43 | k-r from:body -> document.querySelector('.secs [data-key=r]')?.click(); | |
| 44 | + | k-m from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 45 | + | k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click(); | |
| 46 | + | k-next from:body -> { | |
| 47 | + | let ns = q('#diffs .cmt').arr(); if (!ns.length) return; | |
| 48 | + | let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3); | |
| 49 | + | let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' }) | |
| 50 | + | }; | |
| 44 | 51 | k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split'); | |
| 45 | 52 | k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified'); | |
| 46 | 53 | k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false); | |
M internal/adapters/web/keys_templ.go +1 -1 generated file: collapsed
A internal/adapters/web/mr.go +758 -0
| @@ -0,0 +1,758 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "html" | |
| 8 | + | "io" | |
| 9 | + | "net/http" | |
| 10 | + | "net/url" | |
| 11 | + | "strconv" | |
| 12 | + | "strings" | |
| 13 | + | ||
| 14 | + | "connectrpc.com/connect" | |
| 15 | + | "github.com/a-h/templ" | |
| 16 | + | ||
| 17 | + | pb "bnema.dev/ermite-api/gen/ermite" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // The merge request pages (ADR 0017). They read through MergeRequestService | |
| 21 | + | // and GitService as the visitor of the request: anonymous sees the merge | |
| 22 | + | // requests of public repositories, the signed-in owner those of every | |
| 23 | + | // repository, and only the owner gets the forms (mr_post.go handles them). | |
| 24 | + | // | |
| 25 | + | // Routes: | |
| 26 | + | // | |
| 27 | + | // GET /<repo>/merge-requests the list, open ones first | |
| 28 | + | // GET /<repo>/merge-requests/<n> the request with its diff | |
| 29 | + | // GET /<repo>/merge-requests/<n>/since the diff since the last read | |
| 30 | + | // GET /<repo>/compare/<branch> a branch against the default branch | |
| 31 | + | // | |
| 32 | + | // Reading marks nothing as read. MarkRead stores state, and a GET must stay | |
| 33 | + | // safe: a prefetcher, a link preview or a reload of the owner's tab would move | |
| 34 | + | // the read position, and the "since your last read" diff would lose what the | |
| 35 | + | // owner never saw. So the owner marks a head read with a POST button | |
| 36 | + | // ("[mark read]", /<n>/read), which carries the head the page showed; the | |
| 37 | + | // button only shows when that head is not the one already read. The since page | |
| 38 | + | // is the owner's alone (the position is the owner's), and answers 404 to | |
| 39 | + | // anyone else. | |
| 40 | + | // | |
| 41 | + | // Every page here is rendered through page(), so for the owner it is | |
| 42 | + | // "private, no-store" with "Vary: Cookie", and for a visitor "no-cache": the | |
| 43 | + | // forms and the CLI lines exist only in the owner's copy. | |
| 44 | + | ||
| 45 | + | const ( | |
| 46 | + | // mrPerPage is how many decided merge requests a page of the list shows, | |
| 47 | + | // at least; pages of the API are read whole. | |
| 48 | + | mrPerPage = 50 | |
| 49 | + | // maxMRPages bounds the API pages one list page reads. | |
| 50 | + | maxMRPages = 20 | |
| 51 | + | // maxOpenPages bounds the pages read for the open merge requests, which | |
| 52 | + | // the first list page shows in full. | |
| 53 | + | maxOpenPages = 5 | |
| 54 | + | // maxComparePages bounds how many pages of history the compare page reads | |
| 55 | + | // to find where the branch left the target. | |
| 56 | + | maxComparePages = 5 | |
| 57 | + | // railBranches is how many branches the list offers to compare. | |
| 58 | + | railBranches = 15 | |
| 59 | + | ) | |
| 60 | + | ||
| 61 | + | // MRRow is one merge request in the list. | |
| 62 | + | type MRRow struct { | |
| 63 | + | Number string | |
| 64 | + | Href string | |
| 65 | + | Title string | |
| 66 | + | Branches string | |
| 67 | + | Author string | |
| 68 | + | State string | |
| 69 | + | When string | |
| 70 | + | WhenAt string | |
| 71 | + | Cursor bool | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // BranchLink is a branch the list offers to open a merge request for. | |
| 75 | + | type BranchLink struct{ Name, Href string } | |
| 76 | + | ||
| 77 | + | // MRListPage is the list of the merge requests of a repository. | |
| 78 | + | type MRListPage struct { | |
| 79 | + | Open, Done []MRRow | |
| 80 | + | // OpenNote says the open list was cut. | |
| 81 | + | OpenNote string | |
| 82 | + | // Older and Newest are the paging links of the decided requests. | |
| 83 | + | Older, Newest string | |
| 84 | + | Branches []BranchLink | |
| 85 | + | Default string | |
| 86 | + | CLI Clone | |
| 87 | + | // Live is true when the page announces changes. | |
| 88 | + | Live bool | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // CommentView is a comment of a merge request, ready to show. | |
| 92 | + | type CommentView struct { | |
| 93 | + | ID string | |
| 94 | + | Author string | |
| 95 | + | When string | |
| 96 | + | WhenAt string | |
| 97 | + | Abs string | |
| 98 | + | Parts []TextPart | |
| 99 | + | Resolved bool | |
| 100 | + | // Outdated is true when the head moved since the comment was written, so | |
| 101 | + | // its line may have moved too. | |
| 102 | + | Outdated bool | |
| 103 | + | // Where names the line of a comment that is not under a line of the diff | |
| 104 | + | // shown ("path:12 (new)"). | |
| 105 | + | Where string | |
| 106 | + | // Resolve is the form that resolves or unresolves it; nil when the reader | |
| 107 | + | // cannot. | |
| 108 | + | Resolve *ResolveForm | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // ResolveForm is the POST that changes the resolved state of a comment. | |
| 112 | + | type ResolveForm struct { | |
| 113 | + | Action string | |
| 114 | + | // Value is the state the form sets. | |
| 115 | + | Value bool | |
| 116 | + | Label string | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | // CommentForm is the POST that adds a comment. | |
| 120 | + | type CommentForm struct { | |
| 121 | + | ID string | |
| 122 | + | Action string | |
| 123 | + | Title string | |
| 124 | + | // Path, Line and Side designate the line; empty for a general comment. | |
| 125 | + | Path, Line, Side string | |
| 126 | + | Cancel string | |
| 127 | + | Focus bool | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // MRCommit is a commit of the request. | |
| 131 | + | type MRCommit struct { | |
| 132 | + | Short, Href, Subject, Author, When, WhenAt string | |
| 133 | + | // New is true for a commit pushed after the head the reviewer last read. | |
| 134 | + | New bool | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // MergeBox is what the owner does with an open request. | |
| 138 | + | type MergeBox struct { | |
| 139 | + | MergeAction string | |
| 140 | + | CloseAction string | |
| 141 | + | ReadAction string | |
| 142 | + | Head string | |
| 143 | + | Modes []ModeOption | |
| 144 | + | // HeadGone is true when the source branch no longer exists: there is | |
| 145 | + | // nothing to merge. | |
| 146 | + | HeadGone bool | |
| 147 | + | CLI Clone | |
| 148 | + | // CanMarkRead is true when the head differs from the one last read. | |
| 149 | + | CanMarkRead bool | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // ModeOption is a merge mode the repository allows. | |
| 153 | + | type ModeOption struct{ Value, Label string } | |
| 154 | + | ||
| 155 | + | // SinceNav switches between the whole diff and the diff since the last read. | |
| 156 | + | type SinceNav struct { | |
| 157 | + | AllHref, SinceHref string | |
| 158 | + | Files int | |
| 159 | + | ReadShort string | |
| 160 | + | HeadShort string | |
| 161 | + | Current bool | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | // MRPage is a merge request with its diff and comments. | |
| 165 | + | type MRPage struct { | |
| 166 | + | Number string | |
| 167 | + | Title string | |
| 168 | + | State string | |
| 169 | + | Open bool | |
| 170 | + | Signed bool | |
| 171 | + | Source, Target string | |
| 172 | + | SourceHref string | |
| 173 | + | TargetHref string | |
| 174 | + | Author string | |
| 175 | + | Opened Person | |
| 176 | + | Updated string | |
| 177 | + | UpdatedAt string | |
| 178 | + | Desc []TextPart | |
| 179 | + | Head string | |
| 180 | + | HeadShort string | |
| 181 | + | MergedShort string | |
| 182 | + | MergedHref string | |
| 183 | + | MergeMode string | |
| 184 | + | ||
| 185 | + | Since *SinceNav | |
| 186 | + | SinceNote string | |
| 187 | + | ||
| 188 | + | NFiles, Adds, Dels int | |
| 189 | + | Files []DiffFile | |
| 190 | + | DiffView | |
| 191 | + | Note string | |
| 192 | + | Invisible bool | |
| 193 | + | ||
| 194 | + | // General are the comments that are not on a line, Loose those on a line | |
| 195 | + | // that the diff shown does not have. | |
| 196 | + | General, Loose []CommentView | |
| 197 | + | NComments int | |
| 198 | + | NUnresolved int | |
| 199 | + | GeneralForm *CommentForm | |
| 200 | + | ||
| 201 | + | Commits []MRCommit | |
| 202 | + | CommitsNote string | |
| 203 | + | Modes []string | |
| 204 | + | ChecksBlock bool | |
| 205 | + | ||
| 206 | + | Box *MergeBox | |
| 207 | + | // Live is true when the page announces changes. | |
| 208 | + | Live bool | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // CompareCommit is a commit of a branch that the target lacks. | |
| 212 | + | type CompareCommit = CommitRow | |
| 213 | + | ||
| 214 | + | // ComparePage is a branch compared with the default branch. | |
| 215 | + | type ComparePage struct { | |
| 216 | + | Source, Target string | |
| 217 | + | SourceHref string | |
| 218 | + | TargetHref string | |
| 219 | + | Ahead []CommitRow | |
| 220 | + | AheadText string | |
| 221 | + | CLI Clone | |
| 222 | + | // Note says what the page could not tell. | |
| 223 | + | Note string | |
| 224 | + | // The diff is the one of the single commit the branch is ahead by. | |
| 225 | + | NFiles, Adds, Dels int | |
| 226 | + | Files []DiffFile | |
| 227 | + | DiffView | |
| 228 | + | Invisible bool | |
| 229 | + | HasDiff bool | |
| 230 | + | // Existing is the open merge request for this pair, when there is one. | |
| 231 | + | Existing *Link | |
| 232 | + | // Create is the form to open one; nil when the reader cannot or there is | |
| 233 | + | // one already. | |
| 234 | + | Create *CreateForm | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // CreateForm is the POST that opens a merge request. | |
| 238 | + | type CreateForm struct { | |
| 239 | + | Action, Source, Target, Title string | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // mrBase is the URL of the merge request pages of a repository. | |
| 243 | + | func mrBase(repo string) string { return repoHref(repo, "merge-requests") } | |
| 244 | + | ||
| 245 | + | func mrHref(repo string, n int64, sub ...string) string { | |
| 246 | + | return repoHref(repo, append([]string{"merge-requests", strconv.FormatInt(n, 10)}, sub...)...) | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // mrNumber reads the number of the path: a positive decimal number, written | |
| 250 | + | // without sign or leading zeros, else the page is not found. | |
| 251 | + | func mrNumber(r *http.Request) (int64, error) { | |
| 252 | + | s := r.PathValue("n") | |
| 253 | + | n, err := strconv.ParseInt(s, 10, 64) | |
| 254 | + | if err != nil || n < 1 || strconv.FormatInt(n, 10) != s { | |
| 255 | + | return 0, errNotFound | |
| 256 | + | } | |
| 257 | + | return n, nil | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func stateName(s pb.MergeRequestState) string { | |
| 261 | + | switch s { | |
| 262 | + | case pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN: | |
| 263 | + | return "open" | |
| 264 | + | case pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED: | |
| 265 | + | return "merged" | |
| 266 | + | case pb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED: | |
| 267 | + | return "closed" | |
| 268 | + | } | |
| 269 | + | return "unknown" | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | func modeName(m pb.MergeMode) string { | |
| 273 | + | switch m { | |
| 274 | + | case pb.MergeMode_MERGE_MODE_FAST_FORWARD: | |
| 275 | + | return "fast-forward" | |
| 276 | + | case pb.MergeMode_MERGE_MODE_SQUASH: | |
| 277 | + | return "squash" | |
| 278 | + | } | |
| 279 | + | return "" | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // mrRow converts a merge request of a listing. | |
| 283 | + | func (s *Server) mrRow(repo string, m *pb.MergeRequest) MRRow { | |
| 284 | + | row := MRRow{ | |
| 285 | + | Number: "!" + strconv.FormatInt(m.GetNumber(), 10), Href: mrHref(repo, m.GetNumber()), | |
| 286 | + | Title: visibleText(m.GetTitle()), State: stateName(m.GetState()), | |
| 287 | + | Branches: visibleText(m.GetSourceBranch()) + " → " + visibleText(m.GetTargetBranch()), | |
| 288 | + | Author: visibleText(m.GetAuthor()), | |
| 289 | + | } | |
| 290 | + | ts := m.GetUpdatedAt() | |
| 291 | + | if !ts.IsValid() { | |
| 292 | + | ts = m.GetCreatedAt() | |
| 293 | + | } | |
| 294 | + | row.When, row.WhenAt = whenOf(s.cfg.Now(), ts) | |
| 295 | + | return row | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // mrList lists the merge requests: the open ones, then the merged and closed | |
| 299 | + | // ones, a page at a time. The decided ones are paged with the page token of | |
| 300 | + | // the API in "page"; the open ones show on the first page only. | |
| 301 | + | func (s *Server) mrList(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 302 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 303 | + | if err != nil { | |
| 304 | + | return nil, 0, err | |
| 305 | + | } | |
| 306 | + | token := r.URL.Query().Get("page") | |
| 307 | + | p := MRListPage{Default: visibleText(d.repo.GetDefaultBranch())} | |
| 308 | + | p.CLI = Clone{ID: "cli-new", Lines: []string{"ermite mr create " + shellWord(d.name()) + " --source <branch> --title <title>"}} | |
| 309 | + | base := mrBase(d.name()) | |
| 310 | + | ||
| 311 | + | if token == "" { | |
| 312 | + | tok := "" | |
| 313 | + | for i := 0; i < maxOpenPages; i++ { | |
| 314 | + | res, err := s.cfg.MergeRequests.ListMergeRequests(ctx, connect.NewRequest(&pb.ListMergeRequestsRequest{ | |
| 315 | + | Repo: d.name(), State: pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN, PageSize: pageSize, PageToken: tok, | |
| 316 | + | })) | |
| 317 | + | if err != nil { | |
| 318 | + | return nil, 0, err | |
| 319 | + | } | |
| 320 | + | for _, m := range res.Msg.GetMergeRequests() { | |
| 321 | + | p.Open = append(p.Open, s.mrRow(d.name(), m)) | |
| 322 | + | } | |
| 323 | + | if tok = res.Msg.GetNextPageToken(); tok == "" { | |
| 324 | + | break | |
| 325 | + | } | |
| 326 | + | } | |
| 327 | + | if tok != "" { | |
| 328 | + | p.OpenNote = "more open merge requests than are listed here." | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | tok := token | |
| 333 | + | for i := 0; i < maxMRPages; i++ { | |
| 334 | + | res, err := s.cfg.MergeRequests.ListMergeRequests(ctx, connect.NewRequest(&pb.ListMergeRequestsRequest{ | |
| 335 | + | Repo: d.name(), PageSize: pageSize, PageToken: tok, | |
| 336 | + | })) | |
| 337 | + | if err != nil { | |
| 338 | + | return nil, 0, err | |
| 339 | + | } | |
| 340 | + | for _, m := range res.Msg.GetMergeRequests() { | |
| 341 | + | if m.GetState() != pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN { | |
| 342 | + | p.Done = append(p.Done, s.mrRow(d.name(), m)) | |
| 343 | + | } | |
| 344 | + | } | |
| 345 | + | tok = res.Msg.GetNextPageToken() | |
| 346 | + | if tok == "" || len(p.Done) >= mrPerPage { | |
| 347 | + | break | |
| 348 | + | } | |
| 349 | + | } | |
| 350 | + | if tok != "" { | |
| 351 | + | p.Older = base + "?page=" + url.QueryEscape(tok) | |
| 352 | + | } | |
| 353 | + | if token != "" { | |
| 354 | + | p.Newest = base | |
| 355 | + | } | |
| 356 | + | switch { | |
| 357 | + | case len(p.Open) > 0: | |
| 358 | + | p.Open[0].Cursor = true | |
| 359 | + | case len(p.Done) > 0: | |
| 360 | + | p.Done[0].Cursor = true | |
| 361 | + | } | |
| 362 | + | for _, b := range d.branches { | |
| 363 | + | if b.GetShortName() == d.repo.GetDefaultBranch() || len(p.Branches) >= railBranches { | |
| 364 | + | continue | |
| 365 | + | } | |
| 366 | + | p.Branches = append(p.Branches, BranchLink{Name: visibleText(b.GetShortName()), Href: repoHref(d.name(), "compare", b.GetShortName())}) | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | l := s.repoLayout(ctx, d, "merge-requests") | |
| 370 | + | l.Title = "merge requests · " + d.name() + " · " + s.instance | |
| 371 | + | l.Crumbs[0].Current = false | |
| 372 | + | l.Crumbs = append(l.Crumbs, Crumb{Label: "merge requests", Current: true}) | |
| 373 | + | if token == "" { | |
| 374 | + | p.Live = true | |
| 375 | + | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mrs") | |
| 376 | + | } | |
| 377 | + | rows := len(p.Open) + len(p.Done) | |
| 378 | + | l.Status = Status{ | |
| 379 | + | Mode: "merge requests", Context: itoa(len(p.Open)) + " open · " + itoa(len(p.Done)) + " decided", | |
| 380 | + | Hints: "j/k move · enter open · / filter · ? help", Cursor: true, Count: rows, | |
| 381 | + | } | |
| 382 | + | l.HasRail, l.RailSum = true, "states · open one" | |
| 383 | + | return MRList(l, p), http.StatusOK, nil | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | // mrShow shows a merge request: the whole diff against the target. | |
| 387 | + | func (s *Server) mrShow(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 388 | + | return s.mrPage(ctx, r, false) | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | // mrSince shows the diff since the head the reviewer last marked read. | |
| 392 | + | func (s *Server) mrSince(ctx context.Context, r *http.Request) (templ.Component, int, error) { | |
| 393 | + | return s.mrPage(ctx, r, true) | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | func (s *Server) mrPage(ctx context.Context, r *http.Request, since bool) (templ.Component, int, error) { | |
| 397 | + | n, err := mrNumber(r) | |
| 398 | + | if err != nil { | |
| 399 | + | return nil, 0, err | |
| 400 | + | } | |
| 401 | + | if since && !SignedIn(ctx) { | |
| 402 | + | // The read position is the owner's: there is nothing to show others. | |
| 403 | + | return nil, 0, errNotFound | |
| 404 | + | } | |
| 405 | + | d, err := s.loadRepo(ctx, r.PathValue("repo")) | |
| 406 | + | if err != nil { | |
| 407 | + | return nil, 0, err | |
| 408 | + | } | |
| 409 | + | res, err := s.cfg.MergeRequests.GetMergeRequest(ctx, connect.NewRequest(&pb.GetMergeRequestRequest{ | |
| 410 | + | Repo: d.name(), Number: n, SinceLastRead: since, | |
| 411 | + | })) | |
| 412 | + | if err != nil { | |
| 413 | + | return nil, 0, err | |
| 414 | + | } | |
| 415 | + | m := res.Msg.GetMergeRequest() | |
| 416 | + | if m == nil { | |
| 417 | + | return nil, 0, errNotFound | |
| 418 | + | } | |
| 419 | + | now := s.cfg.Now() | |
| 420 | + | signed := SignedIn(ctx) | |
| 421 | + | open := m.GetState() == pb.MergeRequestState_MERGE_REQUEST_STATE_OPEN | |
| 422 | + | head := m.GetHeadId() | |
| 423 | + | self := mrHref(d.name(), n) | |
| 424 | + | if since { | |
| 425 | + | self = mrHref(d.name(), n, "since") | |
| 426 | + | } | |
| 427 | + | ||
| 428 | + | p := MRPage{ | |
| 429 | + | Number: "!" + strconv.FormatInt(n, 10), Title: visibleText(m.GetTitle()), | |
| 430 | + | State: stateName(m.GetState()), Open: open, Signed: signed, | |
| 431 | + | Source: visibleText(m.GetSourceBranch()), Target: visibleText(m.GetTargetBranch()), | |
| 432 | + | Author: visibleText(m.GetAuthor()), | |
| 433 | + | Desc: plainParts(m.GetDescription()), | |
| 434 | + | Head: head, HeadShort: short(head), | |
| 435 | + | NFiles: int(res.Msg.GetStats().GetFilesChanged()), | |
| 436 | + | Adds: int(res.Msg.GetStats().GetAdditions()), | |
| 437 | + | Dels: int(res.Msg.GetStats().GetDeletions()), | |
| 438 | + | Files: diffFiles(res.Msg.GetDiff().GetFiles()), | |
| 439 | + | } | |
| 440 | + | p.Opened.Name = p.Author | |
| 441 | + | p.Opened.When, p.Opened.WhenAt = whenOf(now, m.GetCreatedAt()) | |
| 442 | + | if t := m.GetCreatedAt(); t.IsValid() { | |
| 443 | + | p.Opened.Abs = absolute(t.AsTime()) | |
| 444 | + | } | |
| 445 | + | p.Updated, p.UpdatedAt = whenOf(now, m.GetUpdatedAt()) | |
| 446 | + | for _, b := range d.branches { | |
| 447 | + | switch b.GetShortName() { | |
| 448 | + | case m.GetSourceBranch(): | |
| 449 | + | p.SourceHref = repoHref(d.name(), "tree", b.GetShortName()) | |
| 450 | + | case m.GetTargetBranch(): | |
| 451 | + | p.TargetHref = repoHref(d.name(), "tree", b.GetShortName()) | |
| 452 | + | } | |
| 453 | + | } | |
| 454 | + | if m.GetState() == pb.MergeRequestState_MERGE_REQUEST_STATE_MERGED && m.GetMergedCommitId() != "" { | |
| 455 | + | p.MergedShort = short(m.GetMergedCommitId()) | |
| 456 | + | p.MergedHref = repoHref(d.name(), "commit", m.GetMergedCommitId()) | |
| 457 | + | p.MergeMode = modeName(m.GetMergeMode()) | |
| 458 | + | } | |
| 459 | + | if st := d.repo.GetSettings(); st != nil { | |
| 460 | + | for _, mode := range st.GetAllowedMergeModes() { | |
| 461 | + | if name := modeName(mode); name != "" { | |
| 462 | + | p.Modes = append(p.Modes, name) | |
| 463 | + | } | |
| 464 | + | } | |
| 465 | + | p.ChecksBlock = st.GetFailingChecksBlock() | |
| 466 | + | } | |
| 467 | + | p.Note = diffNote(res.Msg.GetDiff(), len(p.Files), p.NFiles) | |
| 468 | + | ||
| 469 | + | // The commits of the request, newest first. | |
| 470 | + | for _, c := range res.Msg.GetCommits() { | |
| 471 | + | row := commitRow(d.name(), c, now) | |
| 472 | + | p.Commits = append(p.Commits, MRCommit{ | |
| 473 | + | Short: row.Short, Href: row.CHref, Subject: row.Subject, Author: row.Author, When: row.When, WhenAt: row.WhenAt, | |
| 474 | + | }) | |
| 475 | + | } | |
| 476 | + | lastRead := res.Msg.GetLastReadId() | |
| 477 | + | if signed && lastRead != "" { | |
| 478 | + | // The commits are newest first: those before the last read head are new. | |
| 479 | + | // A head that is not among them (the branch was rewritten) marks none. | |
| 480 | + | for i, c := range res.Msg.GetCommits() { | |
| 481 | + | if c.GetId() != lastRead { | |
| 482 | + | continue | |
| 483 | + | } | |
| 484 | + | for j := 0; j < i; j++ { | |
| 485 | + | p.Commits[j].New = true | |
| 486 | + | } | |
| 487 | + | } | |
| 488 | + | } | |
| 489 | + | if res.Msg.GetCommitsTruncated() { | |
| 490 | + | p.CommitsNote = "the branch has more commits than the " + itoa(len(p.Commits)) + " listed." | |
| 491 | + | } | |
| 492 | + | ||
| 493 | + | // Since the last read: the navigation, and what the page says about it. | |
| 494 | + | if !signed { | |
| 495 | + | lastRead = "" // the read position is the owner's | |
| 496 | + | } | |
| 497 | + | if signed && lastRead != "" { | |
| 498 | + | p.Since = &SinceNav{ | |
| 499 | + | AllHref: mrHref(d.name(), n), SinceHref: mrHref(d.name(), n, "since"), | |
| 500 | + | ReadShort: short(lastRead), HeadShort: short(head), Current: since, | |
| 501 | + | } | |
| 502 | + | } | |
| 503 | + | if since { | |
| 504 | + | switch { | |
| 505 | + | case lastRead == "": | |
| 506 | + | p.SinceNote = "nothing was marked read yet: this is the whole change." | |
| 507 | + | case res.Msg.GetLastReadLost(): | |
| 508 | + | p.SinceNote = "the commit you last read (" + short(lastRead) + ") is gone, the branch was rewritten: this is the whole change, review it again." | |
| 509 | + | case len(p.Files) == 0: | |
| 510 | + | p.SinceNote = "nothing changed since " + short(lastRead) + "." | |
| 511 | + | default: | |
| 512 | + | p.SinceNote = "changes since " + short(lastRead) + ", the head you last read." | |
| 513 | + | } | |
| 514 | + | } | |
| 515 | + | if p.Since != nil { | |
| 516 | + | p.Since.Files = len(p.Files) | |
| 517 | + | } | |
| 518 | + | ||
| 519 | + | // Comments: under their line, else in the list below the diff. The forms | |
| 520 | + | // are the owner's, on the whole diff of an open request. | |
| 521 | + | canWrite := signed && open && !since | |
| 522 | + | var on *lineRef | |
| 523 | + | if canWrite { | |
| 524 | + | on = parseOn(r.URL.Query().Get("on")) | |
| 525 | + | } | |
| 526 | + | s.placeComments(&p, d.name(), n, res.Msg.GetComments(), head, !since, canWrite, on) | |
| 527 | + | lines, invisible := diffLines(p.Files) | |
| 528 | + | p.Invisible = invisible | |
| 529 | + | p.DiffView = newDiffView(r, lines, self+"?view=unified", self+"?view=split") | |
| 530 | + | if signed && open { | |
| 531 | + | p.Box = s.mergeBox(d, n, head, lastRead) | |
| 532 | + | } | |
| 533 | + | if canWrite { | |
| 534 | + | p.GeneralForm = &CommentForm{ | |
| 535 | + | ID: "cf-general", Action: mrHref(d.name(), n, "comments"), Title: "comment on the merge request", | |
| 536 | + | } | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | l := s.repoLayout(ctx, d, "merge-requests") | |
| 540 | + | l.Title = p.Number + " " + p.Title + " · " + d.name() + " · " + s.instance | |
| 541 | + | if head != "" { | |
| 542 | + | pinRef(&l, target{rev: head, name: m.GetSourceBranch()}) | |
| 543 | + | if !d.hasBranch(m.GetSourceBranch()) { | |
| 544 | + | pinRef(&l, target{rev: head, name: short(head), detached: true}) | |
| 545 | + | } | |
| 546 | + | } | |
| 547 | + | l.Crumbs[0].Current = false | |
| 548 | + | l.Crumbs = append(l.Crumbs, | |
| 549 | + | Crumb{Label: "merge requests", Href: mrBase(d.name())}, | |
| 550 | + | Crumb{Label: p.Number, Href: mrHref(d.name(), n), Current: !since}, | |
| 551 | + | ) | |
| 552 | + | if since { | |
| 553 | + | l.Crumbs = append(l.Crumbs, Crumb{Label: "since", Current: true}) | |
| 554 | + | } | |
| 555 | + | p.Live = true | |
| 556 | + | l.Live, l.LiveURL = true, liveURL("repo:"+d.name(), "view=mr&n="+strconv.FormatInt(n, 10)+"&ref="+url.QueryEscape(m.GetSourceBranch())) | |
| 557 | + | l.Status = Status{ | |
| 558 | + | Mode: "merge request", Context: p.Number + " · " + p.Source + " → " + p.Target + " · " + p.State, | |
| 559 | + | Hints: "n next note · d split · u unified · x collapse all · ? help", | |
| 560 | + | } | |
| 561 | + | l.HasRail, l.RailSum = true, "merge · checks · commits · files" | |
| 562 | + | return MergeRequest(l, p), http.StatusOK, nil | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | // hasBranch tells whether the repository has the branch. | |
| 566 | + | func (d *repoData) hasBranch(name string) bool { | |
| 567 | + | for _, b := range d.branches { | |
| 568 | + | if b.GetShortName() == name { | |
| 569 | + | return true | |
| 570 | + | } | |
| 571 | + | } | |
| 572 | + | return false | |
| 573 | + | } | |
| 574 | + | ||
| 575 | + | // mergeBox is what the owner can do with an open request. | |
| 576 | + | func (s *Server) mergeBox(d *repoData, n int64, head, lastRead string) *MergeBox { | |
| 577 | + | b := &MergeBox{ | |
| 578 | + | MergeAction: mrHref(d.name(), n, "merge"), CloseAction: mrHref(d.name(), n, "close"), | |
| 579 | + | ReadAction: mrHref(d.name(), n, "read"), Head: head, HeadGone: head == "", | |
| 580 | + | CanMarkRead: head != "" && head != lastRead, | |
| 581 | + | } | |
| 582 | + | ff, sq := false, false | |
| 583 | + | if st := d.repo.GetSettings(); st != nil { | |
| 584 | + | for _, mode := range st.GetAllowedMergeModes() { | |
| 585 | + | switch mode { | |
| 586 | + | case pb.MergeMode_MERGE_MODE_FAST_FORWARD: | |
| 587 | + | ff = true | |
| 588 | + | b.Modes = append(b.Modes, ModeOption{Value: "fast-forward", Label: "fast-forward"}) | |
| 589 | + | case pb.MergeMode_MERGE_MODE_SQUASH: | |
| 590 | + | sq = true | |
| 591 | + | b.Modes = append(b.Modes, ModeOption{Value: "squash", Label: "squash (one signed commit)"}) | |
| 592 | + | } | |
| 593 | + | } | |
| 594 | + | } | |
| 595 | + | if head != "" { | |
| 596 | + | cmd := "ermite mr merge " + shellWord(d.name()) + " " + strconv.FormatInt(n, 10) + " --head " + head | |
| 597 | + | b.CLI = Clone{ID: "cli-merge"} | |
| 598 | + | if ff { | |
| 599 | + | b.CLI.Lines = append(b.CLI.Lines, cmd) | |
| 600 | + | } | |
| 601 | + | if sq { | |
| 602 | + | b.CLI.Lines = append(b.CLI.Lines, cmd+" --squash") | |
| 603 | + | } | |
| 604 | + | } | |
| 605 | + | return b | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | // shellWord quotes s for a shell unless it is made of safe characters, which | |
| 609 | + | // a repository name always is. | |
| 610 | + | func shellWord(s string) string { | |
| 611 | + | for _, c := range []byte(s) { | |
| 612 | + | if (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.' && c != '_' && c != '/' { | |
| 613 | + | return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" | |
| 614 | + | } | |
| 615 | + | } | |
| 616 | + | return s | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | // lineRef designates a line of the diff in the query of the comment form: | |
| 620 | + | // "?on=new:12:path/to/file". | |
| 621 | + | type lineRef struct { | |
| 622 | + | side string | |
| 623 | + | line int | |
| 624 | + | path string | |
| 625 | + | } | |
| 626 | + | ||
| 627 | + | func parseOn(s string) *lineRef { | |
| 628 | + | side, rest, ok := strings.Cut(s, ":") | |
| 629 | + | if !ok || (side != "new" && side != "old") { | |
| 630 | + | return nil | |
| 631 | + | } | |
| 632 | + | num, path, ok := strings.Cut(rest, ":") | |
| 633 | + | line, err := strconv.Atoi(num) | |
| 634 | + | if !ok || err != nil || line < 1 || path == "" { | |
| 635 | + | return nil | |
| 636 | + | } | |
| 637 | + | return &lineRef{side: side, line: line, path: path} | |
| 638 | + | } | |
| 639 | + | ||
| 640 | + | // placeComments puts every comment under its line when the diff shown has it, | |
| 641 | + | // else in the list below the diff. The comments name lines of the diff against | |
| 642 | + | // the target, so a page that shows another diff (since the last read) lists | |
| 643 | + | // them all below, with their line. A reader who can write gets the line numbers | |
| 644 | + | // as links to the form of their line; on designates the line whose form shows. | |
| 645 | + | func (s *Server) placeComments(p *MRPage, repo string, n int64, comments []*pb.Comment, head string, inline, canWrite bool, on *lineRef) { | |
| 646 | + | now := s.cfg.Now() | |
| 647 | + | self := mrHref(repo, n) | |
| 648 | + | resolveAction := func(id string) string { return mrHref(repo, n, "comments", id, "resolve") } | |
| 649 | + | ||
| 650 | + | type lineKey struct { | |
| 651 | + | path string | |
| 652 | + | side pb.DiffSide | |
| 653 | + | line int | |
| 654 | + | } | |
| 655 | + | index := map[lineKey]*DiffLine{} | |
| 656 | + | fileIDs := map[string]string{} | |
| 657 | + | for fi := range p.Files { | |
| 658 | + | f := &p.Files[fi] | |
| 659 | + | fileIDs[f.Key] = f.ID | |
| 660 | + | linkable := canWrite && f.Key != "" && !strings.ContainsFunc(f.Key, invisible) | |
| 661 | + | for hi := range f.Hunks { | |
| 662 | + | for li := range f.Hunks[hi].Lines { | |
| 663 | + | ln := &f.Hunks[hi].Lines[li] | |
| 664 | + | if ln.oldN > 0 && ln.Kind != "add" { | |
| 665 | + | index[lineKey{f.Key, pb.DiffSide_DIFF_SIDE_OLD, ln.oldN}] = ln | |
| 666 | + | if linkable { | |
| 667 | + | ln.OldHref = self + "?on=old:" + itoa(ln.oldN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 668 | + | } | |
| 669 | + | } | |
| 670 | + | if ln.newN > 0 && ln.Kind != "del" { | |
| 671 | + | index[lineKey{f.Key, pb.DiffSide_DIFF_SIDE_NEW, ln.newN}] = ln | |
| 672 | + | if linkable { | |
| 673 | + | ln.NewHref = self + "?on=new:" + itoa(ln.newN) + ":" + url.QueryEscape(f.Key) + "#" + f.ID | |
| 674 | + | } | |
| 675 | + | } | |
| 676 | + | } | |
| 677 | + | } | |
| 678 | + | } | |
| 679 | + | find := func(path string, side pb.DiffSide, line int) *DiffLine { | |
| 680 | + | if !inline { | |
| 681 | + | return nil | |
| 682 | + | } | |
| 683 | + | return index[lineKey{path, side, line}] | |
| 684 | + | } | |
| 685 | + | ||
| 686 | + | for _, c := range comments { | |
| 687 | + | v := CommentView{ | |
| 688 | + | ID: c.GetId(), Author: visibleText(c.GetAuthor()), | |
| 689 | + | Parts: plainParts(c.GetText()), Resolved: c.GetResolved(), | |
| 690 | + | Outdated: c.GetCommitId() != "" && c.GetCommitId() != head, | |
| 691 | + | } | |
| 692 | + | v.When, v.WhenAt = whenOf(now, c.GetCreatedAt()) | |
| 693 | + | if t := c.GetCreatedAt(); t.IsValid() { | |
| 694 | + | v.Abs = absolute(t.AsTime()) | |
| 695 | + | } | |
| 696 | + | p.NComments++ | |
| 697 | + | if !c.GetResolved() { | |
| 698 | + | p.NUnresolved++ | |
| 699 | + | } | |
| 700 | + | general := c.GetPath() == "" | |
| 701 | + | var ln *DiffLine | |
| 702 | + | if !general { | |
| 703 | + | ln = find(c.GetPath(), c.GetSide(), int(c.GetLine())) | |
| 704 | + | if ln == nil { | |
| 705 | + | side := "new" | |
| 706 | + | if c.GetSide() == pb.DiffSide_DIFF_SIDE_OLD { | |
| 707 | + | side = "old" | |
| 708 | + | } | |
| 709 | + | v.Where = visibleText(c.GetPath()) + ":" + itoa(int(c.GetLine())) + " (" + side + ")" | |
| 710 | + | } | |
| 711 | + | } | |
| 712 | + | if canWrite { | |
| 713 | + | v.Resolve = &ResolveForm{Action: resolveAction(c.GetId()), Value: !c.GetResolved(), Label: "[resolve]"} | |
| 714 | + | if c.GetResolved() { | |
| 715 | + | v.Resolve.Label = "[unresolve]" | |
| 716 | + | } | |
| 717 | + | } | |
| 718 | + | switch { | |
| 719 | + | case ln != nil: | |
| 720 | + | ln.Notes = append(ln.Notes, v) | |
| 721 | + | case general: | |
| 722 | + | p.General = append(p.General, v) | |
| 723 | + | default: | |
| 724 | + | p.Loose = append(p.Loose, v) | |
| 725 | + | } | |
| 726 | + | } | |
| 727 | + | ||
| 728 | + | if on != nil { | |
| 729 | + | side := pb.DiffSide_DIFF_SIDE_NEW | |
| 730 | + | if on.side == "old" { | |
| 731 | + | side = pb.DiffSide_DIFF_SIDE_OLD | |
| 732 | + | } | |
| 733 | + | if ln := find(on.path, side, on.line); ln != nil { | |
| 734 | + | ln.Form = &CommentForm{ | |
| 735 | + | ID: "cf-line", Action: mrHref(repo, n, "comments"), Path: on.path, Line: itoa(on.line), Side: on.side, | |
| 736 | + | Title: "comment on " + visibleText(on.path) + ":" + itoa(on.line) + " (" + on.side + ")", | |
| 737 | + | Cancel: self + "#" + fileIDs[on.path], Focus: true, | |
| 738 | + | } | |
| 739 | + | } | |
| 740 | + | } | |
| 741 | + | } | |
| 742 | + | ||
| 743 | + | // userText renders the parts of a plain text: the text escaped, the http(s) | |
| 744 | + | // addresses as links. Its white space is kept by the style of the container. | |
| 745 | + | func userText(parts []TextPart) templ.Component { | |
| 746 | + | return templ.ComponentFunc(func(_ context.Context, w io.Writer) error { | |
| 747 | + | var b strings.Builder | |
| 748 | + | for _, part := range parts { | |
| 749 | + | if part.Href != "" { | |
| 750 | + | b.WriteString(`<a href="` + html.EscapeString(part.Href) + `" rel="noopener nofollow ugc">` + html.EscapeString(part.Text) + `</a>`) | |
| 751 | + | } else { | |
| 752 | + | b.WriteString(html.EscapeString(part.Text)) | |
| 753 | + | } | |
| 754 | + | } | |
| 755 | + | _, err := io.WriteString(w, b.String()) | |
| 756 | + | return err | |
| 757 | + | }) | |
| 758 | + | } | |
A internal/adapters/web/mr.templ +506 -0
| @@ -0,0 +1,506 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import "strings" | |
| 6 | + | ||
| 7 | + | // MRList is the list of the merge requests of a repository: the open ones, | |
| 8 | + | // then the merged and closed ones. | |
| 9 | + | templ MRList(l Layout, p MRListPage) { | |
| 10 | + | @Shell(l, mrListRail(p)) { | |
| 11 | + | if p.Live { | |
| 12 | + | <div id="mr-notice"></div> | |
| 13 | + | } | |
| 14 | + | <div class="filter"> | |
| 15 | + | <label for="mr-filter">filter></label> | |
| 16 | + | <input | |
| 17 | + | id="mr-filter" | |
| 18 | + | type="search" | |
| 19 | + | data-filter | |
| 20 | + | placeholder="title, branch, author or state ( / )" | |
| 21 | + | autocomplete="off" | |
| 22 | + | hx-on:input="let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t)); | |
| 23 | + | let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')" | |
| 24 | + | /> | |
| 25 | + | </div> | |
| 26 | + | if p.Newest == "" { | |
| 27 | + | <h2 class="rule-h"><span>open</span><span>{ itoa(len(p.Open)) }</span></h2> | |
| 28 | + | @mrRows(p.Open, "Open merge requests") | |
| 29 | + | if len(p.Open) == 0 { | |
| 30 | + | <p class="lede dim">no open merge requests.</p> | |
| 31 | + | } | |
| 32 | + | if p.OpenNote != "" { | |
| 33 | + | <p class="note">{ p.OpenNote }</p> | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | <h2 class="rule-h"><span>merged and closed</span><span>{ itoa(len(p.Done)) }</span></h2> | |
| 37 | + | @mrRows(p.Done, "Merged and closed merge requests") | |
| 38 | + | if len(p.Done) == 0 { | |
| 39 | + | <p class="lede dim">nothing merged or closed yet.</p> | |
| 40 | + | } | |
| 41 | + | if p.Older != "" || p.Newest != "" { | |
| 42 | + | <nav class="pager" aria-label="Pages"> | |
| 43 | + | if p.Newest != "" { | |
| 44 | + | <a href={ templ.SafeURL(p.Newest) }>← newest</a> | |
| 45 | + | } else { | |
| 46 | + | <span class="mute">← newest</span> | |
| 47 | + | } | |
| 48 | + | if p.Older != "" { | |
| 49 | + | <a href={ templ.SafeURL(p.Older) } rel="next">older →</a> | |
| 50 | + | } else { | |
| 51 | + | <span class="mute">older →</span> | |
| 52 | + | } | |
| 53 | + | </nav> | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | templ mrRows(rows []MRRow, label string) { | |
| 59 | + | if len(rows) > 0 { | |
| 60 | + | <table class="list mrs" aria-label={ label }> | |
| 61 | + | <tbody data-rows> | |
| 62 | + | for _, m := range rows { | |
| 63 | + | <tr data-cursor?={ m.Cursor } data-state={ m.State }> | |
| 64 | + | <td class="cur" aria-hidden="true"></td> | |
| 65 | + | <td class="c-n mute">{ m.Number }</td> | |
| 66 | + | <td class="c-state">{ m.State }</td> | |
| 67 | + | <td class="c-subject"><a href={ templ.SafeURL(m.Href) }>{ m.Title }</a> <span class="mute">{ m.Branches }</span></td> | |
| 68 | + | <td class="c-who dim">{ m.Author }</td> | |
| 69 | + | <td class="c-when nowrap dim"> | |
| 70 | + | if m.WhenAt != "" { | |
| 71 | + | <time datetime={ m.WhenAt } title={ m.WhenAt }>{ m.When }</time> | |
| 72 | + | } | |
| 73 | + | </td> | |
| 74 | + | </tr> | |
| 75 | + | } | |
| 76 | + | </tbody> | |
| 77 | + | </table> | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | templ mrListRail(p MRListPage) { | |
| 82 | + | <section aria-labelledby="r-states"> | |
| 83 | + | <h2 id="r-states">states</h2> | |
| 84 | + | <dl class="kv"> | |
| 85 | + | <dt>open</dt> | |
| 86 | + | <dd>awaits review</dd> | |
| 87 | + | <dt>merged</dt> | |
| 88 | + | <dd>the target took the branch</dd> | |
| 89 | + | <dt>closed</dt> | |
| 90 | + | <dd>refused, branch kept</dd> | |
| 91 | + | </dl> | |
| 92 | + | </section> | |
| 93 | + | <section aria-labelledby="r-new"> | |
| 94 | + | <h2 id="r-new">open one</h2> | |
| 95 | + | if len(p.Branches) > 0 { | |
| 96 | + | <p class="dim">compare a branch with { p.Default }:</p> | |
| 97 | + | <nav class="outline" aria-label="Branches"> | |
| 98 | + | for _, b := range p.Branches { | |
| 99 | + | <a href={ templ.SafeURL(b.Href) }>{ b.Name }</a> | |
| 100 | + | } | |
| 101 | + | </nav> | |
| 102 | + | } else { | |
| 103 | + | <p class="dim">there is no other branch to merge yet.</p> | |
| 104 | + | } | |
| 105 | + | @CloneBlock(p.CLI) | |
| 106 | + | </section> | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // MergeRequest is a merge request: its facts, its description, the diff with | |
| 110 | + | // the notes under their lines, the general comments; and in the rail the merge | |
| 111 | + | // form, the checks, the settings, the commits and the files. | |
| 112 | + | templ MergeRequest(l Layout, p MRPage) { | |
| 113 | + | @Shell(l, mrRail(p)) { | |
| 114 | + | <header class="show mr-h"> | |
| 115 | + | <h1><span class="mute">{ p.Number }</span> { p.Title }</h1> | |
| 116 | + | <dl class="kv show-kv"> | |
| 117 | + | <dt>state</dt> | |
| 118 | + | <dd class="mr-state" data-state={ p.State }> | |
| 119 | + | { p.State } | |
| 120 | + | if p.MergedShort != "" { | |
| 121 | + | { " · " }<a href={ templ.SafeURL(p.MergedHref) }>{ p.MergedShort }</a>{ " · " + p.MergeMode } | |
| 122 | + | } | |
| 123 | + | </dd> | |
| 124 | + | <dt>branch</dt> | |
| 125 | + | <dd> | |
| 126 | + | @branchLink(p.Source, p.SourceHref) | |
| 127 | + | { " → " } | |
| 128 | + | @branchLink(p.Target, p.TargetHref) | |
| 129 | + | </dd> | |
| 130 | + | <dt>opened</dt> | |
| 131 | + | <dd> | |
| 132 | + | { p.Opened.Name } | |
| 133 | + | if p.Opened.WhenAt != "" { | |
| 134 | + | { " · " }<time datetime={ p.Opened.WhenAt } title={ p.Opened.Abs }>{ p.Opened.When }</time> | |
| 135 | + | } | |
| 136 | + | </dd> | |
| 137 | + | if p.UpdatedAt != "" { | |
| 138 | + | <dt>updated</dt> | |
| 139 | + | <dd><time datetime={ p.UpdatedAt }>{ p.Updated }</time></dd> | |
| 140 | + | } | |
| 141 | + | </dl> | |
| 142 | + | </header> | |
| 143 | + | <div id="mr-notice"></div> | |
| 144 | + | if len(p.Desc) > 0 { | |
| 145 | + | <p class="mr-desc">@userText(p.Desc)</p> | |
| 146 | + | } | |
| 147 | + | if p.Since != nil { | |
| 148 | + | <nav class="since" aria-label="Diff range"> | |
| 149 | + | <a | |
| 150 | + | href={ templ.SafeURL(p.Since.AllHref) } | |
| 151 | + | if !p.Since.Current { | |
| 152 | + | aria-current="page" | |
| 153 | + | } | |
| 154 | + | >all changes <span class="mute">vs { p.Target }</span></a> | |
| 155 | + | <a | |
| 156 | + | href={ templ.SafeURL(p.Since.SinceHref) } | |
| 157 | + | if p.Since.Current { | |
| 158 | + | aria-current="page" | |
| 159 | + | } | |
| 160 | + | >since your last read <span class="mute">{ p.Since.ReadShort } → { p.Since.HeadShort }</span></a> | |
| 161 | + | </nav> | |
| 162 | + | } | |
| 163 | + | if p.SinceNote != "" { | |
| 164 | + | <p class="notice">{ p.SinceNote }</p> | |
| 165 | + | } | |
| 166 | + | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 167 | + | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 168 | + | <span class="grow"></span> | |
| 169 | + | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | |
| 170 | + | @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref) | |
| 171 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | |
| 172 | + | </div> | |
| 173 | + | if p.Invisible { | |
| 174 | + | <p class="notice"> | |
| 175 | + | this diff holds invisible characters (direction controls, zero-width characters). They are shown as | |
| 176 | + | <span class="inv"><U+XXXX></span> markers, never applied. | |
| 177 | + | </p> | |
| 178 | + | } | |
| 179 | + | if p.Note != "" { | |
| 180 | + | <p class="notice">{ p.Note }</p> | |
| 181 | + | } | |
| 182 | + | <div | |
| 183 | + | id="diffs" | |
| 184 | + | if p.Both { | |
| 185 | + | data-diff={ p.View } | |
| 186 | + | } else { | |
| 187 | + | data-view={ p.View } | |
| 188 | + | } | |
| 189 | + | if p.Both && !p.Pinned { | |
| 190 | + | hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff" | |
| 191 | + | } | |
| 192 | + | > | |
| 193 | + | for _, f := range p.Files { | |
| 194 | + | @DiffFileView(f, p.DiffView) | |
| 195 | + | } | |
| 196 | + | if len(p.Files) == 0 { | |
| 197 | + | <p class="note">no changed files.</p> | |
| 198 | + | } | |
| 199 | + | </div> | |
| 200 | + | <h2 class="rule-h" id="comments"><span>comments</span><span>{ itoa(p.NUnresolved) } open / { itoa(p.NComments) }</span></h2> | |
| 201 | + | for _, c := range p.Loose { | |
| 202 | + | @commentView(c, "") | |
| 203 | + | } | |
| 204 | + | for _, c := range p.General { | |
| 205 | + | @commentView(c, "") | |
| 206 | + | } | |
| 207 | + | if p.NComments == 0 { | |
| 208 | + | <p class="dim">no comments yet.</p> | |
| 209 | + | } | |
| 210 | + | if p.GeneralForm != nil { | |
| 211 | + | @commentForm(*p.GeneralForm, "") | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | templ branchLink(name, href string) { | |
| 217 | + | if href != "" { | |
| 218 | + | <a href={ templ.SafeURL(href) }>{ name }</a> | |
| 219 | + | } else { | |
| 220 | + | { name } | |
| 221 | + | } | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // lineNotes are the comments under a line of the diff, and the form to write | |
| 225 | + | // one when the reader asked for it. The diff is in the page twice (unified and | |
| 226 | + | // split); the suffix keeps the ids unique, the unified table having none. | |
| 227 | + | templ lineNotes(notes []CommentView, form *CommentForm, suffix string) { | |
| 228 | + | for _, c := range notes { | |
| 229 | + | @commentView(c, suffix) | |
| 230 | + | } | |
| 231 | + | if form != nil { | |
| 232 | + | @commentForm(*form, suffix) | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // commentView is a note: a ruled block, under its line in the diff or in the | |
| 237 | + | // list of comments. The text is plain: escaped, its line breaks kept, its | |
| 238 | + | // http(s) addresses linked. | |
| 239 | + | templ commentView(c CommentView, suffix string) { | |
| 240 | + | <div class="cmt" id={ "c" + c.ID + suffix } data-resolved={ boolText(c.Resolved) }> | |
| 241 | + | <p class="c-head"> | |
| 242 | + | <span class="c-who">{ c.Author }</span> | |
| 243 | + | if c.WhenAt != "" { | |
| 244 | + | { " " }<time class="mute" datetime={ c.WhenAt } title={ c.Abs }>{ c.When }</time> | |
| 245 | + | } | |
| 246 | + | if c.Resolved { | |
| 247 | + | { " " }<span class="c-state">resolved</span> | |
| 248 | + | } | |
| 249 | + | if c.Outdated { | |
| 250 | + | { " " }<span class="c-state" title="written on an earlier head; the line may have moved">outdated</span> | |
| 251 | + | } | |
| 252 | + | if c.Where != "" { | |
| 253 | + | { " " }<span class="c-where mute">{ c.Where }</span> | |
| 254 | + | } | |
| 255 | + | </p> | |
| 256 | + | <p class="c-text">@userText(c.Parts)</p> | |
| 257 | + | if c.Resolve != nil { | |
| 258 | + | <form class="c-act" method="post" action={ templ.SafeURL(c.Resolve.Action) }> | |
| 259 | + | <input type="hidden" name="resolved" value={ boolText(c.Resolve.Value) }/> | |
| 260 | + | <button type="submit">{ c.Resolve.Label }</button> | |
| 261 | + | </form> | |
| 262 | + | } | |
| 263 | + | </div> | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | func boolText(b bool) string { | |
| 267 | + | if b { | |
| 268 | + | return "true" | |
| 269 | + | } | |
| 270 | + | return "false" | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | // commentForm writes a comment, general or on a line. | |
| 274 | + | templ commentForm(f CommentForm, suffix string) { | |
| 275 | + | <form class="cform" method="post" action={ templ.SafeURL(f.Action) }> | |
| 276 | + | <label for={ f.ID + suffix }>{ f.Title }</label> | |
| 277 | + | <textarea | |
| 278 | + | id={ f.ID + suffix } | |
| 279 | + | name="text" | |
| 280 | + | rows="3" | |
| 281 | + | maxlength="8000" | |
| 282 | + | required | |
| 283 | + | placeholder="plain text, 8000 characters at most" | |
| 284 | + | if f.Focus { | |
| 285 | + | autofocus | |
| 286 | + | } | |
| 287 | + | ></textarea> | |
| 288 | + | if f.Path != "" { | |
| 289 | + | <input type="hidden" name="path" value={ f.Path }/> | |
| 290 | + | <input type="hidden" name="line" value={ f.Line }/> | |
| 291 | + | <input type="hidden" name="side" value={ f.Side }/> | |
| 292 | + | } | |
| 293 | + | <div class="f-btns"> | |
| 294 | + | <button type="submit" class="f-main">[comment]</button> | |
| 295 | + | if f.Cancel != "" { | |
| 296 | + | <a href={ templ.SafeURL(f.Cancel) }>[cancel]</a> | |
| 297 | + | } | |
| 298 | + | </div> | |
| 299 | + | </form> | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | templ mrRail(p MRPage) { | |
| 303 | + | if p.Box != nil { | |
| 304 | + | @mergeBox(p) | |
| 305 | + | } | |
| 306 | + | <section aria-labelledby="r-checks"> | |
| 307 | + | <h2 id="r-checks">checks <span>0</span></h2> | |
| 308 | + | <p class="dim">no checks</p> | |
| 309 | + | <p class="mute">there is no CI yet: nothing blocks the merge.</p> | |
| 310 | + | </section> | |
| 311 | + | <section aria-labelledby="r-settings"> | |
| 312 | + | <h2 id="r-settings">settings</h2> | |
| 313 | + | <dl class="kv"> | |
| 314 | + | <dt>modes</dt> | |
| 315 | + | <dd>{ strings.Join(p.Modes, ", ") }</dd> | |
| 316 | + | <dt>checks</dt> | |
| 317 | + | <dd> | |
| 318 | + | if p.ChecksBlock { | |
| 319 | + | failing ones block the merge | |
| 320 | + | } else { | |
| 321 | + | do not block the merge | |
| 322 | + | } | |
| 323 | + | </dd> | |
| 324 | + | </dl> | |
| 325 | + | </section> | |
| 326 | + | <section aria-labelledby="r-commits"> | |
| 327 | + | <h2 id="r-commits">commits <span>{ itoa(len(p.Commits)) }</span></h2> | |
| 328 | + | <ul class="pcommits"> | |
| 329 | + | for _, c := range p.Commits { | |
| 330 | + | <li data-new?={ c.New }> | |
| 331 | + | <a href={ templ.SafeURL(c.Href) }>{ c.Short }</a> | |
| 332 | + | <span class="s">{ c.Subject }</span> | |
| 333 | + | <span class="mute"> | |
| 334 | + | { c.Author } | |
| 335 | + | if c.WhenAt != "" { | |
| 336 | + | { " · " }<time datetime={ c.WhenAt }>{ c.When }</time> | |
| 337 | + | } | |
| 338 | + | if c.New { | |
| 339 | + | { " · new" } | |
| 340 | + | } | |
| 341 | + | </span> | |
| 342 | + | </li> | |
| 343 | + | } | |
| 344 | + | </ul> | |
| 345 | + | if len(p.Commits) == 0 { | |
| 346 | + | <p class="dim">no commit to merge.</p> | |
| 347 | + | } | |
| 348 | + | if p.CommitsNote != "" { | |
| 349 | + | <p class="note">{ p.CommitsNote }</p> | |
| 350 | + | } | |
| 351 | + | </section> | |
| 352 | + | <section aria-labelledby="r-files"> | |
| 353 | + | <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2> | |
| 354 | + | @FileList(p.Files) | |
| 355 | + | </section> | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | // mergeBox is the first block of the rail of an open request, for the owner: | |
| 359 | + | // the merge form (it carries the head that was read), the terminal lines, and | |
| 360 | + | // the other things one does with the request. | |
| 361 | + | templ mergeBox(p MRPage) { | |
| 362 | + | <section class="decide" aria-labelledby="r-merge" data-state="open"> | |
| 363 | + | <h2 id="r-merge">merge</h2> | |
| 364 | + | <dl class="kv"> | |
| 365 | + | <dt>head</dt> | |
| 366 | + | <dd> | |
| 367 | + | if p.Box.HeadGone { | |
| 368 | + | gone: the source branch was deleted | |
| 369 | + | } else { | |
| 370 | + | { p.HeadShort } | |
| 371 | + | } | |
| 372 | + | </dd> | |
| 373 | + | <dt>into</dt> | |
| 374 | + | <dd>{ p.Target }</dd> | |
| 375 | + | </dl> | |
| 376 | + | if !p.Box.HeadGone { | |
| 377 | + | <form class="d-acts" method="post" action={ templ.SafeURL(p.Box.MergeAction) }> | |
| 378 | + | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 379 | + | <label class="sr" for="merge-mode">merge mode</label> | |
| 380 | + | <select id="merge-mode" name="mode"> | |
| 381 | + | for _, m := range p.Box.Modes { | |
| 382 | + | <option value={ m.Value }>{ m.Label }</option> | |
| 383 | + | } | |
| 384 | + | </select> | |
| 385 | + | <div class="d-btns"> | |
| 386 | + | <button type="submit" class="d-main">[merge]</button> | |
| 387 | + | </div> | |
| 388 | + | </form> | |
| 389 | + | } | |
| 390 | + | <div class="d-btns"> | |
| 391 | + | if p.Box.CanMarkRead { | |
| 392 | + | <form method="post" action={ templ.SafeURL(p.Box.ReadAction) }> | |
| 393 | + | <input type="hidden" name="head_id" value={ p.Box.Head }/> | |
| 394 | + | <button type="submit" title="the next visit can show only what changed after this head">[mark read]</button> | |
| 395 | + | </form> | |
| 396 | + | } | |
| 397 | + | <form method="post" action={ templ.SafeURL(p.Box.CloseAction) }> | |
| 398 | + | <button type="submit" title="close without merging; the branch is kept">[close]</button> | |
| 399 | + | </form> | |
| 400 | + | </div> | |
| 401 | + | if !p.Box.HeadGone { | |
| 402 | + | <p class="mute d-or">or from a terminal</p> | |
| 403 | + | @CloneBlock(p.Box.CLI) | |
| 404 | + | } | |
| 405 | + | </section> | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | // Compare is a branch against the default branch: the commits it is ahead by, | |
| 409 | + | // its diff when the API can give one, and the form that opens a merge request. | |
| 410 | + | templ Compare(l Layout, p ComparePage) { | |
| 411 | + | @Shell(l, compareRail(p)) { | |
| 412 | + | <header class="show mr-h"> | |
| 413 | + | <h1>{ p.Source } → { p.Target }</h1> | |
| 414 | + | <dl class="kv show-kv"> | |
| 415 | + | <dt>branch</dt> | |
| 416 | + | <dd><a href={ templ.SafeURL(p.SourceHref) }>{ p.Source }</a></dd> | |
| 417 | + | <dt>target</dt> | |
| 418 | + | <dd><a href={ templ.SafeURL(p.TargetHref) }>{ p.Target }</a></dd> | |
| 419 | + | <dt>ahead</dt> | |
| 420 | + | <dd>{ p.AheadText }</dd> | |
| 421 | + | </dl> | |
| 422 | + | </header> | |
| 423 | + | if p.Note != "" { | |
| 424 | + | <p class="notice">{ p.Note }</p> | |
| 425 | + | } | |
| 426 | + | if p.Existing != nil { | |
| 427 | + | <p class="notice">an open merge request already covers these branches: <a href={ templ.SafeURL(p.Existing.Href) }>{ p.Existing.Label }</a></p> | |
| 428 | + | } | |
| 429 | + | if p.Create != nil { | |
| 430 | + | <form class="cform create" method="post" action={ templ.SafeURL(p.Create.Action) }> | |
| 431 | + | <input type="hidden" name="source" value={ p.Create.Source }/> | |
| 432 | + | <input type="hidden" name="target" value={ p.Create.Target }/> | |
| 433 | + | <label for="mr-title">title</label> | |
| 434 | + | <input id="mr-title" name="title" type="text" maxlength="120" required value={ p.Create.Title } autocomplete="off"/> | |
| 435 | + | <label for="mr-desc">description</label> | |
| 436 | + | <textarea id="mr-desc" name="description" rows="4" maxlength="8000" placeholder="plain text, optional"></textarea> | |
| 437 | + | <div class="f-btns"> | |
| 438 | + | <button type="submit" class="f-main">[create merge request]</button> | |
| 439 | + | </div> | |
| 440 | + | </form> | |
| 441 | + | } | |
| 442 | + | if p.HasDiff { | |
| 443 | + | <div class="diff-tools" role="toolbar" aria-label="Diff view"> | |
| 444 | + | <span class="dim">{ plural(p.NFiles, "file", "files") } · <span class="plus">+{ itoa(p.Adds) }</span> <span class="minus">-{ itoa(p.Dels) }</span></span> | |
| 445 | + | <span class="grow"></span> | |
| 446 | + | @viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref) | |
| 447 | + | @viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref) | |
| 448 | + | <button type="button" aria-keyshortcuts="x" hx-on:click="document.body.dispatchEvent(new CustomEvent('k-collapse'))">[x] collapse all</button> | |
| 449 | + | </div> | |
| 450 | + | if p.Invisible { | |
| 451 | + | <p class="notice"> | |
| 452 | + | this diff holds invisible characters (direction controls, zero-width characters). They are shown as | |
| 453 | + | <span class="inv"><U+XXXX></span> markers, never applied. | |
| 454 | + | </p> | |
| 455 | + | } | |
| 456 | + | <div | |
| 457 | + | id="diffs" | |
| 458 | + | if p.Both { | |
| 459 | + | data-diff={ p.View } | |
| 460 | + | } else { | |
| 461 | + | data-view={ p.View } | |
| 462 | + | } | |
| 463 | + | if p.Both && !p.Pinned { | |
| 464 | + | hx-live="this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff" | |
| 465 | + | } | |
| 466 | + | > | |
| 467 | + | for _, f := range p.Files { | |
| 468 | + | @DiffFileView(f, p.DiffView) | |
| 469 | + | } | |
| 470 | + | </div> | |
| 471 | + | } | |
| 472 | + | } | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | templ compareRail(p ComparePage) { | |
| 476 | + | <section aria-labelledby="r-ahead"> | |
| 477 | + | <h2 id="r-ahead">commits <span>{ itoa(len(p.Ahead)) }</span></h2> | |
| 478 | + | <ul class="pcommits"> | |
| 479 | + | for _, c := range p.Ahead { | |
| 480 | + | <li> | |
| 481 | + | <a href={ templ.SafeURL(c.CHref) }>{ c.Short }</a> | |
| 482 | + | <span class="s">{ c.Subject }</span> | |
| 483 | + | <span class="mute"> | |
| 484 | + | { c.Author } | |
| 485 | + | if c.WhenAt != "" { | |
| 486 | + | { " · " }<time datetime={ c.WhenAt }>{ c.When }</time> | |
| 487 | + | } | |
| 488 | + | </span> | |
| 489 | + | </li> | |
| 490 | + | } | |
| 491 | + | </ul> | |
| 492 | + | if len(p.Ahead) == 0 { | |
| 493 | + | <p class="dim">no commit ahead of { p.Target }.</p> | |
| 494 | + | } | |
| 495 | + | </section> | |
| 496 | + | if p.HasDiff { | |
| 497 | + | <section aria-labelledby="r-files"> | |
| 498 | + | <h2 id="r-files">files <span>{ itoa(len(p.Files)) }</span></h2> | |
| 499 | + | @FileList(p.Files) | |
| 500 | + | </section> | |
| 501 | + | } | |
| 502 | + | <section aria-labelledby="r-cli"> | |
| 503 | + | <h2 id="r-cli">from a terminal</h2> | |
| 504 | + | @CloneBlock(p.CLI) | |
| 505 | + | </section> | |
| 506 | + | } | |
A internal/adapters/web/mr_post.go +267 -0
| @@ -0,0 +1,267 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "net/http" | |
| 10 | + | "strconv" | |
| 11 | + | "strings" | |
| 12 | + | ||
| 13 | + | "connectrpc.com/connect" | |
| 14 | + | "github.com/bnema/zerowrap" | |
| 15 | + | ||
| 16 | + | pb "bnema.dev/ermite-api/gen/ermite" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // The writes of the merge request pages. Each is a POST that passed the gate | |
| 20 | + | // (an Origin of this site and the owner's session), calls the API as the | |
| 21 | + | // owner, and answers 303 to the page that shows the result, with | |
| 22 | + | // "Cache-Control: no-store". A request the API refuses is answered with a | |
| 23 | + | // state page that says why, in the words of the API for the rules the owner | |
| 24 | + | // can fix (a text rule, a moved branch, a full merge request), and without | |
| 25 | + | // detail for anything else. | |
| 26 | + | ||
| 27 | + | // maxFormBytes bounds the body of a form: a description of 8,000 characters is | |
| 28 | + | // at most 32,000 bytes. | |
| 29 | + | const maxFormBytes = 128 << 10 | |
| 30 | + | ||
| 31 | + | // form reads the form of a POST, bounded. | |
| 32 | + | func (s *Server) form(w http.ResponseWriter, r *http.Request) bool { | |
| 33 | + | r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes) | |
| 34 | + | if err := r.ParseForm(); err != nil { | |
| 35 | + | s.reject(r.Context(), w, r, http.StatusRequestEntityTooLarge, "form too large", "the form is larger than a merge request can be.", mrBase(r.PathValue("repo"))) | |
| 36 | + | return false | |
| 37 | + | } | |
| 38 | + | return true | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // seeOther redirects to a page, never cached. | |
| 42 | + | func seeOther(w http.ResponseWriter, r *http.Request, to string) { | |
| 43 | + | w.Header().Set("Cache-Control", "no-store") | |
| 44 | + | http.Redirect(w, r, to, http.StatusSeeOther) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // mrAction wraps a POST handler: it bounds the API calls, reads the form and | |
| 48 | + | // maps the error of the API to a page. | |
| 49 | + | func (s *Server) mrAction(h func(ctx context.Context, r *http.Request) (to string, back string, err error)) http.HandlerFunc { | |
| 50 | + | return func(w http.ResponseWriter, r *http.Request) { | |
| 51 | + | ctx, cancel := context.WithTimeout(r.Context(), requestTimeout) | |
| 52 | + | defer cancel() | |
| 53 | + | if !s.form(w, r) { | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | to, back, err := h(ctx, r) | |
| 57 | + | if err != nil { | |
| 58 | + | s.actionFailed(ctx, w, r, err, back) | |
| 59 | + | return | |
| 60 | + | } | |
| 61 | + | seeOther(w, r, to) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // actionFailed answers a refused action. | |
| 66 | + | func (s *Server) actionFailed(ctx context.Context, w http.ResponseWriter, r *http.Request, err error, back string) { | |
| 67 | + | msg := "" | |
| 68 | + | var ce *connect.Error | |
| 69 | + | if errors.As(err, &ce) { | |
| 70 | + | msg = ce.Message() | |
| 71 | + | } | |
| 72 | + | switch code := connect.CodeOf(err); { | |
| 73 | + | case code == connect.CodeNotFound: | |
| 74 | + | w.Header().Set("Cache-Control", "no-store") | |
| 75 | + | s.write(ctx, w, r, http.StatusNotFound, s.notFoundPage()) | |
| 76 | + | case code == connect.CodeInvalidArgument: | |
| 77 | + | s.reject(ctx, w, r, http.StatusBadRequest, "not accepted", rule(msg, "the server refused what was sent."), back) | |
| 78 | + | case code == connect.CodeFailedPrecondition, code == connect.CodeAborted: | |
| 79 | + | s.reject(ctx, w, r, http.StatusConflict, "not possible now", rule(msg, "the merge request is not in a state that allows this."), back) | |
| 80 | + | case code == connect.CodeAlreadyExists: | |
| 81 | + | s.reject(ctx, w, r, http.StatusConflict, "already there", rule(msg, "this already exists."), back) | |
| 82 | + | case code == connect.CodeResourceExhausted: | |
| 83 | + | s.reject(ctx, w, r, http.StatusConflict, "limit reached", rule(msg, "a limit of the merge request is reached."), back) | |
| 84 | + | case code == connect.CodePermissionDenied, code == connect.CodeUnauthenticated: | |
| 85 | + | s.reject(ctx, w, r, http.StatusForbidden, "forbidden", "the session is not allowed to do this.", back) | |
| 86 | + | case errors.Is(err, context.Canceled), code == connect.CodeCanceled: | |
| 87 | + | // The browser is gone. | |
| 88 | + | default: | |
| 89 | + | id := requestID() | |
| 90 | + | log := zerowrap.FromCtx(ctx) | |
| 91 | + | log.Error().Err(err).Str("request_id", id).Str("path", logPath(r.URL.Path)).Msg("web action failed") | |
| 92 | + | w.Header().Set("Cache-Control", "no-store") | |
| 93 | + | s.write(ctx, w, r, http.StatusInternalServerError, s.errorPage(id)) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // rule is a message of the API fit to show: the invisible characters marked, | |
| 98 | + | // bounded, and a fallback for an empty one. | |
| 99 | + | func rule(msg, fallback string) string { | |
| 100 | + | msg = strings.TrimSpace(visibleText(msg)) | |
| 101 | + | if msg == "" { | |
| 102 | + | return fallback | |
| 103 | + | } | |
| 104 | + | return cutRunes(msg, 400) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // reject writes the page of a refused action, with the way back. | |
| 108 | + | func (s *Server) reject(ctx context.Context, w http.ResponseWriter, r *http.Request, status int, title, line, back string) { | |
| 109 | + | if back == "" { | |
| 110 | + | back = "/" | |
| 111 | + | } | |
| 112 | + | l := s.stateLayout(title, itoa(status)+" · "+title, []Crumb{{Label: "-"}, {Label: title}}) | |
| 113 | + | w.Header().Set("Cache-Control", "no-store") | |
| 114 | + | s.write(ctx, w, r, status, State(l, StatePage{ | |
| 115 | + | Code: itoa(status), Title: title, | |
| 116 | + | Lines: []string{line, "nothing was changed by this request."}, | |
| 117 | + | Back: []Link{{Label: "back", Href: back}, {Label: "home", Href: "/"}}, | |
| 118 | + | })) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // number reads the merge request number of the path. | |
| 122 | + | func number(r *http.Request) (int64, error) { return mrNumber(r) } | |
| 123 | + | ||
| 124 | + | // mrCreate is POST /<repo>/merge-requests: it opens a merge request. | |
| 125 | + | func (s *Server) mrCreate(ctx context.Context, r *http.Request) (string, string, error) { | |
| 126 | + | repo := r.PathValue("repo") | |
| 127 | + | source := r.PostFormValue("source") | |
| 128 | + | back := repoHref(repo, "compare", source) | |
| 129 | + | res, err := s.cfg.MergeRequests.CreateMergeRequest(ctx, connect.NewRequest(&pb.CreateMergeRequestRequest{ | |
| 130 | + | Repo: repo, Title: r.PostFormValue("title"), Description: normalizeNewlines(r.PostFormValue("description")), | |
| 131 | + | SourceBranch: source, TargetBranch: r.PostFormValue("target"), | |
| 132 | + | })) | |
| 133 | + | if err != nil { | |
| 134 | + | return "", back, err | |
| 135 | + | } | |
| 136 | + | return mrHref(repo, res.Msg.GetMergeRequest().GetNumber()), back, nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // mrComment is POST /<repo>/merge-requests/<n>/comments: a general comment, or | |
| 140 | + | // one on a line (path, line and side). | |
| 141 | + | func (s *Server) mrComment(ctx context.Context, r *http.Request) (string, string, error) { | |
| 142 | + | repo := r.PathValue("repo") | |
| 143 | + | n, err := number(r) | |
| 144 | + | if err != nil { | |
| 145 | + | return "", "", connect.NewError(connect.CodeNotFound, err) | |
| 146 | + | } | |
| 147 | + | back := mrHref(repo, n) | |
| 148 | + | req := &pb.AddCommentRequest{Repo: repo, Number: n, Text: normalizeNewlines(r.PostFormValue("text")), Path: r.PostFormValue("path")} | |
| 149 | + | if req.Path != "" || r.PostFormValue("line") != "" || r.PostFormValue("side") != "" { | |
| 150 | + | line, err := strconv.ParseInt(r.PostFormValue("line"), 10, 32) | |
| 151 | + | if err != nil { | |
| 152 | + | return "", back, connect.NewError(connect.CodeInvalidArgument, errors.New("line is not a number")) | |
| 153 | + | } | |
| 154 | + | req.Line = int32(line) | |
| 155 | + | switch r.PostFormValue("side") { | |
| 156 | + | case "new": | |
| 157 | + | req.Side = pb.DiffSide_DIFF_SIDE_NEW | |
| 158 | + | case "old": | |
| 159 | + | req.Side = pb.DiffSide_DIFF_SIDE_OLD | |
| 160 | + | default: | |
| 161 | + | return "", back, connect.NewError(connect.CodeInvalidArgument, errors.New("side must be new or old")) | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | res, err := s.cfg.MergeRequests.AddComment(ctx, connect.NewRequest(req)) | |
| 165 | + | if err != nil { | |
| 166 | + | return "", back, err | |
| 167 | + | } | |
| 168 | + | return back + "#c" + res.Msg.GetComment().GetId(), back, nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | // mrResolve is POST /<repo>/merge-requests/<n>/comments/<id>/resolve. | |
| 172 | + | // | |
| 173 | + | //hexcheck:ignore suspicious-business-logic-in-adapter reads two form fields into an API call; the state of the comment is decided by the API | |
| 174 | + | func (s *Server) mrResolve(ctx context.Context, r *http.Request) (string, string, error) { | |
| 175 | + | repo := r.PathValue("repo") | |
| 176 | + | n, err := number(r) | |
| 177 | + | if err != nil { | |
| 178 | + | return "", "", connect.NewError(connect.CodeNotFound, err) | |
| 179 | + | } | |
| 180 | + | back := mrHref(repo, n) | |
| 181 | + | resolved, err := formBool(r.PostFormValue("resolved")) | |
| 182 | + | if err != nil { | |
| 183 | + | return "", back, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("resolved: %w", err)) | |
| 184 | + | } | |
| 185 | + | res, err := s.cfg.MergeRequests.ResolveComment(ctx, connect.NewRequest(&pb.ResolveCommentRequest{ | |
| 186 | + | Repo: repo, Number: n, CommentId: r.PathValue("id"), Resolved: resolved, | |
| 187 | + | })) | |
| 188 | + | if err != nil { | |
| 189 | + | return "", back, err | |
| 190 | + | } | |
| 191 | + | return back + "#c" + res.Msg.GetComment().GetId(), back, nil | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // mrClose is POST /<repo>/merge-requests/<n>/close. | |
| 195 | + | func (s *Server) mrClose(ctx context.Context, r *http.Request) (string, string, error) { | |
| 196 | + | repo := r.PathValue("repo") | |
| 197 | + | n, err := number(r) | |
| 198 | + | if err != nil { | |
| 199 | + | return "", "", connect.NewError(connect.CodeNotFound, err) | |
| 200 | + | } | |
| 201 | + | back := mrHref(repo, n) | |
| 202 | + | if _, err := s.cfg.MergeRequests.CloseMergeRequest(ctx, connect.NewRequest(&pb.CloseMergeRequestRequest{Repo: repo, Number: n})); err != nil { | |
| 203 | + | return "", back, err | |
| 204 | + | } | |
| 205 | + | return back, back, nil | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // mrMerge is POST /<repo>/merge-requests/<n>/merge. The form carries the mode | |
| 209 | + | // and the head the owner was shown (head_id): the server merges only if the | |
| 210 | + | // branch still points to it. | |
| 211 | + | func (s *Server) mrMerge(ctx context.Context, r *http.Request) (string, string, error) { | |
| 212 | + | repo := r.PathValue("repo") | |
| 213 | + | n, err := number(r) | |
| 214 | + | if err != nil { | |
| 215 | + | return "", "", connect.NewError(connect.CodeNotFound, err) | |
| 216 | + | } | |
| 217 | + | back := mrHref(repo, n) | |
| 218 | + | var mode pb.MergeMode | |
| 219 | + | switch r.PostFormValue("mode") { | |
| 220 | + | case "fast-forward": | |
| 221 | + | mode = pb.MergeMode_MERGE_MODE_FAST_FORWARD | |
| 222 | + | case "squash": | |
| 223 | + | mode = pb.MergeMode_MERGE_MODE_SQUASH | |
| 224 | + | default: | |
| 225 | + | return "", back, connect.NewError(connect.CodeInvalidArgument, errors.New("mode must be fast-forward or squash")) | |
| 226 | + | } | |
| 227 | + | if _, err := s.cfg.MergeRequests.MergeMergeRequest(ctx, connect.NewRequest(&pb.MergeMergeRequestRequest{ | |
| 228 | + | Repo: repo, Number: n, Mode: mode, HeadId: r.PostFormValue("head_id"), | |
| 229 | + | })); err != nil { | |
| 230 | + | return "", back, err | |
| 231 | + | } | |
| 232 | + | return back, back, nil | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // mrRead is POST /<repo>/merge-requests/<n>/read: the owner marks the head | |
| 236 | + | // they were shown as read. Reading a page never does it (see the top of mr.go). | |
| 237 | + | func (s *Server) mrRead(ctx context.Context, r *http.Request) (string, string, error) { | |
| 238 | + | repo := r.PathValue("repo") | |
| 239 | + | n, err := number(r) | |
| 240 | + | if err != nil { | |
| 241 | + | return "", "", connect.NewError(connect.CodeNotFound, err) | |
| 242 | + | } | |
| 243 | + | back := mrHref(repo, n) | |
| 244 | + | if _, err := s.cfg.MergeRequests.MarkRead(ctx, connect.NewRequest(&pb.MarkReadRequest{ | |
| 245 | + | Repo: repo, Number: n, HeadId: r.PostFormValue("head_id"), | |
| 246 | + | })); err != nil { | |
| 247 | + | return "", back, err | |
| 248 | + | } | |
| 249 | + | return back, back, nil | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // formBool reads the "true" or "false" a form field carries. | |
| 253 | + | func formBool(s string) (bool, error) { | |
| 254 | + | switch s { | |
| 255 | + | case "true": | |
| 256 | + | return true, nil | |
| 257 | + | case "false": | |
| 258 | + | return false, nil | |
| 259 | + | } | |
| 260 | + | return false, errors.New("must be true or false") | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // normalizeNewlines turns the CRLF of a browser textarea into LF, the line | |
| 264 | + | // break the API stores. | |
| 265 | + | func normalizeNewlines(s string) string { | |
| 266 | + | return strings.ReplaceAll(s, "\r\n", "\n") | |
| 267 | + | } | |
A internal/adapters/web/mr_templ.go +2717 -0
| @@ -0,0 +1,2717 @@ | |||
| 1 | + | // Code generated by templ - DO NOT EDIT. | |
| 2 | + | ||
| 3 | + | // templ: version: v0.3.1020 | |
| 4 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 5 | + | ||
| 6 | + | package web | |
| 7 | + | ||
| 8 | + | //lint:file-ignore SA4006 This context is only used if a nested component is present. | |
| 9 | + | ||
| 10 | + | import "github.com/a-h/templ" | |
| 11 | + | import templruntime "github.com/a-h/templ/runtime" | |
| 12 | + | ||
| 13 | + | import "strings" | |
| 14 | + | ||
| 15 | + | // MRList is the list of the merge requests of a repository: the open ones, | |
| 16 | + | // then the merged and closed ones. | |
| 17 | + | func MRList(l Layout, p MRListPage) templ.Component { | |
| 18 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 19 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 20 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 21 | + | return templ_7745c5c3_CtxErr | |
| 22 | + | } | |
| 23 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 24 | + | if !templ_7745c5c3_IsBuffer { | |
| 25 | + | defer func() { | |
| 26 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 27 | + | if templ_7745c5c3_Err == nil { | |
| 28 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 29 | + | } | |
| 30 | + | }() | |
| 31 | + | } | |
| 32 | + | ctx = templ.InitializeContext(ctx) | |
| 33 | + | templ_7745c5c3_Var1 := templ.GetChildren(ctx) | |
| 34 | + | if templ_7745c5c3_Var1 == nil { | |
| 35 | + | templ_7745c5c3_Var1 = templ.NopComponent | |
| 36 | + | } | |
| 37 | + | ctx = templ.ClearChildren(ctx) | |
| 38 | + | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 39 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 40 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 41 | + | if !templ_7745c5c3_IsBuffer { | |
| 42 | + | defer func() { | |
| 43 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 44 | + | if templ_7745c5c3_Err == nil { | |
| 45 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 46 | + | } | |
| 47 | + | }() | |
| 48 | + | } | |
| 49 | + | ctx = templ.InitializeContext(ctx) | |
| 50 | + | if p.Live { | |
| 51 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"mr-notice\"></div>") | |
| 52 | + | if templ_7745c5c3_Err != nil { | |
| 53 | + | return templ_7745c5c3_Err | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " <div class=\"filter\"><label for=\"mr-filter\">filter></label> <input id=\"mr-filter\" type=\"search\" data-filter placeholder=\"title, branch, author or state ( / )\" autocomplete=\"off\" hx-on:input=\"let t = this.value.toLowerCase(); q('.mrs tbody tr').arr().forEach(r => r.hidden = !r.textContent.toLowerCase().includes(t));\n let first = q('.mrs tbody tr:not([hidden])').arr()[0]; first && q(first).take('data-cursor', '.mrs tbody tr')\"></div>") | |
| 57 | + | if templ_7745c5c3_Err != nil { | |
| 58 | + | return templ_7745c5c3_Err | |
| 59 | + | } | |
| 60 | + | if p.Newest == "" { | |
| 61 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<h2 class=\"rule-h\"><span>open</span><span>") | |
| 62 | + | if templ_7745c5c3_Err != nil { | |
| 63 | + | return templ_7745c5c3_Err | |
| 64 | + | } | |
| 65 | + | var templ_7745c5c3_Var3 string | |
| 66 | + | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Open))) | |
| 67 | + | if templ_7745c5c3_Err != nil { | |
| 68 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 27, Col: 64} | |
| 69 | + | } | |
| 70 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) | |
| 71 | + | if templ_7745c5c3_Err != nil { | |
| 72 | + | return templ_7745c5c3_Err | |
| 73 | + | } | |
| 74 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span></h2>") | |
| 75 | + | if templ_7745c5c3_Err != nil { | |
| 76 | + | return templ_7745c5c3_Err | |
| 77 | + | } | |
| 78 | + | templ_7745c5c3_Err = mrRows(p.Open, "Open merge requests").Render(ctx, templ_7745c5c3_Buffer) | |
| 79 | + | if templ_7745c5c3_Err != nil { | |
| 80 | + | return templ_7745c5c3_Err | |
| 81 | + | } | |
| 82 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") | |
| 83 | + | if templ_7745c5c3_Err != nil { | |
| 84 | + | return templ_7745c5c3_Err | |
| 85 | + | } | |
| 86 | + | if len(p.Open) == 0 { | |
| 87 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"lede dim\">no open merge requests.</p>") | |
| 88 | + | if templ_7745c5c3_Err != nil { | |
| 89 | + | return templ_7745c5c3_Err | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") | |
| 93 | + | if templ_7745c5c3_Err != nil { | |
| 94 | + | return templ_7745c5c3_Err | |
| 95 | + | } | |
| 96 | + | if p.OpenNote != "" { | |
| 97 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p class=\"note\">") | |
| 98 | + | if templ_7745c5c3_Err != nil { | |
| 99 | + | return templ_7745c5c3_Err | |
| 100 | + | } | |
| 101 | + | var templ_7745c5c3_Var4 string | |
| 102 | + | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p.OpenNote) | |
| 103 | + | if templ_7745c5c3_Err != nil { | |
| 104 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 33, Col: 32} | |
| 105 | + | } | |
| 106 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) | |
| 107 | + | if templ_7745c5c3_Err != nil { | |
| 108 | + | return templ_7745c5c3_Err | |
| 109 | + | } | |
| 110 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p>") | |
| 111 | + | if templ_7745c5c3_Err != nil { | |
| 112 | + | return templ_7745c5c3_Err | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <h2 class=\"rule-h\"><span>merged and closed</span><span>") | |
| 117 | + | if templ_7745c5c3_Err != nil { | |
| 118 | + | return templ_7745c5c3_Err | |
| 119 | + | } | |
| 120 | + | var templ_7745c5c3_Var5 string | |
| 121 | + | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Done))) | |
| 122 | + | if templ_7745c5c3_Err != nil { | |
| 123 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 36, Col: 76} | |
| 124 | + | } | |
| 125 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) | |
| 126 | + | if templ_7745c5c3_Err != nil { | |
| 127 | + | return templ_7745c5c3_Err | |
| 128 | + | } | |
| 129 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></h2>") | |
| 130 | + | if templ_7745c5c3_Err != nil { | |
| 131 | + | return templ_7745c5c3_Err | |
| 132 | + | } | |
| 133 | + | templ_7745c5c3_Err = mrRows(p.Done, "Merged and closed merge requests").Render(ctx, templ_7745c5c3_Buffer) | |
| 134 | + | if templ_7745c5c3_Err != nil { | |
| 135 | + | return templ_7745c5c3_Err | |
| 136 | + | } | |
| 137 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ") | |
| 138 | + | if templ_7745c5c3_Err != nil { | |
| 139 | + | return templ_7745c5c3_Err | |
| 140 | + | } | |
| 141 | + | if len(p.Done) == 0 { | |
| 142 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<p class=\"lede dim\">nothing merged or closed yet.</p>") | |
| 143 | + | if templ_7745c5c3_Err != nil { | |
| 144 | + | return templ_7745c5c3_Err | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ") | |
| 148 | + | if templ_7745c5c3_Err != nil { | |
| 149 | + | return templ_7745c5c3_Err | |
| 150 | + | } | |
| 151 | + | if p.Older != "" || p.Newest != "" { | |
| 152 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<nav class=\"pager\" aria-label=\"Pages\">") | |
| 153 | + | if templ_7745c5c3_Err != nil { | |
| 154 | + | return templ_7745c5c3_Err | |
| 155 | + | } | |
| 156 | + | if p.Newest != "" { | |
| 157 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<a href=\"") | |
| 158 | + | if templ_7745c5c3_Err != nil { | |
| 159 | + | return templ_7745c5c3_Err | |
| 160 | + | } | |
| 161 | + | var templ_7745c5c3_Var6 templ.SafeURL | |
| 162 | + | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Newest)) | |
| 163 | + | if templ_7745c5c3_Err != nil { | |
| 164 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 44, Col: 38} | |
| 165 | + | } | |
| 166 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) | |
| 167 | + | if templ_7745c5c3_Err != nil { | |
| 168 | + | return templ_7745c5c3_Err | |
| 169 | + | } | |
| 170 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">← newest</a> ") | |
| 171 | + | if templ_7745c5c3_Err != nil { | |
| 172 | + | return templ_7745c5c3_Err | |
| 173 | + | } | |
| 174 | + | } else { | |
| 175 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"mute\">← newest</span> ") | |
| 176 | + | if templ_7745c5c3_Err != nil { | |
| 177 | + | return templ_7745c5c3_Err | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | if p.Older != "" { | |
| 181 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<a href=\"") | |
| 182 | + | if templ_7745c5c3_Err != nil { | |
| 183 | + | return templ_7745c5c3_Err | |
| 184 | + | } | |
| 185 | + | var templ_7745c5c3_Var7 templ.SafeURL | |
| 186 | + | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Older)) | |
| 187 | + | if templ_7745c5c3_Err != nil { | |
| 188 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 49, Col: 37} | |
| 189 | + | } | |
| 190 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) | |
| 191 | + | if templ_7745c5c3_Err != nil { | |
| 192 | + | return templ_7745c5c3_Err | |
| 193 | + | } | |
| 194 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" rel=\"next\">older →</a>") | |
| 195 | + | if templ_7745c5c3_Err != nil { | |
| 196 | + | return templ_7745c5c3_Err | |
| 197 | + | } | |
| 198 | + | } else { | |
| 199 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"mute\">older →</span>") | |
| 200 | + | if templ_7745c5c3_Err != nil { | |
| 201 | + | return templ_7745c5c3_Err | |
| 202 | + | } | |
| 203 | + | } | |
| 204 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</nav>") | |
| 205 | + | if templ_7745c5c3_Err != nil { | |
| 206 | + | return templ_7745c5c3_Err | |
| 207 | + | } | |
| 208 | + | } | |
| 209 | + | return nil | |
| 210 | + | }) | |
| 211 | + | templ_7745c5c3_Err = Shell(l, mrListRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) | |
| 212 | + | if templ_7745c5c3_Err != nil { | |
| 213 | + | return templ_7745c5c3_Err | |
| 214 | + | } | |
| 215 | + | return nil | |
| 216 | + | }) | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | func mrRows(rows []MRRow, label string) templ.Component { | |
| 220 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 221 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 222 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 223 | + | return templ_7745c5c3_CtxErr | |
| 224 | + | } | |
| 225 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 226 | + | if !templ_7745c5c3_IsBuffer { | |
| 227 | + | defer func() { | |
| 228 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 229 | + | if templ_7745c5c3_Err == nil { | |
| 230 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 231 | + | } | |
| 232 | + | }() | |
| 233 | + | } | |
| 234 | + | ctx = templ.InitializeContext(ctx) | |
| 235 | + | templ_7745c5c3_Var8 := templ.GetChildren(ctx) | |
| 236 | + | if templ_7745c5c3_Var8 == nil { | |
| 237 | + | templ_7745c5c3_Var8 = templ.NopComponent | |
| 238 | + | } | |
| 239 | + | ctx = templ.ClearChildren(ctx) | |
| 240 | + | if len(rows) > 0 { | |
| 241 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<table class=\"list mrs\" aria-label=\"") | |
| 242 | + | if templ_7745c5c3_Err != nil { | |
| 243 | + | return templ_7745c5c3_Err | |
| 244 | + | } | |
| 245 | + | var templ_7745c5c3_Var9 string | |
| 246 | + | templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(label) | |
| 247 | + | if templ_7745c5c3_Err != nil { | |
| 248 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 60, Col: 44} | |
| 249 | + | } | |
| 250 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) | |
| 251 | + | if templ_7745c5c3_Err != nil { | |
| 252 | + | return templ_7745c5c3_Err | |
| 253 | + | } | |
| 254 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><tbody data-rows>") | |
| 255 | + | if templ_7745c5c3_Err != nil { | |
| 256 | + | return templ_7745c5c3_Err | |
| 257 | + | } | |
| 258 | + | for _, m := range rows { | |
| 259 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<tr") | |
| 260 | + | if templ_7745c5c3_Err != nil { | |
| 261 | + | return templ_7745c5c3_Err | |
| 262 | + | } | |
| 263 | + | if m.Cursor { | |
| 264 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " data-cursor") | |
| 265 | + | if templ_7745c5c3_Err != nil { | |
| 266 | + | return templ_7745c5c3_Err | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " data-state=\"") | |
| 270 | + | if templ_7745c5c3_Err != nil { | |
| 271 | + | return templ_7745c5c3_Err | |
| 272 | + | } | |
| 273 | + | var templ_7745c5c3_Var10 string | |
| 274 | + | templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.State) | |
| 275 | + | if templ_7745c5c3_Err != nil { | |
| 276 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 63, Col: 55} | |
| 277 | + | } | |
| 278 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) | |
| 279 | + | if templ_7745c5c3_Err != nil { | |
| 280 | + | return templ_7745c5c3_Err | |
| 281 | + | } | |
| 282 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\"><td class=\"cur\" aria-hidden=\"true\"></td><td class=\"c-n mute\">") | |
| 283 | + | if templ_7745c5c3_Err != nil { | |
| 284 | + | return templ_7745c5c3_Err | |
| 285 | + | } | |
| 286 | + | var templ_7745c5c3_Var11 string | |
| 287 | + | templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Number) | |
| 288 | + | if templ_7745c5c3_Err != nil { | |
| 289 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 65, Col: 37} | |
| 290 | + | } | |
| 291 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) | |
| 292 | + | if templ_7745c5c3_Err != nil { | |
| 293 | + | return templ_7745c5c3_Err | |
| 294 | + | } | |
| 295 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</td><td class=\"c-state\">") | |
| 296 | + | if templ_7745c5c3_Err != nil { | |
| 297 | + | return templ_7745c5c3_Err | |
| 298 | + | } | |
| 299 | + | var templ_7745c5c3_Var12 string | |
| 300 | + | templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(m.State) | |
| 301 | + | if templ_7745c5c3_Err != nil { | |
| 302 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 66, Col: 35} | |
| 303 | + | } | |
| 304 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) | |
| 305 | + | if templ_7745c5c3_Err != nil { | |
| 306 | + | return templ_7745c5c3_Err | |
| 307 | + | } | |
| 308 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</td><td class=\"c-subject\"><a href=\"") | |
| 309 | + | if templ_7745c5c3_Err != nil { | |
| 310 | + | return templ_7745c5c3_Err | |
| 311 | + | } | |
| 312 | + | var templ_7745c5c3_Var13 templ.SafeURL | |
| 313 | + | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(m.Href)) | |
| 314 | + | if templ_7745c5c3_Err != nil { | |
| 315 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 59} | |
| 316 | + | } | |
| 317 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) | |
| 318 | + | if templ_7745c5c3_Err != nil { | |
| 319 | + | return templ_7745c5c3_Err | |
| 320 | + | } | |
| 321 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">") | |
| 322 | + | if templ_7745c5c3_Err != nil { | |
| 323 | + | return templ_7745c5c3_Err | |
| 324 | + | } | |
| 325 | + | var templ_7745c5c3_Var14 string | |
| 326 | + | templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(m.Title) | |
| 327 | + | if templ_7745c5c3_Err != nil { | |
| 328 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 71} | |
| 329 | + | } | |
| 330 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) | |
| 331 | + | if templ_7745c5c3_Err != nil { | |
| 332 | + | return templ_7745c5c3_Err | |
| 333 | + | } | |
| 334 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a> <span class=\"mute\">") | |
| 335 | + | if templ_7745c5c3_Err != nil { | |
| 336 | + | return templ_7745c5c3_Err | |
| 337 | + | } | |
| 338 | + | var templ_7745c5c3_Var15 string | |
| 339 | + | templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(m.Branches) | |
| 340 | + | if templ_7745c5c3_Err != nil { | |
| 341 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 67, Col: 109} | |
| 342 | + | } | |
| 343 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) | |
| 344 | + | if templ_7745c5c3_Err != nil { | |
| 345 | + | return templ_7745c5c3_Err | |
| 346 | + | } | |
| 347 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span></td><td class=\"c-who dim\">") | |
| 348 | + | if templ_7745c5c3_Err != nil { | |
| 349 | + | return templ_7745c5c3_Err | |
| 350 | + | } | |
| 351 | + | var templ_7745c5c3_Var16 string | |
| 352 | + | templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(m.Author) | |
| 353 | + | if templ_7745c5c3_Err != nil { | |
| 354 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 68, Col: 38} | |
| 355 | + | } | |
| 356 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) | |
| 357 | + | if templ_7745c5c3_Err != nil { | |
| 358 | + | return templ_7745c5c3_Err | |
| 359 | + | } | |
| 360 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"c-when nowrap dim\">") | |
| 361 | + | if templ_7745c5c3_Err != nil { | |
| 362 | + | return templ_7745c5c3_Err | |
| 363 | + | } | |
| 364 | + | if m.WhenAt != "" { | |
| 365 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<time datetime=\"") | |
| 366 | + | if templ_7745c5c3_Err != nil { | |
| 367 | + | return templ_7745c5c3_Err | |
| 368 | + | } | |
| 369 | + | var templ_7745c5c3_Var17 string | |
| 370 | + | templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt) | |
| 371 | + | if templ_7745c5c3_Err != nil { | |
| 372 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 33} | |
| 373 | + | } | |
| 374 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) | |
| 375 | + | if templ_7745c5c3_Err != nil { | |
| 376 | + | return templ_7745c5c3_Err | |
| 377 | + | } | |
| 378 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" title=\"") | |
| 379 | + | if templ_7745c5c3_Err != nil { | |
| 380 | + | return templ_7745c5c3_Err | |
| 381 | + | } | |
| 382 | + | var templ_7745c5c3_Var18 string | |
| 383 | + | templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.WhenAt) | |
| 384 | + | if templ_7745c5c3_Err != nil { | |
| 385 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 52} | |
| 386 | + | } | |
| 387 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) | |
| 388 | + | if templ_7745c5c3_Err != nil { | |
| 389 | + | return templ_7745c5c3_Err | |
| 390 | + | } | |
| 391 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">") | |
| 392 | + | if templ_7745c5c3_Err != nil { | |
| 393 | + | return templ_7745c5c3_Err | |
| 394 | + | } | |
| 395 | + | var templ_7745c5c3_Var19 string | |
| 396 | + | templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.When) | |
| 397 | + | if templ_7745c5c3_Err != nil { | |
| 398 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 71, Col: 63} | |
| 399 | + | } | |
| 400 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) | |
| 401 | + | if templ_7745c5c3_Err != nil { | |
| 402 | + | return templ_7745c5c3_Err | |
| 403 | + | } | |
| 404 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</time>") | |
| 405 | + | if templ_7745c5c3_Err != nil { | |
| 406 | + | return templ_7745c5c3_Err | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td></tr>") | |
| 410 | + | if templ_7745c5c3_Err != nil { | |
| 411 | + | return templ_7745c5c3_Err | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</tbody></table>") | |
| 415 | + | if templ_7745c5c3_Err != nil { | |
| 416 | + | return templ_7745c5c3_Err | |
| 417 | + | } | |
| 418 | + | } | |
| 419 | + | return nil | |
| 420 | + | }) | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | func mrListRail(p MRListPage) templ.Component { | |
| 424 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 425 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 426 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 427 | + | return templ_7745c5c3_CtxErr | |
| 428 | + | } | |
| 429 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 430 | + | if !templ_7745c5c3_IsBuffer { | |
| 431 | + | defer func() { | |
| 432 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 433 | + | if templ_7745c5c3_Err == nil { | |
| 434 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 435 | + | } | |
| 436 | + | }() | |
| 437 | + | } | |
| 438 | + | ctx = templ.InitializeContext(ctx) | |
| 439 | + | templ_7745c5c3_Var20 := templ.GetChildren(ctx) | |
| 440 | + | if templ_7745c5c3_Var20 == nil { | |
| 441 | + | templ_7745c5c3_Var20 = templ.NopComponent | |
| 442 | + | } | |
| 443 | + | ctx = templ.ClearChildren(ctx) | |
| 444 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<section aria-labelledby=\"r-states\"><h2 id=\"r-states\">states</h2><dl class=\"kv\"><dt>open</dt><dd>awaits review</dd><dt>merged</dt><dd>the target took the branch</dd><dt>closed</dt><dd>refused, branch kept</dd></dl></section><section aria-labelledby=\"r-new\"><h2 id=\"r-new\">open one</h2>") | |
| 445 | + | if templ_7745c5c3_Err != nil { | |
| 446 | + | return templ_7745c5c3_Err | |
| 447 | + | } | |
| 448 | + | if len(p.Branches) > 0 { | |
| 449 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<p class=\"dim\">compare a branch with ") | |
| 450 | + | if templ_7745c5c3_Err != nil { | |
| 451 | + | return templ_7745c5c3_Err | |
| 452 | + | } | |
| 453 | + | var templ_7745c5c3_Var21 string | |
| 454 | + | templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(p.Default) | |
| 455 | + | if templ_7745c5c3_Err != nil { | |
| 456 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 96, Col: 51} | |
| 457 | + | } | |
| 458 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) | |
| 459 | + | if templ_7745c5c3_Err != nil { | |
| 460 | + | return templ_7745c5c3_Err | |
| 461 | + | } | |
| 462 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ":</p><nav class=\"outline\" aria-label=\"Branches\">") | |
| 463 | + | if templ_7745c5c3_Err != nil { | |
| 464 | + | return templ_7745c5c3_Err | |
| 465 | + | } | |
| 466 | + | for _, b := range p.Branches { | |
| 467 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<a href=\"") | |
| 468 | + | if templ_7745c5c3_Err != nil { | |
| 469 | + | return templ_7745c5c3_Err | |
| 470 | + | } | |
| 471 | + | var templ_7745c5c3_Var22 templ.SafeURL | |
| 472 | + | templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(b.Href)) | |
| 473 | + | if templ_7745c5c3_Err != nil { | |
| 474 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 99, Col: 36} | |
| 475 | + | } | |
| 476 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) | |
| 477 | + | if templ_7745c5c3_Err != nil { | |
| 478 | + | return templ_7745c5c3_Err | |
| 479 | + | } | |
| 480 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") | |
| 481 | + | if templ_7745c5c3_Err != nil { | |
| 482 | + | return templ_7745c5c3_Err | |
| 483 | + | } | |
| 484 | + | var templ_7745c5c3_Var23 string | |
| 485 | + | templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Name) | |
| 486 | + | if templ_7745c5c3_Err != nil { | |
| 487 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 99, Col: 47} | |
| 488 | + | } | |
| 489 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) | |
| 490 | + | if templ_7745c5c3_Err != nil { | |
| 491 | + | return templ_7745c5c3_Err | |
| 492 | + | } | |
| 493 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</a>") | |
| 494 | + | if templ_7745c5c3_Err != nil { | |
| 495 | + | return templ_7745c5c3_Err | |
| 496 | + | } | |
| 497 | + | } | |
| 498 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</nav>") | |
| 499 | + | if templ_7745c5c3_Err != nil { | |
| 500 | + | return templ_7745c5c3_Err | |
| 501 | + | } | |
| 502 | + | } else { | |
| 503 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<p class=\"dim\">there is no other branch to merge yet.</p>") | |
| 504 | + | if templ_7745c5c3_Err != nil { | |
| 505 | + | return templ_7745c5c3_Err | |
| 506 | + | } | |
| 507 | + | } | |
| 508 | + | templ_7745c5c3_Err = CloneBlock(p.CLI).Render(ctx, templ_7745c5c3_Buffer) | |
| 509 | + | if templ_7745c5c3_Err != nil { | |
| 510 | + | return templ_7745c5c3_Err | |
| 511 | + | } | |
| 512 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</section>") | |
| 513 | + | if templ_7745c5c3_Err != nil { | |
| 514 | + | return templ_7745c5c3_Err | |
| 515 | + | } | |
| 516 | + | return nil | |
| 517 | + | }) | |
| 518 | + | } | |
| 519 | + | ||
| 520 | + | // MergeRequest is a merge request: its facts, its description, the diff with | |
| 521 | + | // the notes under their lines, the general comments; and in the rail the merge | |
| 522 | + | // form, the checks, the settings, the commits and the files. | |
| 523 | + | func MergeRequest(l Layout, p MRPage) templ.Component { | |
| 524 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 525 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 526 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 527 | + | return templ_7745c5c3_CtxErr | |
| 528 | + | } | |
| 529 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 530 | + | if !templ_7745c5c3_IsBuffer { | |
| 531 | + | defer func() { | |
| 532 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 533 | + | if templ_7745c5c3_Err == nil { | |
| 534 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 535 | + | } | |
| 536 | + | }() | |
| 537 | + | } | |
| 538 | + | ctx = templ.InitializeContext(ctx) | |
| 539 | + | templ_7745c5c3_Var24 := templ.GetChildren(ctx) | |
| 540 | + | if templ_7745c5c3_Var24 == nil { | |
| 541 | + | templ_7745c5c3_Var24 = templ.NopComponent | |
| 542 | + | } | |
| 543 | + | ctx = templ.ClearChildren(ctx) | |
| 544 | + | templ_7745c5c3_Var25 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 545 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 546 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 547 | + | if !templ_7745c5c3_IsBuffer { | |
| 548 | + | defer func() { | |
| 549 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 550 | + | if templ_7745c5c3_Err == nil { | |
| 551 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 552 | + | } | |
| 553 | + | }() | |
| 554 | + | } | |
| 555 | + | ctx = templ.InitializeContext(ctx) | |
| 556 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<header class=\"show mr-h\"><h1><span class=\"mute\">") | |
| 557 | + | if templ_7745c5c3_Err != nil { | |
| 558 | + | return templ_7745c5c3_Err | |
| 559 | + | } | |
| 560 | + | var templ_7745c5c3_Var26 string | |
| 561 | + | templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(p.Number) | |
| 562 | + | if templ_7745c5c3_Err != nil { | |
| 563 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 115, Col: 36} | |
| 564 | + | } | |
| 565 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) | |
| 566 | + | if templ_7745c5c3_Err != nil { | |
| 567 | + | return templ_7745c5c3_Err | |
| 568 | + | } | |
| 569 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span> ") | |
| 570 | + | if templ_7745c5c3_Err != nil { | |
| 571 | + | return templ_7745c5c3_Err | |
| 572 | + | } | |
| 573 | + | var templ_7745c5c3_Var27 string | |
| 574 | + | templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(p.Title) | |
| 575 | + | if templ_7745c5c3_Err != nil { | |
| 576 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 115, Col: 55} | |
| 577 | + | } | |
| 578 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) | |
| 579 | + | if templ_7745c5c3_Err != nil { | |
| 580 | + | return templ_7745c5c3_Err | |
| 581 | + | } | |
| 582 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</h1><dl class=\"kv show-kv\"><dt>state</dt><dd class=\"mr-state\" data-state=\"") | |
| 583 | + | if templ_7745c5c3_Err != nil { | |
| 584 | + | return templ_7745c5c3_Err | |
| 585 | + | } | |
| 586 | + | var templ_7745c5c3_Var28 string | |
| 587 | + | templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.State) | |
| 588 | + | if templ_7745c5c3_Err != nil { | |
| 589 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 118, Col: 45} | |
| 590 | + | } | |
| 591 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28) | |
| 592 | + | if templ_7745c5c3_Err != nil { | |
| 593 | + | return templ_7745c5c3_Err | |
| 594 | + | } | |
| 595 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">") | |
| 596 | + | if templ_7745c5c3_Err != nil { | |
| 597 | + | return templ_7745c5c3_Err | |
| 598 | + | } | |
| 599 | + | var templ_7745c5c3_Var29 string | |
| 600 | + | templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(p.State) | |
| 601 | + | if templ_7745c5c3_Err != nil { | |
| 602 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 119, Col: 14} | |
| 603 | + | } | |
| 604 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) | |
| 605 | + | if templ_7745c5c3_Err != nil { | |
| 606 | + | return templ_7745c5c3_Err | |
| 607 | + | } | |
| 608 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " ") | |
| 609 | + | if templ_7745c5c3_Err != nil { | |
| 610 | + | return templ_7745c5c3_Err | |
| 611 | + | } | |
| 612 | + | if p.MergedShort != "" { | |
| 613 | + | var templ_7745c5c3_Var30 string | |
| 614 | + | templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 615 | + | if templ_7745c5c3_Err != nil { | |
| 616 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 14} | |
| 617 | + | } | |
| 618 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) | |
| 619 | + | if templ_7745c5c3_Err != nil { | |
| 620 | + | return templ_7745c5c3_Err | |
| 621 | + | } | |
| 622 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a href=\"") | |
| 623 | + | if templ_7745c5c3_Err != nil { | |
| 624 | + | return templ_7745c5c3_Err | |
| 625 | + | } | |
| 626 | + | var templ_7745c5c3_Var31 templ.SafeURL | |
| 627 | + | templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.MergedHref)) | |
| 628 | + | if templ_7745c5c3_Err != nil { | |
| 629 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 53} | |
| 630 | + | } | |
| 631 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) | |
| 632 | + | if templ_7745c5c3_Err != nil { | |
| 633 | + | return templ_7745c5c3_Err | |
| 634 | + | } | |
| 635 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\">") | |
| 636 | + | if templ_7745c5c3_Err != nil { | |
| 637 | + | return templ_7745c5c3_Err | |
| 638 | + | } | |
| 639 | + | var templ_7745c5c3_Var32 string | |
| 640 | + | templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(p.MergedShort) | |
| 641 | + | if templ_7745c5c3_Err != nil { | |
| 642 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 71} | |
| 643 | + | } | |
| 644 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) | |
| 645 | + | if templ_7745c5c3_Err != nil { | |
| 646 | + | return templ_7745c5c3_Err | |
| 647 | + | } | |
| 648 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</a>") | |
| 649 | + | if templ_7745c5c3_Err != nil { | |
| 650 | + | return templ_7745c5c3_Err | |
| 651 | + | } | |
| 652 | + | var templ_7745c5c3_Var33 string | |
| 653 | + | templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + p.MergeMode) | |
| 654 | + | if templ_7745c5c3_Err != nil { | |
| 655 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 121, Col: 99} | |
| 656 | + | } | |
| 657 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) | |
| 658 | + | if templ_7745c5c3_Err != nil { | |
| 659 | + | return templ_7745c5c3_Err | |
| 660 | + | } | |
| 661 | + | } | |
| 662 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</dd><dt>branch</dt><dd>") | |
| 663 | + | if templ_7745c5c3_Err != nil { | |
| 664 | + | return templ_7745c5c3_Err | |
| 665 | + | } | |
| 666 | + | templ_7745c5c3_Err = branchLink(p.Source, p.SourceHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 667 | + | if templ_7745c5c3_Err != nil { | |
| 668 | + | return templ_7745c5c3_Err | |
| 669 | + | } | |
| 670 | + | var templ_7745c5c3_Var34 string | |
| 671 | + | templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(" → ") | |
| 672 | + | if templ_7745c5c3_Err != nil { | |
| 673 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 127, Col: 14} | |
| 674 | + | } | |
| 675 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) | |
| 676 | + | if templ_7745c5c3_Err != nil { | |
| 677 | + | return templ_7745c5c3_Err | |
| 678 | + | } | |
| 679 | + | templ_7745c5c3_Err = branchLink(p.Target, p.TargetHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 680 | + | if templ_7745c5c3_Err != nil { | |
| 681 | + | return templ_7745c5c3_Err | |
| 682 | + | } | |
| 683 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</dd><dt>opened</dt><dd>") | |
| 684 | + | if templ_7745c5c3_Err != nil { | |
| 685 | + | return templ_7745c5c3_Err | |
| 686 | + | } | |
| 687 | + | var templ_7745c5c3_Var35 string | |
| 688 | + | templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.Name) | |
| 689 | + | if templ_7745c5c3_Err != nil { | |
| 690 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 132, Col: 20} | |
| 691 | + | } | |
| 692 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) | |
| 693 | + | if templ_7745c5c3_Err != nil { | |
| 694 | + | return templ_7745c5c3_Err | |
| 695 | + | } | |
| 696 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ") | |
| 697 | + | if templ_7745c5c3_Err != nil { | |
| 698 | + | return templ_7745c5c3_Err | |
| 699 | + | } | |
| 700 | + | if p.Opened.WhenAt != "" { | |
| 701 | + | var templ_7745c5c3_Var36 string | |
| 702 | + | templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 703 | + | if templ_7745c5c3_Err != nil { | |
| 704 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 14} | |
| 705 | + | } | |
| 706 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) | |
| 707 | + | if templ_7745c5c3_Err != nil { | |
| 708 | + | return templ_7745c5c3_Err | |
| 709 | + | } | |
| 710 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<time datetime=\"") | |
| 711 | + | if templ_7745c5c3_Err != nil { | |
| 712 | + | return templ_7745c5c3_Err | |
| 713 | + | } | |
| 714 | + | var templ_7745c5c3_Var37 string | |
| 715 | + | templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.WhenAt) | |
| 716 | + | if templ_7745c5c3_Err != nil { | |
| 717 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 48} | |
| 718 | + | } | |
| 719 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) | |
| 720 | + | if templ_7745c5c3_Err != nil { | |
| 721 | + | return templ_7745c5c3_Err | |
| 722 | + | } | |
| 723 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" title=\"") | |
| 724 | + | if templ_7745c5c3_Err != nil { | |
| 725 | + | return templ_7745c5c3_Err | |
| 726 | + | } | |
| 727 | + | var templ_7745c5c3_Var38 string | |
| 728 | + | templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Opened.Abs) | |
| 729 | + | if templ_7745c5c3_Err != nil { | |
| 730 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 71} | |
| 731 | + | } | |
| 732 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38) | |
| 733 | + | if templ_7745c5c3_Err != nil { | |
| 734 | + | return templ_7745c5c3_Err | |
| 735 | + | } | |
| 736 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\">") | |
| 737 | + | if templ_7745c5c3_Err != nil { | |
| 738 | + | return templ_7745c5c3_Err | |
| 739 | + | } | |
| 740 | + | var templ_7745c5c3_Var39 string | |
| 741 | + | templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(p.Opened.When) | |
| 742 | + | if templ_7745c5c3_Err != nil { | |
| 743 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 134, Col: 89} | |
| 744 | + | } | |
| 745 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) | |
| 746 | + | if templ_7745c5c3_Err != nil { | |
| 747 | + | return templ_7745c5c3_Err | |
| 748 | + | } | |
| 749 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</time>") | |
| 750 | + | if templ_7745c5c3_Err != nil { | |
| 751 | + | return templ_7745c5c3_Err | |
| 752 | + | } | |
| 753 | + | } | |
| 754 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</dd>") | |
| 755 | + | if templ_7745c5c3_Err != nil { | |
| 756 | + | return templ_7745c5c3_Err | |
| 757 | + | } | |
| 758 | + | if p.UpdatedAt != "" { | |
| 759 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<dt>updated</dt><dd><time datetime=\"") | |
| 760 | + | if templ_7745c5c3_Err != nil { | |
| 761 | + | return templ_7745c5c3_Err | |
| 762 | + | } | |
| 763 | + | var templ_7745c5c3_Var40 string | |
| 764 | + | templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.UpdatedAt) | |
| 765 | + | if templ_7745c5c3_Err != nil { | |
| 766 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 139, Col: 37} | |
| 767 | + | } | |
| 768 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40) | |
| 769 | + | if templ_7745c5c3_Err != nil { | |
| 770 | + | return templ_7745c5c3_Err | |
| 771 | + | } | |
| 772 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\">") | |
| 773 | + | if templ_7745c5c3_Err != nil { | |
| 774 | + | return templ_7745c5c3_Err | |
| 775 | + | } | |
| 776 | + | var templ_7745c5c3_Var41 string | |
| 777 | + | templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(p.Updated) | |
| 778 | + | if templ_7745c5c3_Err != nil { | |
| 779 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 139, Col: 51} | |
| 780 | + | } | |
| 781 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) | |
| 782 | + | if templ_7745c5c3_Err != nil { | |
| 783 | + | return templ_7745c5c3_Err | |
| 784 | + | } | |
| 785 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</time></dd>") | |
| 786 | + | if templ_7745c5c3_Err != nil { | |
| 787 | + | return templ_7745c5c3_Err | |
| 788 | + | } | |
| 789 | + | } | |
| 790 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</dl></header><div id=\"mr-notice\"></div>") | |
| 791 | + | if templ_7745c5c3_Err != nil { | |
| 792 | + | return templ_7745c5c3_Err | |
| 793 | + | } | |
| 794 | + | if len(p.Desc) > 0 { | |
| 795 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<p class=\"mr-desc\">") | |
| 796 | + | if templ_7745c5c3_Err != nil { | |
| 797 | + | return templ_7745c5c3_Err | |
| 798 | + | } | |
| 799 | + | templ_7745c5c3_Err = userText(p.Desc).Render(ctx, templ_7745c5c3_Buffer) | |
| 800 | + | if templ_7745c5c3_Err != nil { | |
| 801 | + | return templ_7745c5c3_Err | |
| 802 | + | } | |
| 803 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</p>") | |
| 804 | + | if templ_7745c5c3_Err != nil { | |
| 805 | + | return templ_7745c5c3_Err | |
| 806 | + | } | |
| 807 | + | } | |
| 808 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ") | |
| 809 | + | if templ_7745c5c3_Err != nil { | |
| 810 | + | return templ_7745c5c3_Err | |
| 811 | + | } | |
| 812 | + | if p.Since != nil { | |
| 813 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<nav class=\"since\" aria-label=\"Diff range\"><a href=\"") | |
| 814 | + | if templ_7745c5c3_Err != nil { | |
| 815 | + | return templ_7745c5c3_Err | |
| 816 | + | } | |
| 817 | + | var templ_7745c5c3_Var42 templ.SafeURL | |
| 818 | + | templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.AllHref)) | |
| 819 | + | if templ_7745c5c3_Err != nil { | |
| 820 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 150, Col: 42} | |
| 821 | + | } | |
| 822 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) | |
| 823 | + | if templ_7745c5c3_Err != nil { | |
| 824 | + | return templ_7745c5c3_Err | |
| 825 | + | } | |
| 826 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"") | |
| 827 | + | if templ_7745c5c3_Err != nil { | |
| 828 | + | return templ_7745c5c3_Err | |
| 829 | + | } | |
| 830 | + | if !p.Since.Current { | |
| 831 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " aria-current=\"page\"") | |
| 832 | + | if templ_7745c5c3_Err != nil { | |
| 833 | + | return templ_7745c5c3_Err | |
| 834 | + | } | |
| 835 | + | } | |
| 836 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, ">all changes <span class=\"mute\">vs ") | |
| 837 | + | if templ_7745c5c3_Err != nil { | |
| 838 | + | return templ_7745c5c3_Err | |
| 839 | + | } | |
| 840 | + | var templ_7745c5c3_Var43 string | |
| 841 | + | templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 842 | + | if templ_7745c5c3_Err != nil { | |
| 843 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 154, Col: 49} | |
| 844 | + | } | |
| 845 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) | |
| 846 | + | if templ_7745c5c3_Err != nil { | |
| 847 | + | return templ_7745c5c3_Err | |
| 848 | + | } | |
| 849 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</span></a> <a href=\"") | |
| 850 | + | if templ_7745c5c3_Err != nil { | |
| 851 | + | return templ_7745c5c3_Err | |
| 852 | + | } | |
| 853 | + | var templ_7745c5c3_Var44 templ.SafeURL | |
| 854 | + | templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Since.SinceHref)) | |
| 855 | + | if templ_7745c5c3_Err != nil { | |
| 856 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 156, Col: 44} | |
| 857 | + | } | |
| 858 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) | |
| 859 | + | if templ_7745c5c3_Err != nil { | |
| 860 | + | return templ_7745c5c3_Err | |
| 861 | + | } | |
| 862 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\"") | |
| 863 | + | if templ_7745c5c3_Err != nil { | |
| 864 | + | return templ_7745c5c3_Err | |
| 865 | + | } | |
| 866 | + | if p.Since.Current { | |
| 867 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " aria-current=\"page\"") | |
| 868 | + | if templ_7745c5c3_Err != nil { | |
| 869 | + | return templ_7745c5c3_Err | |
| 870 | + | } | |
| 871 | + | } | |
| 872 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, ">since your last read <span class=\"mute\">") | |
| 873 | + | if templ_7745c5c3_Err != nil { | |
| 874 | + | return templ_7745c5c3_Err | |
| 875 | + | } | |
| 876 | + | var templ_7745c5c3_Var45 string | |
| 877 | + | templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.ReadShort) | |
| 878 | + | if templ_7745c5c3_Err != nil { | |
| 879 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 64} | |
| 880 | + | } | |
| 881 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) | |
| 882 | + | if templ_7745c5c3_Err != nil { | |
| 883 | + | return templ_7745c5c3_Err | |
| 884 | + | } | |
| 885 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " → ") | |
| 886 | + | if templ_7745c5c3_Err != nil { | |
| 887 | + | return templ_7745c5c3_Err | |
| 888 | + | } | |
| 889 | + | var templ_7745c5c3_Var46 string | |
| 890 | + | templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(p.Since.HeadShort) | |
| 891 | + | if templ_7745c5c3_Err != nil { | |
| 892 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 160, Col: 90} | |
| 893 | + | } | |
| 894 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) | |
| 895 | + | if templ_7745c5c3_Err != nil { | |
| 896 | + | return templ_7745c5c3_Err | |
| 897 | + | } | |
| 898 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></a></nav>") | |
| 899 | + | if templ_7745c5c3_Err != nil { | |
| 900 | + | return templ_7745c5c3_Err | |
| 901 | + | } | |
| 902 | + | } | |
| 903 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, " ") | |
| 904 | + | if templ_7745c5c3_Err != nil { | |
| 905 | + | return templ_7745c5c3_Err | |
| 906 | + | } | |
| 907 | + | if p.SinceNote != "" { | |
| 908 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "<p class=\"notice\">") | |
| 909 | + | if templ_7745c5c3_Err != nil { | |
| 910 | + | return templ_7745c5c3_Err | |
| 911 | + | } | |
| 912 | + | var templ_7745c5c3_Var47 string | |
| 913 | + | templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(p.SinceNote) | |
| 914 | + | if templ_7745c5c3_Err != nil { | |
| 915 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 164, Col: 34} | |
| 916 | + | } | |
| 917 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) | |
| 918 | + | if templ_7745c5c3_Err != nil { | |
| 919 | + | return templ_7745c5c3_Err | |
| 920 | + | } | |
| 921 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</p>") | |
| 922 | + | if templ_7745c5c3_Err != nil { | |
| 923 | + | return templ_7745c5c3_Err | |
| 924 | + | } | |
| 925 | + | } | |
| 926 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, " <div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 927 | + | if templ_7745c5c3_Err != nil { | |
| 928 | + | return templ_7745c5c3_Err | |
| 929 | + | } | |
| 930 | + | var templ_7745c5c3_Var48 string | |
| 931 | + | templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 932 | + | if templ_7745c5c3_Err != nil { | |
| 933 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 56} | |
| 934 | + | } | |
| 935 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) | |
| 936 | + | if templ_7745c5c3_Err != nil { | |
| 937 | + | return templ_7745c5c3_Err | |
| 938 | + | } | |
| 939 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, " · <span class=\"plus\">+") | |
| 940 | + | if templ_7745c5c3_Err != nil { | |
| 941 | + | return templ_7745c5c3_Err | |
| 942 | + | } | |
| 943 | + | var templ_7745c5c3_Var49 string | |
| 944 | + | templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 945 | + | if templ_7745c5c3_Err != nil { | |
| 946 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 96} | |
| 947 | + | } | |
| 948 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) | |
| 949 | + | if templ_7745c5c3_Err != nil { | |
| 950 | + | return templ_7745c5c3_Err | |
| 951 | + | } | |
| 952 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <span class=\"minus\">-") | |
| 953 | + | if templ_7745c5c3_Err != nil { | |
| 954 | + | return templ_7745c5c3_Err | |
| 955 | + | } | |
| 956 | + | var templ_7745c5c3_Var50 string | |
| 957 | + | templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 958 | + | if templ_7745c5c3_Err != nil { | |
| 959 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 167, Col: 141} | |
| 960 | + | } | |
| 961 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) | |
| 962 | + | if templ_7745c5c3_Err != nil { | |
| 963 | + | return templ_7745c5c3_Err | |
| 964 | + | } | |
| 965 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "</span></span> <span class=\"grow\"></span>") | |
| 966 | + | if templ_7745c5c3_Err != nil { | |
| 967 | + | return templ_7745c5c3_Err | |
| 968 | + | } | |
| 969 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 970 | + | if templ_7745c5c3_Err != nil { | |
| 971 | + | return templ_7745c5c3_Err | |
| 972 | + | } | |
| 973 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 974 | + | if templ_7745c5c3_Err != nil { | |
| 975 | + | return templ_7745c5c3_Err | |
| 976 | + | } | |
| 977 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") | |
| 978 | + | if templ_7745c5c3_Err != nil { | |
| 979 | + | return templ_7745c5c3_Err | |
| 980 | + | } | |
| 981 | + | if p.Invisible { | |
| 982 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 983 | + | if templ_7745c5c3_Err != nil { | |
| 984 | + | return templ_7745c5c3_Err | |
| 985 | + | } | |
| 986 | + | } | |
| 987 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, " ") | |
| 988 | + | if templ_7745c5c3_Err != nil { | |
| 989 | + | return templ_7745c5c3_Err | |
| 990 | + | } | |
| 991 | + | if p.Note != "" { | |
| 992 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, "<p class=\"notice\">") | |
| 993 | + | if templ_7745c5c3_Err != nil { | |
| 994 | + | return templ_7745c5c3_Err | |
| 995 | + | } | |
| 996 | + | var templ_7745c5c3_Var51 string | |
| 997 | + | templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 998 | + | if templ_7745c5c3_Err != nil { | |
| 999 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 180, Col: 29} | |
| 1000 | + | } | |
| 1001 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) | |
| 1002 | + | if templ_7745c5c3_Err != nil { | |
| 1003 | + | return templ_7745c5c3_Err | |
| 1004 | + | } | |
| 1005 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "</p>") | |
| 1006 | + | if templ_7745c5c3_Err != nil { | |
| 1007 | + | return templ_7745c5c3_Err | |
| 1008 | + | } | |
| 1009 | + | } | |
| 1010 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, " <div id=\"diffs\"") | |
| 1011 | + | if templ_7745c5c3_Err != nil { | |
| 1012 | + | return templ_7745c5c3_Err | |
| 1013 | + | } | |
| 1014 | + | if p.Both { | |
| 1015 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, " data-diff=\"") | |
| 1016 | + | if templ_7745c5c3_Err != nil { | |
| 1017 | + | return templ_7745c5c3_Err | |
| 1018 | + | } | |
| 1019 | + | var templ_7745c5c3_Var52 string | |
| 1020 | + | templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1021 | + | if templ_7745c5c3_Err != nil { | |
| 1022 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 185, Col: 22} | |
| 1023 | + | } | |
| 1024 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52) | |
| 1025 | + | if templ_7745c5c3_Err != nil { | |
| 1026 | + | return templ_7745c5c3_Err | |
| 1027 | + | } | |
| 1028 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\"") | |
| 1029 | + | if templ_7745c5c3_Err != nil { | |
| 1030 | + | return templ_7745c5c3_Err | |
| 1031 | + | } | |
| 1032 | + | } else { | |
| 1033 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, " data-view=\"") | |
| 1034 | + | if templ_7745c5c3_Err != nil { | |
| 1035 | + | return templ_7745c5c3_Err | |
| 1036 | + | } | |
| 1037 | + | var templ_7745c5c3_Var53 string | |
| 1038 | + | templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 1039 | + | if templ_7745c5c3_Err != nil { | |
| 1040 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 187, Col: 22} | |
| 1041 | + | } | |
| 1042 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53) | |
| 1043 | + | if templ_7745c5c3_Err != nil { | |
| 1044 | + | return templ_7745c5c3_Err | |
| 1045 | + | } | |
| 1046 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "\"") | |
| 1047 | + | if templ_7745c5c3_Err != nil { | |
| 1048 | + | return templ_7745c5c3_Err | |
| 1049 | + | } | |
| 1050 | + | } | |
| 1051 | + | if p.Both && !p.Pinned { | |
| 1052 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 1053 | + | if templ_7745c5c3_Err != nil { | |
| 1054 | + | return templ_7745c5c3_Err | |
| 1055 | + | } | |
| 1056 | + | } | |
| 1057 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, ">") | |
| 1058 | + | if templ_7745c5c3_Err != nil { | |
| 1059 | + | return templ_7745c5c3_Err | |
| 1060 | + | } | |
| 1061 | + | for _, f := range p.Files { | |
| 1062 | + | templ_7745c5c3_Err = DiffFileView(f, p.DiffView).Render(ctx, templ_7745c5c3_Buffer) | |
| 1063 | + | if templ_7745c5c3_Err != nil { | |
| 1064 | + | return templ_7745c5c3_Err | |
| 1065 | + | } | |
| 1066 | + | } | |
| 1067 | + | if len(p.Files) == 0 { | |
| 1068 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<p class=\"note\">no changed files.</p>") | |
| 1069 | + | if templ_7745c5c3_Err != nil { | |
| 1070 | + | return templ_7745c5c3_Err | |
| 1071 | + | } | |
| 1072 | + | } | |
| 1073 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "</div><h2 class=\"rule-h\" id=\"comments\"><span>comments</span><span>") | |
| 1074 | + | if templ_7745c5c3_Err != nil { | |
| 1075 | + | return templ_7745c5c3_Err | |
| 1076 | + | } | |
| 1077 | + | var templ_7745c5c3_Var54 string | |
| 1078 | + | templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NUnresolved)) | |
| 1079 | + | if templ_7745c5c3_Err != nil { | |
| 1080 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 200, Col: 83} | |
| 1081 | + | } | |
| 1082 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) | |
| 1083 | + | if templ_7745c5c3_Err != nil { | |
| 1084 | + | return templ_7745c5c3_Err | |
| 1085 | + | } | |
| 1086 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, " open / ") | |
| 1087 | + | if templ_7745c5c3_Err != nil { | |
| 1088 | + | return templ_7745c5c3_Err | |
| 1089 | + | } | |
| 1090 | + | var templ_7745c5c3_Var55 string | |
| 1091 | + | templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.NComments)) | |
| 1092 | + | if templ_7745c5c3_Err != nil { | |
| 1093 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 200, Col: 112} | |
| 1094 | + | } | |
| 1095 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) | |
| 1096 | + | if templ_7745c5c3_Err != nil { | |
| 1097 | + | return templ_7745c5c3_Err | |
| 1098 | + | } | |
| 1099 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 105, "</span></h2>") | |
| 1100 | + | if templ_7745c5c3_Err != nil { | |
| 1101 | + | return templ_7745c5c3_Err | |
| 1102 | + | } | |
| 1103 | + | for _, c := range p.Loose { | |
| 1104 | + | templ_7745c5c3_Err = commentView(c, "").Render(ctx, templ_7745c5c3_Buffer) | |
| 1105 | + | if templ_7745c5c3_Err != nil { | |
| 1106 | + | return templ_7745c5c3_Err | |
| 1107 | + | } | |
| 1108 | + | } | |
| 1109 | + | for _, c := range p.General { | |
| 1110 | + | templ_7745c5c3_Err = commentView(c, "").Render(ctx, templ_7745c5c3_Buffer) | |
| 1111 | + | if templ_7745c5c3_Err != nil { | |
| 1112 | + | return templ_7745c5c3_Err | |
| 1113 | + | } | |
| 1114 | + | } | |
| 1115 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, " ") | |
| 1116 | + | if templ_7745c5c3_Err != nil { | |
| 1117 | + | return templ_7745c5c3_Err | |
| 1118 | + | } | |
| 1119 | + | if p.NComments == 0 { | |
| 1120 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<p class=\"dim\">no comments yet.</p>") | |
| 1121 | + | if templ_7745c5c3_Err != nil { | |
| 1122 | + | return templ_7745c5c3_Err | |
| 1123 | + | } | |
| 1124 | + | } | |
| 1125 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, " ") | |
| 1126 | + | if templ_7745c5c3_Err != nil { | |
| 1127 | + | return templ_7745c5c3_Err | |
| 1128 | + | } | |
| 1129 | + | if p.GeneralForm != nil { | |
| 1130 | + | templ_7745c5c3_Err = commentForm(*p.GeneralForm, "").Render(ctx, templ_7745c5c3_Buffer) | |
| 1131 | + | if templ_7745c5c3_Err != nil { | |
| 1132 | + | return templ_7745c5c3_Err | |
| 1133 | + | } | |
| 1134 | + | } | |
| 1135 | + | return nil | |
| 1136 | + | }) | |
| 1137 | + | templ_7745c5c3_Err = Shell(l, mrRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var25), templ_7745c5c3_Buffer) | |
| 1138 | + | if templ_7745c5c3_Err != nil { | |
| 1139 | + | return templ_7745c5c3_Err | |
| 1140 | + | } | |
| 1141 | + | return nil | |
| 1142 | + | }) | |
| 1143 | + | } | |
| 1144 | + | ||
| 1145 | + | func branchLink(name, href string) templ.Component { | |
| 1146 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1147 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1148 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1149 | + | return templ_7745c5c3_CtxErr | |
| 1150 | + | } | |
| 1151 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1152 | + | if !templ_7745c5c3_IsBuffer { | |
| 1153 | + | defer func() { | |
| 1154 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1155 | + | if templ_7745c5c3_Err == nil { | |
| 1156 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1157 | + | } | |
| 1158 | + | }() | |
| 1159 | + | } | |
| 1160 | + | ctx = templ.InitializeContext(ctx) | |
| 1161 | + | templ_7745c5c3_Var56 := templ.GetChildren(ctx) | |
| 1162 | + | if templ_7745c5c3_Var56 == nil { | |
| 1163 | + | templ_7745c5c3_Var56 = templ.NopComponent | |
| 1164 | + | } | |
| 1165 | + | ctx = templ.ClearChildren(ctx) | |
| 1166 | + | if href != "" { | |
| 1167 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "<a href=\"") | |
| 1168 | + | if templ_7745c5c3_Err != nil { | |
| 1169 | + | return templ_7745c5c3_Err | |
| 1170 | + | } | |
| 1171 | + | var templ_7745c5c3_Var57 templ.SafeURL | |
| 1172 | + | templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href)) | |
| 1173 | + | if templ_7745c5c3_Err != nil { | |
| 1174 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 31} | |
| 1175 | + | } | |
| 1176 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) | |
| 1177 | + | if templ_7745c5c3_Err != nil { | |
| 1178 | + | return templ_7745c5c3_Err | |
| 1179 | + | } | |
| 1180 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\">") | |
| 1181 | + | if templ_7745c5c3_Err != nil { | |
| 1182 | + | return templ_7745c5c3_Err | |
| 1183 | + | } | |
| 1184 | + | var templ_7745c5c3_Var58 string | |
| 1185 | + | templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1186 | + | if templ_7745c5c3_Err != nil { | |
| 1187 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 218, Col: 40} | |
| 1188 | + | } | |
| 1189 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) | |
| 1190 | + | if templ_7745c5c3_Err != nil { | |
| 1191 | + | return templ_7745c5c3_Err | |
| 1192 | + | } | |
| 1193 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "</a>") | |
| 1194 | + | if templ_7745c5c3_Err != nil { | |
| 1195 | + | return templ_7745c5c3_Err | |
| 1196 | + | } | |
| 1197 | + | } else { | |
| 1198 | + | var templ_7745c5c3_Var59 string | |
| 1199 | + | templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(name) | |
| 1200 | + | if templ_7745c5c3_Err != nil { | |
| 1201 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 220, Col: 8} | |
| 1202 | + | } | |
| 1203 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) | |
| 1204 | + | if templ_7745c5c3_Err != nil { | |
| 1205 | + | return templ_7745c5c3_Err | |
| 1206 | + | } | |
| 1207 | + | } | |
| 1208 | + | return nil | |
| 1209 | + | }) | |
| 1210 | + | } | |
| 1211 | + | ||
| 1212 | + | // lineNotes are the comments under a line of the diff, and the form to write | |
| 1213 | + | // one when the reader asked for it. The diff is in the page twice (unified and | |
| 1214 | + | // split); the suffix keeps the ids unique, the unified table having none. | |
| 1215 | + | func lineNotes(notes []CommentView, form *CommentForm, suffix string) templ.Component { | |
| 1216 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1217 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1218 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1219 | + | return templ_7745c5c3_CtxErr | |
| 1220 | + | } | |
| 1221 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1222 | + | if !templ_7745c5c3_IsBuffer { | |
| 1223 | + | defer func() { | |
| 1224 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1225 | + | if templ_7745c5c3_Err == nil { | |
| 1226 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1227 | + | } | |
| 1228 | + | }() | |
| 1229 | + | } | |
| 1230 | + | ctx = templ.InitializeContext(ctx) | |
| 1231 | + | templ_7745c5c3_Var60 := templ.GetChildren(ctx) | |
| 1232 | + | if templ_7745c5c3_Var60 == nil { | |
| 1233 | + | templ_7745c5c3_Var60 = templ.NopComponent | |
| 1234 | + | } | |
| 1235 | + | ctx = templ.ClearChildren(ctx) | |
| 1236 | + | for _, c := range notes { | |
| 1237 | + | templ_7745c5c3_Err = commentView(c, suffix).Render(ctx, templ_7745c5c3_Buffer) | |
| 1238 | + | if templ_7745c5c3_Err != nil { | |
| 1239 | + | return templ_7745c5c3_Err | |
| 1240 | + | } | |
| 1241 | + | } | |
| 1242 | + | if form != nil { | |
| 1243 | + | templ_7745c5c3_Err = commentForm(*form, suffix).Render(ctx, templ_7745c5c3_Buffer) | |
| 1244 | + | if templ_7745c5c3_Err != nil { | |
| 1245 | + | return templ_7745c5c3_Err | |
| 1246 | + | } | |
| 1247 | + | } | |
| 1248 | + | return nil | |
| 1249 | + | }) | |
| 1250 | + | } | |
| 1251 | + | ||
| 1252 | + | // commentView is a note: a ruled block, under its line in the diff or in the | |
| 1253 | + | // list of comments. The text is plain: escaped, its line breaks kept, its | |
| 1254 | + | // http(s) addresses linked. | |
| 1255 | + | func commentView(c CommentView, suffix string) templ.Component { | |
| 1256 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1257 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1258 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1259 | + | return templ_7745c5c3_CtxErr | |
| 1260 | + | } | |
| 1261 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1262 | + | if !templ_7745c5c3_IsBuffer { | |
| 1263 | + | defer func() { | |
| 1264 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1265 | + | if templ_7745c5c3_Err == nil { | |
| 1266 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1267 | + | } | |
| 1268 | + | }() | |
| 1269 | + | } | |
| 1270 | + | ctx = templ.InitializeContext(ctx) | |
| 1271 | + | templ_7745c5c3_Var61 := templ.GetChildren(ctx) | |
| 1272 | + | if templ_7745c5c3_Var61 == nil { | |
| 1273 | + | templ_7745c5c3_Var61 = templ.NopComponent | |
| 1274 | + | } | |
| 1275 | + | ctx = templ.ClearChildren(ctx) | |
| 1276 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<div class=\"cmt\" id=\"") | |
| 1277 | + | if templ_7745c5c3_Err != nil { | |
| 1278 | + | return templ_7745c5c3_Err | |
| 1279 | + | } | |
| 1280 | + | var templ_7745c5c3_Var62 string | |
| 1281 | + | templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue("c" + c.ID + suffix) | |
| 1282 | + | if templ_7745c5c3_Err != nil { | |
| 1283 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 42} | |
| 1284 | + | } | |
| 1285 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62) | |
| 1286 | + | if templ_7745c5c3_Err != nil { | |
| 1287 | + | return templ_7745c5c3_Err | |
| 1288 | + | } | |
| 1289 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\" data-resolved=\"") | |
| 1290 | + | if templ_7745c5c3_Err != nil { | |
| 1291 | + | return templ_7745c5c3_Err | |
| 1292 | + | } | |
| 1293 | + | var templ_7745c5c3_Var63 string | |
| 1294 | + | templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolved)) | |
| 1295 | + | if templ_7745c5c3_Err != nil { | |
| 1296 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 240, Col: 81} | |
| 1297 | + | } | |
| 1298 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) | |
| 1299 | + | if templ_7745c5c3_Err != nil { | |
| 1300 | + | return templ_7745c5c3_Err | |
| 1301 | + | } | |
| 1302 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\"><p class=\"c-head\"><span class=\"c-who\">") | |
| 1303 | + | if templ_7745c5c3_Err != nil { | |
| 1304 | + | return templ_7745c5c3_Err | |
| 1305 | + | } | |
| 1306 | + | var templ_7745c5c3_Var64 string | |
| 1307 | + | templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1308 | + | if templ_7745c5c3_Err != nil { | |
| 1309 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 242, Col: 33} | |
| 1310 | + | } | |
| 1311 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64)) | |
| 1312 | + | if templ_7745c5c3_Err != nil { | |
| 1313 | + | return templ_7745c5c3_Err | |
| 1314 | + | } | |
| 1315 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "</span> ") | |
| 1316 | + | if templ_7745c5c3_Err != nil { | |
| 1317 | + | return templ_7745c5c3_Err | |
| 1318 | + | } | |
| 1319 | + | if c.WhenAt != "" { | |
| 1320 | + | var templ_7745c5c3_Var65 string | |
| 1321 | + | templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1322 | + | if templ_7745c5c3_Err != nil { | |
| 1323 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 9} | |
| 1324 | + | } | |
| 1325 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65)) | |
| 1326 | + | if templ_7745c5c3_Err != nil { | |
| 1327 | + | return templ_7745c5c3_Err | |
| 1328 | + | } | |
| 1329 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "<time class=\"mute\" datetime=\"") | |
| 1330 | + | if templ_7745c5c3_Err != nil { | |
| 1331 | + | return templ_7745c5c3_Err | |
| 1332 | + | } | |
| 1333 | + | var templ_7745c5c3_Var66 string | |
| 1334 | + | templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1335 | + | if templ_7745c5c3_Err != nil { | |
| 1336 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 49} | |
| 1337 | + | } | |
| 1338 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66) | |
| 1339 | + | if templ_7745c5c3_Err != nil { | |
| 1340 | + | return templ_7745c5c3_Err | |
| 1341 | + | } | |
| 1342 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\" title=\"") | |
| 1343 | + | if templ_7745c5c3_Err != nil { | |
| 1344 | + | return templ_7745c5c3_Err | |
| 1345 | + | } | |
| 1346 | + | var templ_7745c5c3_Var67 string | |
| 1347 | + | templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Abs) | |
| 1348 | + | if templ_7745c5c3_Err != nil { | |
| 1349 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 65} | |
| 1350 | + | } | |
| 1351 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67) | |
| 1352 | + | if templ_7745c5c3_Err != nil { | |
| 1353 | + | return templ_7745c5c3_Err | |
| 1354 | + | } | |
| 1355 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\">") | |
| 1356 | + | if templ_7745c5c3_Err != nil { | |
| 1357 | + | return templ_7745c5c3_Err | |
| 1358 | + | } | |
| 1359 | + | var templ_7745c5c3_Var68 string | |
| 1360 | + | templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1361 | + | if templ_7745c5c3_Err != nil { | |
| 1362 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 244, Col: 76} | |
| 1363 | + | } | |
| 1364 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) | |
| 1365 | + | if templ_7745c5c3_Err != nil { | |
| 1366 | + | return templ_7745c5c3_Err | |
| 1367 | + | } | |
| 1368 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "</time> ") | |
| 1369 | + | if templ_7745c5c3_Err != nil { | |
| 1370 | + | return templ_7745c5c3_Err | |
| 1371 | + | } | |
| 1372 | + | } | |
| 1373 | + | if c.Resolved { | |
| 1374 | + | var templ_7745c5c3_Var69 string | |
| 1375 | + | templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1376 | + | if templ_7745c5c3_Err != nil { | |
| 1377 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 247, Col: 9} | |
| 1378 | + | } | |
| 1379 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69)) | |
| 1380 | + | if templ_7745c5c3_Err != nil { | |
| 1381 | + | return templ_7745c5c3_Err | |
| 1382 | + | } | |
| 1383 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "<span class=\"c-state\">resolved</span> ") | |
| 1384 | + | if templ_7745c5c3_Err != nil { | |
| 1385 | + | return templ_7745c5c3_Err | |
| 1386 | + | } | |
| 1387 | + | } | |
| 1388 | + | if c.Outdated { | |
| 1389 | + | var templ_7745c5c3_Var70 string | |
| 1390 | + | templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1391 | + | if templ_7745c5c3_Err != nil { | |
| 1392 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 250, Col: 9} | |
| 1393 | + | } | |
| 1394 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var70)) | |
| 1395 | + | if templ_7745c5c3_Err != nil { | |
| 1396 | + | return templ_7745c5c3_Err | |
| 1397 | + | } | |
| 1398 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "<span class=\"c-state\" title=\"written on an earlier head; the line may have moved\">outdated</span> ") | |
| 1399 | + | if templ_7745c5c3_Err != nil { | |
| 1400 | + | return templ_7745c5c3_Err | |
| 1401 | + | } | |
| 1402 | + | } | |
| 1403 | + | if c.Where != "" { | |
| 1404 | + | var templ_7745c5c3_Var71 string | |
| 1405 | + | templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.JoinStringErrs(" ") | |
| 1406 | + | if templ_7745c5c3_Err != nil { | |
| 1407 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 9} | |
| 1408 | + | } | |
| 1409 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var71)) | |
| 1410 | + | if templ_7745c5c3_Err != nil { | |
| 1411 | + | return templ_7745c5c3_Err | |
| 1412 | + | } | |
| 1413 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "<span class=\"c-where mute\">") | |
| 1414 | + | if templ_7745c5c3_Err != nil { | |
| 1415 | + | return templ_7745c5c3_Err | |
| 1416 | + | } | |
| 1417 | + | var templ_7745c5c3_Var72 string | |
| 1418 | + | templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(c.Where) | |
| 1419 | + | if templ_7745c5c3_Err != nil { | |
| 1420 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 253, Col: 47} | |
| 1421 | + | } | |
| 1422 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72)) | |
| 1423 | + | if templ_7745c5c3_Err != nil { | |
| 1424 | + | return templ_7745c5c3_Err | |
| 1425 | + | } | |
| 1426 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "</span>") | |
| 1427 | + | if templ_7745c5c3_Err != nil { | |
| 1428 | + | return templ_7745c5c3_Err | |
| 1429 | + | } | |
| 1430 | + | } | |
| 1431 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</p><p class=\"c-text\">") | |
| 1432 | + | if templ_7745c5c3_Err != nil { | |
| 1433 | + | return templ_7745c5c3_Err | |
| 1434 | + | } | |
| 1435 | + | templ_7745c5c3_Err = userText(c.Parts).Render(ctx, templ_7745c5c3_Buffer) | |
| 1436 | + | if templ_7745c5c3_Err != nil { | |
| 1437 | + | return templ_7745c5c3_Err | |
| 1438 | + | } | |
| 1439 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</p>") | |
| 1440 | + | if templ_7745c5c3_Err != nil { | |
| 1441 | + | return templ_7745c5c3_Err | |
| 1442 | + | } | |
| 1443 | + | if c.Resolve != nil { | |
| 1444 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "<form class=\"c-act\" method=\"post\" action=\"") | |
| 1445 | + | if templ_7745c5c3_Err != nil { | |
| 1446 | + | return templ_7745c5c3_Err | |
| 1447 | + | } | |
| 1448 | + | var templ_7745c5c3_Var73 templ.SafeURL | |
| 1449 | + | templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Resolve.Action)) | |
| 1450 | + | if templ_7745c5c3_Err != nil { | |
| 1451 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 258, Col: 77} | |
| 1452 | + | } | |
| 1453 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var73)) | |
| 1454 | + | if templ_7745c5c3_Err != nil { | |
| 1455 | + | return templ_7745c5c3_Err | |
| 1456 | + | } | |
| 1457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\"><input type=\"hidden\" name=\"resolved\" value=\"") | |
| 1458 | + | if templ_7745c5c3_Err != nil { | |
| 1459 | + | return templ_7745c5c3_Err | |
| 1460 | + | } | |
| 1461 | + | var templ_7745c5c3_Var74 string | |
| 1462 | + | templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolText(c.Resolve.Value)) | |
| 1463 | + | if templ_7745c5c3_Err != nil { | |
| 1464 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 259, Col: 74} | |
| 1465 | + | } | |
| 1466 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74) | |
| 1467 | + | if templ_7745c5c3_Err != nil { | |
| 1468 | + | return templ_7745c5c3_Err | |
| 1469 | + | } | |
| 1470 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\"> <button type=\"submit\">") | |
| 1471 | + | if templ_7745c5c3_Err != nil { | |
| 1472 | + | return templ_7745c5c3_Err | |
| 1473 | + | } | |
| 1474 | + | var templ_7745c5c3_Var75 string | |
| 1475 | + | templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.JoinStringErrs(c.Resolve.Label) | |
| 1476 | + | if templ_7745c5c3_Err != nil { | |
| 1477 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 260, Col: 43} | |
| 1478 | + | } | |
| 1479 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var75)) | |
| 1480 | + | if templ_7745c5c3_Err != nil { | |
| 1481 | + | return templ_7745c5c3_Err | |
| 1482 | + | } | |
| 1483 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "</button></form>") | |
| 1484 | + | if templ_7745c5c3_Err != nil { | |
| 1485 | + | return templ_7745c5c3_Err | |
| 1486 | + | } | |
| 1487 | + | } | |
| 1488 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</div>") | |
| 1489 | + | if templ_7745c5c3_Err != nil { | |
| 1490 | + | return templ_7745c5c3_Err | |
| 1491 | + | } | |
| 1492 | + | return nil | |
| 1493 | + | }) | |
| 1494 | + | } | |
| 1495 | + | ||
| 1496 | + | func boolText(b bool) string { | |
| 1497 | + | if b { | |
| 1498 | + | return "true" | |
| 1499 | + | } | |
| 1500 | + | return "false" | |
| 1501 | + | } | |
| 1502 | + | ||
| 1503 | + | // commentForm writes a comment, general or on a line. | |
| 1504 | + | func commentForm(f CommentForm, suffix string) templ.Component { | |
| 1505 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1506 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1507 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1508 | + | return templ_7745c5c3_CtxErr | |
| 1509 | + | } | |
| 1510 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1511 | + | if !templ_7745c5c3_IsBuffer { | |
| 1512 | + | defer func() { | |
| 1513 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1514 | + | if templ_7745c5c3_Err == nil { | |
| 1515 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1516 | + | } | |
| 1517 | + | }() | |
| 1518 | + | } | |
| 1519 | + | ctx = templ.InitializeContext(ctx) | |
| 1520 | + | templ_7745c5c3_Var76 := templ.GetChildren(ctx) | |
| 1521 | + | if templ_7745c5c3_Var76 == nil { | |
| 1522 | + | templ_7745c5c3_Var76 = templ.NopComponent | |
| 1523 | + | } | |
| 1524 | + | ctx = templ.ClearChildren(ctx) | |
| 1525 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<form class=\"cform\" method=\"post\" action=\"") | |
| 1526 | + | if templ_7745c5c3_Err != nil { | |
| 1527 | + | return templ_7745c5c3_Err | |
| 1528 | + | } | |
| 1529 | + | var templ_7745c5c3_Var77 templ.SafeURL | |
| 1530 | + | templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Action)) | |
| 1531 | + | if templ_7745c5c3_Err != nil { | |
| 1532 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 275, Col: 67} | |
| 1533 | + | } | |
| 1534 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var77)) | |
| 1535 | + | if templ_7745c5c3_Err != nil { | |
| 1536 | + | return templ_7745c5c3_Err | |
| 1537 | + | } | |
| 1538 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "\"><label for=\"") | |
| 1539 | + | if templ_7745c5c3_Err != nil { | |
| 1540 | + | return templ_7745c5c3_Err | |
| 1541 | + | } | |
| 1542 | + | var templ_7745c5c3_Var78 string | |
| 1543 | + | templ_7745c5c3_Var78, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1544 | + | if templ_7745c5c3_Err != nil { | |
| 1545 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 28} | |
| 1546 | + | } | |
| 1547 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var78) | |
| 1548 | + | if templ_7745c5c3_Err != nil { | |
| 1549 | + | return templ_7745c5c3_Err | |
| 1550 | + | } | |
| 1551 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "\">") | |
| 1552 | + | if templ_7745c5c3_Err != nil { | |
| 1553 | + | return templ_7745c5c3_Err | |
| 1554 | + | } | |
| 1555 | + | var templ_7745c5c3_Var79 string | |
| 1556 | + | templ_7745c5c3_Var79, templ_7745c5c3_Err = templ.JoinStringErrs(f.Title) | |
| 1557 | + | if templ_7745c5c3_Err != nil { | |
| 1558 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 276, Col: 40} | |
| 1559 | + | } | |
| 1560 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var79)) | |
| 1561 | + | if templ_7745c5c3_Err != nil { | |
| 1562 | + | return templ_7745c5c3_Err | |
| 1563 | + | } | |
| 1564 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</label> <textarea id=\"") | |
| 1565 | + | if templ_7745c5c3_Err != nil { | |
| 1566 | + | return templ_7745c5c3_Err | |
| 1567 | + | } | |
| 1568 | + | var templ_7745c5c3_Var80 string | |
| 1569 | + | templ_7745c5c3_Var80, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.ID + suffix) | |
| 1570 | + | if templ_7745c5c3_Err != nil { | |
| 1571 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 278, Col: 21} | |
| 1572 | + | } | |
| 1573 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var80) | |
| 1574 | + | if templ_7745c5c3_Err != nil { | |
| 1575 | + | return templ_7745c5c3_Err | |
| 1576 | + | } | |
| 1577 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" name=\"text\" rows=\"3\" maxlength=\"8000\" required placeholder=\"plain text, 8000 characters at most\"") | |
| 1578 | + | if templ_7745c5c3_Err != nil { | |
| 1579 | + | return templ_7745c5c3_Err | |
| 1580 | + | } | |
| 1581 | + | if f.Focus { | |
| 1582 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, " autofocus") | |
| 1583 | + | if templ_7745c5c3_Err != nil { | |
| 1584 | + | return templ_7745c5c3_Err | |
| 1585 | + | } | |
| 1586 | + | } | |
| 1587 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "></textarea> ") | |
| 1588 | + | if templ_7745c5c3_Err != nil { | |
| 1589 | + | return templ_7745c5c3_Err | |
| 1590 | + | } | |
| 1591 | + | if f.Path != "" { | |
| 1592 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "<input type=\"hidden\" name=\"path\" value=\"") | |
| 1593 | + | if templ_7745c5c3_Err != nil { | |
| 1594 | + | return templ_7745c5c3_Err | |
| 1595 | + | } | |
| 1596 | + | var templ_7745c5c3_Var81 string | |
| 1597 | + | templ_7745c5c3_Var81, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Path) | |
| 1598 | + | if templ_7745c5c3_Err != nil { | |
| 1599 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 289, Col: 50} | |
| 1600 | + | } | |
| 1601 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var81) | |
| 1602 | + | if templ_7745c5c3_Err != nil { | |
| 1603 | + | return templ_7745c5c3_Err | |
| 1604 | + | } | |
| 1605 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\"> <input type=\"hidden\" name=\"line\" value=\"") | |
| 1606 | + | if templ_7745c5c3_Err != nil { | |
| 1607 | + | return templ_7745c5c3_Err | |
| 1608 | + | } | |
| 1609 | + | var templ_7745c5c3_Var82 string | |
| 1610 | + | templ_7745c5c3_Var82, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Line) | |
| 1611 | + | if templ_7745c5c3_Err != nil { | |
| 1612 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 290, Col: 50} | |
| 1613 | + | } | |
| 1614 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var82) | |
| 1615 | + | if templ_7745c5c3_Err != nil { | |
| 1616 | + | return templ_7745c5c3_Err | |
| 1617 | + | } | |
| 1618 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "\"> <input type=\"hidden\" name=\"side\" value=\"") | |
| 1619 | + | if templ_7745c5c3_Err != nil { | |
| 1620 | + | return templ_7745c5c3_Err | |
| 1621 | + | } | |
| 1622 | + | var templ_7745c5c3_Var83 string | |
| 1623 | + | templ_7745c5c3_Var83, templ_7745c5c3_Err = templ.ResolveAttributeValue(f.Side) | |
| 1624 | + | if templ_7745c5c3_Err != nil { | |
| 1625 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 291, Col: 50} | |
| 1626 | + | } | |
| 1627 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var83) | |
| 1628 | + | if templ_7745c5c3_Err != nil { | |
| 1629 | + | return templ_7745c5c3_Err | |
| 1630 | + | } | |
| 1631 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "\">") | |
| 1632 | + | if templ_7745c5c3_Err != nil { | |
| 1633 | + | return templ_7745c5c3_Err | |
| 1634 | + | } | |
| 1635 | + | } | |
| 1636 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "<div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[comment]</button> ") | |
| 1637 | + | if templ_7745c5c3_Err != nil { | |
| 1638 | + | return templ_7745c5c3_Err | |
| 1639 | + | } | |
| 1640 | + | if f.Cancel != "" { | |
| 1641 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 143, "<a href=\"") | |
| 1642 | + | if templ_7745c5c3_Err != nil { | |
| 1643 | + | return templ_7745c5c3_Err | |
| 1644 | + | } | |
| 1645 | + | var templ_7745c5c3_Var84 templ.SafeURL | |
| 1646 | + | templ_7745c5c3_Var84, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(f.Cancel)) | |
| 1647 | + | if templ_7745c5c3_Err != nil { | |
| 1648 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 296, Col: 37} | |
| 1649 | + | } | |
| 1650 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var84)) | |
| 1651 | + | if templ_7745c5c3_Err != nil { | |
| 1652 | + | return templ_7745c5c3_Err | |
| 1653 | + | } | |
| 1654 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 144, "\">[cancel]</a>") | |
| 1655 | + | if templ_7745c5c3_Err != nil { | |
| 1656 | + | return templ_7745c5c3_Err | |
| 1657 | + | } | |
| 1658 | + | } | |
| 1659 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 145, "</div></form>") | |
| 1660 | + | if templ_7745c5c3_Err != nil { | |
| 1661 | + | return templ_7745c5c3_Err | |
| 1662 | + | } | |
| 1663 | + | return nil | |
| 1664 | + | }) | |
| 1665 | + | } | |
| 1666 | + | ||
| 1667 | + | func mrRail(p MRPage) templ.Component { | |
| 1668 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1669 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1670 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1671 | + | return templ_7745c5c3_CtxErr | |
| 1672 | + | } | |
| 1673 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1674 | + | if !templ_7745c5c3_IsBuffer { | |
| 1675 | + | defer func() { | |
| 1676 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1677 | + | if templ_7745c5c3_Err == nil { | |
| 1678 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1679 | + | } | |
| 1680 | + | }() | |
| 1681 | + | } | |
| 1682 | + | ctx = templ.InitializeContext(ctx) | |
| 1683 | + | templ_7745c5c3_Var85 := templ.GetChildren(ctx) | |
| 1684 | + | if templ_7745c5c3_Var85 == nil { | |
| 1685 | + | templ_7745c5c3_Var85 = templ.NopComponent | |
| 1686 | + | } | |
| 1687 | + | ctx = templ.ClearChildren(ctx) | |
| 1688 | + | if p.Box != nil { | |
| 1689 | + | templ_7745c5c3_Err = mergeBox(p).Render(ctx, templ_7745c5c3_Buffer) | |
| 1690 | + | if templ_7745c5c3_Err != nil { | |
| 1691 | + | return templ_7745c5c3_Err | |
| 1692 | + | } | |
| 1693 | + | } | |
| 1694 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 146, "<section aria-labelledby=\"r-checks\"><h2 id=\"r-checks\">checks <span>0</span></h2><p class=\"dim\">no checks</p><p class=\"mute\">there is no CI yet: nothing blocks the merge.</p></section><section aria-labelledby=\"r-settings\"><h2 id=\"r-settings\">settings</h2><dl class=\"kv\"><dt>modes</dt><dd>") | |
| 1695 | + | if templ_7745c5c3_Err != nil { | |
| 1696 | + | return templ_7745c5c3_Err | |
| 1697 | + | } | |
| 1698 | + | var templ_7745c5c3_Var86 string | |
| 1699 | + | templ_7745c5c3_Var86, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(p.Modes, ", ")) | |
| 1700 | + | if templ_7745c5c3_Err != nil { | |
| 1701 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 315, Col: 36} | |
| 1702 | + | } | |
| 1703 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var86)) | |
| 1704 | + | if templ_7745c5c3_Err != nil { | |
| 1705 | + | return templ_7745c5c3_Err | |
| 1706 | + | } | |
| 1707 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 147, "</dd><dt>checks</dt><dd>") | |
| 1708 | + | if templ_7745c5c3_Err != nil { | |
| 1709 | + | return templ_7745c5c3_Err | |
| 1710 | + | } | |
| 1711 | + | if p.ChecksBlock { | |
| 1712 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 148, "failing ones block the merge") | |
| 1713 | + | if templ_7745c5c3_Err != nil { | |
| 1714 | + | return templ_7745c5c3_Err | |
| 1715 | + | } | |
| 1716 | + | } else { | |
| 1717 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 149, "do not block the merge") | |
| 1718 | + | if templ_7745c5c3_Err != nil { | |
| 1719 | + | return templ_7745c5c3_Err | |
| 1720 | + | } | |
| 1721 | + | } | |
| 1722 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 150, "</dd></dl></section><section aria-labelledby=\"r-commits\"><h2 id=\"r-commits\">commits <span>") | |
| 1723 | + | if templ_7745c5c3_Err != nil { | |
| 1724 | + | return templ_7745c5c3_Err | |
| 1725 | + | } | |
| 1726 | + | var templ_7745c5c3_Var87 string | |
| 1727 | + | templ_7745c5c3_Var87, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Commits))) | |
| 1728 | + | if templ_7745c5c3_Err != nil { | |
| 1729 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 327, Col: 57} | |
| 1730 | + | } | |
| 1731 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var87)) | |
| 1732 | + | if templ_7745c5c3_Err != nil { | |
| 1733 | + | return templ_7745c5c3_Err | |
| 1734 | + | } | |
| 1735 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 151, "</span></h2><ul class=\"pcommits\">") | |
| 1736 | + | if templ_7745c5c3_Err != nil { | |
| 1737 | + | return templ_7745c5c3_Err | |
| 1738 | + | } | |
| 1739 | + | for _, c := range p.Commits { | |
| 1740 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 152, "<li") | |
| 1741 | + | if templ_7745c5c3_Err != nil { | |
| 1742 | + | return templ_7745c5c3_Err | |
| 1743 | + | } | |
| 1744 | + | if c.New { | |
| 1745 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 153, " data-new") | |
| 1746 | + | if templ_7745c5c3_Err != nil { | |
| 1747 | + | return templ_7745c5c3_Err | |
| 1748 | + | } | |
| 1749 | + | } | |
| 1750 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 154, "><a href=\"") | |
| 1751 | + | if templ_7745c5c3_Err != nil { | |
| 1752 | + | return templ_7745c5c3_Err | |
| 1753 | + | } | |
| 1754 | + | var templ_7745c5c3_Var88 templ.SafeURL | |
| 1755 | + | templ_7745c5c3_Var88, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.Href)) | |
| 1756 | + | if templ_7745c5c3_Err != nil { | |
| 1757 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 36} | |
| 1758 | + | } | |
| 1759 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var88)) | |
| 1760 | + | if templ_7745c5c3_Err != nil { | |
| 1761 | + | return templ_7745c5c3_Err | |
| 1762 | + | } | |
| 1763 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 155, "\">") | |
| 1764 | + | if templ_7745c5c3_Err != nil { | |
| 1765 | + | return templ_7745c5c3_Err | |
| 1766 | + | } | |
| 1767 | + | var templ_7745c5c3_Var89 string | |
| 1768 | + | templ_7745c5c3_Var89, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 1769 | + | if templ_7745c5c3_Err != nil { | |
| 1770 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 331, Col: 48} | |
| 1771 | + | } | |
| 1772 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var89)) | |
| 1773 | + | if templ_7745c5c3_Err != nil { | |
| 1774 | + | return templ_7745c5c3_Err | |
| 1775 | + | } | |
| 1776 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "</a> <span class=\"s\">") | |
| 1777 | + | if templ_7745c5c3_Err != nil { | |
| 1778 | + | return templ_7745c5c3_Err | |
| 1779 | + | } | |
| 1780 | + | var templ_7745c5c3_Var90 string | |
| 1781 | + | templ_7745c5c3_Var90, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 1782 | + | if templ_7745c5c3_Err != nil { | |
| 1783 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 332, Col: 32} | |
| 1784 | + | } | |
| 1785 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var90)) | |
| 1786 | + | if templ_7745c5c3_Err != nil { | |
| 1787 | + | return templ_7745c5c3_Err | |
| 1788 | + | } | |
| 1789 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 157, "</span> <span class=\"mute\">") | |
| 1790 | + | if templ_7745c5c3_Err != nil { | |
| 1791 | + | return templ_7745c5c3_Err | |
| 1792 | + | } | |
| 1793 | + | var templ_7745c5c3_Var91 string | |
| 1794 | + | templ_7745c5c3_Var91, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 1795 | + | if templ_7745c5c3_Err != nil { | |
| 1796 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 334, Col: 16} | |
| 1797 | + | } | |
| 1798 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var91)) | |
| 1799 | + | if templ_7745c5c3_Err != nil { | |
| 1800 | + | return templ_7745c5c3_Err | |
| 1801 | + | } | |
| 1802 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 158, " ") | |
| 1803 | + | if templ_7745c5c3_Err != nil { | |
| 1804 | + | return templ_7745c5c3_Err | |
| 1805 | + | } | |
| 1806 | + | if c.WhenAt != "" { | |
| 1807 | + | var templ_7745c5c3_Var92 string | |
| 1808 | + | templ_7745c5c3_Var92, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 1809 | + | if templ_7745c5c3_Err != nil { | |
| 1810 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 15} | |
| 1811 | + | } | |
| 1812 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var92)) | |
| 1813 | + | if templ_7745c5c3_Err != nil { | |
| 1814 | + | return templ_7745c5c3_Err | |
| 1815 | + | } | |
| 1816 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 159, "<time datetime=\"") | |
| 1817 | + | if templ_7745c5c3_Err != nil { | |
| 1818 | + | return templ_7745c5c3_Err | |
| 1819 | + | } | |
| 1820 | + | var templ_7745c5c3_Var93 string | |
| 1821 | + | templ_7745c5c3_Var93, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 1822 | + | if templ_7745c5c3_Err != nil { | |
| 1823 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 42} | |
| 1824 | + | } | |
| 1825 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var93) | |
| 1826 | + | if templ_7745c5c3_Err != nil { | |
| 1827 | + | return templ_7745c5c3_Err | |
| 1828 | + | } | |
| 1829 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 160, "\">") | |
| 1830 | + | if templ_7745c5c3_Err != nil { | |
| 1831 | + | return templ_7745c5c3_Err | |
| 1832 | + | } | |
| 1833 | + | var templ_7745c5c3_Var94 string | |
| 1834 | + | templ_7745c5c3_Var94, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 1835 | + | if templ_7745c5c3_Err != nil { | |
| 1836 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 336, Col: 53} | |
| 1837 | + | } | |
| 1838 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var94)) | |
| 1839 | + | if templ_7745c5c3_Err != nil { | |
| 1840 | + | return templ_7745c5c3_Err | |
| 1841 | + | } | |
| 1842 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 161, "</time> ") | |
| 1843 | + | if templ_7745c5c3_Err != nil { | |
| 1844 | + | return templ_7745c5c3_Err | |
| 1845 | + | } | |
| 1846 | + | } | |
| 1847 | + | if c.New { | |
| 1848 | + | var templ_7745c5c3_Var95 string | |
| 1849 | + | templ_7745c5c3_Var95, templ_7745c5c3_Err = templ.JoinStringErrs(" · new") | |
| 1850 | + | if templ_7745c5c3_Err != nil { | |
| 1851 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 339, Col: 18} | |
| 1852 | + | } | |
| 1853 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var95)) | |
| 1854 | + | if templ_7745c5c3_Err != nil { | |
| 1855 | + | return templ_7745c5c3_Err | |
| 1856 | + | } | |
| 1857 | + | } | |
| 1858 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 162, "</span></li>") | |
| 1859 | + | if templ_7745c5c3_Err != nil { | |
| 1860 | + | return templ_7745c5c3_Err | |
| 1861 | + | } | |
| 1862 | + | } | |
| 1863 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 163, "</ul>") | |
| 1864 | + | if templ_7745c5c3_Err != nil { | |
| 1865 | + | return templ_7745c5c3_Err | |
| 1866 | + | } | |
| 1867 | + | if len(p.Commits) == 0 { | |
| 1868 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 164, "<p class=\"dim\">no commit to merge.</p>") | |
| 1869 | + | if templ_7745c5c3_Err != nil { | |
| 1870 | + | return templ_7745c5c3_Err | |
| 1871 | + | } | |
| 1872 | + | } | |
| 1873 | + | if p.CommitsNote != "" { | |
| 1874 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 165, "<p class=\"note\">") | |
| 1875 | + | if templ_7745c5c3_Err != nil { | |
| 1876 | + | return templ_7745c5c3_Err | |
| 1877 | + | } | |
| 1878 | + | var templ_7745c5c3_Var96 string | |
| 1879 | + | templ_7745c5c3_Var96, templ_7745c5c3_Err = templ.JoinStringErrs(p.CommitsNote) | |
| 1880 | + | if templ_7745c5c3_Err != nil { | |
| 1881 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 349, Col: 34} | |
| 1882 | + | } | |
| 1883 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var96)) | |
| 1884 | + | if templ_7745c5c3_Err != nil { | |
| 1885 | + | return templ_7745c5c3_Err | |
| 1886 | + | } | |
| 1887 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 166, "</p>") | |
| 1888 | + | if templ_7745c5c3_Err != nil { | |
| 1889 | + | return templ_7745c5c3_Err | |
| 1890 | + | } | |
| 1891 | + | } | |
| 1892 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 167, "</section><section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") | |
| 1893 | + | if templ_7745c5c3_Err != nil { | |
| 1894 | + | return templ_7745c5c3_Err | |
| 1895 | + | } | |
| 1896 | + | var templ_7745c5c3_Var97 string | |
| 1897 | + | templ_7745c5c3_Var97, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 1898 | + | if templ_7745c5c3_Err != nil { | |
| 1899 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 353, Col: 51} | |
| 1900 | + | } | |
| 1901 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var97)) | |
| 1902 | + | if templ_7745c5c3_Err != nil { | |
| 1903 | + | return templ_7745c5c3_Err | |
| 1904 | + | } | |
| 1905 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 168, "</span></h2>") | |
| 1906 | + | if templ_7745c5c3_Err != nil { | |
| 1907 | + | return templ_7745c5c3_Err | |
| 1908 | + | } | |
| 1909 | + | templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer) | |
| 1910 | + | if templ_7745c5c3_Err != nil { | |
| 1911 | + | return templ_7745c5c3_Err | |
| 1912 | + | } | |
| 1913 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 169, "</section>") | |
| 1914 | + | if templ_7745c5c3_Err != nil { | |
| 1915 | + | return templ_7745c5c3_Err | |
| 1916 | + | } | |
| 1917 | + | return nil | |
| 1918 | + | }) | |
| 1919 | + | } | |
| 1920 | + | ||
| 1921 | + | // mergeBox is the first block of the rail of an open request, for the owner: | |
| 1922 | + | // the merge form (it carries the head that was read), the terminal lines, and | |
| 1923 | + | // the other things one does with the request. | |
| 1924 | + | func mergeBox(p MRPage) templ.Component { | |
| 1925 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 1926 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 1927 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 1928 | + | return templ_7745c5c3_CtxErr | |
| 1929 | + | } | |
| 1930 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 1931 | + | if !templ_7745c5c3_IsBuffer { | |
| 1932 | + | defer func() { | |
| 1933 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 1934 | + | if templ_7745c5c3_Err == nil { | |
| 1935 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 1936 | + | } | |
| 1937 | + | }() | |
| 1938 | + | } | |
| 1939 | + | ctx = templ.InitializeContext(ctx) | |
| 1940 | + | templ_7745c5c3_Var98 := templ.GetChildren(ctx) | |
| 1941 | + | if templ_7745c5c3_Var98 == nil { | |
| 1942 | + | templ_7745c5c3_Var98 = templ.NopComponent | |
| 1943 | + | } | |
| 1944 | + | ctx = templ.ClearChildren(ctx) | |
| 1945 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 170, "<section class=\"decide\" aria-labelledby=\"r-merge\" data-state=\"open\"><h2 id=\"r-merge\">merge</h2><dl class=\"kv\"><dt>head</dt><dd>") | |
| 1946 | + | if templ_7745c5c3_Err != nil { | |
| 1947 | + | return templ_7745c5c3_Err | |
| 1948 | + | } | |
| 1949 | + | if p.Box.HeadGone { | |
| 1950 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 171, "gone: the source branch was deleted") | |
| 1951 | + | if templ_7745c5c3_Err != nil { | |
| 1952 | + | return templ_7745c5c3_Err | |
| 1953 | + | } | |
| 1954 | + | } else { | |
| 1955 | + | var templ_7745c5c3_Var99 string | |
| 1956 | + | templ_7745c5c3_Var99, templ_7745c5c3_Err = templ.JoinStringErrs(p.HeadShort) | |
| 1957 | + | if templ_7745c5c3_Err != nil { | |
| 1958 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 370, Col: 18} | |
| 1959 | + | } | |
| 1960 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var99)) | |
| 1961 | + | if templ_7745c5c3_Err != nil { | |
| 1962 | + | return templ_7745c5c3_Err | |
| 1963 | + | } | |
| 1964 | + | } | |
| 1965 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 172, "</dd><dt>into</dt><dd>") | |
| 1966 | + | if templ_7745c5c3_Err != nil { | |
| 1967 | + | return templ_7745c5c3_Err | |
| 1968 | + | } | |
| 1969 | + | var templ_7745c5c3_Var100 string | |
| 1970 | + | templ_7745c5c3_Var100, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 1971 | + | if templ_7745c5c3_Err != nil { | |
| 1972 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 374, Col: 17} | |
| 1973 | + | } | |
| 1974 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var100)) | |
| 1975 | + | if templ_7745c5c3_Err != nil { | |
| 1976 | + | return templ_7745c5c3_Err | |
| 1977 | + | } | |
| 1978 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 173, "</dd></dl>") | |
| 1979 | + | if templ_7745c5c3_Err != nil { | |
| 1980 | + | return templ_7745c5c3_Err | |
| 1981 | + | } | |
| 1982 | + | if !p.Box.HeadGone { | |
| 1983 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 174, "<form class=\"d-acts\" method=\"post\" action=\"") | |
| 1984 | + | if templ_7745c5c3_Err != nil { | |
| 1985 | + | return templ_7745c5c3_Err | |
| 1986 | + | } | |
| 1987 | + | var templ_7745c5c3_Var101 templ.SafeURL | |
| 1988 | + | templ_7745c5c3_Var101, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.MergeAction)) | |
| 1989 | + | if templ_7745c5c3_Err != nil { | |
| 1990 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 377, Col: 79} | |
| 1991 | + | } | |
| 1992 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var101)) | |
| 1993 | + | if templ_7745c5c3_Err != nil { | |
| 1994 | + | return templ_7745c5c3_Err | |
| 1995 | + | } | |
| 1996 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 175, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 1997 | + | if templ_7745c5c3_Err != nil { | |
| 1998 | + | return templ_7745c5c3_Err | |
| 1999 | + | } | |
| 2000 | + | var templ_7745c5c3_Var102 string | |
| 2001 | + | templ_7745c5c3_Var102, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2002 | + | if templ_7745c5c3_Err != nil { | |
| 2003 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 378, Col: 58} | |
| 2004 | + | } | |
| 2005 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var102) | |
| 2006 | + | if templ_7745c5c3_Err != nil { | |
| 2007 | + | return templ_7745c5c3_Err | |
| 2008 | + | } | |
| 2009 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 176, "\"> <label class=\"sr\" for=\"merge-mode\">merge mode</label> <select id=\"merge-mode\" name=\"mode\">") | |
| 2010 | + | if templ_7745c5c3_Err != nil { | |
| 2011 | + | return templ_7745c5c3_Err | |
| 2012 | + | } | |
| 2013 | + | for _, m := range p.Box.Modes { | |
| 2014 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 177, "<option value=\"") | |
| 2015 | + | if templ_7745c5c3_Err != nil { | |
| 2016 | + | return templ_7745c5c3_Err | |
| 2017 | + | } | |
| 2018 | + | var templ_7745c5c3_Var103 string | |
| 2019 | + | templ_7745c5c3_Var103, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value) | |
| 2020 | + | if templ_7745c5c3_Err != nil { | |
| 2021 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 29} | |
| 2022 | + | } | |
| 2023 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var103) | |
| 2024 | + | if templ_7745c5c3_Err != nil { | |
| 2025 | + | return templ_7745c5c3_Err | |
| 2026 | + | } | |
| 2027 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 178, "\">") | |
| 2028 | + | if templ_7745c5c3_Err != nil { | |
| 2029 | + | return templ_7745c5c3_Err | |
| 2030 | + | } | |
| 2031 | + | var templ_7745c5c3_Var104 string | |
| 2032 | + | templ_7745c5c3_Var104, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label) | |
| 2033 | + | if templ_7745c5c3_Err != nil { | |
| 2034 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 382, Col: 41} | |
| 2035 | + | } | |
| 2036 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var104)) | |
| 2037 | + | if templ_7745c5c3_Err != nil { | |
| 2038 | + | return templ_7745c5c3_Err | |
| 2039 | + | } | |
| 2040 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 179, "</option>") | |
| 2041 | + | if templ_7745c5c3_Err != nil { | |
| 2042 | + | return templ_7745c5c3_Err | |
| 2043 | + | } | |
| 2044 | + | } | |
| 2045 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 180, "</select><div class=\"d-btns\"><button type=\"submit\" class=\"d-main\">[merge]</button></div></form>") | |
| 2046 | + | if templ_7745c5c3_Err != nil { | |
| 2047 | + | return templ_7745c5c3_Err | |
| 2048 | + | } | |
| 2049 | + | } | |
| 2050 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 181, "<div class=\"d-btns\">") | |
| 2051 | + | if templ_7745c5c3_Err != nil { | |
| 2052 | + | return templ_7745c5c3_Err | |
| 2053 | + | } | |
| 2054 | + | if p.Box.CanMarkRead { | |
| 2055 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 182, "<form method=\"post\" action=\"") | |
| 2056 | + | if templ_7745c5c3_Err != nil { | |
| 2057 | + | return templ_7745c5c3_Err | |
| 2058 | + | } | |
| 2059 | + | var templ_7745c5c3_Var105 templ.SafeURL | |
| 2060 | + | templ_7745c5c3_Var105, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.ReadAction)) | |
| 2061 | + | if templ_7745c5c3_Err != nil { | |
| 2062 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 392, Col: 64} | |
| 2063 | + | } | |
| 2064 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var105)) | |
| 2065 | + | if templ_7745c5c3_Err != nil { | |
| 2066 | + | return templ_7745c5c3_Err | |
| 2067 | + | } | |
| 2068 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 183, "\"><input type=\"hidden\" name=\"head_id\" value=\"") | |
| 2069 | + | if templ_7745c5c3_Err != nil { | |
| 2070 | + | return templ_7745c5c3_Err | |
| 2071 | + | } | |
| 2072 | + | var templ_7745c5c3_Var106 string | |
| 2073 | + | templ_7745c5c3_Var106, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Box.Head) | |
| 2074 | + | if templ_7745c5c3_Err != nil { | |
| 2075 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 393, Col: 59} | |
| 2076 | + | } | |
| 2077 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var106) | |
| 2078 | + | if templ_7745c5c3_Err != nil { | |
| 2079 | + | return templ_7745c5c3_Err | |
| 2080 | + | } | |
| 2081 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 184, "\"> <button type=\"submit\" title=\"the next visit can show only what changed after this head\">[mark read]</button></form>") | |
| 2082 | + | if templ_7745c5c3_Err != nil { | |
| 2083 | + | return templ_7745c5c3_Err | |
| 2084 | + | } | |
| 2085 | + | } | |
| 2086 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 185, "<form method=\"post\" action=\"") | |
| 2087 | + | if templ_7745c5c3_Err != nil { | |
| 2088 | + | return templ_7745c5c3_Err | |
| 2089 | + | } | |
| 2090 | + | var templ_7745c5c3_Var107 templ.SafeURL | |
| 2091 | + | templ_7745c5c3_Var107, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Box.CloseAction)) | |
| 2092 | + | if templ_7745c5c3_Err != nil { | |
| 2093 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 397, Col: 64} | |
| 2094 | + | } | |
| 2095 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var107)) | |
| 2096 | + | if templ_7745c5c3_Err != nil { | |
| 2097 | + | return templ_7745c5c3_Err | |
| 2098 | + | } | |
| 2099 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 186, "\"><button type=\"submit\" title=\"close without merging; the branch is kept\">[close]</button></form></div>") | |
| 2100 | + | if templ_7745c5c3_Err != nil { | |
| 2101 | + | return templ_7745c5c3_Err | |
| 2102 | + | } | |
| 2103 | + | if !p.Box.HeadGone { | |
| 2104 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 187, "<p class=\"mute d-or\">or from a terminal</p>") | |
| 2105 | + | if templ_7745c5c3_Err != nil { | |
| 2106 | + | return templ_7745c5c3_Err | |
| 2107 | + | } | |
| 2108 | + | templ_7745c5c3_Err = CloneBlock(p.Box.CLI).Render(ctx, templ_7745c5c3_Buffer) | |
| 2109 | + | if templ_7745c5c3_Err != nil { | |
| 2110 | + | return templ_7745c5c3_Err | |
| 2111 | + | } | |
| 2112 | + | } | |
| 2113 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 188, "</section>") | |
| 2114 | + | if templ_7745c5c3_Err != nil { | |
| 2115 | + | return templ_7745c5c3_Err | |
| 2116 | + | } | |
| 2117 | + | return nil | |
| 2118 | + | }) | |
| 2119 | + | } | |
| 2120 | + | ||
| 2121 | + | // Compare is a branch against the default branch: the commits it is ahead by, | |
| 2122 | + | // its diff when the API can give one, and the form that opens a merge request. | |
| 2123 | + | func Compare(l Layout, p ComparePage) templ.Component { | |
| 2124 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2125 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2126 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 2127 | + | return templ_7745c5c3_CtxErr | |
| 2128 | + | } | |
| 2129 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2130 | + | if !templ_7745c5c3_IsBuffer { | |
| 2131 | + | defer func() { | |
| 2132 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 2133 | + | if templ_7745c5c3_Err == nil { | |
| 2134 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 2135 | + | } | |
| 2136 | + | }() | |
| 2137 | + | } | |
| 2138 | + | ctx = templ.InitializeContext(ctx) | |
| 2139 | + | templ_7745c5c3_Var108 := templ.GetChildren(ctx) | |
| 2140 | + | if templ_7745c5c3_Var108 == nil { | |
| 2141 | + | templ_7745c5c3_Var108 = templ.NopComponent | |
| 2142 | + | } | |
| 2143 | + | ctx = templ.ClearChildren(ctx) | |
| 2144 | + | templ_7745c5c3_Var109 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2145 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2146 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2147 | + | if !templ_7745c5c3_IsBuffer { | |
| 2148 | + | defer func() { | |
| 2149 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 2150 | + | if templ_7745c5c3_Err == nil { | |
| 2151 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 2152 | + | } | |
| 2153 | + | }() | |
| 2154 | + | } | |
| 2155 | + | ctx = templ.InitializeContext(ctx) | |
| 2156 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 189, "<header class=\"show mr-h\"><h1>") | |
| 2157 | + | if templ_7745c5c3_Err != nil { | |
| 2158 | + | return templ_7745c5c3_Err | |
| 2159 | + | } | |
| 2160 | + | var templ_7745c5c3_Var110 string | |
| 2161 | + | templ_7745c5c3_Var110, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2162 | + | if templ_7745c5c3_Err != nil { | |
| 2163 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 17} | |
| 2164 | + | } | |
| 2165 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var110)) | |
| 2166 | + | if templ_7745c5c3_Err != nil { | |
| 2167 | + | return templ_7745c5c3_Err | |
| 2168 | + | } | |
| 2169 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 190, " → ") | |
| 2170 | + | if templ_7745c5c3_Err != nil { | |
| 2171 | + | return templ_7745c5c3_Err | |
| 2172 | + | } | |
| 2173 | + | var templ_7745c5c3_Var111 string | |
| 2174 | + | templ_7745c5c3_Var111, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2175 | + | if templ_7745c5c3_Err != nil { | |
| 2176 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 413, Col: 34} | |
| 2177 | + | } | |
| 2178 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var111)) | |
| 2179 | + | if templ_7745c5c3_Err != nil { | |
| 2180 | + | return templ_7745c5c3_Err | |
| 2181 | + | } | |
| 2182 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 191, "</h1><dl class=\"kv show-kv\"><dt>branch</dt><dd><a href=\"") | |
| 2183 | + | if templ_7745c5c3_Err != nil { | |
| 2184 | + | return templ_7745c5c3_Err | |
| 2185 | + | } | |
| 2186 | + | var templ_7745c5c3_Var112 templ.SafeURL | |
| 2187 | + | templ_7745c5c3_Var112, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.SourceHref)) | |
| 2188 | + | if templ_7745c5c3_Err != nil { | |
| 2189 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 45} | |
| 2190 | + | } | |
| 2191 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var112)) | |
| 2192 | + | if templ_7745c5c3_Err != nil { | |
| 2193 | + | return templ_7745c5c3_Err | |
| 2194 | + | } | |
| 2195 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 192, "\">") | |
| 2196 | + | if templ_7745c5c3_Err != nil { | |
| 2197 | + | return templ_7745c5c3_Err | |
| 2198 | + | } | |
| 2199 | + | var templ_7745c5c3_Var113 string | |
| 2200 | + | templ_7745c5c3_Var113, templ_7745c5c3_Err = templ.JoinStringErrs(p.Source) | |
| 2201 | + | if templ_7745c5c3_Err != nil { | |
| 2202 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 416, Col: 58} | |
| 2203 | + | } | |
| 2204 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var113)) | |
| 2205 | + | if templ_7745c5c3_Err != nil { | |
| 2206 | + | return templ_7745c5c3_Err | |
| 2207 | + | } | |
| 2208 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 193, "</a></dd><dt>target</dt><dd><a href=\"") | |
| 2209 | + | if templ_7745c5c3_Err != nil { | |
| 2210 | + | return templ_7745c5c3_Err | |
| 2211 | + | } | |
| 2212 | + | var templ_7745c5c3_Var114 templ.SafeURL | |
| 2213 | + | templ_7745c5c3_Var114, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.TargetHref)) | |
| 2214 | + | if templ_7745c5c3_Err != nil { | |
| 2215 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 45} | |
| 2216 | + | } | |
| 2217 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var114)) | |
| 2218 | + | if templ_7745c5c3_Err != nil { | |
| 2219 | + | return templ_7745c5c3_Err | |
| 2220 | + | } | |
| 2221 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 194, "\">") | |
| 2222 | + | if templ_7745c5c3_Err != nil { | |
| 2223 | + | return templ_7745c5c3_Err | |
| 2224 | + | } | |
| 2225 | + | var templ_7745c5c3_Var115 string | |
| 2226 | + | templ_7745c5c3_Var115, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2227 | + | if templ_7745c5c3_Err != nil { | |
| 2228 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 418, Col: 58} | |
| 2229 | + | } | |
| 2230 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var115)) | |
| 2231 | + | if templ_7745c5c3_Err != nil { | |
| 2232 | + | return templ_7745c5c3_Err | |
| 2233 | + | } | |
| 2234 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 195, "</a></dd><dt>ahead</dt><dd>") | |
| 2235 | + | if templ_7745c5c3_Err != nil { | |
| 2236 | + | return templ_7745c5c3_Err | |
| 2237 | + | } | |
| 2238 | + | var templ_7745c5c3_Var116 string | |
| 2239 | + | templ_7745c5c3_Var116, templ_7745c5c3_Err = templ.JoinStringErrs(p.AheadText) | |
| 2240 | + | if templ_7745c5c3_Err != nil { | |
| 2241 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 420, Col: 21} | |
| 2242 | + | } | |
| 2243 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var116)) | |
| 2244 | + | if templ_7745c5c3_Err != nil { | |
| 2245 | + | return templ_7745c5c3_Err | |
| 2246 | + | } | |
| 2247 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 196, "</dd></dl></header>") | |
| 2248 | + | if templ_7745c5c3_Err != nil { | |
| 2249 | + | return templ_7745c5c3_Err | |
| 2250 | + | } | |
| 2251 | + | if p.Note != "" { | |
| 2252 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 197, "<p class=\"notice\">") | |
| 2253 | + | if templ_7745c5c3_Err != nil { | |
| 2254 | + | return templ_7745c5c3_Err | |
| 2255 | + | } | |
| 2256 | + | var templ_7745c5c3_Var117 string | |
| 2257 | + | templ_7745c5c3_Var117, templ_7745c5c3_Err = templ.JoinStringErrs(p.Note) | |
| 2258 | + | if templ_7745c5c3_Err != nil { | |
| 2259 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 424, Col: 29} | |
| 2260 | + | } | |
| 2261 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var117)) | |
| 2262 | + | if templ_7745c5c3_Err != nil { | |
| 2263 | + | return templ_7745c5c3_Err | |
| 2264 | + | } | |
| 2265 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 198, "</p>") | |
| 2266 | + | if templ_7745c5c3_Err != nil { | |
| 2267 | + | return templ_7745c5c3_Err | |
| 2268 | + | } | |
| 2269 | + | } | |
| 2270 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 199, " ") | |
| 2271 | + | if templ_7745c5c3_Err != nil { | |
| 2272 | + | return templ_7745c5c3_Err | |
| 2273 | + | } | |
| 2274 | + | if p.Existing != nil { | |
| 2275 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 200, "<p class=\"notice\">an open merge request already covers these branches: <a href=\"") | |
| 2276 | + | if templ_7745c5c3_Err != nil { | |
| 2277 | + | return templ_7745c5c3_Err | |
| 2278 | + | } | |
| 2279 | + | var templ_7745c5c3_Var118 templ.SafeURL | |
| 2280 | + | templ_7745c5c3_Var118, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Existing.Href)) | |
| 2281 | + | if templ_7745c5c3_Err != nil { | |
| 2282 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 114} | |
| 2283 | + | } | |
| 2284 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var118)) | |
| 2285 | + | if templ_7745c5c3_Err != nil { | |
| 2286 | + | return templ_7745c5c3_Err | |
| 2287 | + | } | |
| 2288 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\">") | |
| 2289 | + | if templ_7745c5c3_Err != nil { | |
| 2290 | + | return templ_7745c5c3_Err | |
| 2291 | + | } | |
| 2292 | + | var templ_7745c5c3_Var119 string | |
| 2293 | + | templ_7745c5c3_Var119, templ_7745c5c3_Err = templ.JoinStringErrs(p.Existing.Label) | |
| 2294 | + | if templ_7745c5c3_Err != nil { | |
| 2295 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 427, Col: 135} | |
| 2296 | + | } | |
| 2297 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var119)) | |
| 2298 | + | if templ_7745c5c3_Err != nil { | |
| 2299 | + | return templ_7745c5c3_Err | |
| 2300 | + | } | |
| 2301 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 202, "</a></p>") | |
| 2302 | + | if templ_7745c5c3_Err != nil { | |
| 2303 | + | return templ_7745c5c3_Err | |
| 2304 | + | } | |
| 2305 | + | } | |
| 2306 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 203, " ") | |
| 2307 | + | if templ_7745c5c3_Err != nil { | |
| 2308 | + | return templ_7745c5c3_Err | |
| 2309 | + | } | |
| 2310 | + | if p.Create != nil { | |
| 2311 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 204, "<form class=\"cform create\" method=\"post\" action=\"") | |
| 2312 | + | if templ_7745c5c3_Err != nil { | |
| 2313 | + | return templ_7745c5c3_Err | |
| 2314 | + | } | |
| 2315 | + | var templ_7745c5c3_Var120 templ.SafeURL | |
| 2316 | + | templ_7745c5c3_Var120, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(p.Create.Action)) | |
| 2317 | + | if templ_7745c5c3_Err != nil { | |
| 2318 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 430, Col: 83} | |
| 2319 | + | } | |
| 2320 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var120)) | |
| 2321 | + | if templ_7745c5c3_Err != nil { | |
| 2322 | + | return templ_7745c5c3_Err | |
| 2323 | + | } | |
| 2324 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 205, "\"><input type=\"hidden\" name=\"source\" value=\"") | |
| 2325 | + | if templ_7745c5c3_Err != nil { | |
| 2326 | + | return templ_7745c5c3_Err | |
| 2327 | + | } | |
| 2328 | + | var templ_7745c5c3_Var121 string | |
| 2329 | + | templ_7745c5c3_Var121, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Source) | |
| 2330 | + | if templ_7745c5c3_Err != nil { | |
| 2331 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 431, Col: 62} | |
| 2332 | + | } | |
| 2333 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var121) | |
| 2334 | + | if templ_7745c5c3_Err != nil { | |
| 2335 | + | return templ_7745c5c3_Err | |
| 2336 | + | } | |
| 2337 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 206, "\"> <input type=\"hidden\" name=\"target\" value=\"") | |
| 2338 | + | if templ_7745c5c3_Err != nil { | |
| 2339 | + | return templ_7745c5c3_Err | |
| 2340 | + | } | |
| 2341 | + | var templ_7745c5c3_Var122 string | |
| 2342 | + | templ_7745c5c3_Var122, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Target) | |
| 2343 | + | if templ_7745c5c3_Err != nil { | |
| 2344 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 432, Col: 62} | |
| 2345 | + | } | |
| 2346 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var122) | |
| 2347 | + | if templ_7745c5c3_Err != nil { | |
| 2348 | + | return templ_7745c5c3_Err | |
| 2349 | + | } | |
| 2350 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 207, "\"> <label for=\"mr-title\">title</label> <input id=\"mr-title\" name=\"title\" type=\"text\" maxlength=\"120\" required value=\"") | |
| 2351 | + | if templ_7745c5c3_Err != nil { | |
| 2352 | + | return templ_7745c5c3_Err | |
| 2353 | + | } | |
| 2354 | + | var templ_7745c5c3_Var123 string | |
| 2355 | + | templ_7745c5c3_Var123, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.Create.Title) | |
| 2356 | + | if templ_7745c5c3_Err != nil { | |
| 2357 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 434, Col: 97} | |
| 2358 | + | } | |
| 2359 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var123) | |
| 2360 | + | if templ_7745c5c3_Err != nil { | |
| 2361 | + | return templ_7745c5c3_Err | |
| 2362 | + | } | |
| 2363 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 208, "\" autocomplete=\"off\"> <label for=\"mr-desc\">description</label> <textarea id=\"mr-desc\" name=\"description\" rows=\"4\" maxlength=\"8000\" placeholder=\"plain text, optional\"></textarea><div class=\"f-btns\"><button type=\"submit\" class=\"f-main\">[create merge request]</button></div></form>") | |
| 2364 | + | if templ_7745c5c3_Err != nil { | |
| 2365 | + | return templ_7745c5c3_Err | |
| 2366 | + | } | |
| 2367 | + | } | |
| 2368 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 209, " ") | |
| 2369 | + | if templ_7745c5c3_Err != nil { | |
| 2370 | + | return templ_7745c5c3_Err | |
| 2371 | + | } | |
| 2372 | + | if p.HasDiff { | |
| 2373 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 210, "<div class=\"diff-tools\" role=\"toolbar\" aria-label=\"Diff view\"><span class=\"dim\">") | |
| 2374 | + | if templ_7745c5c3_Err != nil { | |
| 2375 | + | return templ_7745c5c3_Err | |
| 2376 | + | } | |
| 2377 | + | var templ_7745c5c3_Var124 string | |
| 2378 | + | templ_7745c5c3_Var124, templ_7745c5c3_Err = templ.JoinStringErrs(plural(p.NFiles, "file", "files")) | |
| 2379 | + | if templ_7745c5c3_Err != nil { | |
| 2380 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 57} | |
| 2381 | + | } | |
| 2382 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var124)) | |
| 2383 | + | if templ_7745c5c3_Err != nil { | |
| 2384 | + | return templ_7745c5c3_Err | |
| 2385 | + | } | |
| 2386 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 211, " · <span class=\"plus\">+") | |
| 2387 | + | if templ_7745c5c3_Err != nil { | |
| 2388 | + | return templ_7745c5c3_Err | |
| 2389 | + | } | |
| 2390 | + | var templ_7745c5c3_Var125 string | |
| 2391 | + | templ_7745c5c3_Var125, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Adds)) | |
| 2392 | + | if templ_7745c5c3_Err != nil { | |
| 2393 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 97} | |
| 2394 | + | } | |
| 2395 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var125)) | |
| 2396 | + | if templ_7745c5c3_Err != nil { | |
| 2397 | + | return templ_7745c5c3_Err | |
| 2398 | + | } | |
| 2399 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 212, "</span> <span class=\"minus\">-") | |
| 2400 | + | if templ_7745c5c3_Err != nil { | |
| 2401 | + | return templ_7745c5c3_Err | |
| 2402 | + | } | |
| 2403 | + | var templ_7745c5c3_Var126 string | |
| 2404 | + | templ_7745c5c3_Var126, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(p.Dels)) | |
| 2405 | + | if templ_7745c5c3_Err != nil { | |
| 2406 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 444, Col: 142} | |
| 2407 | + | } | |
| 2408 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var126)) | |
| 2409 | + | if templ_7745c5c3_Err != nil { | |
| 2410 | + | return templ_7745c5c3_Err | |
| 2411 | + | } | |
| 2412 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 213, "</span></span> <span class=\"grow\"></span>") | |
| 2413 | + | if templ_7745c5c3_Err != nil { | |
| 2414 | + | return templ_7745c5c3_Err | |
| 2415 | + | } | |
| 2416 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "unified", "[u]nified", "u", p.UnifiedHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 2417 | + | if templ_7745c5c3_Err != nil { | |
| 2418 | + | return templ_7745c5c3_Err | |
| 2419 | + | } | |
| 2420 | + | templ_7745c5c3_Err = viewLink(p.DiffView, "split", "[d] split", "d", p.SplitHref).Render(ctx, templ_7745c5c3_Buffer) | |
| 2421 | + | if templ_7745c5c3_Err != nil { | |
| 2422 | + | return templ_7745c5c3_Err | |
| 2423 | + | } | |
| 2424 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 214, "<button type=\"button\" aria-keyshortcuts=\"x\" hx-on:click=\"document.body.dispatchEvent(new CustomEvent('k-collapse'))\">[x] collapse all</button></div>") | |
| 2425 | + | if templ_7745c5c3_Err != nil { | |
| 2426 | + | return templ_7745c5c3_Err | |
| 2427 | + | } | |
| 2428 | + | if p.Invisible { | |
| 2429 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 215, "<p class=\"notice\">this diff holds invisible characters (direction controls, zero-width characters). They are shown as <span class=\"inv\"><U+XXXX></span> markers, never applied.</p>") | |
| 2430 | + | if templ_7745c5c3_Err != nil { | |
| 2431 | + | return templ_7745c5c3_Err | |
| 2432 | + | } | |
| 2433 | + | } | |
| 2434 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 216, " <div id=\"diffs\"") | |
| 2435 | + | if templ_7745c5c3_Err != nil { | |
| 2436 | + | return templ_7745c5c3_Err | |
| 2437 | + | } | |
| 2438 | + | if p.Both { | |
| 2439 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 217, " data-diff=\"") | |
| 2440 | + | if templ_7745c5c3_Err != nil { | |
| 2441 | + | return templ_7745c5c3_Err | |
| 2442 | + | } | |
| 2443 | + | var templ_7745c5c3_Var127 string | |
| 2444 | + | templ_7745c5c3_Var127, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2445 | + | if templ_7745c5c3_Err != nil { | |
| 2446 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 459, Col: 23} | |
| 2447 | + | } | |
| 2448 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var127) | |
| 2449 | + | if templ_7745c5c3_Err != nil { | |
| 2450 | + | return templ_7745c5c3_Err | |
| 2451 | + | } | |
| 2452 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 218, "\"") | |
| 2453 | + | if templ_7745c5c3_Err != nil { | |
| 2454 | + | return templ_7745c5c3_Err | |
| 2455 | + | } | |
| 2456 | + | } else { | |
| 2457 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 219, " data-view=\"") | |
| 2458 | + | if templ_7745c5c3_Err != nil { | |
| 2459 | + | return templ_7745c5c3_Err | |
| 2460 | + | } | |
| 2461 | + | var templ_7745c5c3_Var128 string | |
| 2462 | + | templ_7745c5c3_Var128, templ_7745c5c3_Err = templ.ResolveAttributeValue(p.View) | |
| 2463 | + | if templ_7745c5c3_Err != nil { | |
| 2464 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 461, Col: 23} | |
| 2465 | + | } | |
| 2466 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var128) | |
| 2467 | + | if templ_7745c5c3_Err != nil { | |
| 2468 | + | return templ_7745c5c3_Err | |
| 2469 | + | } | |
| 2470 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 220, "\"") | |
| 2471 | + | if templ_7745c5c3_Err != nil { | |
| 2472 | + | return templ_7745c5c3_Err | |
| 2473 | + | } | |
| 2474 | + | } | |
| 2475 | + | if p.Both && !p.Pinned { | |
| 2476 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 221, " hx-live=\"this.dataset.diff = sessionStorage.getItem('diff') || this.dataset.diff\"") | |
| 2477 | + | if templ_7745c5c3_Err != nil { | |
| 2478 | + | return templ_7745c5c3_Err | |
| 2479 | + | } | |
| 2480 | + | } | |
| 2481 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 222, ">") | |
| 2482 | + | if templ_7745c5c3_Err != nil { | |
| 2483 | + | return templ_7745c5c3_Err | |
| 2484 | + | } | |
| 2485 | + | for _, f := range p.Files { | |
| 2486 | + | templ_7745c5c3_Err = DiffFileView(f, p.DiffView).Render(ctx, templ_7745c5c3_Buffer) | |
| 2487 | + | if templ_7745c5c3_Err != nil { | |
| 2488 | + | return templ_7745c5c3_Err | |
| 2489 | + | } | |
| 2490 | + | } | |
| 2491 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 223, "</div>") | |
| 2492 | + | if templ_7745c5c3_Err != nil { | |
| 2493 | + | return templ_7745c5c3_Err | |
| 2494 | + | } | |
| 2495 | + | } | |
| 2496 | + | return nil | |
| 2497 | + | }) | |
| 2498 | + | templ_7745c5c3_Err = Shell(l, compareRail(p)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var109), templ_7745c5c3_Buffer) | |
| 2499 | + | if templ_7745c5c3_Err != nil { | |
| 2500 | + | return templ_7745c5c3_Err | |
| 2501 | + | } | |
| 2502 | + | return nil | |
| 2503 | + | }) | |
| 2504 | + | } | |
| 2505 | + | ||
| 2506 | + | func compareRail(p ComparePage) templ.Component { | |
| 2507 | + | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { | |
| 2508 | + | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context | |
| 2509 | + | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { | |
| 2510 | + | return templ_7745c5c3_CtxErr | |
| 2511 | + | } | |
| 2512 | + | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) | |
| 2513 | + | if !templ_7745c5c3_IsBuffer { | |
| 2514 | + | defer func() { | |
| 2515 | + | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) | |
| 2516 | + | if templ_7745c5c3_Err == nil { | |
| 2517 | + | templ_7745c5c3_Err = templ_7745c5c3_BufErr | |
| 2518 | + | } | |
| 2519 | + | }() | |
| 2520 | + | } | |
| 2521 | + | ctx = templ.InitializeContext(ctx) | |
| 2522 | + | templ_7745c5c3_Var129 := templ.GetChildren(ctx) | |
| 2523 | + | if templ_7745c5c3_Var129 == nil { | |
| 2524 | + | templ_7745c5c3_Var129 = templ.NopComponent | |
| 2525 | + | } | |
| 2526 | + | ctx = templ.ClearChildren(ctx) | |
| 2527 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 224, "<section aria-labelledby=\"r-ahead\"><h2 id=\"r-ahead\">commits <span>") | |
| 2528 | + | if templ_7745c5c3_Err != nil { | |
| 2529 | + | return templ_7745c5c3_Err | |
| 2530 | + | } | |
| 2531 | + | var templ_7745c5c3_Var130 string | |
| 2532 | + | templ_7745c5c3_Var130, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Ahead))) | |
| 2533 | + | if templ_7745c5c3_Err != nil { | |
| 2534 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 477, Col: 53} | |
| 2535 | + | } | |
| 2536 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var130)) | |
| 2537 | + | if templ_7745c5c3_Err != nil { | |
| 2538 | + | return templ_7745c5c3_Err | |
| 2539 | + | } | |
| 2540 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 225, "</span></h2><ul class=\"pcommits\">") | |
| 2541 | + | if templ_7745c5c3_Err != nil { | |
| 2542 | + | return templ_7745c5c3_Err | |
| 2543 | + | } | |
| 2544 | + | for _, c := range p.Ahead { | |
| 2545 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 226, "<li><a href=\"") | |
| 2546 | + | if templ_7745c5c3_Err != nil { | |
| 2547 | + | return templ_7745c5c3_Err | |
| 2548 | + | } | |
| 2549 | + | var templ_7745c5c3_Var131 templ.SafeURL | |
| 2550 | + | templ_7745c5c3_Var131, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(c.CHref)) | |
| 2551 | + | if templ_7745c5c3_Err != nil { | |
| 2552 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 37} | |
| 2553 | + | } | |
| 2554 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var131)) | |
| 2555 | + | if templ_7745c5c3_Err != nil { | |
| 2556 | + | return templ_7745c5c3_Err | |
| 2557 | + | } | |
| 2558 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 227, "\">") | |
| 2559 | + | if templ_7745c5c3_Err != nil { | |
| 2560 | + | return templ_7745c5c3_Err | |
| 2561 | + | } | |
| 2562 | + | var templ_7745c5c3_Var132 string | |
| 2563 | + | templ_7745c5c3_Var132, templ_7745c5c3_Err = templ.JoinStringErrs(c.Short) | |
| 2564 | + | if templ_7745c5c3_Err != nil { | |
| 2565 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 481, Col: 49} | |
| 2566 | + | } | |
| 2567 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var132)) | |
| 2568 | + | if templ_7745c5c3_Err != nil { | |
| 2569 | + | return templ_7745c5c3_Err | |
| 2570 | + | } | |
| 2571 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 228, "</a> <span class=\"s\">") | |
| 2572 | + | if templ_7745c5c3_Err != nil { | |
| 2573 | + | return templ_7745c5c3_Err | |
| 2574 | + | } | |
| 2575 | + | var templ_7745c5c3_Var133 string | |
| 2576 | + | templ_7745c5c3_Var133, templ_7745c5c3_Err = templ.JoinStringErrs(c.Subject) | |
| 2577 | + | if templ_7745c5c3_Err != nil { | |
| 2578 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 482, Col: 32} | |
| 2579 | + | } | |
| 2580 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var133)) | |
| 2581 | + | if templ_7745c5c3_Err != nil { | |
| 2582 | + | return templ_7745c5c3_Err | |
| 2583 | + | } | |
| 2584 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 229, "</span> <span class=\"mute\">") | |
| 2585 | + | if templ_7745c5c3_Err != nil { | |
| 2586 | + | return templ_7745c5c3_Err | |
| 2587 | + | } | |
| 2588 | + | var templ_7745c5c3_Var134 string | |
| 2589 | + | templ_7745c5c3_Var134, templ_7745c5c3_Err = templ.JoinStringErrs(c.Author) | |
| 2590 | + | if templ_7745c5c3_Err != nil { | |
| 2591 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 484, Col: 16} | |
| 2592 | + | } | |
| 2593 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var134)) | |
| 2594 | + | if templ_7745c5c3_Err != nil { | |
| 2595 | + | return templ_7745c5c3_Err | |
| 2596 | + | } | |
| 2597 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 230, " ") | |
| 2598 | + | if templ_7745c5c3_Err != nil { | |
| 2599 | + | return templ_7745c5c3_Err | |
| 2600 | + | } | |
| 2601 | + | if c.WhenAt != "" { | |
| 2602 | + | var templ_7745c5c3_Var135 string | |
| 2603 | + | templ_7745c5c3_Var135, templ_7745c5c3_Err = templ.JoinStringErrs(" · ") | |
| 2604 | + | if templ_7745c5c3_Err != nil { | |
| 2605 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 15} | |
| 2606 | + | } | |
| 2607 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var135)) | |
| 2608 | + | if templ_7745c5c3_Err != nil { | |
| 2609 | + | return templ_7745c5c3_Err | |
| 2610 | + | } | |
| 2611 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 231, "<time datetime=\"") | |
| 2612 | + | if templ_7745c5c3_Err != nil { | |
| 2613 | + | return templ_7745c5c3_Err | |
| 2614 | + | } | |
| 2615 | + | var templ_7745c5c3_Var136 string | |
| 2616 | + | templ_7745c5c3_Var136, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.WhenAt) | |
| 2617 | + | if templ_7745c5c3_Err != nil { | |
| 2618 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 42} | |
| 2619 | + | } | |
| 2620 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var136) | |
| 2621 | + | if templ_7745c5c3_Err != nil { | |
| 2622 | + | return templ_7745c5c3_Err | |
| 2623 | + | } | |
| 2624 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 232, "\">") | |
| 2625 | + | if templ_7745c5c3_Err != nil { | |
| 2626 | + | return templ_7745c5c3_Err | |
| 2627 | + | } | |
| 2628 | + | var templ_7745c5c3_Var137 string | |
| 2629 | + | templ_7745c5c3_Var137, templ_7745c5c3_Err = templ.JoinStringErrs(c.When) | |
| 2630 | + | if templ_7745c5c3_Err != nil { | |
| 2631 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 486, Col: 53} | |
| 2632 | + | } | |
| 2633 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var137)) | |
| 2634 | + | if templ_7745c5c3_Err != nil { | |
| 2635 | + | return templ_7745c5c3_Err | |
| 2636 | + | } | |
| 2637 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 233, "</time>") | |
| 2638 | + | if templ_7745c5c3_Err != nil { | |
| 2639 | + | return templ_7745c5c3_Err | |
| 2640 | + | } | |
| 2641 | + | } | |
| 2642 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 234, "</span></li>") | |
| 2643 | + | if templ_7745c5c3_Err != nil { | |
| 2644 | + | return templ_7745c5c3_Err | |
| 2645 | + | } | |
| 2646 | + | } | |
| 2647 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 235, "</ul>") | |
| 2648 | + | if templ_7745c5c3_Err != nil { | |
| 2649 | + | return templ_7745c5c3_Err | |
| 2650 | + | } | |
| 2651 | + | if len(p.Ahead) == 0 { | |
| 2652 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 236, "<p class=\"dim\">no commit ahead of ") | |
| 2653 | + | if templ_7745c5c3_Err != nil { | |
| 2654 | + | return templ_7745c5c3_Err | |
| 2655 | + | } | |
| 2656 | + | var templ_7745c5c3_Var138 string | |
| 2657 | + | templ_7745c5c3_Var138, templ_7745c5c3_Err = templ.JoinStringErrs(p.Target) | |
| 2658 | + | if templ_7745c5c3_Err != nil { | |
| 2659 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 493, Col: 47} | |
| 2660 | + | } | |
| 2661 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var138)) | |
| 2662 | + | if templ_7745c5c3_Err != nil { | |
| 2663 | + | return templ_7745c5c3_Err | |
| 2664 | + | } | |
| 2665 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 237, ".</p>") | |
| 2666 | + | if templ_7745c5c3_Err != nil { | |
| 2667 | + | return templ_7745c5c3_Err | |
| 2668 | + | } | |
| 2669 | + | } | |
| 2670 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 238, "</section>") | |
| 2671 | + | if templ_7745c5c3_Err != nil { | |
| 2672 | + | return templ_7745c5c3_Err | |
| 2673 | + | } | |
| 2674 | + | if p.HasDiff { | |
| 2675 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 239, "<section aria-labelledby=\"r-files\"><h2 id=\"r-files\">files <span>") | |
| 2676 | + | if templ_7745c5c3_Err != nil { | |
| 2677 | + | return templ_7745c5c3_Err | |
| 2678 | + | } | |
| 2679 | + | var templ_7745c5c3_Var139 string | |
| 2680 | + | templ_7745c5c3_Var139, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(len(p.Files))) | |
| 2681 | + | if templ_7745c5c3_Err != nil { | |
| 2682 | + | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/mr.templ`, Line: 498, Col: 52} | |
| 2683 | + | } | |
| 2684 | + | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var139)) | |
| 2685 | + | if templ_7745c5c3_Err != nil { | |
| 2686 | + | return templ_7745c5c3_Err | |
| 2687 | + | } | |
| 2688 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 240, "</span></h2>") | |
| 2689 | + | if templ_7745c5c3_Err != nil { | |
| 2690 | + | return templ_7745c5c3_Err | |
| 2691 | + | } | |
| 2692 | + | templ_7745c5c3_Err = FileList(p.Files).Render(ctx, templ_7745c5c3_Buffer) | |
| 2693 | + | if templ_7745c5c3_Err != nil { | |
| 2694 | + | return templ_7745c5c3_Err | |
| 2695 | + | } | |
| 2696 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 241, "</section>") | |
| 2697 | + | if templ_7745c5c3_Err != nil { | |
| 2698 | + | return templ_7745c5c3_Err | |
| 2699 | + | } | |
| 2700 | + | } | |
| 2701 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 242, "<section aria-labelledby=\"r-cli\"><h2 id=\"r-cli\">from a terminal</h2>") | |
| 2702 | + | if templ_7745c5c3_Err != nil { | |
| 2703 | + | return templ_7745c5c3_Err | |
| 2704 | + | } | |
| 2705 | + | templ_7745c5c3_Err = CloneBlock(p.CLI).Render(ctx, templ_7745c5c3_Buffer) | |
| 2706 | + | if templ_7745c5c3_Err != nil { | |
| 2707 | + | return templ_7745c5c3_Err | |
| 2708 | + | } | |
| 2709 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 243, "</section>") | |
| 2710 | + | if templ_7745c5c3_Err != nil { | |
| 2711 | + | return templ_7745c5c3_Err | |
| 2712 | + | } | |
| 2713 | + | return nil | |
| 2714 | + | }) | |
| 2715 | + | } | |
| 2716 | + | ||
| 2717 | + | var _ = templruntime.GeneratedTemplate | |
M internal/adapters/web/overlays.templ +3 -1
| @@ -19,7 +19,9 @@ templ OverlayHelp() { | |||
| 19 | 19 | <div><dt><kbd>:</kbd> <kbd>f</kbd></dt><dd>go to: section, ref, file, repo</dd></div> | |
| 20 | 20 | <div><dt><kbd>y</kbd></dt><dd>permalink: ref becomes the full sha</dd></div> | |
| 21 | 21 | <div><dt><kbd>s</kbd> <kbd>t</kbd> <kbd>c</kbd> <kbd>r</kbd></dt><dd>repo sections (when shown)</dd></div> | |
| 22 | - | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>commit: split / unified / collapse all</dd></div> | |
| 22 | + | <div><dt><kbd>m</kbd> <kbd>g m</kbd></dt><dd>merge requests (when shown)</dd></div> | |
| 23 | + | <div><dt><kbd>d</kbd> <kbd>u</kbd> <kbd>x</kbd></dt><dd>diff: split / unified / collapse all</dd></div> | |
| 24 | + | <div><dt><kbd>n</kbd></dt><dd>merge request: next note in the diff</dd></div> | |
| 23 | 25 | <div><dt><kbd>esc</kbd></dt><dd>close panel</dd></div> | |
| 24 | 26 | </dl> | |
| 25 | 27 | </dialog> | |
M internal/adapters/web/overlays_templ.go +9 -9 generated file: collapsed
A internal/adapters/web/plaintext.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "net/url" | |
| 7 | + | "regexp" | |
| 8 | + | "strings" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // TextPart is a run of plain text, or a link found in it. | |
| 12 | + | type TextPart struct { | |
| 13 | + | Text string | |
| 14 | + | // Href is the target of a link, "" for plain text. Only http and https | |
| 15 | + | // URLs are ever linked. | |
| 16 | + | Href string | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // reURL finds the candidates for a link in plain text. | |
| 20 | + | var reURL = regexp.MustCompile(`https?://[^\s<>"'` + "`" + `]+`) | |
| 21 | + | ||
| 22 | + | // linkTrail is what ends a sentence rather than an address. | |
| 23 | + | const linkTrail = ".,;:!?)]}" | |
| 24 | + | ||
| 25 | + | // plainParts splits a user text (a description, a comment) into runs: the text | |
| 26 | + | // stays text, escaped by the template with its line breaks kept, and the | |
| 27 | + | // http(s) URLs in it become links (ADR 0017: "text is plain everywhere"). The | |
| 28 | + | // invisible characters of the text are shown as markers. | |
| 29 | + | func plainParts(s string) []TextPart { | |
| 30 | + | s = strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\r", "\n") | |
| 31 | + | var out []TextPart | |
| 32 | + | last := 0 | |
| 33 | + | for _, loc := range reURL.FindAllStringIndex(s, -1) { | |
| 34 | + | raw := s[loc[0]:loc[1]] | |
| 35 | + | // A closing bracket belongs to the address only when it has an opening | |
| 36 | + | // one in it: "(see https://x.test/a)" ends before the ")". | |
| 37 | + | end := len(raw) | |
| 38 | + | for end > 0 && strings.ContainsRune(linkTrail, rune(raw[end-1])) { | |
| 39 | + | if raw[end-1] == ')' && strings.Count(raw[:end], "(") >= strings.Count(raw[:end], ")") { | |
| 40 | + | break | |
| 41 | + | } | |
| 42 | + | end-- | |
| 43 | + | } | |
| 44 | + | raw = raw[:end] | |
| 45 | + | u, err := url.Parse(raw) | |
| 46 | + | if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" { | |
| 47 | + | continue | |
| 48 | + | } | |
| 49 | + | if loc[0] > last { | |
| 50 | + | out = append(out, TextPart{Text: visibleText(s[last:loc[0]])}) | |
| 51 | + | } | |
| 52 | + | out = append(out, TextPart{Text: visibleText(raw), Href: u.String()}) | |
| 53 | + | last = loc[0] + len(raw) | |
| 54 | + | } | |
| 55 | + | if last < len(s) { | |
| 56 | + | out = append(out, TextPart{Text: visibleText(s[last:])}) | |
| 57 | + | } | |
| 58 | + | return out | |
| 59 | + | } | |
M internal/adapters/web/repo.go +3 -0
| @@ -107,6 +107,9 @@ func (s *Server) repoLayout(ctx context.Context, d *repoData, section string) La | |||
| 107 | 107 | mk("c", "ommits", repoHref(d.name(), "commits", def), "commits"), | |
| 108 | 108 | mk("r", "efs", repoHref(d.name(), "refs"), "refs"), | |
| 109 | 109 | } | |
| 110 | + | if s.cfg.MergeRequests != nil { | |
| 111 | + | l.Sections = append(l.Sections, mk("m", "erge requests", mrBase(d.name()), "merge-requests")) | |
| 112 | + | } | |
| 110 | 113 | l.Crumbs = []Crumb{{Label: d.name(), Href: repoHref(d.name()), Current: section == "summary"}} | |
| 111 | 114 | ||
| 112 | 115 | l.Cmd = append(l.Cmd, CmdItem{Kind: "go", Label: "home", Href: "/"}) | |
A internal/adapters/web/static/css/60-mr.css +96 -0
| @@ -0,0 +1,96 @@ | |||
| 1 | + | @layer components { | |
| 2 | + | /* merge requests: the review page. Hue only where it carries a verdict. */ | |
| 3 | + | .mr-h .show-kv { margin-top: 0; } | |
| 4 | + | .mr-h + * { margin-top: var(--lh); } | |
| 5 | + | .mr-state[data-state="open"]::before { content: "● "; color: var(--teal); } | |
| 6 | + | .mr-state[data-state="merged"]::before { content: "✓ "; color: var(--teal); } | |
| 7 | + | .mr-state[data-state="closed"]::before { content: "✗ "; color: var(--red); } | |
| 8 | + | .mr-desc { margin-top: var(--lh); max-width: var(--measure); white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 9 | + | .mr-desc a, .c-text a { text-decoration: underline; } | |
| 10 | + | ||
| 11 | + | .since { | |
| 12 | + | display: flex; flex-wrap: wrap; column-gap: 3ch; margin-top: var(--lh); | |
| 13 | + | & a { text-decoration: none; color: var(--ink-dim); } | |
| 14 | + | & a:hover { color: var(--ink); text-decoration: underline; } | |
| 15 | + | & a[aria-current] { color: var(--ink-hi); font-weight: bold; } | |
| 16 | + | & a[aria-current]::before { content: "▸ "; } | |
| 17 | + | & .mute { font-weight: normal; } | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | /* merge block: first thing in the rail */ | |
| 21 | + | .decide { | |
| 22 | + | & .d-acts { margin-top: calc(var(--lh) / 2); } | |
| 23 | + | & select { | |
| 24 | + | display: block; width: 100%; font: inherit; color: inherit; background: var(--paper-alt); | |
| 25 | + | border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | |
| 26 | + | } | |
| 27 | + | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; flex-wrap: wrap; } | |
| 28 | + | & .d-btns button { color: var(--ink-dim); } | |
| 29 | + | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | |
| 30 | + | & .d-btns .d-main { background: var(--ink); color: var(--paper); font-weight: bold; padding: 0 1ch; } | |
| 31 | + | & .d-btns .d-main:hover { background: var(--ink-hi); text-decoration: none; } | |
| 32 | + | & .d-or { margin-top: calc(var(--lh) / 2); } | |
| 33 | + | & .clone { margin-top: 0; } | |
| 34 | + | & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 38 | + | .pcommits li[data-new] { background: var(--row); } | |
| 39 | + | .pcommits .s { color: var(--ink); } | |
| 40 | + | .rail code { border-bottom: 1px dotted var(--ink-mute); } | |
| 41 | + | ||
| 42 | + | /* a note: a ruled block, under its line in the diff and in the list of comments */ | |
| 43 | + | .cmt { | |
| 44 | + | border-left: 2px solid var(--ink); padding-left: 1ch; margin-top: calc(var(--lh) / 2); max-width: var(--measure); | |
| 45 | + | white-space: normal; | |
| 46 | + | & p { white-space: normal; } | |
| 47 | + | & .c-who { font-weight: bold; } | |
| 48 | + | & .c-state { color: var(--ink-dim); } | |
| 49 | + | & .c-text { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 50 | + | & .c-where { font-size: var(--fs-s); overflow-wrap: anywhere; } | |
| 51 | + | & .c-act button { color: var(--ink-dim); } | |
| 52 | + | & .c-act button:hover { color: var(--ink); text-decoration: underline; } | |
| 53 | + | &[data-resolved="true"] { border-left-color: var(--rule); } | |
| 54 | + | &[data-resolved="true"] .c-text { color: var(--ink-dim); } | |
| 55 | + | } | |
| 56 | + | .dt .fnote td { background: var(--paper-alt); padding: 2px 0 4px; white-space: normal; } | |
| 57 | + | .dt .fnote .cmt, .dt .fnote .cform { margin: 0; font-size: var(--fs); line-height: var(--lh); } | |
| 58 | + | .dt .fnote .cmt + .cmt, .dt .fnote .cmt + .cform { margin-top: calc(var(--lh) / 2); } | |
| 59 | + | .ln a { color: inherit; text-decoration: none; } | |
| 60 | + | .ln a:hover, .ln a:focus-visible { text-decoration: underline; color: var(--ink-hi); } | |
| 61 | + | ||
| 62 | + | /* the forms: a comment, a new merge request */ | |
| 63 | + | .cform { | |
| 64 | + | display: grid; gap: 2px; margin-top: var(--lh); max-width: var(--measure); | |
| 65 | + | & label { color: var(--ink-dim); } | |
| 66 | + | & textarea, & input[type="text"] { | |
| 67 | + | display: block; width: 100%; font: inherit; color: inherit; resize: vertical; | |
| 68 | + | background: var(--paper-alt); border: 1px solid var(--rule); border-radius: 0; padding: 0 1ch; | |
| 69 | + | } | |
| 70 | + | & textarea::placeholder { color: var(--ink-mute); opacity: 1; } | |
| 71 | + | & .f-btns { display: flex; gap: 2ch; align-items: baseline; margin-top: 4px; } | |
| 72 | + | & .f-btns a { color: var(--ink-dim); } | |
| 73 | + | & .f-btns button { color: var(--ink-dim); } | |
| 74 | + | & .f-btns button:hover { color: var(--ink); text-decoration: underline; } | |
| 75 | + | & .f-btns .f-main { background: var(--ink); color: var(--paper); font-weight: bold; padding: 0 1ch; } | |
| 76 | + | & .f-btns .f-main:hover { background: var(--ink-hi); text-decoration: none; } | |
| 77 | + | } | |
| 78 | + | .cform.create { margin-top: var(--lh); } | |
| 79 | + | ||
| 80 | + | .list.mrs { | |
| 81 | + | & .c-n { width: 5ch; } | |
| 82 | + | & .c-state { width: 9ch; } | |
| 83 | + | & .c-subject { overflow: hidden; text-overflow: ellipsis; } | |
| 84 | + | & .c-who { width: 12ch; } | |
| 85 | + | & .c-when { width: 11ch; } | |
| 86 | + | & tr[data-state="open"] .c-state { font-weight: bold; } | |
| 87 | + | & tr[data-state="open"] .c-state::before { content: "● "; color: var(--teal); } | |
| 88 | + | & tr[data-state="merged"] .c-state::before { content: "✓ "; color: var(--teal); } | |
| 89 | + | & tr[data-state="closed"] .c-state::before { content: "✗ "; color: var(--red); } | |
| 90 | + | } | |
| 91 | + | #mr-notice:empty { display: none; } | |
| 92 | + | ||
| 93 | + | @media (max-width: 700px) { | |
| 94 | + | .list.mrs .c-who { display: none; } | |
| 95 | + | } | |
| 96 | + | } | |
D internal/adapters/web/static/css/60-proposal.css +0 -93
| @@ -1,93 +0,0 @@ | |||
| 1 | - | @layer components { | |
| 2 | - | /* proposals: the decision gate. Hue only where it carries a verdict: | |
| 3 | - | check state (pass teal, fail red) and open high/medium findings (red). */ | |
| 4 | - | .criteria { | |
| 5 | - | margin-top: calc(var(--lh) / 2); max-width: var(--measure); | |
| 6 | - | & li { padding-left: 4ch; text-indent: -4ch; } | |
| 7 | - | & .ck { color: var(--teal); font-weight: bold; } | |
| 8 | - | & li[data-met="false"] .ck { color: var(--red); } | |
| 9 | - | & .mute { font-size: var(--fs-s); overflow-wrap: anywhere; } | |
| 10 | - | } | |
| 11 | - | .prop-h .show-kv { margin-top: 0; } | |
| 12 | - | .prop-h + .rule-h { margin-top: var(--lh); } | |
| 13 | - | ||
| 14 | - | .since { | |
| 15 | - | display: flex; flex-wrap: wrap; column-gap: 3ch; margin-top: var(--lh); | |
| 16 | - | & a { text-decoration: none; color: var(--ink-dim); } | |
| 17 | - | & a:hover { color: var(--ink); text-decoration: underline; } | |
| 18 | - | & a[aria-current] { color: var(--ink-hi); font-weight: bold; } | |
| 19 | - | & a[aria-current]::before { content: "▸ "; } | |
| 20 | - | & .mute { font-weight: normal; } | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | /* decision block: first thing in the rail */ | |
| 24 | - | .decide { | |
| 25 | - | & .d-state { font-weight: bold; color: var(--ink-hi); text-transform: uppercase; letter-spacing: .1ch; } | |
| 26 | - | &[data-state="ready"] .d-state::before { content: "● "; color: var(--teal); } | |
| 27 | - | &[data-state="accepted"] .d-state::before { content: "✓ "; color: var(--teal); } | |
| 28 | - | &[data-state="refused"] .d-state::before { content: "✗ "; color: var(--red); } | |
| 29 | - | & p + p { margin-top: 0; } | |
| 30 | - | & .kv { grid-template-columns: 9ch minmax(0, 1fr); } | |
| 31 | - | & .kv dd { white-space: normal; } | |
| 32 | - | & .d-acts { margin-top: calc(var(--lh) / 2); } | |
| 33 | - | & textarea { | |
| 34 | - | display: block; width: 100%; resize: vertical; font: inherit; color: inherit; | |
| 35 | - | background: var(--paper-alt); border: 1px solid var(--rule); padding: 0 1ch; | |
| 36 | - | } | |
| 37 | - | & textarea::placeholder { color: var(--ink-mute); opacity: 1; } | |
| 38 | - | & .d-btns { display: flex; gap: 2ch; margin-top: 4px; } | |
| 39 | - | & .d-btns button { color: var(--ink-dim); } | |
| 40 | - | & .d-btns button:hover { color: var(--ink); text-decoration: underline; } | |
| 41 | - | & .d-btns .d-main { background: var(--ink); color: var(--paper); font-weight: bold; padding: 0 1ch; } | |
| 42 | - | & .d-btns .d-main:hover { background: var(--ink-hi); text-decoration: none; } | |
| 43 | - | & .d-or { margin-top: calc(var(--lh) / 2); } | |
| 44 | - | & .clone { margin-top: 0; } | |
| 45 | - | & .clone code { white-space: pre-wrap; overflow-wrap: anywhere; } | |
| 46 | - | } | |
| 47 | - | .closed { margin-top: calc(var(--lh) / 2); & > summary { color: var(--ink-dim); } } | |
| 48 | - | ||
| 49 | - | .checks li { display: grid; grid-template-columns: 5ch minmax(0, 1fr) auto; column-gap: 1ch; } | |
| 50 | - | .checks .c-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 51 | - | .checks .c-st { font-weight: bold; color: var(--teal); } | |
| 52 | - | .checks [data-state="fail"] .c-st { color: var(--red); } | |
| 53 | - | .checks [data-state="run"] .c-st { color: var(--ink-dim); } | |
| 54 | - | ||
| 55 | - | .pcommits li { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 56 | - | .pcommits li[data-new] { background: var(--row); } | |
| 57 | - | .pcommits .s { color: var(--ink); } | |
| 58 | - | ||
| 59 | - | /* a finding: a ruled note, in the rail and under its line in the diff */ | |
| 60 | - | .finding { | |
| 61 | - | border-left: 2px solid var(--ink); padding-left: 1ch; margin-top: calc(var(--lh) / 2); | |
| 62 | - | & p { white-space: normal; } | |
| 63 | - | & .f-sev { font-weight: bold; text-transform: uppercase; } | |
| 64 | - | & .f-state { color: var(--ink-dim); } | |
| 65 | - | & .f-where { font-size: var(--fs-s); overflow-wrap: anywhere; } | |
| 66 | - | & .f-reason { color: var(--ink-dim); } | |
| 67 | - | &[data-state="open"][data-sev="high"], &[data-state="open"][data-sev="medium"] { border-left-color: var(--red); } | |
| 68 | - | &[data-state="open"][data-sev="high"] .f-sev, &[data-state="open"][data-sev="medium"] .f-sev { color: var(--red); } | |
| 69 | - | &[data-state="fixed"], &[data-state="dismissed"] { border-left-color: var(--rule); } | |
| 70 | - | &[data-state="fixed"] .f-text, &[data-state="dismissed"] .f-text { color: var(--ink-dim); } | |
| 71 | - | &[data-state="fixed"] .f-sev, &[data-state="dismissed"] .f-sev { font-weight: normal; color: var(--ink-mute); } | |
| 72 | - | } | |
| 73 | - | .dt .fnote td { background: var(--paper-alt); padding: 2px 0 4px; white-space: normal; } | |
| 74 | - | .dt .fnote .finding { margin: 0; max-width: var(--measure); font-size: var(--fs); line-height: var(--lh); } | |
| 75 | - | .dfile .fcount { color: var(--red); font-weight: bold; } | |
| 76 | - | ||
| 77 | - | .list.props { | |
| 78 | - | & .c-n { width: 5ch; } | |
| 79 | - | & .c-state { width: 18ch; } | |
| 80 | - | & .c-subject { overflow: hidden; text-overflow: ellipsis; } | |
| 81 | - | & .c-chk { width: 6ch; } | |
| 82 | - | & .c-fnd { width: 9ch; } | |
| 83 | - | & .c-who { width: 12ch; } | |
| 84 | - | & .c-when { width: 9ch; } | |
| 85 | - | & tr[data-state="ready"] .c-state { font-weight: bold; } | |
| 86 | - | & tr[data-state="ready"] .c-state::before { content: "● "; color: var(--teal); } | |
| 87 | - | } | |
| 88 | - | .rail code { border-bottom: 1px dotted var(--ink-mute); } | |
| 89 | - | ||
| 90 | - | @media (max-width: 700px) { | |
| 91 | - | .list.props .c-chk, .list.props .c-fnd { display: none; } | |
| 92 | - | } | |
| 93 | - | } | |
M internal/adapters/web/static/js/ermite.js +2 -2
| @@ -4,8 +4,8 @@ | |||
| 4 | 4 | // (cursor, panels, filters, permalink) is declared in templates/components/keys.tmpl with hx-on + hx-live. | |
| 5 | 5 | (() => { | |
| 6 | 6 | const single = { j: 'j', k: 'k', G: 'bottom', '/': 'filter', '?': 'help', b: 'refs', f: 'cmd', ':': 'cmd', y: 'permalink', | |
| 7 | - | s: 's', t: 't', c: 'c', r: 'r', p: 'p', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc' }; | |
| 8 | - | const goto = { h: 'gh', t: 'gt', c: 'gc', r: 'gr', p: 'gp', g: 'top' }; | |
| 7 | + | s: 's', t: 't', c: 'c', r: 'r', m: 'm', n: 'next', d: 'split', u: 'unified', x: 'collapse', Enter: 'open', Escape: 'esc' }; | |
| 8 | + | const goto = { h: 'gh', t: 'gt', c: 'gc', r: 'gr', m: 'gm', g: 'top' }; | |
| 9 | 9 | let pending = 0; | |
| 10 | 10 | const fire = (name) => document.body.dispatchEvent(new CustomEvent('k-' + name, { bubbles: false })); | |
| 11 | 11 | addEventListener('keydown', (e) => { | |
M internal/adapters/web/web.go +17 -0
| @@ -49,6 +49,9 @@ type Config struct { | |||
| 49 | 49 | // Repos and Git are the API clients the pages are built from. | |
| 50 | 50 | Repos ermiteconnect.RepoServiceClient | |
| 51 | 51 | Git ermiteconnect.GitServiceClient | |
| 52 | + | // MergeRequests is the client of the merge request pages (ADR 0017). | |
| 53 | + | // Without it there are no such pages, nor section for them. | |
| 54 | + | MergeRequests ermiteconnect.MergeRequestServiceClient | |
| 52 | 55 | // Events is the client of the event stream behind /-/events. Without | |
| 53 | 56 | // it the pages are not live and the route answers 404. | |
| 54 | 57 | Events ermiteconnect.EventServiceClient | |
| @@ -134,6 +137,20 @@ func (s *Server) routes() *http.ServeMux { | |||
| 134 | 137 | mux.HandleFunc("GET /{repo}/tree/{rest...}", s.page(s.tree)) | |
| 135 | 138 | mux.HandleFunc("GET /{repo}/blob/{rest...}", s.page(s.blob)) | |
| 136 | 139 | mux.HandleFunc("GET /{repo}/raw/{rest...}", s.raw) | |
| 140 | + | if s.cfg.MergeRequests != nil { | |
| 141 | + | mux.HandleFunc("GET /{repo}/merge-requests", s.page(s.mrList)) | |
| 142 | + | mux.HandleFunc("GET /{repo}/merge-requests/{$}", s.page(s.mrList)) | |
| 143 | + | mux.HandleFunc("GET /{repo}/merge-requests/{n}", s.page(s.mrShow)) | |
| 144 | + | mux.HandleFunc("GET /{repo}/merge-requests/{n}/since", s.page(s.mrSince)) | |
| 145 | + | mux.HandleFunc("GET /{repo}/compare/{rest...}", s.page(s.compare)) | |
| 146 | + | // The writes: POST only, behind the gate (ServeHTTP). | |
| 147 | + | mux.HandleFunc("POST /{repo}/merge-requests", s.mrAction(s.mrCreate)) | |
| 148 | + | mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments", s.mrAction(s.mrComment)) | |
| 149 | + | mux.HandleFunc("POST /{repo}/merge-requests/{n}/comments/{id}/resolve", s.mrAction(s.mrResolve)) | |
| 150 | + | mux.HandleFunc("POST /{repo}/merge-requests/{n}/merge", s.mrAction(s.mrMerge)) | |
| 151 | + | mux.HandleFunc("POST /{repo}/merge-requests/{n}/close", s.mrAction(s.mrClose)) | |
| 152 | + | mux.HandleFunc("POST /{repo}/merge-requests/{n}/read", s.mrAction(s.mrRead)) | |
| 153 | + | } | |
| 137 | 154 | mux.HandleFunc("GET /{repo}/{rest...}", s.page(s.notFound)) | |
| 138 | 155 | return mux | |
| 139 | 156 | } | |