Skip to content

Commit 0578fd7

Browse files
Adds support for composing without a base commit
1 parent 558cce1 commit 0578fd7

File tree

12 files changed

+125
-89
lines changed

12 files changed

+125
-89
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[], options?: { intentToAdd?: boolean }): 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
@@ -1582,6 +1582,7 @@ export class ComposerApp extends LitElement {
15821582
.canGenerateCommitsWithAI=${this.canGenerateCommitsWithAI}
15831583
.isPreviewMode=${this.isPreviewMode}
15841584
.baseCommit=${this.state.baseCommit}
1585+
.repoName=${this.state.baseCommit?.repoName ?? this.state.repositoryState?.current.name}
15851586
.customInstructions=${this.customInstructions}
15861587
.hasUsedAutoCompose=${this.state.hasUsedAutoCompose}
15871588
.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
@@ -371,6 +371,9 @@ export class CommitsPanel extends LitElement {
371371
@property({ type: Object })
372372
baseCommit: ComposerBaseCommit | null = null;
373373

374+
@property({ type: String })
375+
repoName: string | null = null;
376+
374377
@property({ type: String })
375378
customInstructions: string = '';
376379

@@ -1232,13 +1235,19 @@ export class CommitsPanel extends LitElement {
12321235
12331236
<!-- Base commit (informational only) -->
12341237
<div class="composer-item is-base">
1235-
<div class="composer-item__commit"></div>
1238+
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
12361239
<div class="composer-item__content">
1237-
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
1240+
<div
1241+
class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}"
1242+
>
1243+
${this.baseCommit?.message || 'No commits yet'}
1244+
</div>
12381245
<div class="composer-item__body">
1239-
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
1240-
<span>/</span>
1241-
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
1246+
<span class="repo-name">${this.repoName || 'Repository'}</span>
1247+
${this.baseCommit?.branchName
1248+
? html`<span>/</span
1249+
><span class="branch-name">${this.baseCommit.branchName}</span>`
1250+
: ''}
12421251
</div>
12431252
</div>
12441253
</div>
@@ -1291,6 +1300,7 @@ export class CommitsPanel extends LitElement {
12911300
.multiSelected=${this.selectedCommitIds.has(commit.id)}
12921301
.isPreviewMode=${this.isPreviewMode}
12931302
?first=${i === 0}
1303+
?last=${i === this.commits.length - 1 && !this.baseCommit}
12941304
@click=${(e: MouseEvent) => this.dispatchCommitSelect(commit.id, e)}
12951305
@keydown=${(e: KeyboardEvent) => this.dispatchCommitSelect(commit.id, e)}
12961306
></gl-commit-item>
@@ -1301,13 +1311,17 @@ export class CommitsPanel extends LitElement {
13011311
13021312
<!-- Base commit (informational only) -->
13031313
<div class="composer-item is-base">
1304-
<div class="composer-item__commit"></div>
1314+
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
13051315
<div class="composer-item__content">
1306-
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
1316+
<div class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}">
1317+
${this.baseCommit?.message || 'No commits yet'}
1318+
</div>
13071319
<div class="composer-item__body">
1308-
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
1309-
<span>/</span>
1310-
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
1320+
<span class="repo-name">${this.repoName || 'Repository'}</span>
1321+
${this.baseCommit?.branchName
1322+
? html`<span>/</span
1323+
><span class="branch-name">${this.baseCommit.branchName}</span>`
1324+
: ''}
13111325
</div>
13121326
</div>
13131327
</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';
@@ -347,30 +348,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
347348
this._hunks = hunks;
348349

349350
const baseCommit = getSettledValue(commitResult);
350-
if (baseCommit == null) {
351-
const errorMessage = 'No base commit found to compose from.';
352-
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
353-
'failure.reason': 'error',
354-
'failure.error.message': errorMessage,
355-
});
356-
return {
357-
...this.initialState,
358-
loadingError: errorMessage,
359-
};
360-
}
361-
362351
const currentBranch = getSettledValue(branchResult);
363-
if (currentBranch == null) {
364-
const errorMessage = 'No current branch found to compose from.';
365-
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
366-
'failure.reason': 'error',
367-
'failure.error.message': errorMessage,
368-
});
369-
return {
370-
...this.initialState,
371-
loadingError: errorMessage,
372-
};
373-
}
374352

375353
// Create initial commit with empty message (user will add message later)
376354
const hasStagedChanges = Boolean(staged?.contents);
@@ -394,7 +372,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
394372
};
395373

396374
// Create safety state snapshot for validation
397-
const safetyState = await createSafetyState(repo, diffs, baseCommit.sha);
375+
const safetyState = await createSafetyState(repo, diffs, baseCommit?.sha);
398376
this._safetyState = safetyState;
399377

400378
const aiEnabled = this.getAiEnabled();
@@ -432,12 +410,14 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
432410
return {
433411
...this.initialState,
434412
hunks: hunks,
435-
baseCommit: {
436-
sha: baseCommit.sha,
437-
message: baseCommit.message ?? '',
438-
repoName: repo.name,
439-
branchName: currentBranch.name,
440-
},
413+
baseCommit: baseCommit
414+
? {
415+
sha: baseCommit.sha,
416+
message: baseCommit.message ?? '',
417+
repoName: repo.name,
418+
branchName: currentBranch?.name ?? 'main',
419+
}
420+
: null,
441421
commits: commits,
442422
aiEnabled: aiEnabled,
443423
ai: {
@@ -1162,8 +1142,23 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
11621142
throw new Error(errorMessage);
11631143
}
11641144

1145+
if (params.baseCommit?.sha == null) {
1146+
const initialCommitSha = await svc.patch?.createEmptyInitialCommit();
1147+
if (initialCommitSha == null) {
1148+
// error base we don't have an initial commit
1149+
this._context.errors.operation.count++;
1150+
this._context.operations.finishAndCommit.errorCount++;
1151+
const errorMessage = 'Could not create base commit';
1152+
this.sendTelemetryEvent('composer/action/finishAndCommit/failed', {
1153+
'failure.reason': 'error',
1154+
'failure.error.message': errorMessage,
1155+
});
1156+
throw new Error(errorMessage);
1157+
}
1158+
}
1159+
11651160
// Create unreachable commits from patches
1166-
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit.sha, diffInfo);
1161+
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit?.sha, diffInfo);
11671162

11681163
if (!shas?.length) {
11691164
this._context.errors.operation.count++;
@@ -1176,9 +1171,10 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
11761171
throw new Error(errorMessage);
11771172
}
11781173

1174+
const baseRef = params.baseCommit?.sha ?? ((await repo.git.commits.getCommit('HEAD')) ? 'HEAD' : rootSha);
11791175
const resultingDiff = (
1180-
await repo.git.diff.getDiff?.(shas[shas.length - 1], params.baseCommit.sha, {
1181-
notation: '...',
1176+
await repo.git.diff.getDiff?.(shas[shas.length - 1], baseRef, {
1177+
notation: params.baseCommit?.sha ? '...' : undefined,
11821178
})
11831179
)?.contents;
11841180

0 commit comments

Comments
 (0)