@chisquare-tech/origine-ngx-apps-code 0.6.0 → 0.8.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.
@@ -210,6 +210,15 @@ class CodeApi {
210
210
  comment(project, name, n, c) {
211
211
  return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/comments`, c).pipe(map((r) => data(r)));
212
212
  }
213
+ /** Arms (method) or disarms (null) auto-merge on a pull request (DEV-25). */
214
+ autoMerge(project, name, n, method) {
215
+ const url = `${this.repoPath(project, name)}/pulls/${n}/auto-merge`;
216
+ return (method === null ? this.api.delete(url) : this.api.post(url, { method })).pipe(map((r) => data(r)));
217
+ }
218
+ /** Commits suggested changes of line comments onto the pull request's branch (DEV-20). */
219
+ applySuggestions(project, name, n, comments, message = '') {
220
+ return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/suggestions`, { comments, message }).pipe(map((r) => data(r)));
221
+ }
213
222
  /** Discards one of the caller's pending review comments. */
214
223
  discardComment(project, name, n, id) {
215
224
  return this.api.delete(`${this.repoPath(project, name)}/pulls/${n}/comments/${enc(id)}`);
@@ -507,6 +516,12 @@ class DiffView {
507
516
  this.reply = output();
508
517
  this.resolve = output();
509
518
  this.discard = output();
519
+ /** The caller can push to the pull request's branch, so may apply suggestions. */
520
+ this.canApply = input(false, ...(ngDevMode ? [{ debugName: "canApply" }] : /* istanbul ignore next */ []));
521
+ /** Suggestions batched to apply together. */
522
+ this.batch = input([], ...(ngDevMode ? [{ debugName: "batch" }] : /* istanbul ignore next */ []));
523
+ this.apply = output();
524
+ this.batchToggle = output();
510
525
  this.viewedChange = output();
511
526
  this.open = signal(true, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
512
527
  this.visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
@@ -614,6 +629,18 @@ class DiffView {
614
629
  this.drafting.set('');
615
630
  this.draftBody.set('');
616
631
  }
632
+ /** A comment's ```suggestion block, split into the text before it and the suggested lines. */
633
+ suggestionOf(body) {
634
+ const m = /```suggestion[^\n]*\n([\s\S]*?)```/.exec(body);
635
+ if (!m)
636
+ return null;
637
+ const text = m[1].replace(/\n$/, '');
638
+ return { before: body.slice(0, m.index).trim(), lines: text === '' ? [] : text.split('\n') };
639
+ }
640
+ insertSuggestion(l) {
641
+ const cur = this.draftBody();
642
+ this.draftBody.set((cur ? cur.trimEnd() + '\n' : '') + '```suggestion\n' + l.text + '\n```');
643
+ }
617
644
  setReply(id, v) {
618
645
  this.replies.set({ ...this.replies(), [id]: v });
619
646
  }
@@ -625,7 +652,7 @@ class DiffView {
625
652
  this.setReply(id, '');
626
653
  }
627
654
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DiffView, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
628
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: DiffView, isStandalone: true, selector: "o-diff", inputs: { repo: { classPropertyName: "repo", publicName: "repo", isSignal: true, isRequired: true, transformFunction: null }, file: { classPropertyName: "file", publicName: "file", isSignal: true, isRequired: true, transformFunction: null }, head: { classPropertyName: "head", publicName: "head", isSignal: true, isRequired: true, transformFunction: null }, base: { classPropertyName: "base", publicName: "base", isSignal: true, isRequired: false, transformFunction: null }, threads: { classPropertyName: "threads", publicName: "threads", isSignal: true, isRequired: false, transformFunction: null }, canComment: { classPropertyName: "canComment", publicName: "canComment", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, ignoreSpace: { classPropertyName: "ignoreSpace", publicName: "ignoreSpace", isSignal: true, isRequired: false, transformFunction: null }, viewed: { classPropertyName: "viewed", publicName: "viewed", isSignal: true, isRequired: false, transformFunction: null }, reviewing: { classPropertyName: "reviewing", publicName: "reviewing", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { comment: "comment", reply: "reply", resolve: "resolve", discard: "discard", viewedChange: "viewedChange" }, ngImport: i0, template: `
655
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: DiffView, isStandalone: true, selector: "o-diff", inputs: { repo: { classPropertyName: "repo", publicName: "repo", isSignal: true, isRequired: true, transformFunction: null }, file: { classPropertyName: "file", publicName: "file", isSignal: true, isRequired: true, transformFunction: null }, head: { classPropertyName: "head", publicName: "head", isSignal: true, isRequired: true, transformFunction: null }, base: { classPropertyName: "base", publicName: "base", isSignal: true, isRequired: false, transformFunction: null }, threads: { classPropertyName: "threads", publicName: "threads", isSignal: true, isRequired: false, transformFunction: null }, canComment: { classPropertyName: "canComment", publicName: "canComment", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, ignoreSpace: { classPropertyName: "ignoreSpace", publicName: "ignoreSpace", isSignal: true, isRequired: false, transformFunction: null }, viewed: { classPropertyName: "viewed", publicName: "viewed", isSignal: true, isRequired: false, transformFunction: null }, reviewing: { classPropertyName: "reviewing", publicName: "reviewing", isSignal: true, isRequired: false, transformFunction: null }, canApply: { classPropertyName: "canApply", publicName: "canApply", isSignal: true, isRequired: false, transformFunction: null }, batch: { classPropertyName: "batch", publicName: "batch", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { comment: "comment", reply: "reply", resolve: "resolve", discard: "discard", apply: "apply", batchToggle: "batchToggle", viewedChange: "viewedChange" }, ngImport: i0, template: `
629
656
  <div class="o-card" [attr.id]="'file-' + file().path" [attr.data-testid]="'diff-' + file().path">
630
657
  <div class="o-ch" style="background:var(--o-well)">
631
658
  <button type="button" class="o-btn o-ghost o-xs" (click)="toggleOpen()" [attr.aria-expanded]="open()" [attr.aria-label]="(open() ? 'Collapse ' : 'Expand ') + file().path">{{ open() ? '▾' : '▸' }}</button>
@@ -712,7 +739,25 @@ class DiffView {
712
739
  @if (c.authorId === me()) {<button type="button" class="o-lnk" style="margin-left:6px;color:var(--o-mut)" (click)="discard.emit(c.id)" [attr.aria-label]="'Discard pending comment: ' + c.body">Discard</button>}
713
740
  }
714
741
  </span>
715
- <span style="white-space:pre-wrap">{{ c.body }}</span>
742
+ @if (suggestionOf(c.body); as sg) {
743
+ @if (sg.before) {<span style="white-space:pre-wrap">{{ sg.before }}</span>}
744
+ <div class="sugg" [attr.data-testid]="'suggestion-' + c.id">
745
+ <div class="sugg-h"><b>Suggested change</b>
746
+ @if (c.appliedSha) {<span class="o-chip o-grn">Applied in {{ c.appliedSha.slice(0, 7) }}</span>}
747
+ @else if (t.outdated) {<span class="o-chip">Outdated</span>}
748
+ @else if (canApply() && !c.pending) {
749
+ <span class="o-r o-flex" style="gap:6px">
750
+ <button type="button" class="o-btn o-xs" (click)="batchToggle.emit(c.id)" [attr.aria-pressed]="batch().includes(c.id)">{{ batch().includes(c.id) ? 'Remove from batch' : 'Add to batch' }}</button>
751
+ <button type="button" class="o-btn o-xs o-pri" (click)="apply.emit([c.id])" data-testid="suggestion-apply">Apply suggestion</button>
752
+ </span>
753
+ }
754
+ </div>
755
+ @for (l of sg.lines; track $index) {<div class="o-ln o-add"><span class="o-s" aria-hidden="true">+</span><span class="o-t">{{ l }}</span></div>}
756
+ @if (!sg.lines.length) {<div class="o-faint o-sm" style="padding:4px 10px">Delete the line(s).</div>}
757
+ </div>
758
+ } @else {
759
+ <span style="white-space:pre-wrap">{{ c.body }}</span>
760
+ }
716
761
  }
717
762
  </div>
718
763
  @if (canComment()) {
@@ -728,14 +773,14 @@ class DiffView {
728
773
  <ng-template #draftTpl let-l>
729
774
  <div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
730
775
  <textarea class="o-in" rows="2" [placeholder]="'Leave a comment on line ' + lineOf(l)" [attr.aria-label]="'Comment on line ' + lineOf(l)" [ngModel]="draftBody()" (ngModelChange)="draftBody.set($event)" data-testid="line-comment"></textarea>
731
- <div class="o-flex o-wrap"><span class="o-faint o-xs">Markdown works.</span>
776
+ <div class="o-flex o-wrap"><button type="button" class="o-btn o-xs" (click)="insertSuggestion(l)" data-testid="insert-suggestion">Insert suggestion</button><span class="o-faint o-xs">Markdown works.</span>
732
777
  <button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button>
733
778
  <button type="button" class="o-btn o-sm" [disabled]="!draftBody().trim()" (click)="send(l, false)" data-testid="line-comment-add">Add single comment</button>
734
779
  <button type="button" class="o-btn o-sm o-pri" [disabled]="!draftBody().trim()" (click)="send(l, true)" data-testid="line-comment-review">{{ reviewing() ? 'Add review comment' : 'Start a review' }}</button>
735
780
  </div>
736
781
  </div></div>
737
782
  </ng-template>
738
- `, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}\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: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
783
+ `, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}.sugg{border:1px solid var(--o-line);border-radius:6px;overflow:hidden;margin-top:2px}.sugg-h{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--o-well);font-size:12px}\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: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
739
784
  }
740
785
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DiffView, decorators: [{
741
786
  type: Component,
@@ -826,7 +871,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
826
871
  @if (c.authorId === me()) {<button type="button" class="o-lnk" style="margin-left:6px;color:var(--o-mut)" (click)="discard.emit(c.id)" [attr.aria-label]="'Discard pending comment: ' + c.body">Discard</button>}
827
872
  }
828
873
  </span>
829
- <span style="white-space:pre-wrap">{{ c.body }}</span>
874
+ @if (suggestionOf(c.body); as sg) {
875
+ @if (sg.before) {<span style="white-space:pre-wrap">{{ sg.before }}</span>}
876
+ <div class="sugg" [attr.data-testid]="'suggestion-' + c.id">
877
+ <div class="sugg-h"><b>Suggested change</b>
878
+ @if (c.appliedSha) {<span class="o-chip o-grn">Applied in {{ c.appliedSha.slice(0, 7) }}</span>}
879
+ @else if (t.outdated) {<span class="o-chip">Outdated</span>}
880
+ @else if (canApply() && !c.pending) {
881
+ <span class="o-r o-flex" style="gap:6px">
882
+ <button type="button" class="o-btn o-xs" (click)="batchToggle.emit(c.id)" [attr.aria-pressed]="batch().includes(c.id)">{{ batch().includes(c.id) ? 'Remove from batch' : 'Add to batch' }}</button>
883
+ <button type="button" class="o-btn o-xs o-pri" (click)="apply.emit([c.id])" data-testid="suggestion-apply">Apply suggestion</button>
884
+ </span>
885
+ }
886
+ </div>
887
+ @for (l of sg.lines; track $index) {<div class="o-ln o-add"><span class="o-s" aria-hidden="true">+</span><span class="o-t">{{ l }}</span></div>}
888
+ @if (!sg.lines.length) {<div class="o-faint o-sm" style="padding:4px 10px">Delete the line(s).</div>}
889
+ </div>
890
+ } @else {
891
+ <span style="white-space:pre-wrap">{{ c.body }}</span>
892
+ }
830
893
  }
831
894
  </div>
832
895
  @if (canComment()) {
@@ -842,15 +905,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
842
905
  <ng-template #draftTpl let-l>
843
906
  <div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
844
907
  <textarea class="o-in" rows="2" [placeholder]="'Leave a comment on line ' + lineOf(l)" [attr.aria-label]="'Comment on line ' + lineOf(l)" [ngModel]="draftBody()" (ngModelChange)="draftBody.set($event)" data-testid="line-comment"></textarea>
845
- <div class="o-flex o-wrap"><span class="o-faint o-xs">Markdown works.</span>
908
+ <div class="o-flex o-wrap"><button type="button" class="o-btn o-xs" (click)="insertSuggestion(l)" data-testid="insert-suggestion">Insert suggestion</button><span class="o-faint o-xs">Markdown works.</span>
846
909
  <button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button>
847
910
  <button type="button" class="o-btn o-sm" [disabled]="!draftBody().trim()" (click)="send(l, false)" data-testid="line-comment-add">Add single comment</button>
848
911
  <button type="button" class="o-btn o-sm o-pri" [disabled]="!draftBody().trim()" (click)="send(l, true)" data-testid="line-comment-review">{{ reviewing() ? 'Add review comment' : 'Start a review' }}</button>
849
912
  </div>
850
913
  </div></div>
851
914
  </ng-template>
852
- `, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}\n"] }]
853
- }], ctorParameters: () => [], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], file: [{ type: i0.Input, args: [{ isSignal: true, alias: "file", required: true }] }], head: [{ type: i0.Input, args: [{ isSignal: true, alias: "head", required: true }] }], base: [{ type: i0.Input, args: [{ isSignal: true, alias: "base", required: false }] }], threads: [{ type: i0.Input, args: [{ isSignal: true, alias: "threads", required: false }] }], canComment: [{ type: i0.Input, args: [{ isSignal: true, alias: "canComment", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], ignoreSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreSpace", required: false }] }], viewed: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewed", required: false }] }], reviewing: [{ type: i0.Input, args: [{ isSignal: true, alias: "reviewing", required: false }] }], comment: [{ type: i0.Output, args: ["comment"] }], reply: [{ type: i0.Output, args: ["reply"] }], resolve: [{ type: i0.Output, args: ["resolve"] }], discard: [{ type: i0.Output, args: ["discard"] }], viewedChange: [{ type: i0.Output, args: ["viewedChange"] }] } });
915
+ `, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}.sugg{border:1px solid var(--o-line);border-radius:6px;overflow:hidden;margin-top:2px}.sugg-h{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--o-well);font-size:12px}\n"] }]
916
+ }], ctorParameters: () => [], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], file: [{ type: i0.Input, args: [{ isSignal: true, alias: "file", required: true }] }], head: [{ type: i0.Input, args: [{ isSignal: true, alias: "head", required: true }] }], base: [{ type: i0.Input, args: [{ isSignal: true, alias: "base", required: false }] }], threads: [{ type: i0.Input, args: [{ isSignal: true, alias: "threads", required: false }] }], canComment: [{ type: i0.Input, args: [{ isSignal: true, alias: "canComment", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], ignoreSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreSpace", required: false }] }], viewed: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewed", required: false }] }], reviewing: [{ type: i0.Input, args: [{ isSignal: true, alias: "reviewing", required: false }] }], comment: [{ type: i0.Output, args: ["comment"] }], reply: [{ type: i0.Output, args: ["reply"] }], resolve: [{ type: i0.Output, args: ["resolve"] }], discard: [{ type: i0.Output, args: ["discard"] }], canApply: [{ type: i0.Input, args: [{ isSignal: true, alias: "canApply", required: false }] }], batch: [{ type: i0.Input, args: [{ isSignal: true, alias: "batch", required: false }] }], apply: [{ type: i0.Output, args: ["apply"] }], batchToggle: [{ type: i0.Output, args: ["batchToggle"] }], viewedChange: [{ type: i0.Output, args: ["viewedChange"] }] } });
854
917
  /** Pairs a hunk's lines side by side: context on both sides, a run of removals beside the run of additions that follows it. */
