Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/env/node/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '../../../git/errors';
import type { GitDir } from '../../../git/gitProvider';
import type { GitDiffFilter } from '../../../git/models/diff';
import { rootSha } from '../../../git/models/revision';
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/utils/revision.utils';
import { getCancellationTokenId } from '../../../system/-webview/cancellation';
Expand Down Expand Up @@ -69,9 +70,6 @@ export const maxGitCliLength = 30000;

const textDecoder = new TextDecoder('utf8');

// This is a root sha of all git repo's if using sha1
const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';

export const GitErrors = {
alreadyCheckedOut: /already checked out/i,
alreadyExists: /already exists/i,
Expand Down
23 changes: 20 additions & 3 deletions src/env/node/git/sub-providers/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
@log<PatchGitSubProvider['createUnreachableCommitsFromPatches']>({ args: { 2: p => p.length } })
async createUnreachableCommitsFromPatches(
repoPath: string,
base: string,
base: string | undefined,
patches: { message: string; patch: string }[],
): Promise<string[]> {
// Create a temporary index file
Expand All @@ -198,7 +198,7 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
private async createUnreachableCommitForPatchCore(
env: Record<string, any>,
repoPath: string,
base: string,
base: string | undefined,
message: string,
patch: string,
): Promise<string> {
Expand All @@ -222,7 +222,14 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
const tree = result.stdout.trim();

// Create new commit from the tree
result = await this.git.exec({ cwd: repoPath, env: env }, 'commit-tree', tree, '-p', base, '-m', message);
result = await this.git.exec(
{ cwd: repoPath, env: env },
'commit-tree',
tree,
...(base ? ['-p', base] : []),
'-m',
message,
);
const sha = result.stdout.trim();

return sha;
Expand All @@ -234,6 +241,16 @@ export class PatchGitSubProvider implements GitPatchSubProvider {
}
}

async createEmptyInitialCommit(repoPath: string): Promise<string> {
const emptyTree = await this.git.exec({ cwd: repoPath }, 'hash-object', '-t', 'tree', '/dev/null');
const result = await this.git.exec({ cwd: repoPath }, 'commit-tree', emptyTree.stdout.trim(), '-m', 'temp');
// create ref/heaads/main and point to it
await this.git.exec({ cwd: repoPath }, 'update-ref', 'refs/heads/main', result.stdout.trim());
// point HEAD to the branch
await this.git.exec({ cwd: repoPath }, 'symbolic-ref', 'HEAD', 'refs/heads/main');
return result.stdout.trim();
}

@log({ args: { 1: false } })
async validatePatch(repoPath: string | undefined, contents: string): Promise<boolean> {
try {
Expand Down
40 changes: 22 additions & 18 deletions src/env/node/git/sub-providers/staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import type { Uri } from 'vscode';
import type { Container } from '../../../../container';
import { GitErrorHandling } from '../../../../git/commandOptions';
import type { DisposableTemporaryGitIndex, GitStagingSubProvider } from '../../../../git/gitProvider';
import { splitPath } from '../../../../system/-webview/path';
import { log } from '../../../../system/decorators/log';
Expand All @@ -18,7 +19,7 @@ export class StagingGitSubProvider implements GitStagingSubProvider {
) {}

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

// Create the temp index file from a base ref/sha
if (base) {
// Get the tree of the base
const newIndexResult = await this.git.exec(
{ cwd: repoPath, env: env },
'ls-tree',
'-z',
'-r',
'--full-name',
base,
);

// Get the tree of the base
const newIndexResult = await this.git.exec(
{ cwd: repoPath, env: env },
'ls-tree',
'-z',
'-r',
'--full-name',
base,
);

// Write the tree to our temp index
await this.git.exec(
{ cwd: repoPath, env: env, stdin: newIndexResult.stdout },
'update-index',
'-z',
'--index-info',
);
if (newIndexResult.stdout.trim()) {
// Write the tree to our temp index
await this.git.exec(
{ cwd: repoPath, env: env, stdin: newIndexResult.stdout },
'update-index',
'-z',
'--index-info',
);
}
}

return mixinAsyncDisposable({ path: tempIndex, env: { GIT_INDEX_FILE: tempIndex } }, dispose);
} catch (ex) {
Expand Down
5 changes: 3 additions & 2 deletions src/git/gitProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,10 @@ export interface GitPatchSubProvider {
): Promise<GitCommit | undefined>;
createUnreachableCommitsFromPatches(
repoPath: string,
base: string,
base: string | undefined,
patches: { message: string; patch: string }[],
): Promise<string[]>;
createEmptyInitialCommit(repoPath: string): Promise<string>;

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

export interface GitStagingSubProvider {
createTemporaryIndex(repoPath: string, base: string): Promise<DisposableTemporaryGitIndex>;
createTemporaryIndex(repoPath: string, base: string | undefined): Promise<DisposableTemporaryGitIndex>;
stageFile(repoPath: string, pathOrUri: string | Uri): Promise<void>;
stageFiles(repoPath: string, pathOrUri: string[] | Uri[]): Promise<void>;
stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise<void>;
Expand Down
2 changes: 2 additions & 0 deletions src/git/models/revision.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const deletedOrMissing = '0000000000000000000000000000000000000000-';
export const uncommitted = '0000000000000000000000000000000000000000';
export const uncommittedStaged = '0000000000000000000000000000000000000000:';
// This is a root sha of all git repo's if using sha1
export const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';

export type GitRevisionRange =
| `${GitRevisionRangeNotation}${string}`
Expand Down
1 change: 1 addition & 0 deletions src/webviews/apps/plus/composer/components/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,7 @@ export class ComposerApp extends LitElement {
.canGenerateCommitsWithAI=${this.canGenerateCommitsWithAI}
.isPreviewMode=${this.isPreviewMode}
.baseCommit=${this.state.baseCommit}
.repoName=${this.state.baseCommit?.repoName ?? this.state.repositoryState?.current.name}
.customInstructions=${this.customInstructions}
.hasUsedAutoCompose=${this.state.hasUsedAutoCompose}
.hasChanges=${this.state.hasChanges}
Expand Down
5 changes: 4 additions & 1 deletion src/webviews/apps/plus/composer/components/commit-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export class CommitItem extends LitElement {
@property({ type: Boolean })
first = false;

@property({ type: Boolean })
last = false;

override connectedCallback() {
super.connectedCallback?.();
// Set the data attribute for sortable access
Expand Down Expand Up @@ -120,7 +123,7 @@ export class CommitItem extends LitElement {
<div
class="composer-item commit-item ${this.selected ? ' is-selected' : ''}${this.multiSelected
? ' multi-selected'
: ''}${this.first ? ' is-first' : ''}"
: ''}${this.first ? ' is-first' : ''}${this.last ? ' is-last' : ''}"
tabindex="0"
@click=${this.handleClick}
@keydown=${this.handleClick}
Expand Down
34 changes: 24 additions & 10 deletions src/webviews/apps/plus/composer/components/commits-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ export class CommitsPanel extends LitElement {
@property({ type: Object })
baseCommit: ComposerBaseCommit | null = null;

@property({ type: String })
repoName: string | null = null;

@property({ type: String })
customInstructions: string = '';

Expand Down Expand Up @@ -1220,13 +1223,19 @@ export class CommitsPanel extends LitElement {

<!-- Base commit (informational only) -->
<div class="composer-item is-base">
<div class="composer-item__commit"></div>
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
<div class="composer-item__content">
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
<div
class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}"
>
${this.baseCommit?.message || 'No commits yet'}
</div>
<div class="composer-item__body">
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
<span>/</span>
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
<span class="repo-name">${this.repoName || 'Repository'}</span>
${this.baseCommit?.branchName
? html`<span>/</span
><span class="branch-name">${this.baseCommit.branchName}</span>`
: ''}
</div>
</div>
</div>
Expand Down Expand Up @@ -1279,6 +1288,7 @@ export class CommitsPanel extends LitElement {
.multiSelected=${this.selectedCommitIds.has(commit.id)}
.isPreviewMode=${this.isPreviewMode}
?first=${i === 0}
?last=${i === this.commits.length - 1 && !this.baseCommit}
@click=${(e: MouseEvent) => this.dispatchCommitSelect(commit.id, e)}
@keydown=${(e: KeyboardEvent) => this.dispatchCommitSelect(commit.id, e)}
></gl-commit-item>
Expand All @@ -1289,13 +1299,17 @@ export class CommitsPanel extends LitElement {

<!-- Base commit (informational only) -->
<div class="composer-item is-base">
<div class="composer-item__commit"></div>
<div class="composer-item__commit${this.baseCommit ? '' : ' is-empty'}"></div>
<div class="composer-item__content">
<div class="composer-item__header">${this.baseCommit?.message || 'HEAD'}</div>
<div class="composer-item__header${this.baseCommit == null ? ' is-placeholder' : ''}">
${this.baseCommit?.message || 'No commits yet'}
</div>
<div class="composer-item__body">
<span class="repo-name">${this.baseCommit?.repoName || 'Repository'}</span>
<span>/</span>
<span class="branch-name">${this.baseCommit?.branchName || 'main'}</span>
<span class="repo-name">${this.repoName || 'Repository'}</span>
${this.baseCommit?.branchName
? html`<span>/</span
><span class="branch-name">${this.baseCommit.branchName}</span>`
: ''}
</div>
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions src/webviews/apps/plus/composer/components/composer.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export const composerItemCommitStyles = css`
height: 50%;
}

.composer-item.is-last .composer-item__commit::before {
display: none;
}

.composer-item__commit::after {
content: '';
position: absolute;
Expand All @@ -168,6 +172,11 @@ export const composerItemCommitStyles = css`
.composer-item.is-base .composer-item__commit::before {
border-left-style: solid;
}

.composer-item__commit.is-empty::before,
.composer-item__commit.is-empty::after {
display: none;
}
`;

export const composerItemContentStyles = css`
Expand Down
62 changes: 29 additions & 33 deletions src/webviews/plus/composer/composerWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
RepositoryFileSystemChangeEvent,
} from '../../../git/models/repository';
import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/models/repository';
import { rootSha } from '../../../git/models/revision';
import { sendFeedbackEvent, showUnhelpfulFeedbackPicker } from '../../../plus/ai/aiFeedbackUtils';
import type { AIModelChangeEvent } from '../../../plus/ai/aiProviderService';
import { getRepositoryPickerTitleAndPlaceholder, showRepositoryPicker } from '../../../quickpicks/repositoryPicker';
Expand Down Expand Up @@ -311,30 +312,7 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
const { hunkMap, hunks } = createHunksFromDiffs(staged?.contents, unstaged?.contents);

const baseCommit = getSettledValue(commitResult);
if (baseCommit == null) {
const errorMessage = 'No base commit found to compose from.';
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
'failure.reason': 'error',
'failure.error.message': errorMessage,
});
return {
...this.initialState,
loadingError: errorMessage,
};
}

const currentBranch = getSettledValue(branchResult);
if (currentBranch == null) {
const errorMessage = 'No current branch found to compose from.';
this.sendTelemetryEvent(isReload ? 'composer/reloaded' : 'composer/loaded', {
'failure.reason': 'error',
'failure.error.message': errorMessage,
});
return {
...this.initialState,
loadingError: errorMessage,
};
}

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

// Create safety state snapshot for validation
const safetyState = await createSafetyState(repo, diffs, baseCommit.sha);
const safetyState = await createSafetyState(repo, diffs, baseCommit?.sha);

const aiEnabled = this.getAiEnabled();
const aiModel = await this.container.ai.getModel(
Expand Down Expand Up @@ -396,12 +374,14 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
...this.initialState,
hunks: hunks,
hunkMap: hunkMap,
baseCommit: {
sha: baseCommit.sha,
message: baseCommit.message ?? '',
repoName: repo.name,
branchName: currentBranch.name,
},
baseCommit: baseCommit
? {
sha: baseCommit.sha,
message: baseCommit.message ?? '',
repoName: repo.name,
branchName: currentBranch?.name ?? 'main',
}
: null,
commits: commits,
safetyState: safetyState,
aiEnabled: aiEnabled,
Expand Down Expand Up @@ -1087,8 +1067,23 @@ export class ComposerWebviewProvider implements WebviewProvider<State, State, Co
throw new Error(errorMessage);
}

if (params.baseCommit?.sha == null) {
const initialCommitSha = await svc.patch?.createEmptyInitialCommit();
if (initialCommitSha == null) {
// error base we don't have an initial commit
this._context.errors.operation.count++;
this._context.operations.finishAndCommit.errorCount++;
const errorMessage = 'Could not create base commit';
this.sendTelemetryEvent('composer/action/finishAndCommit/failed', {
'failure.reason': 'error',
'failure.error.message': errorMessage,
});
throw new Error(errorMessage);
}
}

// Create unreachable commits from patches
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit.sha, diffInfo);
const shas = await repo.git.patch?.createUnreachableCommitsFromPatches(params.baseCommit?.sha, diffInfo);

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

const baseRef = params.baseCommit?.sha ?? ((await repo.git.commits.getCommit('HEAD')) ? 'HEAD' : rootSha);
const resultingDiff = (
await repo.git.diff.getDiff?.(shas[shas.length - 1], params.baseCommit.sha, {
notation: '...',
await repo.git.diff.getDiff?.(shas[shas.length - 1], baseRef, {
notation: params.baseCommit?.sha ? '...' : undefined,
})
)?.contents;

Expand Down
Loading