@chisquare-tech/origine-ngx-apps-code 0.11.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { inject, Injectable, signal, computed, effect, untracked, Directive, ElementRef, input, output, HostListener, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
|
|
3
3
|
import { ApiService } from '@thescaffold/ngx-core';
|
|
4
|
-
import { map,
|
|
4
|
+
import { map, catchError, of, forkJoin } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/forms';
|
|
6
6
|
import { FormsModule } from '@angular/forms';
|
|
7
7
|
import { ActivatedRoute, RouterLink, Router } from '@angular/router';
|
|
8
8
|
import { NgTemplateOutlet } from '@angular/common';
|
|
9
|
-
import {
|
|
9
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
10
|
+
import { map as map$1, catchError as catchError$1 } from 'rxjs/operators';
|
|
10
11
|
import { marked } from 'marked';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -29,6 +30,17 @@ class CodeApi {
|
|
|
29
30
|
repoPath(project, name) {
|
|
30
31
|
return `${C}/project/${enc(project)}/repo/${enc(name)}`;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether this installation signs the commits it makes (DEV-31, D-67): GET /signing-key answers.
|
|
35
|
+
* A web edit is then signed, so a rule that only asks for signed commits takes it directly.
|
|
36
|
+
*/
|
|
37
|
+
platformSigns() {
|
|
38
|
+
return this.api.get(`${C}/signing-key`).pipe(map((r) => !!data(r)), catchError(() => of(false)));
|
|
39
|
+
}
|
|
40
|
+
/** The organization's custom roles (V4-01), for the bypass list of a branch rule. */
|
|
41
|
+
customRoles() {
|
|
42
|
+
return this.api.get('v1/projects/roles').pipe(map((r) => (data(r)?.custom ?? []).map((c) => ({ id: c.id, name: c.name }))));
|
|
43
|
+
}
|
|
32
44
|
repos(project) {
|
|
33
45
|
return this.api.get(`${C}/project/${enc(project)}/repo`).pipe(map((r) => data(r) ?? []));
|
|
34
46
|
}
|
|
@@ -38,6 +50,21 @@ class CodeApi {
|
|
|
38
50
|
import(project, body) {
|
|
39
51
|
return this.api.post(`${C}/project/${enc(project)}/repo/import`, body).pipe(map((r) => data(r)));
|
|
40
52
|
}
|
|
53
|
+
/** A repository's pull mirror (V6-01). */
|
|
54
|
+
mirror(project, name) {
|
|
55
|
+
return this.api.get(`${this.repoPath(project, name)}/mirror`).pipe(map((r) => data(r)));
|
|
56
|
+
}
|
|
57
|
+
/** Changes the mirror's source, credentials or interval; the saver becomes whom it syncs as. */
|
|
58
|
+
updateMirror(project, name, body) {
|
|
59
|
+
return this.api.patch(`${this.repoPath(project, name)}/mirror`, body).pipe(map((r) => data(r)));
|
|
60
|
+
}
|
|
61
|
+
syncMirror(project, name) {
|
|
62
|
+
return this.api.post(`${this.repoPath(project, name)}/mirror/sync`, {}).pipe(map((r) => data(r)));
|
|
63
|
+
}
|
|
64
|
+
/** Stops mirroring: an ordinary repository that takes pushes. */
|
|
65
|
+
stopMirror(project, name) {
|
|
66
|
+
return this.api.delete(`${this.repoPath(project, name)}/mirror`).pipe(map((r) => data(r)));
|
|
67
|
+
}
|
|
41
68
|
repo(project, name) {
|
|
42
69
|
return this.api.get(this.repoPath(project, name)).pipe(map((r) => data(r)));
|
|
43
70
|
}
|
|
@@ -211,6 +238,14 @@ class CodeApi {
|
|
|
211
238
|
merge(project, name, n, body) {
|
|
212
239
|
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/merge`, body).pipe(map((r) => data(r)));
|
|
213
240
|
}
|
|
241
|
+
/** A pull request's conflicts with its target, for the resolver (V6-03). */
|
|
242
|
+
conflicts(project, name, n) {
|
|
243
|
+
return this.api.get(`${this.repoPath(project, name)}/pulls/${n}/conflicts`).pipe(map((r) => data(r)));
|
|
244
|
+
}
|
|
245
|
+
/** Commits a resolution of the conflicts (V6-03). */
|
|
246
|
+
resolveConflicts(project, name, n, body) {
|
|
247
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/resolve`, body).pipe(map((r) => data(r)));
|
|
248
|
+
}
|
|
214
249
|
conversation(project, name, n) {
|
|
215
250
|
return this.api.get(`${this.repoPath(project, name)}/pulls/${n}/conversation`).pipe(map((r) => data(r) ?? { comments: [], reviews: [], threads: [] }));
|
|
216
251
|
}
|
|
@@ -356,6 +391,18 @@ const TOKEN_CSS = `
|
|
|
356
391
|
:host ::ng-deep .tk-nb, :host ::ng-deep .tk-nt, :host ::ng-deep .tk-bp{color:var(--o-tk-p)}
|
|
357
392
|
:host ::ng-deep .tk-gi{color:var(--o-add)} :host ::ng-deep .tk-gd{color:var(--o-bad-tx)}
|
|
358
393
|
`;
|
|
394
|
+
/** A commit's verification badge (DEV-31, V6-04): commits Origine made read "Verified · Origine". */
|
|
395
|
+
function verifyLabel(v) {
|
|
396
|
+
if (!v.verified)
|
|
397
|
+
return 'Unverified';
|
|
398
|
+
return v.signer === 'origine' ? 'Verified · Origine' : 'Verified';
|
|
399
|
+
}
|
|
400
|
+
/** The badge's hover text: whose key signed it, or why it is not verified. */
|
|
401
|
+
function verifyTitle(v, name) {
|
|
402
|
+
if (!v.verified)
|
|
403
|
+
return verifyWhy(v.reason);
|
|
404
|
+
return v.signer === 'origine' ? 'Made and signed by Origine, with the signing key of its region' : 'Signed with a key of ' + name(v.signer ?? '');
|
|
405
|
+
}
|
|
359
406
|
/** Why a signed commit is not verified (DEV-31). */
|
|
360
407
|
function verifyWhy(reason) {
|
|
361
408
|
return { unknown_key: 'Signed with a key that is not the committer’s signing key', bad_signature: 'The signature does not match the commit' }[reason] ?? 'Not verified';
|
|
@@ -367,6 +414,13 @@ function verifyWhy(reason) {
|
|
|
367
414
|
* their own data in onRepo().
|
|
368
415
|
*/
|
|
369
416
|
class RepoPage {
|
|
417
|
+
verifyTitle(v) {
|
|
418
|
+
return verifyTitle(v, (id) => this.people().name(id));
|
|
419
|
+
}
|
|
420
|
+
/** Whether the caller holds code capability id on this repository (V4-01): its caps, a collaborator row included. */
|
|
421
|
+
can(id) {
|
|
422
|
+
return (this.repo()?.caps ?? []).includes(id);
|
|
423
|
+
}
|
|
370
424
|
constructor() {
|
|
371
425
|
this.host = inject(CodeHost);
|
|
372
426
|
this.api = inject(CodeApi);
|
|
@@ -378,11 +432,20 @@ class RepoPage {
|
|
|
378
432
|
this.note = signal('', ...(ngDevMode ? [{ debugName: "note" }] : /* istanbul ignore next */ []));
|
|
379
433
|
this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
|
|
380
434
|
this.verifyWhy = verifyWhy;
|
|
381
|
-
this.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.
|
|
385
|
-
|
|
435
|
+
this.verifyLabel = verifyLabel;
|
|
436
|
+
/** Takes writes: not archived, and not a pull mirror (V6-01). */
|
|
437
|
+
this.live = computed(() => !!this.repo() && !this.repo()?.archivedAt && !this.repo()?.mirror, ...(ngDevMode ? [{ debugName: "live" }] : /* istanbul ignore next */ []));
|
|
438
|
+
this.canWrite = computed(() => this.live() && this.can('code.push'), ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
439
|
+
/** Every repository setting, a rename and the visibility included. */
|
|
440
|
+
this.canSettings = computed(() => this.can('code.repo.settings'), ...(ngDevMode ? [{ debugName: "canSettings" }] : /* istanbul ignore next */ []));
|
|
441
|
+
/** Repository settings short of a rename and the visibility: the above, and maintain collaborators. */
|
|
442
|
+
this.canMaintain = computed(() => this.canSettings() || this.repo()?.collaborator === 'maintain', ...(ngDevMode ? [{ debugName: "canMaintain" }] : /* istanbul ignore next */ []));
|
|
443
|
+
this.canDelete = computed(() => this.can('code.repo.delete'), ...(ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []));
|
|
444
|
+
this.canCollaborators = computed(() => this.can('code.collaborators.manage'), ...(ngDevMode ? [{ debugName: "canCollaborators" }] : /* istanbul ignore next */ []));
|
|
445
|
+
this.canProtect = computed(() => this.can('code.branch.protect'), ...(ngDevMode ? [{ debugName: "canProtect" }] : /* istanbul ignore next */ []));
|
|
446
|
+
this.canRelease = computed(() => this.live() && this.can('code.release.manage'), ...(ngDevMode ? [{ debugName: "canRelease" }] : /* istanbul ignore next */ []));
|
|
447
|
+
this.canOpenPull = computed(() => this.live() && this.can('code.pr.create'), ...(ngDevMode ? [{ debugName: "canOpenPull" }] : /* istanbul ignore next */ []));
|
|
448
|
+
this.canMerge = computed(() => this.live() && this.can('code.pr.merge'), ...(ngDevMode ? [{ debugName: "canMerge" }] : /* istanbul ignore next */ []));
|
|
386
449
|
this.route.paramMap.subscribe((m) => this.name.set(m.get('repo') ?? ''));
|
|
387
450
|
effect(() => {
|
|
388
451
|
const name = this.name();
|
|
@@ -960,7 +1023,7 @@ class ReposPage {
|
|
|
960
1023
|
this.ago = (s) => ago(s);
|
|
961
1024
|
this.canWrite = computed(() => {
|
|
962
1025
|
const p = this.host.project();
|
|
963
|
-
return !!p && !p.archivedAt &&
|
|
1026
|
+
return !!p && !p.archivedAt && this.host.can('code.repo.create');
|
|
964
1027
|
}, ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
965
1028
|
effect(() => {
|
|
966
1029
|
const p = this.host.project();
|
|
@@ -1072,6 +1135,8 @@ class NewRepoPage {
|
|
|
1072
1135
|
this.url_ = '';
|
|
1073
1136
|
this.user = '';
|
|
1074
1137
|
this.token = '';
|
|
1138
|
+
this.mirror = false;
|
|
1139
|
+
this.interval = 60;
|
|
1075
1140
|
this.route.queryParamMap.subscribe((q) => this.mode.set(q.get('import') ? 'import' : 'create'));
|
|
1076
1141
|
}
|
|
1077
1142
|
nameFromUrl() {
|
|
@@ -1101,7 +1166,7 @@ class NewRepoPage {
|
|
|
1101
1166
|
return;
|
|
1102
1167
|
this.busy.set(true);
|
|
1103
1168
|
this.error.set('');
|
|
1104
|
-
this.api.import(p.id, { name: this.repoName.trim(), url: this.url_.trim(), username: this.user.trim() || undefined, token: this.token || undefined, visibility: this.vis }).subscribe({
|
|
1169
|
+
this.api.import(p.id, { name: this.repoName.trim(), url: this.url_.trim(), username: this.user.trim() || undefined, token: this.token || undefined, visibility: this.vis, mirror: this.mirror || undefined, intervalMinutes: this.mirror ? Number(this.interval) || undefined : undefined }).subscribe({
|
|
1105
1170
|
next: (r) => { this.busy.set(false); this.token = ''; this.made.set(r); },
|
|
1106
1171
|
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not start the import.')); },
|
|
1107
1172
|
});
|
|
@@ -1159,11 +1224,15 @@ class NewRepoPage {
|
|
|
1159
1224
|
<div class="o-fld"><label for="ir-user">Username <span class="o-faint">(private repos)</span></label><input id="ir-user" class="o-in" name="user" [(ngModel)]="user" autocomplete="off" /></div>
|
|
1160
1225
|
<div class="o-fld"><label for="ir-tok">Access token</label><input id="ir-tok" class="o-in" type="password" name="token" [(ngModel)]="token" autocomplete="off" /></div>
|
|
1161
1226
|
</div>
|
|
1162
|
-
<div class="o-
|
|
1227
|
+
<div class="o-g2">
|
|
1228
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0" id="ir-mirror">Keep in sync with the source</span><button type="button" class="o-tog" role="switch" aria-labelledby="ir-mirror" [class.o-on]="mirror" [attr.aria-checked]="mirror" (click)="mirror = !mirror" data-testid="import-mirror"></button><span class="o-hint">A mirror fetches new commits on a schedule and refuses pushes. The token is kept encrypted for its syncs.</span></div>
|
|
1229
|
+
@if (mirror) {<div class="o-fld"><label for="ir-int">Sync every (minutes)</label><input id="ir-int" class="o-in" type="number" min="1" name="interval" [(ngModel)]="interval" data-testid="import-interval" /></div>}
|
|
1230
|
+
</div>
|
|
1231
|
+
<div class="o-flex"><span class="o-faint o-sm">Imports run in the background; you'll get an inbox message when done.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !url_.trim() || !repoName.trim()" data-testid="import-submit">Import</button></div>
|
|
1163
1232
|
</form>
|
|
1164
1233
|
}
|
|
1165
1234
|
</div>
|
|
1166
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1235
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1167
1236
|
}
|
|
1168
1237
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewRepoPage, decorators: [{
|
|
1169
1238
|
type: Component,
|
|
@@ -1223,7 +1292,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1223
1292
|
<div class="o-fld"><label for="ir-user">Username <span class="o-faint">(private repos)</span></label><input id="ir-user" class="o-in" name="user" [(ngModel)]="user" autocomplete="off" /></div>
|
|
1224
1293
|
<div class="o-fld"><label for="ir-tok">Access token</label><input id="ir-tok" class="o-in" type="password" name="token" [(ngModel)]="token" autocomplete="off" /></div>
|
|
1225
1294
|
</div>
|
|
1226
|
-
<div class="o-
|
|
1295
|
+
<div class="o-g2">
|
|
1296
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0" id="ir-mirror">Keep in sync with the source</span><button type="button" class="o-tog" role="switch" aria-labelledby="ir-mirror" [class.o-on]="mirror" [attr.aria-checked]="mirror" (click)="mirror = !mirror" data-testid="import-mirror"></button><span class="o-hint">A mirror fetches new commits on a schedule and refuses pushes. The token is kept encrypted for its syncs.</span></div>
|
|
1297
|
+
@if (mirror) {<div class="o-fld"><label for="ir-int">Sync every (minutes)</label><input id="ir-int" class="o-in" type="number" min="1" name="interval" [(ngModel)]="interval" data-testid="import-interval" /></div>}
|
|
1298
|
+
</div>
|
|
1299
|
+
<div class="o-flex"><span class="o-faint o-sm">Imports run in the background; you'll get an inbox message when done.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !url_.trim() || !repoName.trim()" data-testid="import-submit">Import</button></div>
|
|
1227
1300
|
</form>
|
|
1228
1301
|
}
|
|
1229
1302
|
</div>
|
|
@@ -1408,8 +1481,9 @@ class RepoHomePage extends RepoPage {
|
|
|
1408
1481
|
<div class="o-ph">
|
|
1409
1482
|
<div class="o-col">
|
|
1410
1483
|
<span class="o-eyebrow">{{ r.projectKey }}</span>
|
|
1411
|
-
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}</div>
|
|
1484
|
+
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}@if (r.mirror) {<span class="o-chip o-blu" data-testid="repo-mirror">Mirror</span>}</div>
|
|
1412
1485
|
@if (r.description) {<span class="o-mut">{{ r.description }}</span>}
|
|
1486
|
+
@if (r.mirror) {<span class="o-faint o-sm">Mirror of <span class="o-mono">{{ r.importSource }}</span>, read-only.</span>}
|
|
1413
1487
|
</div>
|
|
1414
1488
|
@if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
|
|
1415
1489
|
</div>
|
|
@@ -1540,8 +1614,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1540
1614
|
<div class="o-ph">
|
|
1541
1615
|
<div class="o-col">
|
|
1542
1616
|
<span class="o-eyebrow">{{ r.projectKey }}</span>
|
|
1543
|
-
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}</div>
|
|
1617
|
+
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}@if (r.mirror) {<span class="o-chip o-blu" data-testid="repo-mirror">Mirror</span>}</div>
|
|
1544
1618
|
@if (r.description) {<span class="o-mut">{{ r.description }}</span>}
|
|
1619
|
+
@if (r.mirror) {<span class="o-faint o-sm">Mirror of <span class="o-mono">{{ r.importSource }}</span>, read-only.</span>}
|
|
1545
1620
|
</div>
|
|
1546
1621
|
@if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
|
|
1547
1622
|
</div>
|
|
@@ -1690,9 +1765,11 @@ function matchesPattern(pattern, branch) {
|
|
|
1690
1765
|
}
|
|
1691
1766
|
// The web editor's commit box (prototype code_file "Commit and open PR", DEV-13): a message, and
|
|
1692
1767
|
// either the branch itself or a new branch with a pull request. A branch whose rules block direct
|
|
1693
|
-
// pushes only offers the second
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1768
|
+
// pushes only offers the second, and so does one that requires signed commits when the platform
|
|
1769
|
+
// does not sign what it commits (D-67: when it does, the web edit is signed and goes straight in).
|
|
1770
|
+
// The rules load asynchronously: until they have, nothing can be committed (a quick commit straight
|
|
1771
|
+
// to a protected branch was refused by the server); when they cannot be loaded, the change goes to
|
|
1772
|
+
// a new branch with a pull request, which any branch takes.
|
|
1696
1773
|
class CommitBox {
|
|
1697
1774
|
constructor() {
|
|
1698
1775
|
this.api = inject(CodeApi);
|
|
@@ -1707,13 +1784,17 @@ class CommitBox {
|
|
|
1707
1784
|
this.toNew = signal(false, ...(ngDevMode ? [{ debugName: "toNew" }] : /* istanbul ignore next */ []));
|
|
1708
1785
|
this.newBranch = signal('', ...(ngDevMode ? [{ debugName: "newBranch" }] : /* istanbul ignore next */ []));
|
|
1709
1786
|
this.rules = signal([], ...(ngDevMode ? [{ debugName: "rules" }] : /* istanbul ignore next */ []));
|
|
1787
|
+
/** Whether the platform signs web commits (GET /signing-key answers). */
|
|
1788
|
+
this.signs = signal(false, ...(ngDevMode ? [{ debugName: "signs" }] : /* istanbul ignore next */ []));
|
|
1710
1789
|
/** Whether the branch's rules are known yet. */
|
|
1711
1790
|
this.rulesState = signal('loading', ...(ngDevMode ? [{ debugName: "rulesState" }] : /* istanbul ignore next */ []));
|
|
1712
1791
|
/**
|
|
1713
1792
|
* Direct commits are refused (or may be): the rules block direct pushes, or require signed
|
|
1714
|
-
* commits (a browser commit is unsigned), or they could not
|
|
1793
|
+
* commits while the platform signs nothing (a browser commit is then unsigned), or they could not
|
|
1794
|
+
* be loaded.
|
|
1715
1795
|
*/
|
|
1716
|
-
this.blocked = computed(() => this.rulesState() === 'failed' ||
|
|
1796
|
+
this.blocked = computed(() => this.rulesState() === 'failed' ||
|
|
1797
|
+
this.rules().some((r) => (r.blockDirectPush || (r.requireSignedCommits && !this.signs())) && matchesPattern(r.pattern, this.branch())), ...(ngDevMode ? [{ debugName: "blocked" }] : /* istanbul ignore next */ []));
|
|
1717
1798
|
this.rulesFor = null;
|
|
1718
1799
|
effect(() => {
|
|
1719
1800
|
const m = this.defaultMessage();
|
|
@@ -1737,11 +1818,12 @@ class CommitBox {
|
|
|
1737
1818
|
if (!at)
|
|
1738
1819
|
return;
|
|
1739
1820
|
this.rulesState.set('loading');
|
|
1740
|
-
this.api.protections(at.project, at.repo).subscribe({
|
|
1741
|
-
next: (
|
|
1821
|
+
forkJoin({ rules: this.api.protections(at.project, at.repo), signs: this.api.platformSigns() }).subscribe({
|
|
1822
|
+
next: ({ rules, signs }) => {
|
|
1742
1823
|
if (this.rulesFor !== at)
|
|
1743
1824
|
return;
|
|
1744
|
-
this.rules.set(
|
|
1825
|
+
this.rules.set(rules);
|
|
1826
|
+
this.signs.set(signs);
|
|
1745
1827
|
this.rulesState.set('loaded');
|
|
1746
1828
|
},
|
|
1747
1829
|
error: () => {
|
|
@@ -1810,7 +1892,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1810
1892
|
}], ctorParameters: () => [], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], branch: [{ type: i0.Input, args: [{ isSignal: true, alias: "branch", required: true }] }], defaultMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultMessage", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], commit: [{ type: i0.Output, args: ["commit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
|
|
1811
1893
|
|
|
1812
1894
|
// /{org}/{project}/code/{repo}/blob?ref=&path= (prototype "code_file"): one file, highlighted, with
|
|
1813
|
-
// Markdown preview, blame, raw, copy and download; a line number links to that line (#L12).
|
|
1895
|
+
// Markdown preview, blame, raw, copy and download; a line number links to that line (#L12). A CSV
|
|
1896
|
+
// or TSV previews as a table of its first rows, and a PDF in the browser's own viewer (V0-21).
|
|
1814
1897
|
class FilePage extends RepoPage {
|
|
1815
1898
|
constructor() {
|
|
1816
1899
|
super();
|
|
@@ -1822,6 +1905,12 @@ class FilePage extends RepoPage {
|
|
|
1822
1905
|
this.path = signal('', ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
1823
1906
|
this.line = signal(0, ...(ngDevMode ? [{ debugName: "line" }] : /* istanbul ignore next */ []));
|
|
1824
1907
|
this.view = signal('preview', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
1908
|
+
this.sanitizer = inject(DomSanitizer);
|
|
1909
|
+
/** The PDF's signed raw link, trusted as a frame source: the cookieless raw host serves it inline. */
|
|
1910
|
+
this.pdfUrl = computed(() => {
|
|
1911
|
+
const b = this.blob();
|
|
1912
|
+
return b?.preview === 'pdf' && b.rawUrl ? this.sanitizer.bypassSecurityTrustResourceUrl(b.rawUrl) : null;
|
|
1913
|
+
}, ...(ngDevMode ? [{ debugName: "pdfUrl" }] : /* istanbul ignore next */ []));
|
|
1825
1914
|
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
1826
1915
|
this.ago = (s) => ago(s);
|
|
1827
1916
|
this.initials = initials;
|
|
@@ -1949,7 +2038,7 @@ class FilePage extends RepoPage {
|
|
|
1949
2038
|
this.blame.set([]);
|
|
1950
2039
|
this.blaming.set(false);
|
|
1951
2040
|
this.api.blob(this.projectId(), r.name, this.ref() || r.defaultBranch, this.path()).subscribe({
|
|
1952
|
-
next: (b) => { this.blob.set(b); this.view.set(b.markdown ? 'preview' : 'code'); if (this.line())
|
|
2041
|
+
next: (b) => { this.blob.set(b); this.view.set(b.markdown || b.preview === 'csv' ? 'preview' : 'code'); if (this.line())
|
|
1953
2042
|
setTimeout(() => document.getElementById('L' + this.line())?.scrollIntoView({ block: 'center' })); },
|
|
1954
2043
|
error: (e) => this.error.set(apiMessage(e, `Could not open ${this.path()}.`)),
|
|
1955
2044
|
});
|
|
@@ -1982,7 +2071,7 @@ class FilePage extends RepoPage {
|
|
|
1982
2071
|
<div class="o-ch">
|
|
1983
2072
|
<span class="o-faint o-sm">{{ meta() }}</span>
|
|
1984
2073
|
<span class="o-r o-flex" style="gap:6px">
|
|
1985
|
-
@if (b.markdown) {
|
|
2074
|
+
@if (b.markdown || b.preview === 'csv') {
|
|
1986
2075
|
<div class="o-seg" role="group" aria-label="View"><button type="button" [class.o-on]="view() === 'preview'" (click)="view.set('preview')">Preview</button><button type="button" [class.o-on]="view() === 'code'" (click)="view.set('code')">Code</button></div>
|
|
1987
2076
|
}
|
|
1988
2077
|
@if (!b.binary && !b.truncated && !b.lfs) {<button type="button" class="o-btn o-xs" [class.o-on]="blaming()" [attr.aria-pressed]="blaming()" (click)="toggleBlame()">Blame</button>}
|
|
@@ -2011,6 +2100,21 @@ class FilePage extends RepoPage {
|
|
|
2011
2100
|
}
|
|
2012
2101
|
@if (b.image && b.rawUrl) {
|
|
2013
2102
|
<div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
|
|
2103
|
+
} @else if (b.preview === 'pdf' && pdfUrl(); as src) {
|
|
2104
|
+
<iframe [src]="src" [title]="b.name" style="display:block;width:100%;height:78vh;border:0" data-testid="pdf-preview"></iframe>
|
|
2105
|
+
} @else if (b.preview === 'csv' && b.table && view() === 'preview') {
|
|
2106
|
+
@let t = b.table;
|
|
2107
|
+
<div style="overflow:auto;max-height:78vh" data-testid="csv-preview">
|
|
2108
|
+
<div class="o-tbl" role="table" [attr.aria-label]="b.path">
|
|
2109
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th o-num" role="columnheader" aria-label="Row"></div>@for (h of t.header; track $index) {<div class="o-th" role="columnheader">{{ h }}</div>}</div></div>
|
|
2110
|
+
<div class="o-tbody" role="rowgroup">
|
|
2111
|
+
@for (row of t.rows; track $index; let i = $index) {
|
|
2112
|
+
<div class="o-tr" role="row"><div class="o-td o-num o-faint" role="rowheader">{{ i + 1 }}</div>@for (c of row; track $index) {<div class="o-td" role="cell">{{ c }}</div>}</div>
|
|
2113
|
+
}
|
|
2114
|
+
</div>
|
|
2115
|
+
</div>
|
|
2116
|
+
</div>
|
|
2117
|
+
@if (t.truncated) {<div class="o-cf o-faint o-sm" data-testid="csv-more">Showing the first {{ t.rows.length }} rows. @if (b.rawUrl) {<a [href]="b.rawUrl" target="_blank" rel="noopener">View raw</a> for the rest.}</div>}
|
|
2014
2118
|
} @else if (b.lfs && !b.lfs.stored) {
|
|
2015
2119
|
<div class="o-empty" data-testid="lfs-missing">This file is a Git LFS pointer to an object that was never uploaded (<span class="o-mono">{{ b.lfs.oid.slice(0, 12) }}</span>, {{ size(b.lfs.size) }}).</div>
|
|
2016
2120
|
} @else if (b.binary || b.truncated) {
|
|
@@ -2068,7 +2172,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2068
2172
|
<div class="o-ch">
|
|
2069
2173
|
<span class="o-faint o-sm">{{ meta() }}</span>
|
|
2070
2174
|
<span class="o-r o-flex" style="gap:6px">
|
|
2071
|
-
@if (b.markdown) {
|
|
2175
|
+
@if (b.markdown || b.preview === 'csv') {
|
|
2072
2176
|
<div class="o-seg" role="group" aria-label="View"><button type="button" [class.o-on]="view() === 'preview'" (click)="view.set('preview')">Preview</button><button type="button" [class.o-on]="view() === 'code'" (click)="view.set('code')">Code</button></div>
|
|
2073
2177
|
}
|
|
2074
2178
|
@if (!b.binary && !b.truncated && !b.lfs) {<button type="button" class="o-btn o-xs" [class.o-on]="blaming()" [attr.aria-pressed]="blaming()" (click)="toggleBlame()">Blame</button>}
|
|
@@ -2097,6 +2201,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2097
2201
|
}
|
|
2098
2202
|
@if (b.image && b.rawUrl) {
|
|
2099
2203
|
<div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
|
|
2204
|
+
} @else if (b.preview === 'pdf' && pdfUrl(); as src) {
|
|
2205
|
+
<iframe [src]="src" [title]="b.name" style="display:block;width:100%;height:78vh;border:0" data-testid="pdf-preview"></iframe>
|
|
2206
|
+
} @else if (b.preview === 'csv' && b.table && view() === 'preview') {
|
|
2207
|
+
@let t = b.table;
|
|
2208
|
+
<div style="overflow:auto;max-height:78vh" data-testid="csv-preview">
|
|
2209
|
+
<div class="o-tbl" role="table" [attr.aria-label]="b.path">
|
|
2210
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th o-num" role="columnheader" aria-label="Row"></div>@for (h of t.header; track $index) {<div class="o-th" role="columnheader">{{ h }}</div>}</div></div>
|
|
2211
|
+
<div class="o-tbody" role="rowgroup">
|
|
2212
|
+
@for (row of t.rows; track $index; let i = $index) {
|
|
2213
|
+
<div class="o-tr" role="row"><div class="o-td o-num o-faint" role="rowheader">{{ i + 1 }}</div>@for (c of row; track $index) {<div class="o-td" role="cell">{{ c }}</div>}</div>
|
|
2214
|
+
}
|
|
2215
|
+
</div>
|
|
2216
|
+
</div>
|
|
2217
|
+
</div>
|
|
2218
|
+
@if (t.truncated) {<div class="o-cf o-faint o-sm" data-testid="csv-more">Showing the first {{ t.rows.length }} rows. @if (b.rawUrl) {<a [href]="b.rawUrl" target="_blank" rel="noopener">View raw</a> for the rest.}</div>}
|
|
2100
2219
|
} @else if (b.lfs && !b.lfs.stored) {
|
|
2101
2220
|
<div class="o-empty" data-testid="lfs-missing">This file is a Git LFS pointer to an object that was never uploaded (<span class="o-mono">{{ b.lfs.oid.slice(0, 12) }}</span>, {{ size(b.lfs.size) }}).</div>
|
|
2102
2221
|
} @else if (b.binary || b.truncated) {
|
|
@@ -2200,7 +2319,7 @@ class CommitsPage extends RepoPage {
|
|
|
2200
2319
|
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
2201
2320
|
</span>
|
|
2202
2321
|
</div>
|
|
2203
|
-
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
2322
|
+
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" [attr.data-testid]="'verify-' + c.short">{{ verifyLabel(v) }}</span>}
|
|
2204
2323
|
<a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
|
|
2205
2324
|
<a class="o-btn o-sm" [routerLink]="link()" [queryParams]="{ ref: c.oid }" title="Browse files at this commit" [attr.aria-label]="'Browse files at ' + c.short">‹›</a>
|
|
2206
2325
|
</div>
|
|
@@ -2241,7 +2360,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2241
2360
|
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
2242
2361
|
</span>
|
|
2243
2362
|
</div>
|
|
2244
|
-
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
2363
|
+
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" [attr.data-testid]="'verify-' + c.short">{{ verifyLabel(v) }}</span>}
|
|
2245
2364
|
<a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
|
|
2246
2365
|
<a class="o-btn o-sm" [routerLink]="link()" [queryParams]="{ ref: c.oid }" title="Browse files at this commit" [attr.aria-label]="'Browse files at ' + c.short">‹›</a>
|
|
2247
2366
|
</div>
|
|
@@ -2291,7 +2410,7 @@ class CommitPage extends RepoPage {
|
|
|
2291
2410
|
@if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
|
|
2292
2411
|
<div class="o-flex o-wrap o-sm">
|
|
2293
2412
|
<span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><b class="o-b5">{{ c.author.name }}</b><span class="o-faint">committed {{ ago(c.committer.when) }}</span>
|
|
2294
|
-
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
2413
|
+
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" data-testid="commit-verified">{{ verifyLabel(v) }}</span>}
|
|
2295
2414
|
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
2296
2415
|
<span class="o-r o-mono o-faint">commit {{ c.oid.slice(0, 10) }}@for (p of c.parents; track p) { · parent <a [routerLink]="link('commit/' + p)">{{ p.slice(0, 7) }}</a>}</span>
|
|
2297
2416
|
</div>
|
|
@@ -2323,7 +2442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2323
2442
|
@if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
|
|
2324
2443
|
<div class="o-flex o-wrap o-sm">
|
|
2325
2444
|
<span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><b class="o-b5">{{ c.author.name }}</b><span class="o-faint">committed {{ ago(c.committer.when) }}</span>
|
|
2326
|
-
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
2445
|
+
@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" data-testid="commit-verified">{{ verifyLabel(v) }}</span>}
|
|
2327
2446
|
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
2328
2447
|
<span class="o-r o-mono o-faint">commit {{ c.oid.slice(0, 10) }}@for (p of c.parents; track p) { · parent <a [routerLink]="link('commit/' + p)">{{ p.slice(0, 7) }}</a>}</span>
|
|
2329
2448
|
</div>
|
|
@@ -2444,7 +2563,7 @@ class BranchesPage extends RepoPage {
|
|
|
2444
2563
|
</div>
|
|
2445
2564
|
<div class="o-td" role="cell">
|
|
2446
2565
|
@if (pullFor(b.name); as pr) {<a [class]="pr.state === 'merged' ? 'o-chip o-vio' : pr.state === 'closed' ? 'o-chip' : 'o-chip o-blu'" [routerLink]="link('pull/' + pr.number)">#{{ pr.number }} {{ pr.state }}</a>}
|
|
2447
|
-
@else if (!b.default && b.ahead > 0 &&
|
|
2566
|
+
@else if (!b.default && b.ahead > 0 && canOpenPull()) {<a class="o-btn o-xs" [routerLink]="link('pulls')" [queryParams]="{ new: 1, source: b.name }">New PR</a>}
|
|
2448
2567
|
</div>
|
|
2449
2568
|
<div class="o-td o-num" role="cell">@if (b.canDelete && canWrite()) {<button type="button" class="o-btn o-xs o-ghost" (click)="remove(b)" [attr.aria-label]="'Delete ' + b.name">Delete</button>}</div>
|
|
2450
2569
|
</div>
|
|
@@ -2503,7 +2622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2503
2622
|
</div>
|
|
2504
2623
|
<div class="o-td" role="cell">
|
|
2505
2624
|
@if (pullFor(b.name); as pr) {<a [class]="pr.state === 'merged' ? 'o-chip o-vio' : pr.state === 'closed' ? 'o-chip' : 'o-chip o-blu'" [routerLink]="link('pull/' + pr.number)">#{{ pr.number }} {{ pr.state }}</a>}
|
|
2506
|
-
@else if (!b.default && b.ahead > 0 &&
|
|
2625
|
+
@else if (!b.default && b.ahead > 0 && canOpenPull()) {<a class="o-btn o-xs" [routerLink]="link('pulls')" [queryParams]="{ new: 1, source: b.name }">New PR</a>}
|
|
2507
2626
|
</div>
|
|
2508
2627
|
<div class="o-td o-num" role="cell">@if (b.canDelete && canWrite()) {<button type="button" class="o-btn o-xs o-ghost" (click)="remove(b)" [attr.aria-label]="'Delete ' + b.name">Delete</button>}</div>
|
|
2509
2628
|
</div>
|
|
@@ -2558,7 +2677,7 @@ class PullsPage {
|
|
|
2558
2677
|
this.review = (r) => REVIEW[r] ?? r.replace(/_/g, ' ');
|
|
2559
2678
|
this.canWrite = computed(() => {
|
|
2560
2679
|
const p = this.host.project();
|
|
2561
|
-
return !!p && !p.archivedAt &&
|
|
2680
|
+
return !!p && !p.archivedAt && this.host.can('code.pr.create');
|
|
2562
2681
|
}, ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
2563
2682
|
this.route.paramMap.subscribe((m) => this.repoName.set(m.get('repo') ?? ''));
|
|
2564
2683
|
this.route.queryParamMap.subscribe((q) => { if (q.get('new'))
|
|
@@ -2592,7 +2711,7 @@ class PullsPage {
|
|
|
2592
2711
|
return;
|
|
2593
2712
|
}
|
|
2594
2713
|
this.loading.set(true);
|
|
2595
|
-
forkJoin(repos.map((r) => this.api.pulls(p.id, r.name, q).pipe(map$1((ps) => ps.map((x) => ({ ...x, repo: r.name }))), catchError(() => of([]))))).subscribe({
|
|
2714
|
+
forkJoin(repos.map((r) => this.api.pulls(p.id, r.name, q).pipe(map$1((ps) => ps.map((x) => ({ ...x, repo: r.name }))), catchError$1(() => of([]))))).subscribe({
|
|
2596
2715
|
next: (lists) => {
|
|
2597
2716
|
this.loading.set(false);
|
|
2598
2717
|
this.rows.set(lists.flat().sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')));
|
|
@@ -2792,6 +2911,13 @@ class PullPage extends RepoPage {
|
|
|
2792
2911
|
this.method = signal('merge', ...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
|
|
2793
2912
|
this.delBranch = signal(false, ...(ngDevMode ? [{ debugName: "delBranch" }] : /* istanbul ignore next */ []));
|
|
2794
2913
|
this.number = 0;
|
|
2914
|
+
/** The conflict resolver (V6-03): the conflicts, and each file's resolution as edited. */
|
|
2915
|
+
this.resolver = signal(null, ...(ngDevMode ? [{ debugName: "resolver" }] : /* istanbul ignore next */ []));
|
|
2916
|
+
this.resolved = signal({}, ...(ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []));
|
|
2917
|
+
this.resolutionReady = computed(() => {
|
|
2918
|
+
const cf = this.resolver();
|
|
2919
|
+
return !!cf && cf.canResolve && cf.files.every((f) => f.resolvable && !this.hasMarkers(this.resolved()[f.path]));
|
|
2920
|
+
}, ...(ngDevMode ? [{ debugName: "resolutionReady" }] : /* istanbul ignore next */ []));
|
|
2795
2921
|
this.ago = (s) => ago(s);
|
|
2796
2922
|
this.initials = initials;
|
|
2797
2923
|
this.dot = checkDot;
|
|
@@ -2961,6 +3087,47 @@ class PullPage extends RepoPage {
|
|
|
2961
3087
|
return;
|
|
2962
3088
|
this.run(this.api.merge(this.projectId(), this.name(), this.number, { method: this.method(), deleteBranch: this.delBranch(), expectHead: p.headSha }), 'Could not merge.', () => this.note.set(`Merged into ${p.target}.`));
|
|
2963
3089
|
}
|
|
3090
|
+
openResolver() {
|
|
3091
|
+
this.busy.set(true);
|
|
3092
|
+
this.error.set('');
|
|
3093
|
+
this.api.conflicts(this.projectId(), this.name(), this.number).subscribe({
|
|
3094
|
+
next: (cf) => {
|
|
3095
|
+
this.busy.set(false);
|
|
3096
|
+
const start = {};
|
|
3097
|
+
for (const f of cf.files)
|
|
3098
|
+
if (f.resolvable)
|
|
3099
|
+
start[f.path] = f.merged ?? '';
|
|
3100
|
+
this.resolved.set(start);
|
|
3101
|
+
this.resolver.set(cf);
|
|
3102
|
+
},
|
|
3103
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not load the conflicts.')); },
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
take(path, text) {
|
|
3107
|
+
this.resolved.set({ ...this.resolved(), [path]: text });
|
|
3108
|
+
}
|
|
3109
|
+
hasMarkers(text) {
|
|
3110
|
+
return /^(<{7}|>{7})( |$)|^={7}$/m.test(text ?? '');
|
|
3111
|
+
}
|
|
3112
|
+
conflictKind(kind) {
|
|
3113
|
+
return { binary: 'Binary file', 'modify/delete': 'Changed and deleted', rename: 'Renamed', large: 'Over 1 MB', lfs: 'Git LFS file' }[kind] ?? 'Command line only';
|
|
3114
|
+
}
|
|
3115
|
+
commitResolution() {
|
|
3116
|
+
const cf = this.resolver();
|
|
3117
|
+
if (!cf)
|
|
3118
|
+
return;
|
|
3119
|
+
this.busy.set(true);
|
|
3120
|
+
this.error.set('');
|
|
3121
|
+
this.api.resolveConflicts(this.projectId(), this.name(), this.number, { files: this.resolved(), expectHead: cf.headSha, expectBase: cf.baseSha }).subscribe({
|
|
3122
|
+
next: (r) => {
|
|
3123
|
+
this.busy.set(false);
|
|
3124
|
+
this.resolver.set(null);
|
|
3125
|
+
this.note.set(r.pull ? `Committed the merge to ${r.branch} and opened pull request #${r.pull} into ${cf.source}.` : `Committed the merge to ${cf.source}.`);
|
|
3126
|
+
this.load(false);
|
|
3127
|
+
},
|
|
3128
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not commit the resolution.')); },
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
2964
3131
|
close(reopen) {
|
|
2965
3132
|
this.run(this.api.closePull(this.projectId(), this.name(), this.number, reopen), reopen ? 'Could not reopen.' : 'Could not close.');
|
|
2966
3133
|
}
|
|
@@ -3089,8 +3256,8 @@ class PullPage extends RepoPage {
|
|
|
3089
3256
|
@if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
|
|
3090
3257
|
@if (p.autoMergeBy) {
|
|
3091
3258
|
<span class="o-chip o-blu" data-testid="auto-merge-on">Auto-merge on · {{ methodLabel(p.autoMergeMethod || 'merge') }} · by {{ people().name(p.autoMergeBy) }}</span>
|
|
3092
|
-
@if (
|
|
3093
|
-
} @else if (repo()?.allowAutoMerge &&
|
|
3259
|
+
@if (canMerge()) {<button type="button" class="o-btn o-sm o-ghost" (click)="setAutoMerge(null)" [disabled]="busy()" data-testid="auto-merge-off">Disable auto-merge</button>}
|
|
3260
|
+
} @else if (repo()?.allowAutoMerge && canMerge() && !p.gate.mergeable) {
|
|
3094
3261
|
<button type="button" class="o-btn o-sm" (click)="setAutoMerge(method())" [disabled]="busy()" data-testid="auto-merge-enable" title="Merges as you once every requirement passes">Enable auto-merge</button>
|
|
3095
3262
|
}
|
|
3096
3263
|
@if (p.canMerge) {
|
|
@@ -3106,6 +3273,9 @@ class PullPage extends RepoPage {
|
|
|
3106
3273
|
</div>
|
|
3107
3274
|
}
|
|
3108
3275
|
</div>
|
|
3276
|
+
@if (p.gate.conflicts?.length && canWrite() && !resolver()) {
|
|
3277
|
+
<div class="o-flex" style="padding:10px 14px;border-top:1px solid var(--o-line-soft)"><span class="o-mut o-sm">Text conflicts can be resolved here; the result is a merge of {{ p.target }} into {{ p.source }}.</span><button type="button" class="o-btn o-sm o-r" (click)="openResolver()" [disabled]="busy()" data-testid="resolve-open">Resolve conflicts</button></div>
|
|
3278
|
+
}
|
|
3109
3279
|
} @else if (p.state === 'closed' && p.canEdit) {
|
|
3110
3280
|
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><button type="button" class="o-btn o-r" (click)="close(true)" [disabled]="busy()">Reopen</button></div>
|
|
3111
3281
|
} @else if (p.state === 'merged') {
|
|
@@ -3113,6 +3283,42 @@ class PullPage extends RepoPage {
|
|
|
3113
3283
|
}
|
|
3114
3284
|
</div>
|
|
3115
3285
|
|
|
3286
|
+
@if (resolver(); as cf) {
|
|
3287
|
+
<div class="o-card" data-testid="resolver" aria-label="Resolve conflicts">
|
|
3288
|
+
<div class="o-ch"><b>Resolve conflicts</b><span class="o-faint o-sm">{{ cf.source }} with {{ cf.target }}</span><button type="button" class="o-btn o-xs o-ghost o-r" (click)="resolver.set(null)">Cancel</button></div>
|
|
3289
|
+
<div class="o-cb o-col" style="gap:16px">
|
|
3290
|
+
@if (!cf.resolvable) {
|
|
3291
|
+
<div class="o-note" role="status">Some conflicts can only be resolved on the command line:
|
|
3292
|
+
<div class="o-term" style="margin-top:8px"><div>git fetch origin</div><div>git switch {{ cf.source }}</div><div>git merge origin/{{ cf.target }}</div><div># resolve, then</div><div>git commit && git push</div></div>
|
|
3293
|
+
</div>
|
|
3294
|
+
}
|
|
3295
|
+
@for (f of cf.files; track f.path) {
|
|
3296
|
+
<div class="o-col" style="gap:8px" [attr.data-testid]="'conflict-' + f.path">
|
|
3297
|
+
<div class="o-flex"><b class="o-mono">{{ f.path }}</b>@if (!f.resolvable) {<span class="o-chip o-amb">{{ conflictKind(f.kind) }}</span>}</div>
|
|
3298
|
+
@if (f.resolvable) {
|
|
3299
|
+
<div class="o-g3" style="gap:8px">
|
|
3300
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">{{ cf.source }} (this branch)</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.ours }}</pre><button type="button" class="o-btn o-xs" (click)="take(f.path, f.ours ?? '')" [attr.data-testid]="'take-ours-' + f.path">Use this branch's</button></div>
|
|
3301
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">Common ancestor</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.base ?? '' }}</pre></div>
|
|
3302
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">{{ cf.target }}</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.theirs }}</pre><button type="button" class="o-btn o-xs" (click)="take(f.path, f.theirs ?? '')" [attr.data-testid]="'take-theirs-' + f.path">Use {{ cf.target }}'s</button></div>
|
|
3303
|
+
</div>
|
|
3304
|
+
<label class="o-lbl" style="padding:0" [attr.for]="'res-' + $index">Result</label>
|
|
3305
|
+
<textarea class="o-in o-mono" style="min-height:160px;height:auto" [id]="'res-' + $index" [ngModel]="resolved()[f.path]" (ngModelChange)="take(f.path, $event)" [attr.data-testid]="'resolution-' + f.path"></textarea>
|
|
3306
|
+
@if (hasMarkers(resolved()[f.path])) {<span class="o-hint">Remove the conflict markers (<<<<<<<, =======, >>>>>>>) to finish this file.</span>}
|
|
3307
|
+
} @else {
|
|
3308
|
+
<span class="o-mut o-sm">{{ f.message || 'This conflict can only be resolved on the command line.' }}</span>
|
|
3309
|
+
}
|
|
3310
|
+
</div>
|
|
3311
|
+
}
|
|
3312
|
+
</div>
|
|
3313
|
+
@if (cf.canResolve) {
|
|
3314
|
+
<div class="o-cf">
|
|
3315
|
+
<span class="o-faint o-xs">{{ cf.direct ? 'Commits a merge of ' + cf.target + ' into ' + cf.source + ', signed by Origine.' : cf.source + ' takes no direct commits: the merge goes to a new branch with a pull request into ' + cf.source + '.' }}</span>
|
|
3316
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="commitResolution()" [disabled]="busy() || !resolutionReady()" data-testid="resolve-commit">Commit merge</button>
|
|
3317
|
+
</div>
|
|
3318
|
+
}
|
|
3319
|
+
</div>
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3116
3322
|
<div class="o-card">
|
|
3117
3323
|
<label class="o-sr-only" for="pr-cmt">Add a comment</label>
|
|
3118
3324
|
<textarea id="pr-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;height:auto;padding:10px 12px" placeholder="Add a comment. Markdown works." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="pull-comment"></textarea>
|
|
@@ -3122,7 +3328,7 @@ class PullPage extends RepoPage {
|
|
|
3122
3328
|
@case ('commits') {
|
|
3123
3329
|
<div class="o-card">
|
|
3124
3330
|
@for (c of cmp()?.commits ?? []; track c.oid) {
|
|
3125
|
-
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
3331
|
+
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" [attr.data-testid]="'verify-' + c.short">{{ verifyLabel(v) }}</span>}<a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
|
|
3126
3332
|
} @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
|
|
3127
3333
|
</div>
|
|
3128
3334
|
}
|
|
@@ -3272,8 +3478,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3272
3478
|
@if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
|
|
3273
3479
|
@if (p.autoMergeBy) {
|
|
3274
3480
|
<span class="o-chip o-blu" data-testid="auto-merge-on">Auto-merge on · {{ methodLabel(p.autoMergeMethod || 'merge') }} · by {{ people().name(p.autoMergeBy) }}</span>
|
|
3275
|
-
@if (
|
|
3276
|
-
} @else if (repo()?.allowAutoMerge &&
|
|
3481
|
+
@if (canMerge()) {<button type="button" class="o-btn o-sm o-ghost" (click)="setAutoMerge(null)" [disabled]="busy()" data-testid="auto-merge-off">Disable auto-merge</button>}
|
|
3482
|
+
} @else if (repo()?.allowAutoMerge && canMerge() && !p.gate.mergeable) {
|
|
3277
3483
|
<button type="button" class="o-btn o-sm" (click)="setAutoMerge(method())" [disabled]="busy()" data-testid="auto-merge-enable" title="Merges as you once every requirement passes">Enable auto-merge</button>
|
|
3278
3484
|
}
|
|
3279
3485
|
@if (p.canMerge) {
|
|
@@ -3289,6 +3495,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3289
3495
|
</div>
|
|
3290
3496
|
}
|
|
3291
3497
|
</div>
|
|
3498
|
+
@if (p.gate.conflicts?.length && canWrite() && !resolver()) {
|
|
3499
|
+
<div class="o-flex" style="padding:10px 14px;border-top:1px solid var(--o-line-soft)"><span class="o-mut o-sm">Text conflicts can be resolved here; the result is a merge of {{ p.target }} into {{ p.source }}.</span><button type="button" class="o-btn o-sm o-r" (click)="openResolver()" [disabled]="busy()" data-testid="resolve-open">Resolve conflicts</button></div>
|
|
3500
|
+
}
|
|
3292
3501
|
} @else if (p.state === 'closed' && p.canEdit) {
|
|
3293
3502
|
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><button type="button" class="o-btn o-r" (click)="close(true)" [disabled]="busy()">Reopen</button></div>
|
|
3294
3503
|
} @else if (p.state === 'merged') {
|
|
@@ -3296,6 +3505,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3296
3505
|
}
|
|
3297
3506
|
</div>
|
|
3298
3507
|
|
|
3508
|
+
@if (resolver(); as cf) {
|
|
3509
|
+
<div class="o-card" data-testid="resolver" aria-label="Resolve conflicts">
|
|
3510
|
+
<div class="o-ch"><b>Resolve conflicts</b><span class="o-faint o-sm">{{ cf.source }} with {{ cf.target }}</span><button type="button" class="o-btn o-xs o-ghost o-r" (click)="resolver.set(null)">Cancel</button></div>
|
|
3511
|
+
<div class="o-cb o-col" style="gap:16px">
|
|
3512
|
+
@if (!cf.resolvable) {
|
|
3513
|
+
<div class="o-note" role="status">Some conflicts can only be resolved on the command line:
|
|
3514
|
+
<div class="o-term" style="margin-top:8px"><div>git fetch origin</div><div>git switch {{ cf.source }}</div><div>git merge origin/{{ cf.target }}</div><div># resolve, then</div><div>git commit && git push</div></div>
|
|
3515
|
+
</div>
|
|
3516
|
+
}
|
|
3517
|
+
@for (f of cf.files; track f.path) {
|
|
3518
|
+
<div class="o-col" style="gap:8px" [attr.data-testid]="'conflict-' + f.path">
|
|
3519
|
+
<div class="o-flex"><b class="o-mono">{{ f.path }}</b>@if (!f.resolvable) {<span class="o-chip o-amb">{{ conflictKind(f.kind) }}</span>}</div>
|
|
3520
|
+
@if (f.resolvable) {
|
|
3521
|
+
<div class="o-g3" style="gap:8px">
|
|
3522
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">{{ cf.source }} (this branch)</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.ours }}</pre><button type="button" class="o-btn o-xs" (click)="take(f.path, f.ours ?? '')" [attr.data-testid]="'take-ours-' + f.path">Use this branch's</button></div>
|
|
3523
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">Common ancestor</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.base ?? '' }}</pre></div>
|
|
3524
|
+
<div class="o-col" style="gap:4px;min-width:0"><span class="o-lbl" style="padding:0">{{ cf.target }}</span><pre class="o-term" style="margin:0;max-height:220px;overflow:auto">{{ f.theirs }}</pre><button type="button" class="o-btn o-xs" (click)="take(f.path, f.theirs ?? '')" [attr.data-testid]="'take-theirs-' + f.path">Use {{ cf.target }}'s</button></div>
|
|
3525
|
+
</div>
|
|
3526
|
+
<label class="o-lbl" style="padding:0" [attr.for]="'res-' + $index">Result</label>
|
|
3527
|
+
<textarea class="o-in o-mono" style="min-height:160px;height:auto" [id]="'res-' + $index" [ngModel]="resolved()[f.path]" (ngModelChange)="take(f.path, $event)" [attr.data-testid]="'resolution-' + f.path"></textarea>
|
|
3528
|
+
@if (hasMarkers(resolved()[f.path])) {<span class="o-hint">Remove the conflict markers (<<<<<<<, =======, >>>>>>>) to finish this file.</span>}
|
|
3529
|
+
} @else {
|
|
3530
|
+
<span class="o-mut o-sm">{{ f.message || 'This conflict can only be resolved on the command line.' }}</span>
|
|
3531
|
+
}
|
|
3532
|
+
</div>
|
|
3533
|
+
}
|
|
3534
|
+
</div>
|
|
3535
|
+
@if (cf.canResolve) {
|
|
3536
|
+
<div class="o-cf">
|
|
3537
|
+
<span class="o-faint o-xs">{{ cf.direct ? 'Commits a merge of ' + cf.target + ' into ' + cf.source + ', signed by Origine.' : cf.source + ' takes no direct commits: the merge goes to a new branch with a pull request into ' + cf.source + '.' }}</span>
|
|
3538
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="commitResolution()" [disabled]="busy() || !resolutionReady()" data-testid="resolve-commit">Commit merge</button>
|
|
3539
|
+
</div>
|
|
3540
|
+
}
|
|
3541
|
+
</div>
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3299
3544
|
<div class="o-card">
|
|
3300
3545
|
<label class="o-sr-only" for="pr-cmt">Add a comment</label>
|
|
3301
3546
|
<textarea id="pr-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;height:auto;padding:10px 12px" placeholder="Add a comment. Markdown works." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="pull-comment"></textarea>
|
|
@@ -3305,7 +3550,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3305
3550
|
@case ('commits') {
|
|
3306
3551
|
<div class="o-card">
|
|
3307
3552
|
@for (c of cmp()?.commits ?? []; track c.oid) {
|
|
3308
|
-
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="
|
|
3553
|
+
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>@if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="verifyTitle(v)" [attr.data-testid]="'verify-' + c.short">{{ verifyLabel(v) }}</span>}<a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
|
|
3309
3554
|
} @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
|
|
3310
3555
|
</div>
|
|
3311
3556
|
}
|
|
@@ -3420,7 +3665,12 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3420
3665
|
{ key: 'requireSignedCommits', label: 'Require signed commits', hint: 'Verified against members’ signing keys, on push and at merge.' },
|
|
3421
3666
|
{ key: 'requireLinearHistory', label: 'Require linear history', hint: 'No merge commits: squash or rebase only.' },
|
|
3422
3667
|
];
|
|
3423
|
-
|
|
3668
|
+
/** Who may bypass a rule: the built-in Owner, Admin and Maintainer, and the organization's custom roles. */
|
|
3669
|
+
this.customRoles = signal([], ...(ngDevMode ? [{ debugName: "customRoles" }] : /* istanbul ignore next */ []));
|
|
3670
|
+
this.bypassRoles = computed(() => [{ id: 'owner', label: 'Owners' }, { id: 'admin', label: 'Admins' }, { id: 'maintainer', label: 'Maintainers' }, ...this.customRoles()], ...(ngDevMode ? [{ debugName: "bypassRoles" }] : /* istanbul ignore next */ []));
|
|
3671
|
+
this.ago = ago;
|
|
3672
|
+
this.mirror = signal(null, ...(ngDevMode ? [{ debugName: "mirror" }] : /* istanbul ignore next */ []));
|
|
3673
|
+
this.mf = { url: '', username: '', token: '', interval: 60 };
|
|
3424
3674
|
this.collabs = signal([], ...(ngDevMode ? [{ debugName: "collabs" }] : /* istanbul ignore next */ []));
|
|
3425
3675
|
this.perms = [{ id: 'read', label: 'Read' }, { id: 'write', label: 'Write' }, { id: 'maintain', label: 'Maintain' }];
|
|
3426
3676
|
this.addable = computed(() => this.host.people().filter((p) => !this.collabs().some((c) => c.userId === p.id)), ...(ngDevMode ? [{ debugName: "addable" }] : /* istanbul ignore next */ []));
|
|
@@ -3428,11 +3678,62 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3428
3678
|
this.newPerm = 'write';
|
|
3429
3679
|
this.route.queryParamMap.subscribe((q) => this.section.set(q.get('section') || 'general'));
|
|
3430
3680
|
}
|
|
3681
|
+
loadMirror(r) {
|
|
3682
|
+
if (!r.mirror)
|
|
3683
|
+
return this.mirror.set(null);
|
|
3684
|
+
this.api.mirror(this.projectId(), r.name).subscribe({
|
|
3685
|
+
next: (m) => {
|
|
3686
|
+
this.mirror.set(m);
|
|
3687
|
+
this.mf = { url: m.source, username: '', token: '', interval: m.intervalMinutes };
|
|
3688
|
+
if (m.syncing)
|
|
3689
|
+
setTimeout(() => this.loadMirror(r), 1500);
|
|
3690
|
+
},
|
|
3691
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not load the mirror.')),
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
syncNow() {
|
|
3695
|
+
const r = this.repo();
|
|
3696
|
+
if (!r)
|
|
3697
|
+
return;
|
|
3698
|
+
this.api.syncMirror(this.projectId(), r.name).subscribe({
|
|
3699
|
+
next: (m) => { this.mirror.set(m); this.note.set('Syncing with the source.'); setTimeout(() => this.loadMirror(r), 1000); },
|
|
3700
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not start the sync.')),
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
saveMirror() {
|
|
3704
|
+
const r = this.repo();
|
|
3705
|
+
const m = this.mirror();
|
|
3706
|
+
if (!r || !m)
|
|
3707
|
+
return;
|
|
3708
|
+
const body = { intervalMinutes: Number(this.mf.interval) || m.intervalMinutes };
|
|
3709
|
+
if (this.mf.url.trim() && this.mf.url.trim() !== m.source)
|
|
3710
|
+
body.url = this.mf.url.trim();
|
|
3711
|
+
if (this.mf.token) {
|
|
3712
|
+
body.token = this.mf.token;
|
|
3713
|
+
body.username = this.mf.username.trim();
|
|
3714
|
+
}
|
|
3715
|
+
this.busy.set(true);
|
|
3716
|
+
this.error.set('');
|
|
3717
|
+
this.api.updateMirror(this.projectId(), r.name, body).subscribe({
|
|
3718
|
+
next: (n) => { this.busy.set(false); this.mirror.set(n); this.mf.token = ''; this.note.set('Mirror saved. It syncs as you now.'); },
|
|
3719
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not save the mirror.')); },
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
stopMirror() {
|
|
3723
|
+
const r = this.repo();
|
|
3724
|
+
if (!r)
|
|
3725
|
+
return;
|
|
3726
|
+
this.busy.set(true);
|
|
3727
|
+
this.api.stopMirror(this.projectId(), r.name).subscribe({
|
|
3728
|
+
next: (n) => { this.busy.set(false); this.repo.set({ ...r, ...n, mirror: false }); this.mirror.set(null); this.note.set(`${r.name} is an ordinary repository now.`); this.go('general'); },
|
|
3729
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not stop mirroring.')); },
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3431
3732
|
roleLabel(r) {
|
|
3432
3733
|
return r ? r[0].toUpperCase() + r.slice(1) : 'Not in the project';
|
|
3433
3734
|
}
|
|
3434
3735
|
loadCollabs(r) {
|
|
3435
|
-
if (!this.
|
|
3736
|
+
if (!this.canCollaborators())
|
|
3436
3737
|
return;
|
|
3437
3738
|
this.api.collaborators(this.projectId(), r.name).subscribe({ next: (l) => this.collabs.set(l), error: () => undefined });
|
|
3438
3739
|
}
|
|
@@ -3461,10 +3762,14 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3461
3762
|
}
|
|
3462
3763
|
onRepo(r) {
|
|
3463
3764
|
this.loadCollabs(r);
|
|
3765
|
+
this.loadMirror(r);
|
|
3464
3766
|
this.g = { name: r.name, description: r.description, visibility: r.visibility, defaultBranch: r.defaultBranch, mergeMethods: [...(r.mergeMethods ?? [])], deleteBranchOnMerge: r.deleteBranchOnMerge, allowAutoMerge: !!r.allowAutoMerge };
|
|
3465
3767
|
if (!r.empty)
|
|
3466
3768
|
this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.branches.set(b), error: () => undefined });
|
|
3467
3769
|
this.api.protections(this.projectId(), r.name).subscribe({ next: (p) => this.rules.set(p), error: () => undefined });
|
|
3770
|
+
if (this.canProtect() && !this.customRoles().length) {
|
|
3771
|
+
this.api.customRoles().subscribe({ next: (l) => this.customRoles.set(l.map((c) => ({ id: c.id, label: c.name }))), error: () => undefined });
|
|
3772
|
+
}
|
|
3468
3773
|
}
|
|
3469
3774
|
go(s) {
|
|
3470
3775
|
void this.router.navigate([], { relativeTo: this.route, queryParams: { section: s === 'general' ? null : s }, replaceUrl: true });
|
|
@@ -3523,7 +3828,7 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3523
3828
|
this.busy.set(true);
|
|
3524
3829
|
const renamed = this.g.name.trim() !== r.name;
|
|
3525
3830
|
// a maintain collaborator sends what they may change; the name and visibility stay a Maintainer's
|
|
3526
|
-
const body = this.
|
|
3831
|
+
const body = this.canSettings()
|
|
3527
3832
|
? { name: this.g.name.trim(), description: this.g.description, visibility: this.g.visibility, defaultBranch: this.g.defaultBranch, mergeMethods: this.g.mergeMethods, deleteBranchOnMerge: this.g.deleteBranchOnMerge, allowAutoMerge: this.g.allowAutoMerge }
|
|
3528
3833
|
: { description: this.g.description, defaultBranch: this.g.defaultBranch, mergeMethods: this.g.mergeMethods, deleteBranchOnMerge: this.g.deleteBranchOnMerge, allowAutoMerge: this.g.allowAutoMerge };
|
|
3529
3834
|
this.api.update(this.projectId(), r.name, body).subscribe({
|
|
@@ -3590,6 +3895,7 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3590
3895
|
<nav class="o-setnav" aria-label="Repository settings">
|
|
3591
3896
|
<span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
|
|
3592
3897
|
<button type="button" [class.o-on]="section() === 'general'" (click)="go('general')">General</button>
|
|
3898
|
+
@if (r.mirror) {<button type="button" [class.o-on]="section() === 'mirror'" (click)="go('mirror')" data-testid="settings-mirror">Mirror</button>}
|
|
3593
3899
|
<button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
|
|
3594
3900
|
<button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators</button>
|
|
3595
3901
|
<button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
|
|
@@ -3602,14 +3908,14 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3602
3908
|
<h1 class="o-t">General</h1>
|
|
3603
3909
|
<form class="o-card o-cb o-form" (ngSubmit)="saveGeneral()">
|
|
3604
3910
|
<div class="o-g2">
|
|
3605
|
-
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!
|
|
3911
|
+
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!canSettings()" /><span class="o-hint">Renaming keeps the old clone URL working as a redirect.</span></div>
|
|
3606
3912
|
<div class="o-fld"><label for="rs-def">Default branch</label><select id="rs-def" class="o-in" name="def" [(ngModel)]="g.defaultBranch" [disabled]="!canMaintain()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}@if (!branches().length) {<option [value]="r.defaultBranch">{{ r.defaultBranch }}</option>}</select></div>
|
|
3607
3913
|
</div>
|
|
3608
3914
|
<div class="o-fld"><label for="rs-desc">Description</label><input id="rs-desc" class="o-in" name="desc" [(ngModel)]="g.description" [disabled]="!canMaintain()" /></div>
|
|
3609
3915
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
3610
3916
|
<div class="o-g2">
|
|
3611
|
-
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!
|
|
3612
|
-
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!
|
|
3917
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!canSettings()"><span class="o-rad" [class.o-on]="g.visibility === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
3918
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!canSettings()"><span class="o-rad" [class.o-on]="g.visibility === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
3613
3919
|
</div>
|
|
3614
3920
|
</div>
|
|
3615
3921
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed merge methods</span>
|
|
@@ -3617,9 +3923,41 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3617
3923
|
</div>
|
|
3618
3924
|
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-del" [class.o-on]="g.deleteBranchOnMerge" [attr.aria-checked]="g.deleteBranchOnMerge" (click)="g.deleteBranchOnMerge = !g.deleteBranchOnMerge" [disabled]="!canMaintain()"></button><span id="rs-del">Delete head branches after merge</span></div>
|
|
3619
3925
|
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-auto" [class.o-on]="g.allowAutoMerge" [attr.aria-checked]="g.allowAutoMerge" (click)="g.allowAutoMerge = !g.allowAutoMerge" [disabled]="!canMaintain()" data-testid="allow-auto-merge"></button><span id="rs-auto">Allow auto-merge when all requirements pass</span></div>
|
|
3620
|
-
@if (canMaintain()) {<div class="o-flex">@if (!
|
|
3926
|
+
@if (canMaintain()) {<div class="o-flex">@if (!canSettings()) {<span class="o-faint o-sm">As a maintain collaborator you change these settings; a project Maintainer renames the repository or changes its visibility.</span>}<button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !g.mergeMethods.length" data-testid="settings-save">Save</button></div>}
|
|
3621
3927
|
</form>
|
|
3622
3928
|
}
|
|
3929
|
+
@case ('mirror') {
|
|
3930
|
+
<h1 class="o-t">Mirror</h1>
|
|
3931
|
+
@if (mirror(); as m) {
|
|
3932
|
+
<div class="o-card" data-testid="mirror-status">
|
|
3933
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Source</span><span class="o-mono o-ell">{{ m.source }}</span></div>
|
|
3934
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Syncs</span><span>every {{ m.intervalMinutes }} minutes, as {{ people().name(m.setBy) }}</span></div>
|
|
3935
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Last sync</span><span data-testid="mirror-last">{{ m.syncing ? 'Syncing now…' : m.lastSyncAt ? ago(m.lastSyncAt) : 'Not yet' }}</span>@if (!m.lastError && m.lastSuccessAt) {<span class="o-chip o-grn">Up to date</span>}</div>
|
|
3936
|
+
@if (m.lastError) {<div class="o-row"><span class="o-mut" style="width:140px">Problem</span><span class="o-red" role="alert" data-testid="mirror-error">{{ m.lastError }}</span></div>}
|
|
3937
|
+
@if (m.lfsMissing) {<div class="o-row"><span class="o-mut" style="width:140px">Git LFS</span><span>{{ m.lfsMissing }} files the source could not supply stay pointers.</span></div>}
|
|
3938
|
+
<div class="o-cf">
|
|
3939
|
+
<button type="button" class="o-btn o-sm" (click)="syncNow()" [disabled]="!can('code.push') || m.syncing || busy()" data-testid="mirror-sync">Sync now</button>
|
|
3940
|
+
<span class="o-faint o-xs">Pushes to this repository are refused while it mirrors its source.</span>
|
|
3941
|
+
</div>
|
|
3942
|
+
</div>
|
|
3943
|
+
@if (canSettings()) {
|
|
3944
|
+
<form class="o-card o-cb o-form" (ngSubmit)="saveMirror()" aria-label="Mirror settings">
|
|
3945
|
+
<div class="o-fld"><label for="mi-url">Source URL</label><input id="mi-url" class="o-in o-mono" name="murl" [(ngModel)]="mf.url" /></div>
|
|
3946
|
+
<div class="o-g2">
|
|
3947
|
+
<div class="o-fld"><label for="mi-user">Username</label><input id="mi-user" class="o-in" name="muser" [(ngModel)]="mf.username" autocomplete="off" /></div>
|
|
3948
|
+
<div class="o-fld"><label for="mi-tok">New access token</label><input id="mi-tok" class="o-in" type="password" name="mtok" [(ngModel)]="mf.token" autocomplete="off" [placeholder]="m.hasCredentials ? 'A token is kept, encrypted' : 'None'" /></div>
|
|
3949
|
+
</div>
|
|
3950
|
+
<div class="o-fld"><label for="mi-int">Sync every (minutes)</label><input id="mi-int" class="o-in" type="number" min="1" name="mint" [(ngModel)]="mf.interval" style="width:140px" /></div>
|
|
3951
|
+
<div class="o-flex"><span class="o-faint o-sm">Saving makes the mirror sync as you.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy()" data-testid="mirror-save">Save</button></div>
|
|
3952
|
+
</form>
|
|
3953
|
+
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
3954
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>Stop mirroring</b><span class="o-hint">The repository keeps its history and takes pushes; the kept token is deleted.</span></div><button type="button" class="o-btn o-sm" (click)="stopMirror()" [disabled]="busy()" data-testid="mirror-stop">Stop mirroring</button></div>
|
|
3955
|
+
</div>
|
|
3956
|
+
}
|
|
3957
|
+
} @else {
|
|
3958
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3623
3961
|
@case ('protect') {
|
|
3624
3962
|
<div class="o-flex"><h1 class="o-t">Branch protection</h1>@if (canProtect()) {<button type="button" class="o-btn o-sm o-r" (click)="edit(null)" data-testid="add-rule">+ Add rule</button>}</div>
|
|
3625
3963
|
<div class="o-card">
|
|
@@ -3642,7 +3980,7 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3642
3980
|
</div>
|
|
3643
3981
|
</div>
|
|
3644
3982
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass</span><div class="o-flex o-wrap">
|
|
3645
|
-
@for (b of bypassRoles; track b.id) {<button type="button" class="o-chip" style="cursor:pointer;border:none" [class.o-blu]="f.bypass.includes(b.id)" [attr.aria-pressed]="f.bypass.includes(b.id)" (click)="flipBypass(f, b.id)" [attr.data-testid]="'bypass-' + b.id">{{ b.label }}</button>}
|
|
3983
|
+
@for (b of bypassRoles(); track b.id) {<button type="button" class="o-chip" style="cursor:pointer;border:none" [class.o-blu]="f.bypass.includes(b.id)" [attr.aria-pressed]="f.bypass.includes(b.id)" (click)="flipBypass(f, b.id)" [attr.data-testid]="'bypass-' + b.id">{{ b.label }}</button>}
|
|
3646
3984
|
<span class="o-hint">Pushes by these roles go through this rule; every bypass is written to the audit log.</span></div></div>
|
|
3647
3985
|
<div class="o-fld"><span class="o-lbl" style="padding:0">What a blocked push sees</span><div class="o-term" style="font-size:11.5px"><div>$ git push origin {{ f.pattern || 'main' }}</div><div>remote: ✕ {{ f.pattern || 'main' }} is protected by rule “{{ f.pattern || 'main' }}”: direct pushes are blocked.</div><div>remote: Open a pull request instead.</div></div></div>
|
|
3648
3986
|
<div class="o-flex">
|
|
@@ -3657,7 +3995,7 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3657
3995
|
@case ('collab') {
|
|
3658
3996
|
<h1 class="o-t">Collaborators</h1>
|
|
3659
3997
|
<span class="o-mut">Access beyond the project role, for this repository only.</span>
|
|
3660
|
-
@if (
|
|
3998
|
+
@if (canCollaborators()) {
|
|
3661
3999
|
<div class="o-card" style="overflow-x:auto" data-testid="collaborators">
|
|
3662
4000
|
<div class="o-tbl" role="table" aria-label="Collaborators" style="min-width:560px">
|
|
3663
4001
|
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Person</div><div class="o-th" role="columnheader">Project role</div><div class="o-th" role="columnheader">Repository access</div><div class="o-th" role="columnheader"></div></div></div>
|
|
@@ -3688,10 +4026,10 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3688
4026
|
@case ('danger') {
|
|
3689
4027
|
<h1 class="o-t">Danger zone</h1>
|
|
3690
4028
|
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
3691
|
-
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!
|
|
4029
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!canDelete() || busy()">{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</button></div>
|
|
3692
4030
|
<div class="o-row" style="padding:14px;flex-wrap:wrap"><div class="o-col" style="gap:2px;flex:1;min-width:240px"><b>Delete this repository</b><span class="o-hint">Recoverable for 7 days, then gone for good. Type <b class="o-mono">{{ r.name }}</b> to confirm.</span></div>
|
|
3693
|
-
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!
|
|
3694
|
-
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!
|
|
4031
|
+
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!canDelete()" />
|
|
4032
|
+
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!canDelete() || confirm() !== r.name || busy()">Delete</button>
|
|
3695
4033
|
</div>
|
|
3696
4034
|
</div>
|
|
3697
4035
|
}
|
|
@@ -3702,7 +4040,7 @@ class RepoSettingsPage extends RepoPage {
|
|
|
3702
4040
|
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
3703
4041
|
}
|
|
3704
4042
|
</div>
|
|
3705
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
4043
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3706
4044
|
}
|
|
3707
4045
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoSettingsPage, decorators: [{
|
|
3708
4046
|
type: Component,
|
|
@@ -3717,6 +4055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3717
4055
|
<nav class="o-setnav" aria-label="Repository settings">
|
|
3718
4056
|
<span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
|
|
3719
4057
|
<button type="button" [class.o-on]="section() === 'general'" (click)="go('general')">General</button>
|
|
4058
|
+
@if (r.mirror) {<button type="button" [class.o-on]="section() === 'mirror'" (click)="go('mirror')" data-testid="settings-mirror">Mirror</button>}
|
|
3720
4059
|
<button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
|
|
3721
4060
|
<button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators</button>
|
|
3722
4061
|
<button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
|
|
@@ -3729,14 +4068,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3729
4068
|
<h1 class="o-t">General</h1>
|
|
3730
4069
|
<form class="o-card o-cb o-form" (ngSubmit)="saveGeneral()">
|
|
3731
4070
|
<div class="o-g2">
|
|
3732
|
-
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!
|
|
4071
|
+
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!canSettings()" /><span class="o-hint">Renaming keeps the old clone URL working as a redirect.</span></div>
|
|
3733
4072
|
<div class="o-fld"><label for="rs-def">Default branch</label><select id="rs-def" class="o-in" name="def" [(ngModel)]="g.defaultBranch" [disabled]="!canMaintain()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}@if (!branches().length) {<option [value]="r.defaultBranch">{{ r.defaultBranch }}</option>}</select></div>
|
|
3734
4073
|
</div>
|
|
3735
4074
|
<div class="o-fld"><label for="rs-desc">Description</label><input id="rs-desc" class="o-in" name="desc" [(ngModel)]="g.description" [disabled]="!canMaintain()" /></div>
|
|
3736
4075
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
3737
4076
|
<div class="o-g2">
|
|
3738
|
-
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!
|
|
3739
|
-
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!
|
|
4077
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!canSettings()"><span class="o-rad" [class.o-on]="g.visibility === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
4078
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!canSettings()"><span class="o-rad" [class.o-on]="g.visibility === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
3740
4079
|
</div>
|
|
3741
4080
|
</div>
|
|
3742
4081
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed merge methods</span>
|
|
@@ -3744,9 +4083,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3744
4083
|
</div>
|
|
3745
4084
|
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-del" [class.o-on]="g.deleteBranchOnMerge" [attr.aria-checked]="g.deleteBranchOnMerge" (click)="g.deleteBranchOnMerge = !g.deleteBranchOnMerge" [disabled]="!canMaintain()"></button><span id="rs-del">Delete head branches after merge</span></div>
|
|
3746
4085
|
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-auto" [class.o-on]="g.allowAutoMerge" [attr.aria-checked]="g.allowAutoMerge" (click)="g.allowAutoMerge = !g.allowAutoMerge" [disabled]="!canMaintain()" data-testid="allow-auto-merge"></button><span id="rs-auto">Allow auto-merge when all requirements pass</span></div>
|
|
3747
|
-
@if (canMaintain()) {<div class="o-flex">@if (!
|
|
4086
|
+
@if (canMaintain()) {<div class="o-flex">@if (!canSettings()) {<span class="o-faint o-sm">As a maintain collaborator you change these settings; a project Maintainer renames the repository or changes its visibility.</span>}<button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !g.mergeMethods.length" data-testid="settings-save">Save</button></div>}
|
|
3748
4087
|
</form>
|
|
3749
4088
|
}
|
|
4089
|
+
@case ('mirror') {
|
|
4090
|
+
<h1 class="o-t">Mirror</h1>
|
|
4091
|
+
@if (mirror(); as m) {
|
|
4092
|
+
<div class="o-card" data-testid="mirror-status">
|
|
4093
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Source</span><span class="o-mono o-ell">{{ m.source }}</span></div>
|
|
4094
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Syncs</span><span>every {{ m.intervalMinutes }} minutes, as {{ people().name(m.setBy) }}</span></div>
|
|
4095
|
+
<div class="o-row"><span class="o-mut" style="width:140px">Last sync</span><span data-testid="mirror-last">{{ m.syncing ? 'Syncing now…' : m.lastSyncAt ? ago(m.lastSyncAt) : 'Not yet' }}</span>@if (!m.lastError && m.lastSuccessAt) {<span class="o-chip o-grn">Up to date</span>}</div>
|
|
4096
|
+
@if (m.lastError) {<div class="o-row"><span class="o-mut" style="width:140px">Problem</span><span class="o-red" role="alert" data-testid="mirror-error">{{ m.lastError }}</span></div>}
|
|
4097
|
+
@if (m.lfsMissing) {<div class="o-row"><span class="o-mut" style="width:140px">Git LFS</span><span>{{ m.lfsMissing }} files the source could not supply stay pointers.</span></div>}
|
|
4098
|
+
<div class="o-cf">
|
|
4099
|
+
<button type="button" class="o-btn o-sm" (click)="syncNow()" [disabled]="!can('code.push') || m.syncing || busy()" data-testid="mirror-sync">Sync now</button>
|
|
4100
|
+
<span class="o-faint o-xs">Pushes to this repository are refused while it mirrors its source.</span>
|
|
4101
|
+
</div>
|
|
4102
|
+
</div>
|
|
4103
|
+
@if (canSettings()) {
|
|
4104
|
+
<form class="o-card o-cb o-form" (ngSubmit)="saveMirror()" aria-label="Mirror settings">
|
|
4105
|
+
<div class="o-fld"><label for="mi-url">Source URL</label><input id="mi-url" class="o-in o-mono" name="murl" [(ngModel)]="mf.url" /></div>
|
|
4106
|
+
<div class="o-g2">
|
|
4107
|
+
<div class="o-fld"><label for="mi-user">Username</label><input id="mi-user" class="o-in" name="muser" [(ngModel)]="mf.username" autocomplete="off" /></div>
|
|
4108
|
+
<div class="o-fld"><label for="mi-tok">New access token</label><input id="mi-tok" class="o-in" type="password" name="mtok" [(ngModel)]="mf.token" autocomplete="off" [placeholder]="m.hasCredentials ? 'A token is kept, encrypted' : 'None'" /></div>
|
|
4109
|
+
</div>
|
|
4110
|
+
<div class="o-fld"><label for="mi-int">Sync every (minutes)</label><input id="mi-int" class="o-in" type="number" min="1" name="mint" [(ngModel)]="mf.interval" style="width:140px" /></div>
|
|
4111
|
+
<div class="o-flex"><span class="o-faint o-sm">Saving makes the mirror sync as you.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy()" data-testid="mirror-save">Save</button></div>
|
|
4112
|
+
</form>
|
|
4113
|
+
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
4114
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>Stop mirroring</b><span class="o-hint">The repository keeps its history and takes pushes; the kept token is deleted.</span></div><button type="button" class="o-btn o-sm" (click)="stopMirror()" [disabled]="busy()" data-testid="mirror-stop">Stop mirroring</button></div>
|
|
4115
|
+
</div>
|
|
4116
|
+
}
|
|
4117
|
+
} @else {
|
|
4118
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
3750
4121
|
@case ('protect') {
|
|
3751
4122
|
<div class="o-flex"><h1 class="o-t">Branch protection</h1>@if (canProtect()) {<button type="button" class="o-btn o-sm o-r" (click)="edit(null)" data-testid="add-rule">+ Add rule</button>}</div>
|
|
3752
4123
|
<div class="o-card">
|
|
@@ -3769,7 +4140,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3769
4140
|
</div>
|
|
3770
4141
|
</div>
|
|
3771
4142
|
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass</span><div class="o-flex o-wrap">
|
|
3772
|
-
@for (b of bypassRoles; track b.id) {<button type="button" class="o-chip" style="cursor:pointer;border:none" [class.o-blu]="f.bypass.includes(b.id)" [attr.aria-pressed]="f.bypass.includes(b.id)" (click)="flipBypass(f, b.id)" [attr.data-testid]="'bypass-' + b.id">{{ b.label }}</button>}
|
|
4143
|
+
@for (b of bypassRoles(); track b.id) {<button type="button" class="o-chip" style="cursor:pointer;border:none" [class.o-blu]="f.bypass.includes(b.id)" [attr.aria-pressed]="f.bypass.includes(b.id)" (click)="flipBypass(f, b.id)" [attr.data-testid]="'bypass-' + b.id">{{ b.label }}</button>}
|
|
3773
4144
|
<span class="o-hint">Pushes by these roles go through this rule; every bypass is written to the audit log.</span></div></div>
|
|
3774
4145
|
<div class="o-fld"><span class="o-lbl" style="padding:0">What a blocked push sees</span><div class="o-term" style="font-size:11.5px"><div>$ git push origin {{ f.pattern || 'main' }}</div><div>remote: ✕ {{ f.pattern || 'main' }} is protected by rule “{{ f.pattern || 'main' }}”: direct pushes are blocked.</div><div>remote: Open a pull request instead.</div></div></div>
|
|
3775
4146
|
<div class="o-flex">
|
|
@@ -3784,7 +4155,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3784
4155
|
@case ('collab') {
|
|
3785
4156
|
<h1 class="o-t">Collaborators</h1>
|
|
3786
4157
|
<span class="o-mut">Access beyond the project role, for this repository only.</span>
|
|
3787
|
-
@if (
|
|
4158
|
+
@if (canCollaborators()) {
|
|
3788
4159
|
<div class="o-card" style="overflow-x:auto" data-testid="collaborators">
|
|
3789
4160
|
<div class="o-tbl" role="table" aria-label="Collaborators" style="min-width:560px">
|
|
3790
4161
|
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Person</div><div class="o-th" role="columnheader">Project role</div><div class="o-th" role="columnheader">Repository access</div><div class="o-th" role="columnheader"></div></div></div>
|
|
@@ -3815,10 +4186,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
3815
4186
|
@case ('danger') {
|
|
3816
4187
|
<h1 class="o-t">Danger zone</h1>
|
|
3817
4188
|
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
3818
|
-
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!
|
|
4189
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!canDelete() || busy()">{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</button></div>
|
|
3819
4190
|
<div class="o-row" style="padding:14px;flex-wrap:wrap"><div class="o-col" style="gap:2px;flex:1;min-width:240px"><b>Delete this repository</b><span class="o-hint">Recoverable for 7 days, then gone for good. Type <b class="o-mono">{{ r.name }}</b> to confirm.</span></div>
|
|
3820
|
-
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!
|
|
3821
|
-
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!
|
|
4191
|
+
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!canDelete()" />
|
|
4192
|
+
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!canDelete() || confirm() !== r.name || busy()">Delete</button>
|
|
3822
4193
|
</div>
|
|
3823
4194
|
</div>
|
|
3824
4195
|
}
|
|
@@ -3989,7 +4360,7 @@ class ReleasesPage extends RepoPage {
|
|
|
3989
4360
|
<div class="o-page o-narrow">
|
|
3990
4361
|
<div class="o-ph">
|
|
3991
4362
|
<div class="o-col"><span class="o-eyebrow">{{ name() }}</span><h1 class="o-t">Releases and tags</h1></div>
|
|
3992
|
-
@if (
|
|
4363
|
+
@if (canRelease()) {
|
|
3993
4364
|
<div class="o-acts">
|
|
3994
4365
|
<button type="button" class="o-btn o-sm" (click)="tagging.set(!tagging()); draft.set(null)" data-testid="tag-new">New tag</button>
|
|
3995
4366
|
<button type="button" class="o-btn o-sm o-dark" (click)="startRelease()" data-testid="release-new">Draft release</button>
|
|
@@ -4060,11 +4431,11 @@ class ReleasesPage extends RepoPage {
|
|
|
4060
4431
|
<span>Assets</span>
|
|
4061
4432
|
@for (a of r.assets; track a.id) {
|
|
4062
4433
|
<button type="button" class="o-lnk o-mono" (click)="download(r, a.id, a.name)" [attr.aria-label]="'Download ' + a.name">{{ a.name }}</button>
|
|
4063
|
-
@if (
|
|
4434
|
+
@if (canRelease()) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeAsset(r, a.id)" [attr.aria-label]="'Remove ' + a.name">×</button>}
|
|
4064
4435
|
}
|
|
4065
4436
|
<button type="button" class="o-lnk" (click)="archive(r, 'zip')">Source (zip)</button>
|
|
4066
4437
|
<button type="button" class="o-lnk" (click)="archive(r, 'tar.gz')">Source (tar.gz)</button>
|
|
4067
|
-
@if (
|
|
4438
|
+
@if (canRelease()) {
|
|
4068
4439
|
<span class="o-r o-flex" style="gap:10px">
|
|
4069
4440
|
<label class="o-lnk" style="cursor:pointer">Add file<input type="file" class="o-sr-only" (change)="upload(r, $event)" [attr.aria-label]="'Add a file to ' + r.tag" /></label>
|
|
4070
4441
|
<button type="button" class="o-lnk" (click)="edit(r)" [attr.aria-label]="'Edit ' + r.tag">Edit</button>
|
|
@@ -4079,7 +4450,7 @@ class ReleasesPage extends RepoPage {
|
|
|
4079
4450
|
</div>
|
|
4080
4451
|
</div>
|
|
4081
4452
|
} @empty {
|
|
4082
|
-
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (
|
|
4453
|
+
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (canRelease() ? ' Draft one from a tag; its notes can come from the issues it finishes.' : '') : 'Loading…' }}</div></div>
|
|
4083
4454
|
}
|
|
4084
4455
|
|
|
4085
4456
|
<div class="o-card" data-testid="tags">
|
|
@@ -4088,7 +4459,7 @@ class ReleasesPage extends RepoPage {
|
|
|
4088
4459
|
<div class="o-row" [attr.data-testid]="'tag-' + t.name">
|
|
4089
4460
|
<span class="o-mono o-sm o-b5">{{ t.name }}</span><span class="o-faint o-sm o-mono">{{ t.commit?.short }}</span>
|
|
4090
4461
|
<span class="o-r o-faint o-sm">{{ ago(t.commit?.committer?.when) }}</span>
|
|
4091
|
-
@if (
|
|
4462
|
+
@if (canRelease() && !used(t.name)) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeTag(t)" [attr.aria-label]="'Delete tag ' + t.name">Delete</button>}
|
|
4092
4463
|
</div>
|
|
4093
4464
|
} @empty {<div class="o-empty o-sm">No tags yet.</div>}
|
|
4094
4465
|
</div>
|
|
@@ -4101,7 +4472,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4101
4472
|
<div class="o-page o-narrow">
|
|
4102
4473
|
<div class="o-ph">
|
|
4103
4474
|
<div class="o-col"><span class="o-eyebrow">{{ name() }}</span><h1 class="o-t">Releases and tags</h1></div>
|
|
4104
|
-
@if (
|
|
4475
|
+
@if (canRelease()) {
|
|
4105
4476
|
<div class="o-acts">
|
|
4106
4477
|
<button type="button" class="o-btn o-sm" (click)="tagging.set(!tagging()); draft.set(null)" data-testid="tag-new">New tag</button>
|
|
4107
4478
|
<button type="button" class="o-btn o-sm o-dark" (click)="startRelease()" data-testid="release-new">Draft release</button>
|
|
@@ -4172,11 +4543,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4172
4543
|
<span>Assets</span>
|
|
4173
4544
|
@for (a of r.assets; track a.id) {
|
|
4174
4545
|
<button type="button" class="o-lnk o-mono" (click)="download(r, a.id, a.name)" [attr.aria-label]="'Download ' + a.name">{{ a.name }}</button>
|
|
4175
|
-
@if (
|
|
4546
|
+
@if (canRelease()) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeAsset(r, a.id)" [attr.aria-label]="'Remove ' + a.name">×</button>}
|
|
4176
4547
|
}
|
|
4177
4548
|
<button type="button" class="o-lnk" (click)="archive(r, 'zip')">Source (zip)</button>
|
|
4178
4549
|
<button type="button" class="o-lnk" (click)="archive(r, 'tar.gz')">Source (tar.gz)</button>
|
|
4179
|
-
@if (
|
|
4550
|
+
@if (canRelease()) {
|
|
4180
4551
|
<span class="o-r o-flex" style="gap:10px">
|
|
4181
4552
|
<label class="o-lnk" style="cursor:pointer">Add file<input type="file" class="o-sr-only" (change)="upload(r, $event)" [attr.aria-label]="'Add a file to ' + r.tag" /></label>
|
|
4182
4553
|
<button type="button" class="o-lnk" (click)="edit(r)" [attr.aria-label]="'Edit ' + r.tag">Edit</button>
|
|
@@ -4191,7 +4562,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4191
4562
|
</div>
|
|
4192
4563
|
</div>
|
|
4193
4564
|
} @empty {
|
|
4194
|
-
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (
|
|
4565
|
+
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (canRelease() ? ' Draft one from a tag; its notes can come from the issues it finishes.' : '') : 'Loading…' }}</div></div>
|
|
4195
4566
|
}
|
|
4196
4567
|
|
|
4197
4568
|
<div class="o-card" data-testid="tags">
|
|
@@ -4200,7 +4571,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4200
4571
|
<div class="o-row" [attr.data-testid]="'tag-' + t.name">
|
|
4201
4572
|
<span class="o-mono o-sm o-b5">{{ t.name }}</span><span class="o-faint o-sm o-mono">{{ t.commit?.short }}</span>
|
|
4202
4573
|
<span class="o-r o-faint o-sm">{{ ago(t.commit?.committer?.when) }}</span>
|
|
4203
|
-
@if (
|
|
4574
|
+
@if (canRelease() && !used(t.name)) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeTag(t)" [attr.aria-label]="'Delete tag ' + t.name">Delete</button>}
|
|
4204
4575
|
</div>
|
|
4205
4576
|
} @empty {<div class="o-empty o-sm">No tags yet.</div>}
|
|
4206
4577
|
</div>
|
|
@@ -4495,5 +4866,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
4495
4866
|
* Generated bundle index. Do not edit.
|
|
4496
4867
|
*/
|
|
4497
4868
|
|
|
4498
|
-
export { BranchesPage, CodeApi, CodeHost, CodeSearchPage, CommitBox, CommitPage, CommitsPage, CurrentRepo, CurrentRepoPage, DiffView, FilePage, NewFilePage, NewRepoPage, People, PullPage, PullsPage, RefPicker, ReleasesPage, RepoHomePage, RepoPage, RepoSettingsPage, ReposPage, TOKEN_CSS, ago, apiMessage, checkDot, copy, dayLabel, initials, langColour, matchesPattern, pair, shortDate, size, verifyWhy };
|
|
4869
|
+
export { BranchesPage, CodeApi, CodeHost, CodeSearchPage, CommitBox, CommitPage, CommitsPage, CurrentRepo, CurrentRepoPage, DiffView, FilePage, NewFilePage, NewRepoPage, People, PullPage, PullsPage, RefPicker, ReleasesPage, RepoHomePage, RepoPage, RepoSettingsPage, ReposPage, TOKEN_CSS, ago, apiMessage, checkDot, copy, dayLabel, initials, langColour, matchesPattern, pair, shortDate, size, verifyLabel, verifyTitle, verifyWhy };
|
|
4499
4870
|
//# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map
|