Skip to content

Commit a5465b5

Browse files
authored
Merge branch 'main' into osortega/chat-sessions-view
2 parents 39d63ce + b5c5403 commit a5465b5

File tree

110 files changed

+2495
-585
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+2495
-585
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm
1+
FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm
22

33
ADD install-vscode.sh /root/
44
RUN /root/install-vscode.sh

cli/src/commands/serve_web.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result<i3
127127
};
128128
let builder = Server::try_bind(&addr).map_err(CodeError::CouldNotListenOnInterface)?;
129129

130-
let mut listening = format!("Web UI available at http://{addr}");
130+
// Get the actual bound address (important when port 0 is used for random port assignment)
131+
let bound_addr = builder.local_addr();
132+
let mut listening = format!("Web UI available at http://{bound_addr}");
131133
if let Some(base) = args.server_base_path {
132134
if !base.starts_with('/') {
133135
listening.push('/');

extensions/git/package.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,6 +1335,14 @@
13351335
"command": "git.deleteWorktree",
13361336
"when": "false"
13371337
},
1338+
{
1339+
"command": "git.openWorktree",
1340+
"when": "false"
1341+
},
1342+
{
1343+
"command": "git.openWorktreeInNewWindow",
1344+
"when": "false"
1345+
},
13381346
{
13391347
"command": "git.deleteWorktreeFromPalette",
13401348
"when": "config.git.enabled && !git.missing && gitOpenRepositoryCount > 1"
@@ -1690,13 +1698,13 @@
16901698
},
16911699
{
16921700
"command": "git.openWorktree",
1693-
"group": "worktree@1",
1694-
"when": "scmProvider == git && gitOpenRepositoryCount > 1"
1701+
"group": "1_worktree@1",
1702+
"when": "scmProvider == git && scmProviderContext == worktree"
16951703
},
16961704
{
16971705
"command": "git.openWorktreeInNewWindow",
1698-
"group": "worktree@2",
1699-
"when": "scmProvider == git && gitOpenRepositoryCount > 1"
1706+
"group": "1_worktree@2",
1707+
"when": "scmProvider == git && scmProviderContext == worktree"
17001708
}
17011709
],
17021710
"scm/resourceGroup/context": [
@@ -3071,7 +3079,7 @@
30713079
"git.detectWorktrees": {
30723080
"type": "boolean",
30733081
"scope": "resource",
3074-
"default": false,
3082+
"default": true,
30753083
"description": "%config.detectWorktrees%"
30763084
},
30773085
"git.detectWorktreesLimit": {

extensions/git/src/api/git.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,5 +441,6 @@ export const enum GitErrorCodes {
441441
TagConflict = 'TagConflict',
442442
CherryPickEmpty = 'CherryPickEmpty',
443443
CherryPickConflict = 'CherryPickConflict',
444-
WorktreeContainsChanges = 'WorktreeContainsChanges'
444+
WorktreeContainsChanges = 'WorktreeContainsChanges',
445+
WorktreeAlreadyExists = 'WorktreeAlreadyExists'
445446
}

extensions/git/src/askpass.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode';
7-
import { IDisposable, EmptyDisposable, toDisposable } from './util';
7+
import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util';
88
import * as path from 'path';
99
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
1010
import { CredentialsProvider, Credentials } from './api/git';
@@ -108,7 +108,7 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
108108
if (/passphrase/i.test(request)) {
109109
// Commit signing - Enter passphrase:
110110
// Git operation - Enter passphrase for key '/c/Users/<username>/.ssh/id_ed25519':
111-
const file = argv[6]?.replace(/^["']+|["':]+$/g, '');
111+
const file = extractFilePathFromArgs(argv, 6);
112112

113113
this.logger.trace(`[Askpass][handleSSHAskpass] request: ${request}, file: ${file}`);
114114

extensions/git/src/commands.ts

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ class StashItem implements QuickPickItem {
340340

341341
interface ScmCommandOptions {
342342
repository?: boolean;
343+
repositoryFilter?: ('repository' | 'submodule' | 'worktree')[];
343344
}
344345

345346
interface ScmCommand {
@@ -1681,7 +1682,11 @@ export class CommandCenter {
16811682

16821683
@command('git.diff.stageHunk')
16831684
async diffStageHunk(changes: DiffEditorSelectionHunkToolbarContext | undefined): Promise<void> {
1684-
this.diffStageHunkOrSelection(changes);
1685+
if (changes) {
1686+
this.diffStageHunkOrSelection(changes);
1687+
} else {
1688+
await this.stageHunkAtCursor();
1689+
}
16851690
}
16861691

16871692
@command('git.diff.stageSelection')
@@ -1719,6 +1724,36 @@ export class CommandCenter {
17191724
await repository.stage(resource, result, modifiedDocument.encoding));
17201725
}
17211726

1727+
private async stageHunkAtCursor(): Promise<void> {
1728+
const textEditor = window.activeTextEditor;
1729+
1730+
if (!textEditor) {
1731+
return;
1732+
}
1733+
1734+
const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor);
1735+
if (!workingTreeDiffInformation) {
1736+
return;
1737+
}
1738+
1739+
const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation);
1740+
const modifiedDocument = textEditor.document;
1741+
const cursorPosition = textEditor.selection.active;
1742+
1743+
// Find the hunk that contains the cursor position
1744+
const hunkAtCursor = workingTreeLineChanges.find(change => {
1745+
const hunkRange = getModifiedRange(modifiedDocument, change);
1746+
return hunkRange.contains(cursorPosition);
1747+
});
1748+
1749+
if (!hunkAtCursor) {
1750+
window.showInformationMessage(l10n.t('No hunk found at cursor position.'));
1751+
return;
1752+
}
1753+
1754+
await this._stageChanges(textEditor, [hunkAtCursor]);
1755+
}
1756+
17221757
@command('git.stageSelectedRanges')
17231758
async stageSelectedChanges(): Promise<void> {
17241759
const textEditor = window.activeTextEditor;
@@ -2882,10 +2917,15 @@ export class CommandCenter {
28822917
try {
28832918
await item.run(repository, opts);
28842919
} catch (err) {
2885-
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) {
2920+
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree && err.gitErrorCode !== GitErrorCodes.WorktreeAlreadyExists) {
28862921
throw err;
28872922
}
28882923

2924+
if (err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
2925+
this.handleWorktreeError(err);
2926+
return false;
2927+
}
2928+
28892929
const stash = l10n.t('Stash & Checkout');
28902930
const migrate = l10n.t('Migrate Changes');
28912931
const force = l10n.t('Force Checkout');
@@ -3351,33 +3391,45 @@ export class CommandCenter {
33513391
}
33523392
}
33533393

3354-
@command('git.createWorktree', { repository: true })
3394+
@command('git.createWorktree', { repository: true, repositoryFilter: ['repository', 'submodule'] })
33553395
async createWorktree(repository: Repository): Promise<void> {
3356-
await this._createWorktree(repository, undefined, undefined);
3396+
await this._createWorktree(repository);
33573397
}
33583398

3359-
private async _createWorktree(repository: Repository, worktreePath?: string, name?: string): Promise<void> {
3399+
private async _createWorktree(repository: Repository, worktreePath?: string, name?: string, newBranch?: boolean): Promise<void> {
33603400
const config = workspace.getConfiguration('git');
33613401
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
33623402

33633403
if (!name) {
3404+
const createBranch = new CreateBranchItem();
33643405
const getBranchPicks = async () => {
3365-
const refs = await repository.getRefs({
3366-
pattern: 'refs/heads',
3367-
includeCommitDetails: showRefDetails
3368-
});
3369-
const processors = [new RefProcessor(RefType.Head, BranchItem)];
3370-
const itemsProcessor = new RefItemsProcessor(repository, processors);
3371-
return itemsProcessor.processRefs(refs);
3406+
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
3407+
const itemsProcessor = new RefItemsProcessor(repository, [
3408+
new RefProcessor(RefType.Head),
3409+
new RefProcessor(RefType.RemoteHead),
3410+
new RefProcessor(RefType.Tag)
3411+
]);
3412+
const branchItems = itemsProcessor.processRefs(refs);
3413+
return [createBranch, { label: '', kind: QuickPickItemKind.Separator }, ...branchItems];
33723414
};
33733415

33743416
const placeHolder = l10n.t('Select a branch to create the new worktree from');
33753417
const choice = await this.pickRef(getBranchPicks(), placeHolder);
33763418

3377-
if (!(choice instanceof BranchItem) || !choice.refName) {
3419+
if (choice === createBranch) {
3420+
const branchName = await this.promptForBranchName(repository);
3421+
3422+
if (!branchName) {
3423+
return;
3424+
}
3425+
3426+
newBranch = true;
3427+
name = branchName;
3428+
} else if (choice instanceof BranchItem && choice.refName) {
3429+
name = choice.refName;
3430+
} else {
33783431
return;
33793432
}
3380-
name = choice.refName;
33813433
}
33823434

33833435
const disposables: Disposable[] = [];
@@ -3395,6 +3447,10 @@ export class CommandCenter {
33953447
dispose(disposables);
33963448
inputBox.dispose();
33973449

3450+
if (!worktreeName) {
3451+
return;
3452+
}
3453+
33983454
// Default to view parent directory of repository root
33993455
const defaultUri = Uri.file(path.dirname(repository.root));
34003456

@@ -3416,10 +3472,48 @@ export class CommandCenter {
34163472

34173473
worktreePath = path.join(uris[0].fsPath, worktreeName);
34183474

3419-
await repository.worktree({
3420-
name: name,
3421-
path: worktreePath,
3422-
});
3475+
try {
3476+
await repository.worktree({ name: name, path: worktreePath, newBranch: newBranch });
3477+
} catch (err) {
3478+
if (err.gitErrorCode !== GitErrorCodes.WorktreeAlreadyExists) {
3479+
throw err;
3480+
}
3481+
3482+
this.handleWorktreeError(err);
3483+
return;
3484+
3485+
}
3486+
}
3487+
3488+
private async handleWorktreeError(err: any): Promise<void> {
3489+
const errorMessage = err.stderr;
3490+
const match = errorMessage.match(/worktree at '([^']+)'/) || errorMessage.match(/'([^']+)'/);
3491+
const path = match ? match[1] : undefined;
3492+
3493+
if (!path) {
3494+
return;
3495+
}
3496+
3497+
const worktreeRepository = this.model.getRepository(path) || this.model.getRepository(Uri.file(path));
3498+
3499+
if (!worktreeRepository) {
3500+
return;
3501+
}
3502+
3503+
const openWorktree = l10n.t('Open in current window');
3504+
const openWorktreeInNewWindow = l10n.t('Open in new window');
3505+
const message = l10n.t(errorMessage);
3506+
const choice = await window.showWarningMessage(message, { modal: true }, openWorktree, openWorktreeInNewWindow);
3507+
3508+
3509+
3510+
if (choice === openWorktree) {
3511+
await this.openWorktreeInCurrentWindow(worktreeRepository);
3512+
} else if (choice === openWorktreeInNewWindow) {
3513+
await this.openWorktreeInNewWindow(worktreeRepository);
3514+
}
3515+
3516+
return;
34233517
}
34243518

34253519
@command('git.deleteWorktree', { repository: true })
@@ -3456,18 +3550,10 @@ export class CommandCenter {
34563550
}
34573551
}
34583552

3459-
@command('git.deleteWorktreeFromPalette')
3460-
async deleteWorktreeFromPalette(): Promise<void> {
3461-
const mainRepository = this.model.repositories.find(repo =>
3462-
!repo.dotGit.commonPath
3463-
);
3464-
3465-
if (!mainRepository) {
3466-
return;
3467-
}
3468-
3553+
@command('git.deleteWorktreeFromPalette', { repository: true, repositoryFilter: ['repository', 'submodule'] })
3554+
async deleteWorktreeFromPalette(repository: Repository): Promise<void> {
34693555
const worktreePicks = async (): Promise<WorktreeDeleteItem[] | QuickPickItem[]> => {
3470-
const worktrees = await mainRepository.getWorktrees();
3556+
const worktrees = await repository.getWorktrees();
34713557
return worktrees.length === 0
34723558
? [{ label: l10n.t('$(info) This repository has no worktrees.') }]
34733559
: worktrees.map(worktree => new WorktreeDeleteItem(worktree));
@@ -3477,14 +3563,13 @@ export class CommandCenter {
34773563
const choice = await this.pickRef<WorktreeDeleteItem | QuickPickItem>(worktreePicks(), placeHolder);
34783564

34793565
if (choice instanceof WorktreeDeleteItem) {
3480-
await choice.run(mainRepository);
3566+
await choice.run(repository);
34813567
}
34823568
}
34833569

34843570
@command('git.openWorktree', { repository: true })
3485-
async openWorktreeInCurrentWindow(repository: Repository, ...args: SourceControl[]): Promise<void> {
3486-
// If multiple repositories are selected, no action is taken
3487-
if (args.length > 0) {
3571+
async openWorktreeInCurrentWindow(repository: Repository): Promise<void> {
3572+
if (!repository) {
34883573
return;
34893574
}
34903575

@@ -3493,9 +3578,8 @@ export class CommandCenter {
34933578
}
34943579

34953580
@command('git.openWorktreeInNewWindow', { repository: true })
3496-
async openWorktreeInNewWindow(repository: Repository, ...args: SourceControl[]): Promise<void> {
3497-
// If multiple repositories are selected, no action is taken
3498-
if (args.length > 0) {
3581+
async openWorktreeInNewWindow(repository: Repository): Promise<void> {
3582+
if (!repository) {
34993583
return;
35003584
}
35013585

@@ -4863,10 +4947,8 @@ export class CommandCenter {
48634947

48644948
if (repository) {
48654949
repositoryPromise = Promise.resolve(repository);
4866-
} else if (this.model.repositories.length === 1) {
4867-
repositoryPromise = Promise.resolve(this.model.repositories[0]);
48684950
} else {
4869-
repositoryPromise = this.model.pickRepository();
4951+
repositoryPromise = this.model.pickRepository(options.repositoryFilter);
48704952
}
48714953

48724954
result = repositoryPromise.then(repository => {

extensions/git/src/git.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ function getGitErrorCode(stderr: string): string | undefined {
351351
return GitErrorCodes.NotASafeGitRepository;
352352
} else if (/contains modified or untracked files|use --force to delete it/.test(stderr)) {
353353
return GitErrorCodes.WorktreeContainsChanges;
354+
} else if (/is already used by worktree at|already exists/.test(stderr)) {
355+
return GitErrorCodes.WorktreeAlreadyExists;
354356
}
355357

356358
return undefined;
@@ -2035,8 +2037,19 @@ export class Repository {
20352037
await this.exec(args);
20362038
}
20372039

2038-
async worktree(options: { path: string; name: string }): Promise<void> {
2039-
const args = ['worktree', 'add', options.path, options.name];
2040+
async worktree(options: { path: string; name: string; newBranch?: boolean }): Promise<void> {
2041+
const args = ['worktree', 'add'];
2042+
2043+
if (options?.newBranch) {
2044+
args.push('-b', options.name);
2045+
}
2046+
2047+
args.push(options.path);
2048+
2049+
if (!options.newBranch) {
2050+
args.push(options.name);
2051+
}
2052+
20402053
await this.exec(args);
20412054
}
20422055

0 commit comments

Comments
 (0)