855
918
  function pair(lines) {
856
919
  const rows = [];
@@ -2178,7 +2241,7 @@ class CommitPage extends RepoPage {
2178
2241
  <div class="o-card"><div class="o-empty">Loading…</div></div>
2179
2242
  }
2180
2243
  </div>
2181
- `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment", "mode", "ignoreSpace", "viewed", "reviewing"], outputs: ["comment", "reply", "resolve", "discard", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2244
+ `, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment", "mode", "ignoreSpace", "viewed", "reviewing", "canApply", "batch"], outputs: ["comment", "reply", "resolve", "discard", "apply", "batchToggle", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2182
2245
  }
2183
2246
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitPage, decorators: [{
2184
2247
  type: Component,
@@ -2733,6 +2796,7 @@ class PullPage extends RepoPage {
2733
2796
  out.push({ id: 't' + th.id, at: th.createdAt, kind: 'thread', who: th.createdBy, thread: th });
2734
2797
  return out.sort((a, b) => a.at.localeCompare(b.at));
2735
2798
  }, ...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
2799
+ this.batch = signal([], ...(ngDevMode ? [{ debugName: "batch" }] : /* istanbul ignore next */ []));
2736
2800
  this.route.paramMap.subscribe((m) => {
2737
2801
  this.number = Number(m.get('number') ?? 0);
2738
2802
  const r = this.repo();
@@ -2781,6 +2845,21 @@ class PullPage extends RepoPage {
2781
2845
  this.run(this.api.comment(this.projectId(), this.name(), this.number, { body: c.body, path: c.path, side: c.side, line: c.line, pending: c.pending }), 'Could not add the comment.', () => { if (c.pending)
2782
2846
  this.note.set('Added to your review. Submit it from Review changes to post your comments.'); });
2783
2847
  }
2848
+ /** Arms auto-merge with a method, or disarms it (null). */
2849
+ setAutoMerge(method) {
2850
+ this.run(this.api.autoMerge(this.projectId(), this.name(), this.number, method), method === null ? 'Could not turn auto-merge off.' : 'Could not turn auto-merge on.', () => this.note.set(method === null ? 'Auto-merge is off.' : 'Auto-merge is on: this pull request merges as you once every requirement passes.'));
2851
+ }
2852
+ toggleBatch(id) {
2853
+ const b = this.batch();
2854
+ this.batch.set(b.includes(id) ? b.filter((x) => x !== id) : [...b, id]);
2855
+ }
2856
+ /** Commits one suggestion, or the batch, onto the pull request's branch. */
2857
+ applySuggestions(ids) {
2858
+ this.run(this.api.applySuggestions(this.projectId(), this.name(), this.number, ids), 'Could not apply the suggestion.', () => {
2859
+ this.note.set(ids.length === 1 ? 'Suggestion applied.' : `${ids.length} suggestions applied in one commit.`);
2860
+ this.batch.set([]);
2861
+ });
2862
+ }
2784
2863
  discard(id) {
2785
2864
  this.run(this.api.discardComment(this.projectId(), this.name(), this.number, id), 'Could not discard the comment.');
2786
2865
  }
@@ -2943,6 +3022,12 @@ class PullPage extends RepoPage {
2943
3022
  <div class="o-flex o-wrap" style="padding:12px 14px;background:var(--o-well)">
2944
3023
  @if (p.canReview && !approvedByMe()) {<button type="button" class="o-btn" (click)="approve()" [disabled]="busy()" data-testid="approve">Approve</button>}
2945
3024
  @if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
3025
+ @if (p.autoMergeBy) {
3026
+ <span class="o-chip o-blu" data-testid="auto-merge-on">Auto-merge on · {{ methodLabel(p.autoMergeMethod || 'merge') }} · by {{ people().name(p.autoMergeBy) }}</span>
3027
+ @if (canWrite()) {<button type="button" class="o-btn o-sm o-ghost" (click)="setAutoMerge(null)" [disabled]="busy()" data-testid="auto-merge-off">Disable auto-merge</button>}
3028
+ } @else if (repo()?.allowAutoMerge && canWrite() && !p.gate.mergeable) {
3029
+ <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>
3030
+ }
2946
3031
  @if (p.canMerge) {
2947
3032
  <span class="o-faint o-sm">Delete branch after merge</span><button type="button" class="o-tog" role="switch" aria-label="Delete branch after merge" [class.o-on]="delBranch()" [attr.aria-checked]="delBranch()" (click)="delBranch.set(!delBranch())"></button>
2948
3033
  <div class="merge o-r">
@@ -2977,6 +3062,12 @@ class PullPage extends RepoPage {
2977
3062
  </div>
2978
3063
  }
2979
3064
  @case ('files') {
3065
+ @if (batch().length) {
3066
+ <div class="o-note o-blu o-flex" data-testid="suggestion-batch"><span>{{ batch().length }} {{ batch().length === 1 ? 'suggestion' : 'suggestions' }} in a batch</span>
3067
+ <button type="button" class="o-btn o-sm o-r" (click)="batch.set([])">Clear</button>
3068
+ <button type="button" class="o-btn o-sm o-pri" (click)="applySuggestions(batch())" [disabled]="busy()" data-testid="suggestion-commit">Commit suggestions</button>
3069
+ </div>
3070
+ }
2980
3071
  <div class="o-flex o-wrap">
2981
3072
  <div class="o-seg" role="group" aria-label="Diff view"><button type="button" [class.o-on]="mode() === 'unified'" [attr.aria-pressed]="mode() === 'unified'" (click)="setMode('unified')">Unified</button><button type="button" [class.o-on]="mode() === 'split'" [attr.aria-pressed]="mode() === 'split'" (click)="setMode('split')" data-testid="diff-split">Split</button></div>
2982
3073
  <button type="button" class="o-btn o-sm" [class.o-on]="hideWs()" [attr.aria-pressed]="hideWs()" (click)="hideWs.set(!hideWs())" data-testid="hide-ws">Hide whitespace</button>
@@ -2995,6 +3086,7 @@ class PullPage extends RepoPage {
2995
3086
  @for (f of cmp()?.files ?? []; track f.path) {
2996
3087
  <o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" [mode]="mode()" [ignoreSpace]="hideWs()"
2997
3088
  [viewed]="isViewed(f.path)" [reviewing]="myPending().length > 0" (viewedChange)="setViewed(f.path, $event)"
3089
+ [canApply]="p.state === 'open' && canWrite()" [batch]="batch()" (batchToggle)="toggleBatch($event)" (apply)="applySuggestions($event)"
2998
3090
  (comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" (discard)="discard($event)" />
2999
3091
  } @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
3000
3092
  </div>
@@ -3031,7 +3123,7 @@ class PullPage extends RepoPage {
3031
3123
  <div class="o-card"><div class="o-empty">Loading…</div></div>
3032
3124
  }
3033
3125
  </div>
3034
- `, isInline: true, styles: [".badge{padding:3px 10px;border-radius:12px;font-size:12px;font-weight:500;color:var(--o-acc-fg)}.anchor{position:relative;align-self:center}.merge{display:flex;align-items:center;position:relative}.merge .go{height:32px;padding:0 14px;border-radius:7px 0 0 7px;border:none;font-size:13px;font-weight:500;color:var(--o-acc-fg);background:var(--o-ok);cursor:pointer}.merge .mn{height:32px;padding:0 10px;border:none;border-radius:0 7px 7px 0;font-size:12px;color:var(--o-acc-fg);background:var(--o-ok);border-left:1px solid rgba(255,255,255,.25);cursor:pointer}.merge button:disabled{background:var(--o-line-mid);cursor:not-allowed}.tl{display:flex;flex-direction:column;gap:10px;padding-left:14px;border-left:2px solid var(--o-line);margin-left:14px}.files-split{display:grid;grid-template-columns:210px minmax(0,1fr);gap:14px;align-items:start}.files-split:has(>:only-child){grid-template-columns:minmax(0,1fr)}.files-tree{position:sticky;top:8px;max-height:calc(100vh - 120px);overflow:auto}@media(max-width:900px){.files-split{grid-template-columns:minmax(0,1fr)}.files-tree{display:none}}\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: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment", "mode", "ignoreSpace", "viewed", "reviewing"], outputs: ["comment", "reply", "resolve", "discard", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3126
+ `, isInline: true, styles: [".badge{padding:3px 10px;border-radius:12px;font-size:12px;font-weight:500;color:var(--o-acc-fg)}.anchor{position:relative;align-self:center}.merge{display:flex;align-items:center;position:relative}.merge .go{height:32px;padding:0 14px;border-radius:7px 0 0 7px;border:none;font-size:13px;font-weight:500;color:var(--o-acc-fg);background:var(--o-ok);cursor:pointer}.merge .mn{height:32px;padding:0 10px;border:none;border-radius:0 7px 7px 0;font-size:12px;color:var(--o-acc-fg);background:var(--o-ok);border-left:1px solid rgba(255,255,255,.25);cursor:pointer}.merge button:disabled{background:var(--o-line-mid);cursor:not-allowed}.tl{display:flex;flex-direction:column;gap:10px;padding-left:14px;border-left:2px solid var(--o-line);margin-left:14px}.files-split{display:grid;grid-template-columns:210px minmax(0,1fr);gap:14px;align-items:start}.files-split:has(>:only-child){grid-template-columns:minmax(0,1fr)}.files-tree{position:sticky;top:8px;max-height:calc(100vh - 120px);overflow:auto}@media(max-width:900px){.files-split{grid-template-columns:minmax(0,1fr)}.files-tree{display:none}}\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: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment", "mode", "ignoreSpace", "viewed", "reviewing", "canApply", "batch"], outputs: ["comment", "reply", "resolve", "discard", "apply", "batchToggle", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3035
3127
  }
3036
3128
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullPage, decorators: [{
3037
3129
  type: Component,
@@ -3113,6 +3205,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3113
3205
  <div class="o-flex o-wrap" style="padding:12px 14px;background:var(--o-well)">
3114
3206
  @if (p.canReview && !approvedByMe()) {<button type="button" class="o-btn" (click)="approve()" [disabled]="busy()" data-testid="approve">Approve</button>}
3115
3207
  @if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
3208
+ @if (p.autoMergeBy) {
3209
+ <span class="o-chip o-blu" data-testid="auto-merge-on">Auto-merge on · {{ methodLabel(p.autoMergeMethod || 'merge') }} · by {{ people().name(p.autoMergeBy) }}</span>
3210
+ @if (canWrite()) {<button type="button" class="o-btn o-sm o-ghost" (click)="setAutoMerge(null)" [disabled]="busy()" data-testid="auto-merge-off">Disable auto-merge</button>}
3211
+ } @else if (repo()?.allowAutoMerge && canWrite() && !p.gate.mergeable) {
3212
+ <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>
3213
+ }
3116
3214
  @if (p.canMerge) {
3117
3215
  <span class="o-faint o-sm">Delete branch after merge</span><button type="button" class="o-tog" role="switch" aria-label="Delete branch after merge" [class.o-on]="delBranch()" [attr.aria-checked]="delBranch()" (click)="delBranch.set(!delBranch())"></button>
3118
3216
  <div class="merge o-r">
@@ -3147,6 +3245,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3147
3245
  </div>
3148
3246
  }
3149
3247
  @case ('files') {
3248
+ @if (batch().length) {
3249
+ <div class="o-note o-blu o-flex" data-testid="suggestion-batch"><span>{{ batch().length }} {{ batch().length === 1 ? 'suggestion' : 'suggestions' }} in a batch</span>
3250
+ <button type="button" class="o-btn o-sm o-r" (click)="batch.set([])">Clear</button>
3251
+ <button type="button" class="o-btn o-sm o-pri" (click)="applySuggestions(batch())" [disabled]="busy()" data-testid="suggestion-commit">Commit suggestions</button>
3252
+ </div>
3253
+ }
3150
3254
  <div class="o-flex o-wrap">
3151
3255
  <div class="o-seg" role="group" aria-label="Diff view"><button type="button" [class.o-on]="mode() === 'unified'" [attr.aria-pressed]="mode() === 'unified'" (click)="setMode('unified')">Unified</button><button type="button" [class.o-on]="mode() === 'split'" [attr.aria-pressed]="mode() === 'split'" (click)="setMode('split')" data-testid="diff-split">Split</button></div>
3152
3256
  <button type="button" class="o-btn o-sm" [class.o-on]="hideWs()" [attr.aria-pressed]="hideWs()" (click)="hideWs.set(!hideWs())" data-testid="hide-ws">Hide whitespace</button>
@@ -3165,6 +3269,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3165
3269
  @for (f of cmp()?.files ?? []; track f.path) {
3166
3270
  <o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" [mode]="mode()" [ignoreSpace]="hideWs()"
3167
3271
  [viewed]="isViewed(f.path)" [reviewing]="myPending().length > 0" (viewedChange)="setViewed(f.path, $event)"
3272
+ [canApply]="p.state === 'open' && canWrite()" [batch]="batch()" (batchToggle)="toggleBatch($event)" (apply)="applySuggestions($event)"
3168
3273
  (comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" (discard)="discard($event)" />
3169
3274
  } @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
3170
3275
  </div>
@@ -3238,7 +3343,7 @@ class RepoSettingsPage extends RepoPage {
3238
3343
  this.check = signal('', ...(ngDevMode ? [{ debugName: "check" }] : /* istanbul ignore next */ []));
3239
3344
  this.confirm = signal('', ...(ngDevMode ? [{ debugName: "confirm" }] : /* istanbul ignore next */ []));
3240
3345
  this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
3241
- this.g = { name: '', description: '', visibility: 'private', defaultBranch: '', mergeMethods: [], deleteBranchOnMerge: false };
3346
+ this.g = { name: '', description: '', visibility: 'private', defaultBranch: '', mergeMethods: [], deleteBranchOnMerge: false, allowAutoMerge: false };
3242
3347
  this.methods = [{ id: 'merge', label: 'Merge commit' }, { id: 'squash', label: 'Squash' }, { id: 'rebase', label: 'Rebase' }];
3243
3348
  this.toggles = [
3244
3349
  { key: 'blockDirectPush', label: 'Require a pull request before merging', hint: 'Direct pushes are blocked; changes arrive through pull requests.' },
@@ -3288,7 +3393,7 @@ class RepoSettingsPage extends RepoPage {
3288
3393
  }
3289
3394
  onRepo(r) {
3290
3395
  this.loadCollabs(r);
3291
- this.g = { name: r.name, description: r.description, visibility: r.visibility, defaultBranch: r.defaultBranch, mergeMethods: [...(r.mergeMethods ?? [])], deleteBranchOnMerge: r.deleteBranchOnMerge };
3396
+ this.g = { name: r.name, description: r.description, visibility: r.visibility, defaultBranch: r.defaultBranch, mergeMethods: [...(r.mergeMethods ?? [])], deleteBranchOnMerge: r.deleteBranchOnMerge, allowAutoMerge: !!r.allowAutoMerge };
3292
3397
  if (!r.empty)
3293
3398
  this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.branches.set(b), error: () => undefined });
3294
3399
  this.api.protections(this.projectId(), r.name).subscribe({ next: (p) => this.rules.set(p), error: () => undefined });
@@ -3344,8 +3449,8 @@ class RepoSettingsPage extends RepoPage {
3344
3449
  const renamed = this.g.name.trim() !== r.name;
3345
3450
  // a maintain collaborator sends what they may change; the name and visibility stay a Maintainer's
3346
3451
  const body = this.canAdmin()
3347
- ? { 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 }
3348
- : { description: this.g.description, defaultBranch: this.g.defaultBranch, mergeMethods: this.g.mergeMethods, deleteBranchOnMerge: this.g.deleteBranchOnMerge };
3452
+ ? { 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 }
3453
+ : { description: this.g.description, defaultBranch: this.g.defaultBranch, mergeMethods: this.g.mergeMethods, deleteBranchOnMerge: this.g.deleteBranchOnMerge, allowAutoMerge: this.g.allowAutoMerge };
3349
3454
  this.api.update(this.projectId(), r.name, body).subscribe({
3350
3455
  next: (n) => {
3351
3456
  this.busy.set(false);
@@ -3436,6 +3541,7 @@ class RepoSettingsPage extends RepoPage {
3436
3541
  @for (m of methods; track m.id) {<label class="o-flex"><input type="checkbox" [checked]="g.mergeMethods.includes(m.id)" (change)="flipMethod(m.id)" [disabled]="!canMaintain()" />{{ m.label }}</label>}
3437
3542
  </div>
3438
3543
  <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>
3544
+ <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>
3439
3545
  <div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Allow auto-merge"></button>Allow auto-merge when all requirements pass<b class="o-ph-tag o-ga">GA</b></div>
3440
3546
  @if (canMaintain()) {<div class="o-flex">@if (!canAdmin()) {<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>}
3441
3547
  </form>
@@ -3561,6 +3667,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3561
3667
  @for (m of methods; track m.id) {<label class="o-flex"><input type="checkbox" [checked]="g.mergeMethods.includes(m.id)" (change)="flipMethod(m.id)" [disabled]="!canMaintain()" />{{ m.label }}</label>}
3562
3668
  </div>
3563
3669
  <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>
3670
+ <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>
3564
3671
  <div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Allow auto-merge"></button>Allow auto-merge when all requirements pass<b class="o-ph-tag o-ga">GA</b></div>
3565
3672
  @if (canMaintain()) {<div class="o-flex">@if (!canAdmin()) {<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>}
3566
3673
  </form>