Skip to content

Commit 83cccc1

Browse files
committed
Adds a click-to-suppress toggle to the graph signing indicator
Makes the commit box's signing indicator a clickable toggle that temporarily commits without signing — in-memory and per-repository for the webview session, never changing Git config or VS Code settings. The key icon dims with a diagonal slash when suppressed (state carried by `aria-pressed`, not color alone) and the tooltip explains how to re-enable. Suppression survives selection changes and panel hide/show, and applies to both the commit box and the Compose apply path. Adds a `graph/wip/signing/toggled` telemetry event.
1 parent 698c6a7 commit 83cccc1

9 files changed

Lines changed: 148 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1010

1111
- Adds ConfigCat-based feature flag service for A/B testing and experimentation support ([#5092](https://github.com/gitkraken/vscode-gitlens/issues/5092))
1212
- Adds an optional `avatar` URL template to custom remotes in the `gitlens.remotes` setting — enables corporate and self-hosted setups to resolve commit-author avatars via a templated URL with `${email}`, `${emailName}`, `${domain}`, and `${size}` tokens; identity values are component-encoded before interpolation to keep attacker-controllable commit emails from injecting URL-structural characters, and templates configured via workspace settings require explicit user approval on first use in a trusted workspace (revocable via _GitLens: Reset > Approved Avatar URL Templates..._) ([#302](https://github.com/gitkraken/vscode-gitlens/issues/302), [#5155](https://github.com/gitkraken/vscode-gitlens/pull/5155)) — thanks to [PR #1636](https://github.com/gitkraken/vscode-gitlens/pull/1636) by Tmk ([@tmkx](https://github.com/tmkx))
13-
- Adds a commit signing indicator to the _Commit Graph_'s working changes (WIP) commit box — a key icon appears when commits will be signed (via the repo's `commit.gpgsign` Git config or VS Code's `git.enableCommitSigning` setting), with the signing format (GPG, SSH, X.509, or OpenPGP) shown on hover
13+
- Adds a commit signing indicator to the _Commit Graph_'s working changes (WIP) commit box — a key icon appears when commits will be signed (via the repo's `commit.gpgsign` Git config or VS Code's `git.enableCommitSigning` setting), with the signing format (GPG, SSH, X.509, or OpenPGP) shown on hover; click it to temporarily commit without signing (in-memory, per-repository, for the session — never changes your Git config or settings) and click again to re-enable
1414

1515
### Changed
1616

docs/telemetry-events.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3096,6 +3096,26 @@ background-upgraded the extension while the host kept running the old build
30963096
}
30973097
```
30983098

3099+
### graph/wip/signing/toggled
3100+
3101+
> Sent when the user toggles the temporary commit-signing suppression in the Graph's WIP panel
3102+
3103+
```typescript
3104+
{
3105+
'context.repository.closed': boolean,
3106+
'context.repository.folder.scheme': string,
3107+
'context.repository.id': string,
3108+
'context.repository.provider.id': string,
3109+
'context.repository.scheme': string,
3110+
'context.webview.host': 'view' | 'editor' | 'panel',
3111+
'context.webview.id': string,
3112+
'context.webview.instanceId': string,
3113+
'context.webview.type': string,
3114+
// Commit-signing state after the toggle (`false` = temporarily suppressed)
3115+
'enabled': boolean
3116+
}
3117+
```
3118+
30993119
### graphDetails/closed
31003120

31013121
> Sent when the integrated graph details panel is collapsed

src/constants.telemetry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ export interface TelemetryEvents extends WebviewShowAbortedEvents, WebviewShownE
284284
'graph/wip/commit/succeeded': GraphWipCommitSucceededEvent;
285285
/** Sent when a commit from the Graph's WIP panel fails (e.g. a hook rejection or signing failure) */
286286
'graph/wip/commit/failed': GraphWipCommitFailedEvent;
287+
/** Sent when the user toggles the temporary commit-signing suppression in the Graph's WIP panel */
288+
'graph/wip/signing/toggled': GraphWipSigningToggledEvent;
287289

288290
/** Sent when a virtual-FS-backed file (e.g. a Graph Compose proposed commit) is opened */
289291
'graph/virtualFile/opened': GraphVirtualFileOpenedEvent;
@@ -1417,6 +1419,11 @@ interface GraphDetailsCompareGenerateChangelogEvent extends GraphContextEventDat
14171419
includeWorkingTree: boolean;
14181420
}
14191421

1422+
interface GraphWipSigningToggledEvent extends GraphContextEventData {
1423+
/** Commit-signing state after the toggle (`false` = temporarily suppressed) */
1424+
enabled: boolean;
1425+
}
1426+
14201427
interface GraphVirtualFileOpenedEvent extends GraphContextEventData {
14211428
/** Which open operation the user triggered */
14221429
mode: GraphVirtualFileMode;

src/webviews/apps/plus/graph/components/__tests__/detailsActions.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ suite('DetailsActions', () => {
195195
commits: composeResult.result.commits,
196196
base: composeResult.result.baseCommit,
197197
includedCommitIds: undefined,
198+
suppressSigning: false,
198199
});
199200
assert.strictEqual(state.activeMode.get(), null);
200201
assert.strictEqual(state.activeModeContext.get(), null);
@@ -229,9 +230,35 @@ suite('DetailsActions', () => {
229230
commits: composeResult.result.commits,
230231
base: composeResult.result.baseCommit,
231232
includedCommitIds: ['c1'],
233+
suppressSigning: false,
232234
});
233235
});
234236

237+
test('composeCommitAll forwards suppressSigning when the repo signing is suppressed', async () => {
238+
const state = createDetailsState();
239+
state.activeMode.set('compose');
240+
state.activeModeContext.set('wip');
241+
state.suppressedSigningRepos.set(new Set(['/repo']));
242+
243+
let committedPlan: { suppressSigning?: boolean } | undefined;
244+
const resources = createResources();
245+
resources.compose.mutate(composeResult);
246+
247+
const actions = new DetailsActions(
248+
state,
249+
createServices(async (_repoPath, plan) => {
250+
committedPlan = plan as { suppressSigning?: boolean };
251+
return { success: true };
252+
}),
253+
resources,
254+
);
255+
actions.fetchDetails = async () => undefined;
256+
257+
await actions.composeCommitAll('/repo', 'abc');
258+
259+
assert.strictEqual(committedPlan?.suppressSigning, true);
260+
});
261+
235262
test('toggleCompareWorkingTree invalidates side data and refetches summary with the toggle enabled', async () => {
236263
const state = createDetailsState();
237264
state.branchCompareLeftRef.set('main');

src/webviews/apps/plus/graph/components/detailsActions.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ export class DetailsActions {
415415
* {@link clearEnrichmentCaches}.
416416
* - Capability flags (`preferences`, `orgSettings`, `aiModel`, etc.) and pure UI toggles
417417
* are not repo-scoped — they intentionally survive switches.
418+
* - `suppressedSigningRepos` is keyed by repoPath and must survive switches for the whole
419+
* session (it's the commit box's signing toggle); never cleared here.
418420
*/
419421
resetRepoScopedState(repoPath?: string): void {
420422
const s = this.state;
@@ -2589,6 +2591,7 @@ export class DetailsActions {
25892591
commits: composeValue.result.commits,
25902592
base: composeValue.result.baseCommit,
25912593
includedCommitIds: includedCommitIds,
2594+
suppressSigning: this.state.suppressedSigningRepos.get().has(repoPath),
25922595
});
25932596
if ('error' in result && result.error) {
25942597
this.resources.compose.mutate({ error: { message: result.error.message } });
@@ -3018,10 +3021,19 @@ export class DetailsActions {
30183021
this.state.committing.set(true);
30193022
// Suppress host WIP pushes for this repo until the commit settles (see `applyPushedWip`).
30203023
this._committingRepoPath = repoPath;
3024+
// When the user has temporarily suppressed signing for this repo, pass `sign: false` so the
3025+
// host adds `--no-gpg-sign` (defeating repo `commit.gpgsign` and the host override). Otherwise
3026+
// leave it undefined so signing follows the configured behavior.
3027+
const sign = this.state.suppressedSigningRepos.get().has(repoPath) ? false : undefined;
3028+
30213029
try {
30223030
// `commit` returns a discriminated result and never throws for git failures — the host
30233031
// classifies the error and presents the modal/full-output document itself.
3024-
const result = await this.services.repository.commit(repoPath, message, { amend: isAmend, all: all });
3032+
const result = await this.services.repository.commit(repoPath, message, {
3033+
amend: isAmend,
3034+
all: all,
3035+
sign: sign,
3036+
});
30253037
if (result.status === 'committed') {
30263038
this.state.commitMessage.set('');
30273039
this.state.commitMessageDirty.set(false);

src/webviews/apps/plus/graph/components/detailsState.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,19 @@ export function createDetailsState() {
527527
const { resetAll: resetDurable, ...durableSignals } = durable;
528528
const { resetAll: resetTransient, ...transientSignals } = transient;
529529

530+
// Session layer — repoPaths whose commit signing the user has temporarily suppressed via the
531+
// commit box's signing toggle (in-memory only, never persisted). Deliberately created outside
532+
// both signal groups so `resetDurable()`/`resetTransient()`/`resetAll()` leave it untouched: the
533+
// suppression must survive selection changes, repo switches, and panel hide/show for the whole
534+
// webview session, clearing only on webview reload / VS Code restart.
535+
const suppressedSigningRepos = new Signal.State<ReadonlySet<string>>(new Set());
536+
530537
return {
531538
...durableSignals,
532539
...transientSignals,
533540

541+
suppressedSigningRepos: suppressedSigningRepos,
542+
534543
/** Reset the durable (fetched) layer. Primarily used on panel teardown. */
535544
resetDurable: resetDurable,
536545

src/webviews/apps/plus/graph/components/gl-commit-box.css.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,34 @@ export const commitBoxStyles = css`
3535
gap: 0.4rem;
3636
}
3737
38-
.signing-indicator {
39-
display: inline-flex;
38+
.signing-toggle {
4039
color: var(--vscode-descriptionForeground);
4140
}
4241
43-
.signing-indicator:focus-visible {
44-
outline: 0.1rem solid var(--vscode-focusBorder);
45-
outline-offset: 0.2rem;
42+
.signing-toggle__icon {
43+
position: relative;
44+
display: inline-flex;
45+
}
46+
47+
/* Suppressed state — dim the key and draw a diagonal slash through it. State is conveyed by the
48+
slash shape + the button's aria-pressed, never by color alone. The slash lives on the wrapper
49+
span (owned by this component's shadow tree), not on code-icon, whose own shadow styles use
50+
::before/::after for some glyphs. */
51+
.signing-toggle.is-suppressed .signing-toggle__icon {
52+
opacity: 0.6;
53+
}
54+
55+
.signing-toggle.is-suppressed .signing-toggle__icon::after {
56+
content: '';
57+
position: absolute;
58+
left: -10%;
59+
top: 50%;
60+
width: 120%;
61+
height: 0.15rem;
62+
background: currentColor;
63+
transform: rotate(-45deg);
64+
transform-origin: center;
65+
pointer-events: none;
4666
}
4767
4868
.compose-icon {

src/webviews/apps/plus/graph/components/gl-commit-box.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export class GlCommitBox extends LitElement {
5959
@property({ type: Object })
6060
signing?: WipSigning;
6161

62+
@property({ type: Boolean })
63+
signingSuppressed = false;
64+
6265
override render() {
6366
return html`
6467
<div class="options">
@@ -80,12 +83,24 @@ export class GlCommitBox extends LitElement {
8083
private renderSigningIndicator() {
8184
if (!this.signing?.enabled) return nothing;
8285

83-
const label = `Commits will be signed using ${getSigningFormatLabel(this.signing.format)}`;
86+
const formatLabel = getSigningFormatLabel(this.signing.format);
87+
const suppressed = this.signingSuppressed;
88+
const tooltip = suppressed
89+
? `Commit signing temporarily off — click to re-enable ${formatLabel} signing`
90+
: `Commits will be signed using ${formatLabel} — click to temporarily commit without signing`;
8491
return html`
85-
<gl-tooltip content=${label} placement="bottom">
86-
<span class="signing-indicator" tabindex="0" role="img" aria-label=${label}>
87-
<code-icon icon="key"></code-icon>
88-
</span>
92+
<gl-tooltip content=${tooltip} placement="bottom">
93+
<gl-button
94+
class="signing-toggle ${suppressed ? 'is-suppressed' : ''}"
95+
appearance="toolbar"
96+
density="compact"
97+
aria-pressed=${suppressed ? 'false' : 'true'}
98+
aria-label="Commit signing (${formatLabel})"
99+
?disabled=${this.committing}
100+
@click=${this.onToggleSigning}
101+
>
102+
<span class="signing-toggle__icon"><code-icon icon="key"></code-icon></span>
103+
</gl-button>
89104
</gl-tooltip>
90105
`;
91106
}
@@ -245,6 +260,12 @@ export class GlCommitBox extends LitElement {
245260
private onCompose() {
246261
this.dispatchEvent(new CustomEvent('compose', { bubbles: true, composed: true }));
247262
}
263+
264+
private onToggleSigning() {
265+
if (this.committing) return;
266+
267+
this.dispatchEvent(new CustomEvent('signing-toggle', { bubbles: true, composed: true }));
268+
}
248269
}
249270

250271
function getSigningFormatLabel(format: WipSigning['format']): string {

src/webviews/apps/plus/graph/components/gl-graph-details-panel.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,12 +1908,15 @@ export class GlGraphDetailsPanel extends SignalWatcher(LitElement) {
19081908
.aiEnabled=${this._state.preferences.get()?.aiEnabled ?? false}
19091909
.commitError=${this._state.commitError.get()}
19101910
.signing=${wip.signing}
1911+
.signingSuppressed=${this.effectiveRepoPath != null &&
1912+
this._state.suppressedSigningRepos.get().has(this.effectiveRepoPath)}
19111913
@message-change=${this.handleCommitMessageChange}
19121914
@amend-change=${this.handleAmendChange}
19131915
@commit=${this.handleCommit}
19141916
@generate-message=${this.handleGenerateMessage}
19151917
@add-coauthors=${this.handleAddCoauthors}
19161918
@compose=${this.handleCompose}
1919+
@signing-toggle=${this.handleSigningToggle}
19171920
></gl-commit-box>
19181921
`
19191922
: html`
@@ -2935,6 +2938,23 @@ export class GlGraphDetailsPanel extends SignalWatcher(LitElement) {
29352938
}
29362939
};
29372940

2941+
private handleSigningToggle = () => {
2942+
const repoPath = this.effectiveRepoPath;
2943+
if (repoPath == null) return;
2944+
2945+
// Copy-on-write toggle of the per-repo suppression set (the signal holds a ReadonlySet).
2946+
const current = this._state.suppressedSigningRepos.get();
2947+
const next = new Set(current);
2948+
const suppressed = !next.has(repoPath);
2949+
if (suppressed) {
2950+
next.add(repoPath);
2951+
} else {
2952+
next.delete(repoPath);
2953+
}
2954+
this._state.suppressedSigningRepos.set(next);
2955+
this._actions.sendTelemetryEvent('graph/wip/signing/toggled', { enabled: !suppressed });
2956+
};
2957+
29382958
private handleCommit = () => void this._actions.commit(this.effectiveRepoPath, this.sha);
29392959

29402960
private handleGenerateMessage = () => this._workflow.runGenerateMessage(this.effectiveRepoPath);

0 commit comments

Comments
 (0)