@chisquare-tech/origine-ngx-apps-code 0.8.0 → 0.10.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.
@@ -122,6 +122,13 @@ class CodeApi {
122
122
  p['q'] = q;
123
123
  return this.api.get(`${this.repoPath(project, name)}/refs`, p).pipe(map((r) => data(r) ?? []));
124
124
  }
125
+ /** Searches the repository's default branch (text, or a regular expression). */
126
+ searchCode(project, name, q, regex = false) {
127
+ const p = { q };
128
+ if (regex)
129
+ p['regex'] = 'true';
130
+ return this.api.get(`${this.repoPath(project, name)}/search`, p).pipe(map((r) => data(r) ?? { hits: [], repos: 0 }));
131
+ }
125
132
  collaborators(project, name) {
126
133
  return this.api.get(`${this.repoPath(project, name)}/collaborators`).pipe(map((r) => data(r) ?? []));
127
134
  }
@@ -349,6 +356,10 @@ const TOKEN_CSS = `
349
356
  :host ::ng-deep .tk-nb, :host ::ng-deep .tk-nt, :host ::ng-deep .tk-bp{color:var(--o-tk-p)}
350
357
  :host ::ng-deep .tk-gi{color:var(--o-add)} :host ::ng-deep .tk-gd{color:var(--o-bad-tx)}
351
358
  `;
359
+ /** Why a signed commit is not verified (DEV-31). */
360
+ function verifyWhy(reason) {
361
+ 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';
362
+ }
352
363
 
353
364
  /**
354
365
  * What every page under /code/:repo shares: the project, the repository named in the URL (loaded
@@ -366,6 +377,7 @@ class RepoPage {
366
377
  this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
367
378
  this.note = signal('', ...(ngDevMode ? [{ debugName: "note" }] : /* istanbul ignore next */ []));
368
379
  this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
