Skip to content

Commit 4b8ebee

Browse files
Adds support for composing without a base commit
1 parent 868eccf commit 4b8ebee

File tree

12 files changed

+127
-91
lines changed

12 files changed

+127
-91
lines changed

src/env/node/git/git.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from '../../../git/errors';
3535
import type { GitDir } from '../../../git/gitProvider';
3636
import type { GitDiffFilter } from '../../../git/models/diff';
37+
import { rootSha } from '../../../git/models/revision';
3738
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
3839
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/utils/revision.utils';
3940
import { getCancellationTokenId } from '../../../system/-webview/cancellation';
@@ -69,9 +70,6 @@ export const maxGitCliLength = 30000;
6970

7071
const textDecoder = new TextDecoder('utf8');
7172

72-
// This is a root sha of all git repo's if using sha1
73-
const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
74-
7573
export const GitErrors = {
7674
alreadyCheckedOut: /already checked out/i,
7775
alreadyExists: /already exists/i,

src/env/node/git/sub-providers/patch.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
177177
@log<PatchGitSubProvider['createUnreachableCommitsFromPatches']>({ args: { 2: p => p.length } })
178178
async createUnreachableCommitsFromPatches(
179179
repoPath: string,
180-
base: string,
180+
base: string | undefined,
181181
patches: { message: string; patch: string }[],
182182
): Promise<string[]> {
183183
// Create a temporary index file
@@ -198,7 +198,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
198198
private async createUnreachableCommitForPatchCore(
199199
env: Record<string, any>,
200200
repoPath: string,
201-
base: string,
201+
base: string | undefined,
202202
message: string,
203203
patch: string,
204204
): Promise<string> {
@@ -222,7 +222,14 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
222222
const tree = result.stdout.trim();
223223

224224
// Create new commit from the tree
225-
result = await this.git.exec({ cwd: repoPath, env: env }, 'commit-tree', tree, '-p', base, '-m', message);
225+
result = await this.git.exec(
226+
{ cwd: repoPath, env: env },
227+
'commit-tree',
228+
tree,
229+
...(base ? ['-p', base] : []),
230+
'-m',
231+
message,
232+
);
226233
const sha = result.stdout.trim();
227234

228235
return sha;
@@ -234,6 +241,16 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
234241
}
235242
}
236243

244+
async createEmptyInitialCommit(repoPath: string): Promise<string> {
245+
const emptyTree = await this.git.exec({ cwd: repoPath }, 'hash-object', '-t', 'tree', '/dev/null');
246+
const result = await this.git.exec({ cwd: repoPath }, 'commit-tree', emptyTree.stdout.trim(), '-m', 'temp');
247+
// create ref/heaads/main and point to it
248+
await this.git.exec({ cwd: repoPath }, 'update-ref', 'refs/heads/main', result.stdout.trim());
249+
// point HEAD to the branch
250+
await this.git.exec({ cwd: repoPath }, 'symbolic-ref', 'HEAD', 'refs/heads/main');
251+
return result.stdout.trim();
252+
}
253+
237254
@log({ args: { 1: false } })
238255
async validatePatch(repoPath: string | undefined, contents: string): Promise<boolean> {
239256
try {

src/env/node/git/sub-providers/staging.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
22
import { tmpdir } from 'os';
33
import type { Uri } from 'vscode';
44
import type { Container } from '../../../../container';
5+
import { GitErrorHandling } from '../../../../git/commandOptions';
56
import type { DisposableTemporaryGitIndex, GitStagingSubProvider } from '../../../../git/gitProvider';
67
import { splitPath } from '../../../../system/-webview/path';
78
import { log } from '../../../../system/decorators/log';
@@ -18,7 +19,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
1819
) {}
1920

2021
@log()
21-
async createTemporaryIndex(repoPath: string, base: string): Promise<DisposableTemporaryGitIndex> {
22+
async createTemporaryIndex(repoPath: string, base: string | undefined): Promise<DisposableTemporaryGitIndex> {
2223
// Create a temporary index file
2324
const tempDir = await fs.mkdtemp(joinPaths(tmpdir(), 'gl-'));
2425
const tempIndex = joinPaths(tempDir, 'index');
@@ -37,24 +38,27 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
3738
const env = { GIT_INDEX_FILE: tempIndex };
3839

3940
// Create the temp index file from a base ref/sha
41+
if (base) {
42+
// Get the tree of the base
43+
const newIndexResult = await this.git.exec(
44+
{ cwd: repoPath, env: env },
45+
'ls-tree',
46+
'-z',
47+
'-r',
48+
'--full-name',
49+
base,
50+
);
4051

41-
// Get the tree of the base
42-
const newIndexResult = await this.git.exec(
43-
{ cwd: repoPath, env: env },
44-
'ls-tree',
45-
'-z',
46-
'-r',
47-
'--full-name',
48-
base,
49-
);
50-
51-
// Write the tree to our temp index
52-
await this.git.exec(
53-
{ cwd: repoPath, env: env, stdin: newIndexResult.stdout },
54-
'update-index',
55-
'-z',
56-
'--index-info',
57-
);
52+
if (newIndexResult.stdout.trim()) {
53+
// Write the tree to our temp index
54+
await this.git.exec(
55+
{ cwd: repoPath, env: env, stdin: newIndexResult.stdout },
56+
'update-index',
57+
'-z',
58+
'--index-info',
59+
);
60+
}
61+
}
5862

5963
return mixinAsyncDisposable({ path: tempIndex, env: { GIT_INDEX_FILE: tempIndex } }, dispose);
6064
} catch (ex) {

src/git/gitProvider.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,10 @@ export interface GitPatchSubProvider {
534534
): Promise<GitCommit | undefined>;
535535
createUnreachableCommitsFromPatches(
536536
repoPath: string,
537-
base: string,
537+
base: string | undefined,
538538
patches: { message: string; patch: string }[],
539539
): Promise<string[]>;
540+
createEmptyInitialCommit(repoPath: string): Promise<string>;
540541

541542
validatePatch(repoPath: string | undefined, contents: string): Promise<boolean>;
542543
}
@@ -651,7 +652,7 @@ export interface DisposableTemporaryGitIndex extends UnifiedAsyncDisposable {
651652
}
652653

653654
export interface GitStagingSubProvider {
654-
createTemporaryIndex(repoPath: string, base: string): Promise<DisposableTemporaryGitIndex>;
655+
createTemporaryIndex(repoPath: string, base: string | undefined): Promise<DisposableTemporaryGitIndex>;
655656
stageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
656657
stageFiles(repoPath: string, pathOrUri: string[] | Uri[]): Promise<void>;
657658
stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;

src/git/models/revision.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export const deletedOrMissing = '0000000000000000000000000000000000000000-';
22
export const uncommitted = '0000000000000000000000000000000000000000';
33
export const uncommittedStaged = '0000000000000000000000000000000000000000:';
4+
// This is a root sha of all git repo's if using sha1
5+
export const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
46

57
export type GitRevisionRange =
68
| `${GitRevisionRangeNotation}${string}`

src/webviews/apps/plus/composer/components/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,7 @@ export class ComposerApp extends LitElement {
16301630
.canGenerateCommitsWithAI=${this.canGenerateCommitsWithAI}
16311631
.isPreviewMode=${this.isPreviewMode}
16321632
.baseCommit=${this.state.baseCommit}
1633+
.repoName=${this.state.baseCommit?.repoName ?? this.state.repositoryState?.current.name}
16331634
.customInstructions=${this.customInstructions}
16341635
.hasUsedAutoCompose=${this.state.hasUsedAutoCompose}
16351636
.hasChanges=${this.state.hasChanges}

src/webviews/apps/plus/composer/components/commit-item.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export class CommitItem extends LitElement {
8686
@property({ type: Boolean })
8787
first = false;
8888

89+
@property({ type: Boolean })
90+
last = false;
91+
8992
override connectedCallback() {
9093
super.connectedCallback?.();
9194
// Set the data attribute for sortable access
@@ -120,7 +123,7 @@ export class CommitItem extends LitElement {
120123
<div
121124
class="composer-item commit-item ${this.selected ? ' is-selected' : ''}${this.multiSelected
122125
? ' multi-selected'
123-
: ''}${this.first ? ' is-first' : ''}"
126+
: ''}${this.first ? ' is-first' : ''}${this.last ? ' is-last' : ''}"
124127
tabindex="0"
125128
@click=${this.handleClick}
126129
@keydown=${this.handleClick}

src/webviews/apps/plus/composer/components/commits-panel.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ export class CommitsPanel extends LitElement {
362362
@property({ type: Object })
363363
baseCommit: ComposerBaseCommit | null = null;
364364

365+
@property({ type: String })
366+
repoName: string | null = null;
367+
365368
@property({ type: String })
366369
customInstructions: string = '';
367370

@@ -1220,13 +1223,19 @@ export class CommitsPanel extends LitElement {
12201223
12211224
<!-- Base commit (informational only) -->
12221225
<div class="composer-item is-base">
1223-
<div class="composer-item__commit"></div>
1226+
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
12241227
<div class="composer-item__content">
1225-
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
1228+
<div
1229+
class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}"
1230+
>
1231+
${this.baseCommit?.message || 'No commits yet'}
1232+
</div>
12261233
<div class="composer-item__body">
1227-
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
1228-
<span>/</span>
1229-
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
1234+
<span class="repo-name">${this.repoName || 'Repository'}</span>
1235+
${this.baseCommit?.branchName
1236+
? html`<span>/</span
1237+
><span class="branch-name">${this.baseCommit.branchName}</span>`
1238+
: ''}
12301239
</div>
12311240
</div>
12321241
</div>
@@ -1279,6 +1288,7 @@ export class CommitsPanel extends LitElement {
12791288
.multiSelected=${this.selectedCommitIds.has(commit.id)}
12801289
.isPreviewMode=${this.isPreviewMode}
12811290
?first=${i === 0}
1291+
?last=${i === this.commits.length - 1 && !this.baseCommit}
12821292
@click=${(e: MouseEvent) => this.dispatchCommitSelect(commit.id, e)}
12831293
@keydown=${(e: KeyboardEvent) => this.dispatchCommitSelect(commit.id, e)}
12841294
></gl-commit-item>
@@ -1289,13 +1299,17 @@ export class CommitsPanel extends LitElement {
12891299
12901300
<!-- Base commit (informational only) -->
12911301
<div class="composer-item is-base">
1292-
<div class="composer-item__commit"></div>
1302+
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
12931303
<div class="composer-item__content">
1294-
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
1304+
<div class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}">
1305+
${this.baseCommit?.message || 'No commits yet'}
1306+
</div>
12951307
<div class="composer-item__body">
1296-
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
1297-
<span>/</span>
1298-
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
1308+
<span class="repo-name">${this.repoName || 'Repository'}</span>
1309+
${this.baseCommit?.branchName
1310+
? html`<span>/</span
1311+
><span class="branch-name">${this.baseCommit.branchName}</span>`
1312+
: ''}
12991313
</div>
13001314
</div>
13011315
</div>

src/webviews/apps/plus/composer/components/composer.css.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export const composerItemCommitStyles = css`
148148
height: 50%;
149149
}
150150
151+
.composer-item.is-last .composer-item__commit::before {
152+
display: none;
153+
}
154+
151155
.composer-item__commit::after {
152156
content: '';
153157
position: absolute;
@@ -168,6 +172,11 @@ export const composerItemCommitStyles = css`
168172
.composer-item.is-base .composer-item__commit::before {
169173
border-left-style: solid;
170174
}
175+
176+
.composer-item__commit.is-empty::before,
177+
.composer-item__commit.is-empty::after {
178+
display: none;
179+
}
171180
`;
172181

173182
export const composerItemContentStyles = css`

src/webviews/plus/composer/composerWebview.ts

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
RepositoryFileSystemChangeEvent,
1111
} from '../../../git/models/repository';
1212
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
13+
import { rootSha } from '../../../git/models/revision';
1314
import { sendFeedbackEvent, showUnhelpfulFeedbackPicker } from '../../../plus/ai/aiFeedbackUtils';
1415
import type { AIModelChangeEvent } from '../../../plus/ai/aiProviderService';
1516
import { getRepositoryPickerTitleAndPlaceholder, showRepositoryPicker } from '../../../quickpicks/repositoryPicker';
@@ -311,30 +312,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
311312
const { hunkMap, hunks } = createHunksFromDiffs(staged?.contents, unstaged?.contents);
312313

313314
const baseCommit = getSettledValue(commitResult);
314-
if (baseCommit == null) {
315-
const errorMessage = 'No base commit found to compose from.';
316-
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
317-
'failure.reason': 'error',
318-
'failure.error.message': errorMessage,
319-
});
320-
return {
321-
...this.initialState,
322-
loadingError: errorMessage,
323-
};
324-
}
325-
326315
const currentBranch = getSettledValue(branchResult);
327-
if (currentBranch == null) {
328-
const errorMessage = 'No current branch found to compose from.';
329-
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
330-
'failure.reason': 'error',
331-
'failure.error.message': errorMessage,
332-
});
333-
return {
334-
...this.initialState,
335-
loadingError: errorMessage,
336-
};
337-
}
338316

339317
// Create initial commit with empty message (user will add message later)
340318
const hasStagedChanges = Boolean(staged?.contents);
@@ -358,7 +336,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
358336
};
359337

360338
// Create safety state snapshot for validation
361-
const safetyState = await createSafetyState(repo, diffs, baseCommit.sha);
339+
const safetyState = await createSafetyState(repo, diffs, baseCommit?.sha);
362340

363341
const aiEnabled = this.getAiEnabled();
364342
const aiModel = await this.container.ai.getModel(
@@ -396,12 +374,14 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
396374
...this.initialState,
397375
hunks: hunks,
398376
hunkMap: hunkMap,
399-
baseCommit: {
400-
sha: baseCommit.sha,
401-
message: baseCommit.message ?? '',
402-
repoName: repo.name,
403-
branchName: currentBranch.name,
404-
},
377+
baseCommit: baseCommit
378+
? {
379+
sha: baseCommit.sha,
380+
message: baseCommit.message ?? '',
381+
repoName: repo.name,
382+
branchName: currentBranch?.name ?? 'main',
383+
}
384+
: null,
405385
commits: commits,
406386
safetyState: safetyState,
407387
aiEnabled: aiEnabled,
@@ -1087,8 +1067,23 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
10871067
throw new Error(errorMessage);
10881068
}
10891069

1070+
if (params.baseCommit?.sha == null) {
1071+
const initialCommitSha = await svc.patch?.createEmptyInitialCommit();
1072+
if (initialCommitSha == null) {
1073+
// error base we don't have an initial commit
1074+
this._context.errors.operation.count++;
1075+
this._context.operations.finishAndCommit.errorCount++;
1076+
const errorMessage = 'Could not create base commit';
1077+
this.sendTelemetryEvent('composer/action/finishAndCommit/failed', {
1078+
'failure.reason': 'error',
1079+
'failure.error.message': errorMessage,
1080+
});
1081+
throw new Error(errorMessage);
1082+
}
1083+
}
1084+
10901085
// Create unreachable commits from patches
1091-
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit.sha, diffInfo);
1086+
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit?.sha, diffInfo);
10921087

10931088
if (!shas?.length) {
10941089
this._context.errors.operation.count++;
@@ -1101,9 +1096,10 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
11011096
throw new Error(errorMessage);
11021097
}
11031098

1099+
const baseRef = params.baseCommit?.sha ?? ((await repo.git.commits.getCommit('HEAD')) ? 'HEAD' : rootSha);
11041100
const resultingDiff = (
1105-
await repo.git.diff.getDiff?.(shas[shas.length - 1], params.baseCommit.sha, {
1106-
notation: '...',
1101+
await repo.git.diff.getDiff?.(shas[shas.length - 1], baseRef, {
1102+
notation: params.baseCommit?.sha ? '...' : undefined,
11071103
})
11081104
)?.contents;
11091105

0 commit comments

Comments
 (0)