@chisquare-tech/origine-ngx-apps-code 0.12.0 → 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.
@@ -50,6 +50,21 @@ class CodeApi {
50
50
  import(project, body) {
51
51
  return this.api.post(`${C}/project/${enc(project)}/repo/import`, body).pipe(map((r) => data(r)));
52
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
+ }
53
68
  repo(project, name) {
54
69
  return this.api.get(this.repoPath(project, name)).pipe(map((r) => data(r)));
55
70
  }
@@ -223,6 +238,14 @@ class CodeApi {
223
238
  merge(project, name, n, body) {
224
239
  return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/merge`, body).pipe(map((r) => data(r)));
225
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
+ }
226
249
  conversation(project, name, n) {
227
250
  return this.api.get(`${this.repoPath(project, name)}/pulls/${n}/conversation`).pipe(map((r) => data(r) ?? { comments: [], reviews: [], threads: [] }));
228
251
  }
@@ -368,6 +391,18 @@ const TOKEN_CSS = `
368
391
  :host ::ng-deep .tk-nb, :host ::ng-deep .tk-nt, :host ::ng-deep .tk-bp{color:var(--o-tk-p)}
369
392
  :host ::ng-deep .tk-gi{color:var(--o-add)} :host ::ng-deep .tk-gd{color:var(--o-bad-tx)}
370
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
+ }
371
406
  /** Why a signed commit is not verified (DEV-31). */
372
407
  function verifyWhy(reason) {
373
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';
@@ -379,6 +414,9 @@ function verifyWhy(reason) {
379
414
  * their own data in onRepo().
380
415
  */
381
416
  class RepoPage {
417
+ verifyTitle(v) {
418
+ return verifyTitle(v, (id) => this.people().name(id));
419
+ }
382
420
  /** Whether the caller holds code capability id on this repository (V4-01): its caps, a collaborator row included. */
383
421
  can(id) {
384
422
  return (this.repo()?.caps ?? []).includes(id);
@@ -394,7 +432,9 @@ class RepoPage {
394
432
  this.note = signal('', ...(ngDevMode ? [{ debugName: "note" }] : /* istanbul ignore next */ []));
395
433
  this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
396
434
  this.verifyWhy = verifyWhy;
397
- this.live = computed(() => !!this.repo() && !this.repo()?.archivedAt, ...(ngDevMode ? [{ debugName: "live" }] : /* istanbul ignore next */ []));
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 */ []));
398
438
  this.canWrite = computed(() => this.live() && this.can('code.push'), ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
399
439
  /** Every repository setting, a rename and the visibility included. */
400
440
  this.canSettings = computed(() => this.can('code.repo.settings'), ...(ngDevMode ? [{ debugName: "canSettings" }] : /* istanbul ignore next */ []));
@@ -1095,6 +1135,8 @@ class NewRepoPage {
1095
1135
  this.url_ = '';
1096
1136
  this.user = '';
1097
1137
  this.token = '';
1138
+ this.mirror = false;
1139
+ this.interval = 60;
1098
1140
  this.route.queryParamMap.subscribe((q) => this.mode.set(q.get('import') ? 'import' : 'create'));
1099
1141
  }
1100
1142
  nameFromUrl() {
@@ -1124,7 +1166,7 @@ class NewRepoPage {
1124
1166
  return;
1125
1167
  this.busy.set(true);
1126
1168
  this.error.set('');
1127
- 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({
1128
1170
  next: (r) => { this.busy.set(false); this.token = ''; this.made.set(r); },
1129
1171
  error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not start the import.')); },
1130
1172
  });
@@ -1182,11 +1224,15 @@ class NewRepoPage {
1182
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>
1183
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>
1184
1226
  </div>
1185
- <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()">Import</button></div>
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>
1186
1232
  </form>
1187
1233
  }
1188
1234
  </div>
1189
- `, 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 }); }
1190
1236
  }
1191
1237
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewRepoPage, decorators: [{
1192
1238
  type: Component,
@@ -1246,7 +1292,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1246
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>
1247
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>
1248
1294
  </div>
1249
- <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()">Import</button></div>
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>
1250
1300
  </form>
1251
1301
  }
1252
1302
  </div>
@@ -1431,8 +1481,9 @@ class RepoHomePage extends RepoPage {
1431
1481
  <div class="o-ph">
1432
1482
  <div class="o-col">
1433
1483
  <span class="o-eyebrow">{{ r.projectKey }}</span>
1434
- <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>
1435
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>}
1436
1487
  </div>
1437
1488
  @if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
1438
1489
  </div>
@@ -1563,8 +1614,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1563
1614
  <div class="o-ph">
1564
1615
  <div class="o-col">
1565
1616
  <span class="o-eyebrow">{{ r.projectKey }}</span>
1566
- <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>
1567
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>}
1568
1620
  </div>
1569
1621
  @if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
1570
1622
  </div>
@@ -2267,7 +2319,7 @@ class CommitsPage extends RepoPage {
2267
2319
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2268
2320
  </span>
2269
2321
  </div>
2270
- @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>}
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>}
2271
2323
  <a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
2272
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>
2273
2325
  </div>
@@ -2308,7 +2360,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2308
2360
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2309
2361
  </span>
2310
2362
  </div>
2311
- @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>}
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>}
2312
2364
  <a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
2313
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>
2314
2366
  </div>
@@ -2358,7 +2410,7 @@ class CommitPage extends RepoPage {
2358
2410
  @if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
2359
2411
  <div class="o-flex o-wrap o-sm">
2360
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>
2361
- @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>}
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>}
2362
2414
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2363
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>
2364
2416
  </div>
@@ -2390,7 +2442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2390
2442
  @if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
2391
2443
  <div class="o-flex o-wrap o-sm">
2392
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>
2393
- @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>}
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>}
2394
2446
  @for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
2395
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>
2396
2448
  </div>
@@ -2859,6 +2911,13 @@ class PullPage extends RepoPage {
2859
2911
  this.method = signal('merge', ...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
2860
2912
  this.delBranch = signal(false, ...(ngDevMode ? [{ debugName: "delBranch" }] : /* istanbul ignore next */ []));
2861
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 */ []));
2862
2921
  this.ago = (s) => ago(s);
2863
2922
  this.initials = initials;
2864
2923
  this.dot = checkDot;
@@ -3028,6 +3087,47 @@ class PullPage extends RepoPage {
3028
3087
  return;
3029
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}.`));
3030
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
+ }
3031
3131
  close(reopen) {
3032
3132
  this.run(this.api.closePull(this.projectId(), this.name(), this.number, reopen), reopen ? 'Could not reopen.' : 'Could not close.');
3033
3133
  }
@@ -3173,6 +3273,9 @@ class PullPage extends RepoPage {
3173
3273
  </div>
3174
3274
  }
3175
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
+ }
3176
3279
  } @else if (p.state === 'closed' && p.canEdit) {
3177
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>
3178
3281
  } @else if (p.state === 'merged') {
@@ -3180,6 +3283,42 @@ class PullPage extends RepoPage {
3180
3283
  }
3181
3284
  </div>
3182
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 &amp;&amp; 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 (&lt;&lt;&lt;&lt;&lt;&lt;&lt;, =======, &gt;&gt;&gt;&gt;&gt;&gt;&gt;) 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
+
3183
3322
  <div class="o-card">
3184
3323
  <label class="o-sr-only" for="pr-cmt">Add a comment</label>
3185
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>
@@ -3189,7 +3328,7 @@ class PullPage extends RepoPage {
3189
3328
  @case ('commits') {
3190
3329
  <div class="o-card">
3191
3330
  @for (c of cmp()?.commits ?? []; track c.oid) {
3192
- <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>
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>
3193
3332
  } @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
3194
3333
  </div>
3195
3334
  }
@@ -3356,6 +3495,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3356
3495
  </div>
3357
3496
  }
3358
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
+ }
3359
3501
  } @else if (p.state === 'closed' && p.canEdit) {
3360
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>
3361
3503
  } @else if (p.state === 'merged') {
@@ -3363,6 +3505,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3363
3505
  }
3364
3506
  </div>
3365
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 &amp;&amp; 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 (&lt;&lt;&lt;&lt;&lt;&lt;&lt;, =======, &gt;&gt;&gt;&gt;&gt;&gt;&gt;) 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
+
3366
3544
  <div class="o-card">
3367
3545
  <label class="o-sr-only" for="pr-cmt">Add a comment</label>
3368
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>
@@ -3372,7 +3550,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3372
3550
  @case ('commits') {
3373
3551
  <div class="o-card">
3374
3552
  @for (c of cmp()?.commits ?? []; track c.oid) {
3375
- <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>
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>
3376
3554
  } @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
3377
3555
  </div>
3378
3556
  }
@@ -3490,6 +3668,9 @@ class RepoSettingsPage extends RepoPage {
3490
3668
  /** Who may bypass a rule: the built-in Owner, Admin and Maintainer, and the organization's custom roles. */
3491
3669
  this.customRoles = signal([], ...(ngDevMode ? [{ debugName: "customRoles" }] : /* istanbul ignore next */ []));
3492
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 };
3493
3674
  this.collabs = signal([], ...(ngDevMode ? [{ debugName: "collabs" }] : /* istanbul ignore next */ []));
3494
3675
  this.perms = [{ id: 'read', label: 'Read' }, { id: 'write', label: 'Write' }, { id: 'maintain', label: 'Maintain' }];
3495
3676
  this.addable = computed(() => this.host.people().filter((p) => !this.collabs().some((c) => c.userId === p.id)), ...(ngDevMode ? [{ debugName: "addable" }] : /* istanbul ignore next */ []));
@@ -3497,6 +3678,57 @@ class RepoSettingsPage extends RepoPage {
3497
3678
  this.newPerm = 'write';
3498
3679
  this.route.queryParamMap.subscribe((q) => this.section.set(q.get('section') || 'general'));
3499
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
+ }
3500
3732
  roleLabel(r) {
3501
3733
  return r ? r[0].toUpperCase() + r.slice(1) : 'Not in the project';
3502
3734
  }
@@ -3530,6 +3762,7 @@ class RepoSettingsPage extends RepoPage {
3530
3762
  }
3531
3763
  onRepo(r) {
3532
3764
  this.loadCollabs(r);
3765
+ this.loadMirror(r);
3533
3766
  this.g = { name: r.name, description: r.description, visibility: r.visibility, defaultBranch: r.defaultBranch, mergeMethods: [...(r.mergeMethods ?? [])], deleteBranchOnMerge: r.deleteBranchOnMerge, allowAutoMerge: !!r.allowAutoMerge };
3534
3767
  if (!r.empty)
3535
3768
  this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.branches.set(b), error: () => undefined });
@@ -3662,6 +3895,7 @@ class RepoSettingsPage extends RepoPage {
3662
3895
  <nav class="o-setnav" aria-label="Repository settings">
3663
3896
  <span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
3664
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>}
3665
3899
  <button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
3666
3900
  <button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators</button>
3667
3901
  <button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
@@ -3692,6 +3926,38 @@ class RepoSettingsPage extends RepoPage {
3692
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>}
3693
3927
  </form>
3694
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
+ }
3695
3961
  @case ('protect') {
3696
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>
3697
3963
  <div class="o-card">
@@ -3774,7 +4040,7 @@ class RepoSettingsPage extends RepoPage {
3774
4040
  <div class="o-card o-empty"><p>{{ error() }}</p></div>
3775
4041
  }
3776
4042
  </div>
3777
- `, 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 }); }
3778
4044
  }
3779
4045
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoSettingsPage, decorators: [{
3780
4046
  type: Component,
@@ -3789,6 +4055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3789
4055
  <nav class="o-setnav" aria-label="Repository settings">
3790
4056
  <span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
3791
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>}
3792
4059
  <button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
3793
4060
  <button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators</button>
3794
4061
  <button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
@@ -3819,6 +4086,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3819
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>}
3820
4087
  </form>
3821
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
+ }
3822
4121
  @case ('protect') {
3823
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>
3824
4123
  <div class="o-card">
@@ -4567,5 +4866,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4567
4866
  * Generated bundle index. Do not edit.
4568
4867
  */
4569
4868
 
4570
- 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 };
4571
4870
  //# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map