380
+ this.verifyWhy = verifyWhy;
369
381
  this.canWrite = computed(() => !this.repo()?.archivedAt && (this.repo()?.capabilities ?? []).includes('code.write'), ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
370
382
  this.canAdmin = computed(() => (this.repo()?.capabilities ?? []).includes('resources.manage'), ...(ngDevMode ? [{ debugName: "canAdmin" }] : /* istanbul ignore next */ []));
371
383
  /** Repository settings short of a rename, the visibility and deletion: Maintainers, and maintain collaborators. */
@@ -1286,6 +1298,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1286
1298
  // /{org}/{project}/code/{repo}?ref=&path= (prototype "code_repo"): the repository's files at a
1287
1299
  // branch, tag or commit, the last commit of each entry, the README, the clone menu and languages.
1288
1300
  class RepoHomePage extends RepoPage {
1301
+ searchCode() {
1302
+ const q = this.codeQuery.trim();
1303
+ if (q)
1304
+ void this.router.navigate(this.link('search'), { queryParams: { q } });
1305
+ }
1289
1306
  constructor() {
1290
1307
  super();
1291
1308
  this.router = inject(Router);
@@ -1297,6 +1314,7 @@ class RepoHomePage extends RepoPage {
1297
1314
  this.proto = signal('https', ...(ngDevMode ? [{ debugName: "proto" }] : /* istanbul ignore next */ []));
1298
1315
  this.cloneOpen = signal(false, ...(ngDevMode ? [{ debugName: "cloneOpen" }] : /* istanbul ignore next */ []));
1299
1316
  this.addOpen = signal(false, ...(ngDevMode ? [{ debugName: "addOpen" }] : /* istanbul ignore next */ []));
1317
+ this.codeQuery = '';
1300
1318
  this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
1301
1319
  this.finding = signal(false, ...(ngDevMode ? [{ debugName: "finding" }] : /* istanbul ignore next */ []));
1302
1320
  this.fq = signal('', ...(ngDevMode ? [{ debugName: "fq" }] : /* istanbul ignore next */ []));
@@ -1424,6 +1442,7 @@ class RepoHomePage extends RepoPage {
1424
1442
  @for (c of crumbs(); track c.path) {<span class="o-faint">/</span><a [routerLink]="link()" [queryParams]="qp(c.path)">{{ c.name }}</a>}
1425
1443
  </nav>
1426
1444
  <span class="o-r"></span>
1445
+ <form class="o-flex" (ngSubmit)="searchCode()" role="search"><input class="o-in o-sm o-mono" style="width:180px" name="cq" [(ngModel)]="codeQuery" placeholder="Search code" aria-label="Search code" data-testid="repo-code-search" /></form>
1427
1446
  <button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
1428
1447
  @if (canWrite()) {
1429
1448
  <span class="anchor add">
@@ -1513,7 +1532,7 @@ class RepoHomePage extends RepoPage {
1513
1532
  </div>
1514
1533
  </div>
1515
1534
  }
1516
- `, isInline: true, styles: [".anchor{position:relative}.ic{width:18px;flex:none;color:var(--o-dim);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1535
+ `, isInline: true, styles: [".anchor{position:relative}.ic{width:18px;flex:none;color:var(--o-dim);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.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"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1517
1536
  }
1518
1537
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoHomePage, decorators: [{
1519
1538
  type: Component,
@@ -1555,6 +1574,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1555
1574
  @for (c of crumbs(); track c.path) {<span class="o-faint">/</span><a [routerLink]="link()" [queryParams]="qp(c.path)">{{ c.name }}</a>}
1556
1575
  </nav>
1557
1576
  <span class="o-r"></span>
1577
+ <form class="o-flex" (ngSubmit)="searchCode()" role="search"><input class="o-in o-sm o-mono" style="width:180px" name="cq" [(ngModel)]="codeQuery" placeholder="Search code" aria-label="Search code" data-testid="repo-code-search" /></form>
1558
1578
  <button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
1559
1579
  @if (canWrite()) {
1560
1580
  <span class="anchor add">
@@ -1687,7 +1707,8 @@ class CommitBox {
1687
1707
  this.toNew = signal(false, ...(ngDevMode ? [{ debugName: "toNew" }] : /* istanbul ignore next */ []));
1688
1708
  this.newBranch = signal('', ...(ngDevMode ? [{ debugName: "newBranch" }] : /* istanbul ignore next */ []));
1689
1709
  this.rules = signal([], ...(ngDevMode ? [{ debugName: "rules" }] : /* istanbul ignore next */ []));
1690
- this.blocked = computed(() => this.rules().some((r) => r.blockDirectPush && matchesPattern(r.pattern, this.branch())), ...(ngDevMode ? [{ debugName: "blocked" }] : /* istanbul ignore next */ []));
1710
+ /** Direct commits are refused: the rules block direct pushes, or require signed commits (a browser commit is unsigned). */
1711
+ this.blocked = computed(() => this.rules().some((r) => (r.blockDirectPush || r.requireSignedCommits) && matchesPattern(r.pattern, this.branch())), ...(ngDevMode ? [{ debugName: "blocked" }] : /* istanbul ignore next */ []));
1691
1712
  effect(() => {
1692
1713
  const m = this.defaultMessage();
1693
1714
  untracked(() => this.message.set(m));
@@ -2139,6 +2160,7 @@ class CommitsPage extends RepoPage {
2139
2160
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2140
2161
  </span>
2141
2162
  </div>
2163
+ @if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" [attr.data-testid]="'verify-' + c.short">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}
2142
2164
  <a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
2143
2165
  <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>
2144
2166
  </div>
@@ -2179,6 +2201,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2179
2201
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2180
2202
  </span>
2181
2203
  </div>
2204
+ @if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" [attr.data-testid]="'verify-' + c.short">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}
2182
2205
  <a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
2183
2206
  <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>
2184
2207
  </div>
@@ -2228,6 +2251,7 @@ class CommitPage extends RepoPage {
2228
2251
  @if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
2229
2252
  <div class="o-flex o-wrap o-sm">
2230
2253
  <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>
2254
+ @if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" data-testid="commit-verified">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}
2231
2255
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2232
2256
  <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>
2233
2257
  </div>
@@ -2259,6 +2283,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2259
2283
  @if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
2260
2284
  <div class="o-flex o-wrap o-sm">
2261
2285
  <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>
2286
+ @if (c.verification; as v) {<span class="o-chip" [class.o-grn]="v.verified" [attr.title]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" data-testid="commit-verified">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}
2262
2287
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2263
2288
  <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>
2264
2289
  </div>
@@ -3057,7 +3082,7 @@ class PullPage extends RepoPage {
3057
3082
  @case ('commits') {
3058
3083
  <div class="o-card">
3059
3084
  @for (c of cmp()?.commits ?? []; track c.oid) {
3060
- <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><a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
3085
+ <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]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" [attr.data-testid]="'verify-' + c.short">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}<a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
3061
3086
  } @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
3062
3087
  </div>
3063
3088
  }
@@ -3240,7 +3265,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3240
3265
  @case ('commits') {
3241
3266
  <div class="o-card">
3242
3267
  @for (c of cmp()?.commits ?? []; track c.oid) {
3243
- <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><a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
3268
+ <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]="v.verified ? 'Signed with a key of ' + people().name(v.signer) : verifyWhy(v.reason)" [attr.data-testid]="'verify-' + c.short">{{ v.verified ? 'Verified' : 'Unverified' }}</span>}<a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
3244
3269
  } @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
3245
3270
  </div>
3246
3271
  }
@@ -3329,7 +3354,7 @@ function writePref(key, value) {
3329
3354
  }
3330
3355
  }
3331
3356
 
3332
- const BLANK = { id: '', pattern: '', blockDirectPush: true, blockForcePush: true, blockDeletion: true, requiredApprovals: 1, dismissStaleApprovals: true, requireUpToDate: false, requireCodeOwnerReview: false, requiredChecks: [] };
3357
+ const BLANK = { id: '', pattern: '', blockDirectPush: true, blockForcePush: true, blockDeletion: true, requiredApprovals: 1, dismissStaleApprovals: true, requireUpToDate: false, requireCodeOwnerReview: false, requireSignedCommits: false, requireLinearHistory: false, bypass: [], requiredChecks: [] };
3333
3358
  // /{org}/{project}/code/{repo}/settings?section= (prototype "code_settings"): general settings and
3334
3359
  // merge methods, branch protection rules, and the danger zone (archive, delete).
3335
3360
  class RepoSettingsPage extends RepoPage {
@@ -3352,7 +3377,10 @@ class RepoSettingsPage extends RepoPage {
3352
3377
  { key: 'dismissStaleApprovals', label: 'Dismiss stale approvals', hint: 'A new commit dismisses approvals given on an older one.' },
3353
3378
  { key: 'requireUpToDate', label: 'Require branches to be up to date', hint: 'The branch must contain the target’s latest commit before it merges.' },
3354
3379
  { key: 'requireCodeOwnerReview', label: 'Require review from CODEOWNERS', hint: 'Owners of changed paths are requested automatically; one of them approves each owned file.' },
3380
+ { key: 'requireSignedCommits', label: 'Require signed commits', hint: 'Verified against members’ signing keys, on push and at merge.' },
3381
+ { key: 'requireLinearHistory', label: 'Require linear history', hint: 'No merge commits: squash or rebase only.' },
3355
3382
  ];
3383
+ this.bypassRoles = [{ id: 'owner', label: 'Owners' }, { id: 'admin', label: 'Admins' }, { id: 'maintainer', label: 'Maintainers' }];
3356
3384
  this.collabs = signal([], ...(ngDevMode ? [{ debugName: "collabs" }] : /* istanbul ignore next */ []));
3357
3385
  this.perms = [{ id: 'read', label: 'Read' }, { id: 'write', label: 'Write' }, { id: 'maintain', label: 'Maintain' }];
3358
3386
  this.addable = computed(() => this.host.people().filter((p) => !this.collabs().some((c) => c.userId === p.id)), ...(ngDevMode ? [{ debugName: "addable" }] : /* istanbul ignore next */ []));
@@ -3419,12 +3447,19 @@ class RepoSettingsPage extends RepoPage {
3419
3447
  parts.push('up to date');
3420
3448
  if (p.requireCodeOwnerReview)
3421
3449
  parts.push('code owners');
3450
+ if (p.requireSignedCommits)
3451
+ parts.push('signed commits');
3452
+ if (p.requireLinearHistory)
3453
+ parts.push('linear');
3422
3454
  if (p.blockDeletion)
3423
3455
  parts.push('no deletion');
3424
3456
  return parts.join(' · ') || 'no restrictions';
3425
3457
  }
3426
3458
  edit(p) {
3427
- this.form.set(p ? { ...BLANK, ...p, requiredChecks: [...(p.requiredChecks ?? [])] } : { ...BLANK, pattern: this.rules().length ? '' : this.repo()?.defaultBranch ?? 'main', requiredChecks: [] });
3459
+ this.form.set(p ? { ...BLANK, ...p, requiredChecks: [...(p.requiredChecks ?? [])], bypass: [...(p.bypass ?? [])] } : { ...BLANK, pattern: this.rules().length ? '' : this.repo()?.defaultBranch ?? 'main', requiredChecks: [] });
3460
+ }
3461
+ flipBypass(f, role) {
3462
+ f.bypass = f.bypass.includes(role) ? f.bypass.filter((x) => x !== role) : [...f.bypass, role];
3428
3463
  }
3429
3464
  flip(f, k) {
3430
3465
  f[k] = !f[k];
@@ -3567,7 +3602,9 @@ class RepoSettingsPage extends RepoPage {
3567
3602
  <input id="rule-check" class="o-in o-sm o-mono" style="width:160px" placeholder="+ check name, Enter" [ngModel]="check()" (ngModelChange)="check.set($event)" name="check" (keydown.enter)="$event.preventDefault(); addCheck(f)" />
3568
3603
  </div>
3569
3604
  </div>
3570
- <div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass <b class="o-ph-tag o-ga">GA</b></span><div class="o-flex"><span class="o-chip">Owners</span><span class="o-hint">Every bypass is written to the audit log.</span></div></div>
3605
+ <div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass</span><div class="o-flex o-wrap">
3606
+ @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>}
3607
+ <span class="o-hint">Pushes by these roles go through this rule; every bypass is written to the audit log.</span></div></div>
3571
3608
  <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>
3572
3609
  <div class="o-flex">
3573
3610
  @if (f.id) {<button type="button" class="o-btn o-sm o-ghost" style="color:var(--o-bad-tx)" (click)="removeRule(f)">Delete rule</button>}
@@ -3693,7 +3730,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3693
3730
  <input id="rule-check" class="o-in o-sm o-mono" style="width:160px" placeholder="+ check name, Enter" [ngModel]="check()" (ngModelChange)="check.set($event)" name="check" (keydown.enter)="$event.preventDefault(); addCheck(f)" />
3694
3731
  </div>
3695
3732
  </div>
3696
- <div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass <b class="o-ph-tag o-ga">GA</b></span><div class="o-flex"><span class="o-chip">Owners</span><span class="o-hint">Every bypass is written to the audit log.</span></div></div>
3733
+ <div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass</span><div class="o-flex o-wrap">
3734
+ @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>}
3735
+ <span class="o-hint">Pushes by these roles go through this rule; every bypass is written to the audit log.</span></div></div>
3697
3736
  <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>
3698
3737
  <div class="o-flex">
3699
3738
  @if (f.id) {<button type="button" class="o-btn o-sm o-ghost" style="color:var(--o-bad-tx)" (click)="removeRule(f)">Delete rule</button>}
@@ -4299,6 +4338,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4299
4338
  }]
4300
4339
  }], ctorParameters: () => [] });
4301
4340
 
4341
+ // /{org}/{project}/code/{repo}/search?q=&regex= (DEV-11): code search over the repository's default
4342
+ // branch, text or a regular expression; each file with its matching lines, the matches marked, a
4343
+ // line opening the file there.
4344
+ class CodeSearchPage extends RepoPage {
4345
+ constructor() {
4346
+ super();
4347
+ this.router = inject(Router);
4348
+ this.hits = signal([], ...(ngDevMode ? [{ debugName: "hits" }] : /* istanbul ignore next */ []));
4349
+ this.searched = signal(false, ...(ngDevMode ? [{ debugName: "searched" }] : /* istanbul ignore next */ []));
4350
+ this.text = '';
4351
+ this.regex = false;
4352
+ this.route.queryParamMap.subscribe((q) => {
4353
+ this.text = q.get('q') ?? '';
4354
+ this.regex = q.get('regex') === 'true';
4355
+ const r = this.repo();
4356
+ if (r && r.name === this.name())
4357
+ this.load(r);
4358
+ });
4359
+ }
4360
+ onRepo(r) {
4361
+ this.load(r);
4362
+ }
4363
+ go() {
4364
+ void this.router.navigate(this.link('search'), { queryParams: { q: this.text.trim(), regex: this.regex ? 'true' : null } });
4365
+ }
4366
+ load(r) {
4367
+ const q = this.text.trim();
4368
+ if (!q)
4369
+ return;
4370
+ this.error.set('');
4371
+ this.api.searchCode(this.projectId(), r.name, q, this.regex).subscribe({
4372
+ next: (res) => { this.hits.set(res.hits); this.searched.set(true); },
4373
+ error: (e) => { this.hits.set([]); this.searched.set(false); this.error.set(apiMessage(e, 'Could not search the code.')); },
4374
+ });
4375
+ }
4376
+ /** A line cut into plain and matched pieces (ranges are byte offsets; the text is UTF-8). */
4377
+ pieces(text, ranges) {
4378
+ if (!ranges?.length)
4379
+ return [{ text, hit: false }];
4380
+ const bytes = new TextEncoder().encode(text);
4381
+ const dec = new TextDecoder();
4382
+ const out = [];
4383
+ let at = 0;
4384
+ for (const [a, b] of [...ranges].sort((x, y) => x[0] - y[0])) {
4385
+ if (a > at)
4386
+ out.push({ text: dec.decode(bytes.slice(at, a)), hit: false });
4387
+ out.push({ text: dec.decode(bytes.slice(Math.max(a, at), b)), hit: true });
4388
+ at = Math.max(at, b);
4389
+ }
4390
+ if (at < bytes.length)
4391
+ out.push({ text: dec.decode(bytes.slice(at)), hit: false });
4392
+ return out;
4393
+ }
4394
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CodeSearchPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4395
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: CodeSearchPage, isStandalone: true, selector: "o-code-search", usesInheritance: true, ngImport: i0, template: `
4396
+ <div class="o-page o-wide">
4397
+ @if (repo(); as r) {
4398
+ <form class="o-flex o-wrap" (ngSubmit)="go()" role="search" aria-label="Search code">
4399
+ <input class="o-in o-mono" style="flex:1;min-width:220px" name="q" [(ngModel)]="text" placeholder="Search {{ r.name }} (default branch)" aria-label="Search code" data-testid="code-search-q" />
4400
+ <label class="o-flex o-sm"><input type="checkbox" name="regex" [(ngModel)]="regex" data-testid="code-search-regex" /> Regular expression</label>
4401
+ <button type="submit" class="o-btn o-dark" [disabled]="!text.trim()">Search</button>
4402
+ </form>
4403
+ @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
4404
+ @if (searched()) {
4405
+ <span class="o-faint o-sm" aria-live="polite" data-testid="code-search-count">{{ hits().length }} {{ hits().length === 1 ? 'file' : 'files' }}{{ hits().length >= 50 ? ' (the first 50)' : '' }} on {{ r.defaultBranch }}</span>
4406
+ @for (h of hits(); track h.path) {
4407
+ <div class="o-card" [attr.data-testid]="'code-hit-' + h.path">
4408
+ <div class="o-ch" style="background:var(--o-well)"><a class="o-mono o-sm" [routerLink]="link('blob')" [queryParams]="{ path: h.path }">{{ h.path }}</a>@if (h.nameMatch && !h.lines.length) {<span class="o-faint o-sm">matches the file name</span>}</div>
4409
+ @for (l of h.lines; track l.number) {
4410
+ <a class="ln" [routerLink]="link('blob')" [queryParams]="{ path: h.path }" [fragment]="'L' + l.number"><span class="n">{{ l.number }}</span><span>@for (p of pieces(l.text, l.ranges); track $index) {@if (p.hit) {<mark>{{ p.text }}</mark>} @else {{{ p.text }}}}</span></a>
4411
+ }
4412
+ </div>
4413
+ } @empty {
4414
+ <div class="o-card"><div class="o-empty">Nothing matches on {{ r.defaultBranch }}.</div></div>
4415
+ }
4416
+ }
4417
+ }
4418
+ </div>
4419
+ `, isInline: true, styles: [".ln{display:flex;gap:10px;font-family:var(--o-mono);font-size:12px;padding:2px 12px;color:var(--o-tx);text-decoration:none;white-space:pre}.ln:hover{background:var(--o-well)}.ln .n{color:var(--o-faint);min-width:40px;text-align:right}mark{background:#fee484;color:inherit;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { 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 }); }
4420
+ }
4421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CodeSearchPage, decorators: [{
4422
+ type: Component,
4423
+ args: [{ selector: 'o-code-search', imports: [FormsModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4424
+ <div class="o-page o-wide">
4425
+ @if (repo(); as r) {
4426
+ <form class="o-flex o-wrap" (ngSubmit)="go()" role="search" aria-label="Search code">
4427
+ <input class="o-in o-mono" style="flex:1;min-width:220px" name="q" [(ngModel)]="text" placeholder="Search {{ r.name }} (default branch)" aria-label="Search code" data-testid="code-search-q" />
4428
+ <label class="o-flex o-sm"><input type="checkbox" name="regex" [(ngModel)]="regex" data-testid="code-search-regex" /> Regular expression</label>
4429
+ <button type="submit" class="o-btn o-dark" [disabled]="!text.trim()">Search</button>
4430
+ </form>
4431
+ @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
4432
+ @if (searched()) {
4433
+ <span class="o-faint o-sm" aria-live="polite" data-testid="code-search-count">{{ hits().length }} {{ hits().length === 1 ? 'file' : 'files' }}{{ hits().length >= 50 ? ' (the first 50)' : '' }} on {{ r.defaultBranch }}</span>
4434
+ @for (h of hits(); track h.path) {
4435
+ <div class="o-card" [attr.data-testid]="'code-hit-' + h.path">
4436
+ <div class="o-ch" style="background:var(--o-well)"><a class="o-mono o-sm" [routerLink]="link('blob')" [queryParams]="{ path: h.path }">{{ h.path }}</a>@if (h.nameMatch && !h.lines.length) {<span class="o-faint o-sm">matches the file name</span>}</div>
4437
+ @for (l of h.lines; track l.number) {
4438
+ <a class="ln" [routerLink]="link('blob')" [queryParams]="{ path: h.path }" [fragment]="'L' + l.number"><span class="n">{{ l.number }}</span><span>@for (p of pieces(l.text, l.ranges); track $index) {@if (p.hit) {<mark>{{ p.text }}</mark>} @else {{{ p.text }}}}</span></a>
4439
+ }
4440
+ </div>
4441
+ } @empty {
4442
+ <div class="o-card"><div class="o-empty">Nothing matches on {{ r.defaultBranch }}.</div></div>
4443
+ }
4444
+ }
4445
+ }
4446
+ </div>
4447
+ `, styles: [".ln{display:flex;gap:10px;font-family:var(--o-mono);font-size:12px;padding:2px 12px;color:var(--o-tx);text-decoration:none;white-space:pre}.ln:hover{background:var(--o-well)}.ln .n{color:var(--o-faint);min-width:40px;text-align:right}mark{background:#fee484;color:inherit;border-radius:2px}\n"] }]
4448
+ }], ctorParameters: () => [] });
4449
+
4302
4450
  /*
4303
4451
  * @chisquare-tech/origine-ngx-apps-code: the Code module's screens (TRD §6.10). The host app provides
4304
4452
  * CodeHost and mounts the pages at /{org}/{project}/code, /code/new, /code/pulls and
@@ -4309,5 +4457,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4309
4457
  * Generated bundle index. Do not edit.
4310
4458
  */
4311
4459
 
4312
- export { BranchesPage, CodeApi, CodeHost, 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 };
4460
+ 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 };
4313
4461
  //# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map