mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 02:28:34 +01:00
Merge branch 'master' into pr/99324
This commit is contained in:
@@ -9,11 +9,10 @@ import * as os from 'os';
|
||||
import * as cp from 'child_process';
|
||||
import * as which from 'which';
|
||||
import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import * as iconv from 'iconv-lite-umd';
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
|
||||
import { CancellationToken, Progress, Uri } from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git';
|
||||
import * as byline from 'byline';
|
||||
@@ -139,18 +138,28 @@ function findGitWin32(onLookup: (path: string) => void): Promise<IGit> {
|
||||
.then(undefined, () => findGitWin32InPath(onLookup));
|
||||
}
|
||||
|
||||
export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> {
|
||||
const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null);
|
||||
export async function findGit(hint: string | string[] | undefined, onLookup: (path: string) => void): Promise<IGit> {
|
||||
const hints = Array.isArray(hint) ? hint : hint ? [hint] : [];
|
||||
|
||||
return first
|
||||
.then(undefined, () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return findGitDarwin(onLookup);
|
||||
case 'win32': return findGitWin32(onLookup);
|
||||
default: return findSpecificGit('git', onLookup);
|
||||
}
|
||||
})
|
||||
.then(null, () => Promise.reject(new Error('Git installation not found.')));
|
||||
for (const hint of hints) {
|
||||
try {
|
||||
return await findSpecificGit(hint, onLookup);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return await findGitDarwin(onLookup);
|
||||
case 'win32': return await findGitWin32(onLookup);
|
||||
default: return await findSpecificGit('git', onLookup);
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
throw new Error('Git installation not found.');
|
||||
}
|
||||
|
||||
export interface IExecutionResult<T extends string | Buffer> {
|
||||
@@ -251,6 +260,7 @@ export interface IGitErrorData {
|
||||
exitCode?: number;
|
||||
gitErrorCode?: string;
|
||||
gitCommand?: string;
|
||||
gitArgs?: string[];
|
||||
}
|
||||
|
||||
export class GitError {
|
||||
@@ -262,6 +272,7 @@ export class GitError {
|
||||
exitCode?: number;
|
||||
gitErrorCode?: string;
|
||||
gitCommand?: string;
|
||||
gitArgs?: string[];
|
||||
|
||||
constructor(data: IGitErrorData) {
|
||||
if (data.error) {
|
||||
@@ -278,6 +289,7 @@ export class GitError {
|
||||
this.exitCode = data.exitCode;
|
||||
this.gitErrorCode = data.gitErrorCode;
|
||||
this.gitCommand = data.gitCommand;
|
||||
this.gitArgs = data.gitArgs;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
@@ -341,6 +353,12 @@ function sanitizePath(path: string): string {
|
||||
|
||||
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B';
|
||||
|
||||
export interface ICloneOptions {
|
||||
readonly parentPath: string;
|
||||
readonly progress: Progress<{ increment: number }>;
|
||||
readonly recursive?: boolean;
|
||||
}
|
||||
|
||||
export class Git {
|
||||
|
||||
readonly path: string;
|
||||
@@ -363,18 +381,18 @@ export class Git {
|
||||
return;
|
||||
}
|
||||
|
||||
async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise<string> {
|
||||
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
|
||||
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
|
||||
let folderName = baseFolderName;
|
||||
let folderPath = path.join(parentPath, folderName);
|
||||
let folderPath = path.join(options.parentPath, folderName);
|
||||
let count = 1;
|
||||
|
||||
while (count < 20 && await new Promise(c => exists(folderPath, c))) {
|
||||
folderName = `${baseFolderName}-${count++}`;
|
||||
folderPath = path.join(parentPath, folderName);
|
||||
folderPath = path.join(options.parentPath, folderName);
|
||||
}
|
||||
|
||||
await mkdirp(parentPath);
|
||||
await mkdirp(options.parentPath);
|
||||
|
||||
const onSpawn = (child: cp.ChildProcess) => {
|
||||
const decoder = new StringDecoder('utf8');
|
||||
@@ -398,14 +416,18 @@ export class Git {
|
||||
}
|
||||
|
||||
if (totalProgress !== previousProgress) {
|
||||
progress.report({ increment: totalProgress - previousProgress });
|
||||
options.progress.report({ increment: totalProgress - previousProgress });
|
||||
previousProgress = totalProgress;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn });
|
||||
let command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'];
|
||||
if (options.recursive) {
|
||||
command.push('--recursive');
|
||||
}
|
||||
await this.exec(options.parentPath, command, { cancellationToken, onSpawn });
|
||||
} catch (err) {
|
||||
if (err.stderr) {
|
||||
err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
|
||||
@@ -419,10 +441,10 @@ export class Git {
|
||||
}
|
||||
|
||||
async getRepositoryRoot(repositoryPath: string): Promise<string> {
|
||||
const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']);
|
||||
const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel'], { log: false });
|
||||
|
||||
// Keep trailing spaces which are part of the directory name
|
||||
const repoPath = path.normalize(result.stdout.trimLeft().replace(/(\r\n|\r|\n)+$/, ''));
|
||||
const repoPath = path.normalize(result.stdout.trimLeft().replace(/[\r\n]+$/, ''));
|
||||
|
||||
if (isWindows) {
|
||||
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive.
|
||||
@@ -435,10 +457,9 @@ export class Git {
|
||||
const [, letter] = match;
|
||||
|
||||
try {
|
||||
const networkPath = await new Promise<string>(resolve =>
|
||||
const networkPath = await new Promise<string | undefined>(resolve =>
|
||||
realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) =>
|
||||
// eslint-disable-next-line eqeqeq
|
||||
resolve(err != null ? undefined : resolvedPath),
|
||||
resolve(err !== null ? undefined : resolvedPath),
|
||||
),
|
||||
);
|
||||
if (networkPath !== undefined) {
|
||||
@@ -517,7 +538,8 @@ export class Git {
|
||||
stderr: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
gitErrorCode: getGitErrorCode(result.stderr),
|
||||
gitCommand: args[0]
|
||||
gitCommand: args[0],
|
||||
gitArgs: args
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -670,7 +692,7 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line);
|
||||
const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line);
|
||||
|
||||
if (!propertyMatch) {
|
||||
return;
|
||||
@@ -905,7 +927,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
async buffer(object: string): Promise<Buffer> {
|
||||
const child = this.stream(['show', object]);
|
||||
const child = this.stream(['show', '--textconv', object]);
|
||||
|
||||
if (!child.stdout) {
|
||||
return Promise.reject<Buffer>('Can\'t open file from git');
|
||||
@@ -978,7 +1000,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
|
||||
const child = await this.stream(['show', object]);
|
||||
const child = await this.stream(['show', '--textconv', object]);
|
||||
const buffer = await readBytes(child.stdout!, 4100);
|
||||
|
||||
try {
|
||||
@@ -1146,7 +1168,7 @@ export class Repository {
|
||||
break;
|
||||
}
|
||||
|
||||
const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
|
||||
const originalUri = Uri.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
|
||||
let status: Status = Status.UNTRACKED;
|
||||
|
||||
// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
|
||||
@@ -1174,7 +1196,7 @@ export class Repository {
|
||||
break;
|
||||
}
|
||||
|
||||
const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
|
||||
const uri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
|
||||
result.push({
|
||||
uri,
|
||||
renameUri: uri,
|
||||
@@ -1224,7 +1246,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
if (paths && paths.length) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
@@ -1277,13 +1299,17 @@ export class Repository {
|
||||
await this.run(['update-index', add, '--cacheinfo', mode, hash, path]);
|
||||
}
|
||||
|
||||
async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
|
||||
async checkout(treeish: string, paths: string[], opts: { track?: boolean, detached?: boolean } = Object.create(null)): Promise<void> {
|
||||
const args = ['checkout', '-q'];
|
||||
|
||||
if (opts.track) {
|
||||
args.push('--track');
|
||||
}
|
||||
|
||||
if (opts.detached) {
|
||||
args.push('--detach');
|
||||
}
|
||||
|
||||
if (treeish) {
|
||||
args.push(treeish);
|
||||
}
|
||||
@@ -1299,6 +1325,7 @@ export class Repository {
|
||||
} catch (err) {
|
||||
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
|
||||
err.gitTreeish = treeish;
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -1323,10 +1350,15 @@ export class Repository {
|
||||
if (opts.signCommit) {
|
||||
args.push('-S');
|
||||
}
|
||||
|
||||
if (opts.empty) {
|
||||
args.push('--allow-empty');
|
||||
}
|
||||
|
||||
if (opts.noVerify) {
|
||||
args.push('--no-verify');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.run(args, { input: message || '' });
|
||||
} catch (commitErr) {
|
||||
@@ -1391,6 +1423,11 @@ export class Repository {
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async move(from: string, to: string): Promise<void> {
|
||||
const args = ['mv', from, to];
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async setBranchUpstream(name: string, upstream: string): Promise<void> {
|
||||
const args = ['branch', '--set-upstream-to', upstream, name];
|
||||
await this.run(args);
|
||||
@@ -1441,7 +1478,7 @@ export class Repository {
|
||||
const args = ['clean', '-f', '-q'];
|
||||
|
||||
for (const paths of groups) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
promises.push(limiter.queue(() => this.run([...args, '--', ...chunk])));
|
||||
}
|
||||
}
|
||||
@@ -1481,7 +1518,7 @@ export class Repository {
|
||||
|
||||
try {
|
||||
if (paths && paths.length > 0) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
@@ -1596,7 +1633,25 @@ export class Repository {
|
||||
}
|
||||
}
|
||||
|
||||
async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
async rebase(branch: string, options: PullOptions = {}): Promise<void> {
|
||||
const args = ['rebase'];
|
||||
|
||||
args.push(branch);
|
||||
|
||||
try {
|
||||
await this.run(args, options);
|
||||
} catch (err) {
|
||||
if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.Conflict;
|
||||
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
|
||||
const args = ['push'];
|
||||
|
||||
if (forcePushMode === ForcePushMode.ForceWithLease) {
|
||||
@@ -1609,10 +1664,14 @@ export class Repository {
|
||||
args.push('-u');
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
if (followTags) {
|
||||
args.push('--follow-tags');
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
args.push('--tags');
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
args.push(remote);
|
||||
}
|
||||
@@ -1630,6 +1689,8 @@ export class Repository {
|
||||
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
|
||||
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
|
||||
} else if (/Permission.*denied/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.PermissionDenied;
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -1720,11 +1781,17 @@ export class Repository {
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
|
||||
getStatus(opts?: { limit?: number, ignoreSubmodules?: boolean }): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
|
||||
return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
|
||||
const parser = new GitStatusParser();
|
||||
const env = { GIT_OPTIONAL_LOCKS: '0' };
|
||||
const child = this.stream(['status', '-z', '-u'], { env });
|
||||
const args = ['status', '-z', '-u'];
|
||||
|
||||
if (opts?.ignoreSubmodules) {
|
||||
args.push('--ignore-submodules');
|
||||
}
|
||||
|
||||
const child = this.stream(args, { env });
|
||||
|
||||
const onExit = (exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
@@ -1734,13 +1801,15 @@ export class Repository {
|
||||
stderr,
|
||||
exitCode,
|
||||
gitErrorCode: getGitErrorCode(stderr),
|
||||
gitCommand: 'status'
|
||||
gitCommand: 'status',
|
||||
gitArgs: args
|
||||
}));
|
||||
}
|
||||
|
||||
c({ status: parser.status, didHitLimit: false });
|
||||
};
|
||||
|
||||
const limit = opts?.limit ?? 5000;
|
||||
const onStdoutData = (raw: string) => {
|
||||
parser.update(raw);
|
||||
|
||||
@@ -1793,13 +1862,23 @@ export class Repository {
|
||||
.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
|
||||
}
|
||||
|
||||
async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate', contains?: string }): Promise<Ref[]> {
|
||||
const args = ['for-each-ref', '--format', '%(refname) %(objectname)'];
|
||||
async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate', contains?: string, pattern?: string, count?: number }): Promise<Ref[]> {
|
||||
const args = ['for-each-ref'];
|
||||
|
||||
if (opts?.count) {
|
||||
args.push(`--count=${opts.count}`);
|
||||
}
|
||||
|
||||
if (opts && opts.sort && opts.sort !== 'alphabetically') {
|
||||
args.push('--sort', `-${opts.sort}`);
|
||||
}
|
||||
|
||||
args.push('--format', '%(refname) %(objectname) %(*objectname)');
|
||||
|
||||
if (opts?.pattern) {
|
||||
args.push(opts.pattern);
|
||||
}
|
||||
|
||||
if (opts?.contains) {
|
||||
args.push('--contains', opts.contains);
|
||||
}
|
||||
@@ -1809,12 +1888,12 @@ export class Repository {
|
||||
const fn = (line: string): Ref | null => {
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: match[1], commit: match[2], type: RefType.Head };
|
||||
} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] };
|
||||
} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
return { name: match[1], commit: match[2], type: RefType.Tag };
|
||||
} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: match[1], commit: match[3] ?? match[2], type: RefType.Tag };
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1863,7 +1942,7 @@ export class Repository {
|
||||
remote.pushUrl = url;
|
||||
}
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/45271
|
||||
// https://github.com/microsoft/vscode/issues/45271
|
||||
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
|
||||
}
|
||||
|
||||
@@ -1922,7 +2001,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
async getBranches(query: BranchQuery): Promise<Ref[]> {
|
||||
const refs = await this.getRefs({ contains: query.contains });
|
||||
const refs = await this.getRefs({ contains: query.contains, pattern: query.pattern ? `refs/${query.pattern}` : undefined, count: query.count });
|
||||
return refs.filter(value => (value.type !== RefType.Tag) && (query.remote || !value.remote));
|
||||
}
|
||||
|
||||
@@ -1931,6 +2010,17 @@ export class Repository {
|
||||
return message.replace(/^\s*#.*$\n?/gm, '').trim();
|
||||
}
|
||||
|
||||
async getSquashMessage(): Promise<string | undefined> {
|
||||
const squashMsgPath = path.join(this.repositoryRoot, '.git', 'SQUASH_MSG');
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(squashMsgPath, 'utf8');
|
||||
return this.stripCommitMessageComments(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getMergeMessage(): Promise<string | undefined> {
|
||||
const mergeMsgPath = path.join(this.repositoryRoot, '.git', 'MERGE_MSG');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user