1/*--------------------------------------------------------------------------------------------- 2 * Copyright (c) Microsoft Corporation. All rights reserved. 3 * Licensed under the MIT License. See License.txt in the project root for license information. 4 *--------------------------------------------------------------------------------------------*/ 5 6import * as fs from 'fs'; 7import * as path from 'path'; 8import * as os from 'os'; 9import * as cp from 'child_process'; 10import * as which from 'which'; 11import { EventEmitter } from 'events'; 12import iconv = require('iconv-lite'); 13import * as filetype from 'file-type'; 14import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util'; 15import { CancellationToken } from 'vscode'; 16import { URI } from 'vscode-uri'; 17import { detectEncoding } from './encoding'; 18import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; 19 20// https://github.com/microsoft/vscode/issues/65693 21const MAX_CLI_LENGTH = 30000; 22 23const readfile = denodeify<string, string | null, string>(fs.readFile); 24 25export interface IGit { 26 path: string; 27 version: string; 28} 29 30export interface IFileStatus { 31 x: string; 32 y: string; 33 path: string; 34 rename?: string; 35} 36 37export interface Stash { 38 index: number; 39 description: string; 40} 41 42interface MutableRemote extends Remote { 43 fetchUrl?: string; 44 pushUrl?: string; 45 isReadOnly: boolean; 46} 47 48function parseVersion(raw: string): string { 49 return raw.replace(/^git version /, ''); 50} 51 52function findSpecificGit(path: string, onLookup: (path: string) => void): Promise<IGit> { 53 return new Promise<IGit>((c, e) => { 54 onLookup(path); 55 56 const buffers: Buffer[] = []; 57 const child = cp.spawn(path, ['--version']); 58 child.stdout.on('data', (b: Buffer) => buffers.push(b)); 59 child.on('error', cpErrorHandler(e)); 60 child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) })); 61 }); 62} 63 64function findGitDarwin(onLookup: (path: string) => void): Promise<IGit> { 65 return new Promise<IGit>((c, e) => { 66 cp.exec('which git', (err, gitPathBuffer) => { 67 if (err) { 68 return e('git not found'); 69 } 70 71 const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, ''); 72 73 function getVersion(path: string) { 74 onLookup(path); 75 76 // make sure git executes 77 cp.exec('git --version', (err, stdout) => { 78 79 if (err) { 80 return e('git not found'); 81 } 82 83 return c({ path, version: parseVersion(stdout.trim()) }); 84 }); 85 } 86 87 if (path !== '/usr/bin/git') { 88 return getVersion(path); 89 } 90 91 // must check if XCode is installed 92 cp.exec('xcode-select -p', (err: any) => { 93 if (err && err.code === 2) { 94 // git is not installed, and launching /usr/bin/git 95 // will prompt the user to install it 96 97 return e('git not found'); 98 } 99 100 getVersion(path); 101 }); 102 }); 103 }); 104} 105 106function findSystemGitWin32(base: string, onLookup: (path: string) => void): Promise<IGit> { 107 if (!base) { 108 return Promise.reject<IGit>('Not found'); 109 } 110 111 return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'), onLookup); 112} 113 114function findGitWin32InPath(onLookup: (path: string) => void): Promise<IGit> { 115 const whichPromise = new Promise<string>((c, e) => which('git.exe', (err, path) => err ? e(err) : c(path))); 116 return whichPromise.then(path => findSpecificGit(path, onLookup)); 117} 118 119function findGitWin32(onLookup: (path: string) => void): Promise<IGit> { 120 return findSystemGitWin32(process.env['ProgramW6432'] as string, onLookup) 121 .then(undefined, () => findSystemGitWin32(process.env['ProgramFiles(x86)'] as string, onLookup)) 122 .then(undefined, () => findSystemGitWin32(process.env['ProgramFiles'] as string, onLookup)) 123 .then(undefined, () => findSystemGitWin32(path.join(process.env['LocalAppData'] as string, 'Programs'), onLookup)) 124 .then(undefined, () => findGitWin32InPath(onLookup)); 125} 126 127export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> { 128 const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null); 129 130 return first 131 .then(undefined, () => { 132 switch (process.platform) { 133 case 'darwin': return findGitDarwin(onLookup); 134 case 'win32': return findGitWin32(onLookup); 135 default: return findSpecificGit('git', onLookup); 136 } 137 }) 138 .then(null, () => Promise.reject(new Error('Git installation not found.'))); 139} 140 141export interface IExecutionResult<T extends string | Buffer> { 142 exitCode: number; 143 stdout: T; 144 stderr: string; 145} 146 147function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void { 148 return err => { 149 if (/ENOENT/.test(err.message)) { 150 err = new GitError({ 151 error: err, 152 message: 'Failed to execute git (ENOENT)', 153 gitErrorCode: GitErrorCodes.NotAGitRepository 154 }); 155 } 156 157 cb(err); 158 }; 159} 160 161export interface SpawnOptions extends cp.SpawnOptions { 162 input?: string; 163 encoding?: string; 164 log?: boolean; 165 cancellationToken?: CancellationToken; 166} 167 168async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> { 169 if (!child.stdout || !child.stderr) { 170 throw new GitError({ message: 'Failed to get stdout or stderr from git process.' }); 171 } 172 173 if (cancellationToken && cancellationToken.isCancellationRequested) { 174 throw new GitError({ message: 'Cancelled' }); 175 } 176 177 const disposables: IDisposable[] = []; 178 179 const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => { 180 ee.once(name, fn); 181 disposables.push(toDisposable(() => ee.removeListener(name, fn))); 182 }; 183 184 const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => { 185 ee.on(name, fn); 186 disposables.push(toDisposable(() => ee.removeListener(name, fn))); 187 }; 188 189 let result = Promise.all<any>([ 190 new Promise<number>((c, e) => { 191 once(child, 'error', cpErrorHandler(e)); 192 once(child, 'exit', c); 193 }), 194 new Promise<Buffer>(c => { 195 const buffers: Buffer[] = []; 196 on(child.stdout, 'data', (b: Buffer) => buffers.push(b)); 197 once(child.stdout, 'close', () => c(Buffer.concat(buffers))); 198 }), 199 new Promise<string>(c => { 200 const buffers: Buffer[] = []; 201 on(child.stderr, 'data', (b: Buffer) => buffers.push(b)); 202 once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8'))); 203 }) 204 ]) as Promise<[number, Buffer, string]>; 205 206 if (cancellationToken) { 207 const cancellationPromise = new Promise<[number, Buffer, string]>((_, e) => { 208 onceEvent(cancellationToken.onCancellationRequested)(() => { 209 try { 210 child.kill(); 211 } catch (err) { 212 // noop 213 } 214 215 e(new GitError({ message: 'Cancelled' })); 216 }); 217 }); 218 219 result = Promise.race([result, cancellationPromise]); 220 } 221 222 try { 223 const [exitCode, stdout, stderr] = await result; 224 return { exitCode, stdout, stderr }; 225 } finally { 226 dispose(disposables); 227 } 228} 229 230export interface IGitErrorData { 231 error?: Error; 232 message?: string; 233 stdout?: string; 234 stderr?: string; 235 exitCode?: number; 236 gitErrorCode?: string; 237 gitCommand?: string; 238} 239 240export class GitError { 241 242 error?: Error; 243 message: string; 244 stdout?: string; 245 stderr?: string; 246 exitCode?: number; 247 gitErrorCode?: string; 248 gitCommand?: string; 249 250 constructor(data: IGitErrorData) { 251 if (data.error) { 252 this.error = data.error; 253 this.message = data.error.message; 254 } else { 255 this.error = undefined; 256 this.message = ''; 257 } 258 259 this.message = this.message || data.message || 'Git error'; 260 this.stdout = data.stdout; 261 this.stderr = data.stderr; 262 this.exitCode = data.exitCode; 263 this.gitErrorCode = data.gitErrorCode; 264 this.gitCommand = data.gitCommand; 265 } 266 267 toString(): string { 268 let result = this.message + ' ' + JSON.stringify({ 269 exitCode: this.exitCode, 270 gitErrorCode: this.gitErrorCode, 271 gitCommand: this.gitCommand, 272 stdout: this.stdout, 273 stderr: this.stderr 274 }, null, 2); 275 276 if (this.error) { 277 result += (<any>this.error).stack; 278 } 279 280 return result; 281 } 282} 283 284export interface IGitOptions { 285 gitPath: string; 286 version: string; 287 env?: any; 288} 289 290function getGitErrorCode(stderr: string): string | undefined { 291 if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) { 292 return GitErrorCodes.RepositoryIsLocked; 293 } else if (/Authentication failed/.test(stderr)) { 294 return GitErrorCodes.AuthenticationFailed; 295 } else if (/Not a git repository/i.test(stderr)) { 296 return GitErrorCodes.NotAGitRepository; 297 } else if (/bad config file/.test(stderr)) { 298 return GitErrorCodes.BadConfigFile; 299 } else if (/cannot make pipe for command substitution|cannot create standard input pipe/.test(stderr)) { 300 return GitErrorCodes.CantCreatePipe; 301 } else if (/Repository not found/.test(stderr)) { 302 return GitErrorCodes.RepositoryNotFound; 303 } else if (/unable to access/.test(stderr)) { 304 return GitErrorCodes.CantAccessRemote; 305 } else if (/branch '.+' is not fully merged/.test(stderr)) { 306 return GitErrorCodes.BranchNotFullyMerged; 307 } else if (/Couldn\'t find remote ref/.test(stderr)) { 308 return GitErrorCodes.NoRemoteReference; 309 } else if (/A branch named '.+' already exists/.test(stderr)) { 310 return GitErrorCodes.BranchAlreadyExists; 311 } else if (/'.+' is not a valid branch name/.test(stderr)) { 312 return GitErrorCodes.InvalidBranchName; 313 } else if (/Please,? commit your changes or stash them/.test(stderr)) { 314 return GitErrorCodes.DirtyWorkTree; 315 } 316 317 return undefined; 318} 319 320const COMMIT_FORMAT = '%H\n%ae\n%P\n%B'; 321 322export class Git { 323 324 readonly path: string; 325 private env: any; 326 327 private _onOutput = new EventEmitter(); 328 get onOutput(): EventEmitter { return this._onOutput; } 329 330 constructor(options: IGitOptions) { 331 this.path = options.gitPath; 332 this.env = options.env || {}; 333 } 334 335 open(repository: string, dotGit: string): Repository { 336 return new Repository(this, repository, dotGit); 337 } 338 339 async init(repository: string): Promise<void> { 340 await this.exec(repository, ['init']); 341 return; 342 } 343 344 async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise<string> { 345 let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository'; 346 let folderName = baseFolderName; 347 let folderPath = path.join(parentPath, folderName); 348 let count = 1; 349 350 while (count < 20 && await new Promise(c => fs.exists(folderPath, c))) { 351 folderName = `${baseFolderName}-${count++}`; 352 folderPath = path.join(parentPath, folderName); 353 } 354 355 await mkdirp(parentPath); 356 357 try { 358 await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath], { cancellationToken }); 359 } catch (err) { 360 if (err.stderr) { 361 err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim(); 362 err.stderr = err.stderr.replace(/^ERROR:\s+/, '').trim(); 363 } 364 365 throw err; 366 } 367 368 return folderPath; 369 } 370 371 async getRepositoryRoot(repositoryPath: string): Promise<string> { 372 const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']); 373 return path.normalize(result.stdout.trim()); 374 } 375 376 async getRepositoryDotGit(repositoryPath: string): Promise<string> { 377 const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir']); 378 let dotGitPath = result.stdout.trim(); 379 380 if (!path.isAbsolute(dotGitPath)) { 381 dotGitPath = path.join(repositoryPath, dotGitPath); 382 } 383 384 return path.normalize(dotGitPath); 385 } 386 387 async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> { 388 options = assign({ cwd }, options || {}); 389 return await this._exec(args, options); 390 } 391 392 async exec2(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> { 393 return await this._exec(args, options); 394 } 395 396 stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess { 397 options = assign({ cwd }, options || {}); 398 return this.spawn(args, options); 399 } 400 401 private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> { 402 const child = this.spawn(args, options); 403 404 if (options.input) { 405 child.stdin.end(options.input, 'utf8'); 406 } 407 408 const bufferResult = await exec(child, options.cancellationToken); 409 410 if (options.log !== false && bufferResult.stderr.length > 0) { 411 this.log(`${bufferResult.stderr}\n`); 412 } 413 414 let encoding = options.encoding || 'utf8'; 415 encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; 416 417 const result: IExecutionResult<string> = { 418 exitCode: bufferResult.exitCode, 419 stdout: iconv.decode(bufferResult.stdout, encoding), 420 stderr: bufferResult.stderr 421 }; 422 423 if (bufferResult.exitCode) { 424 return Promise.reject<IExecutionResult<string>>(new GitError({ 425 message: 'Failed to execute git', 426 stdout: result.stdout, 427 stderr: result.stderr, 428 exitCode: result.exitCode, 429 gitErrorCode: getGitErrorCode(result.stderr), 430 gitCommand: args[0] 431 })); 432 } 433 434 return result; 435 } 436 437 spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess { 438 if (!this.path) { 439 throw new Error('git could not be found in the system.'); 440 } 441 442 if (!options) { 443 options = {}; 444 } 445 446 if (!options.stdio && !options.input) { 447 options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr 448 } 449 450 options.env = assign({}, process.env, this.env, options.env || {}, { 451 VSCODE_GIT_COMMAND: args[0], 452 LC_ALL: 'en_US.UTF-8', 453 LANG: 'en_US.UTF-8' 454 }); 455 456 if (options.log !== false) { 457 this.log(`> git ${args.join(' ')}\n`); 458 } 459 460 return cp.spawn(this.path, args, options); 461 } 462 463 private log(output: string): void { 464 this._onOutput.emit('log', output); 465 } 466} 467 468export interface Commit { 469 hash: string; 470 message: string; 471 parents: string[]; 472 authorEmail?: string | undefined; 473} 474 475export class GitStatusParser { 476 477 private lastRaw = ''; 478 private result: IFileStatus[] = []; 479 480 get status(): IFileStatus[] { 481 return this.result; 482 } 483 484 update(raw: string): void { 485 let i = 0; 486 let nextI: number | undefined; 487 488 raw = this.lastRaw + raw; 489 490 while ((nextI = this.parseEntry(raw, i)) !== undefined) { 491 i = nextI; 492 } 493 494 this.lastRaw = raw.substr(i); 495 } 496 497 private parseEntry(raw: string, i: number): number | undefined { 498 if (i + 4 >= raw.length) { 499 return; 500 } 501 502 let lastIndex: number; 503 const entry: IFileStatus = { 504 x: raw.charAt(i++), 505 y: raw.charAt(i++), 506 rename: undefined, 507 path: '' 508 }; 509 510 // space 511 i++; 512 513 if (entry.x === 'R' || entry.x === 'C') { 514 lastIndex = raw.indexOf('\0', i); 515 516 if (lastIndex === -1) { 517 return; 518 } 519 520 entry.rename = raw.substring(i, lastIndex); 521 i = lastIndex + 1; 522 } 523 524 lastIndex = raw.indexOf('\0', i); 525 526 if (lastIndex === -1) { 527 return; 528 } 529 530 entry.path = raw.substring(i, lastIndex); 531 532 // If path ends with slash, it must be a nested git repo 533 if (entry.path[entry.path.length - 1] !== '/') { 534 this.result.push(entry); 535 } 536 537 return lastIndex + 1; 538 } 539} 540 541export interface Submodule { 542 name: string; 543 path: string; 544 url: string; 545} 546 547export function parseGitmodules(raw: string): Submodule[] { 548 const regex = /\r?\n/g; 549 let position = 0; 550 let match: RegExpExecArray | null = null; 551 552 const result: Submodule[] = []; 553 let submodule: Partial<Submodule> = {}; 554 555 function parseLine(line: string): void { 556 const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line); 557 558 if (sectionMatch) { 559 if (submodule.name && submodule.path && submodule.url) { 560 result.push(submodule as Submodule); 561 } 562 563 const name = sectionMatch[1]; 564 565 if (name) { 566 submodule = { name }; 567 return; 568 } 569 } 570 571 if (!submodule) { 572 return; 573 } 574 575 const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line); 576 577 if (!propertyMatch) { 578 return; 579 } 580 581 const [, key, value] = propertyMatch; 582 583 switch (key) { 584 case 'path': submodule.path = value; break; 585 case 'url': submodule.url = value; break; 586 } 587 } 588 589 while (match = regex.exec(raw)) { 590 parseLine(raw.substring(position, match.index)); 591 position = match.index + match[0].length; 592 } 593 594 parseLine(raw.substring(position)); 595 596 if (submodule.name && submodule.path && submodule.url) { 597 result.push(submodule as Submodule); 598 } 599 600 return result; 601} 602 603export function parseGitCommit(raw: string): Commit | null { 604 const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim()); 605 if (!match) { 606 return null; 607 } 608 609 const parents = match[3] ? match[3].split(' ') : []; 610 return { hash: match[1], message: match[5], parents, authorEmail: match[2] }; 611} 612 613interface LsTreeElement { 614 mode: string; 615 type: string; 616 object: string; 617 size: string; 618 file: string; 619} 620 621export function parseLsTree(raw: string): LsTreeElement[] { 622 return raw.split('\n') 623 .filter(l => !!l) 624 .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) 625 .filter(m => !!m) 626 .map(([, mode, type, object, size, file]) => ({ mode, type, object, size, file })); 627} 628 629interface LsFilesElement { 630 mode: string; 631 object: string; 632 stage: string; 633 file: string; 634} 635 636export function parseLsFiles(raw: string): LsFilesElement[] { 637 return raw.split('\n') 638 .filter(l => !!l) 639 .map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!) 640 .filter(m => !!m) 641 .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); 642} 643 644export interface CommitOptions { 645 all?: boolean | 'tracked'; 646 amend?: boolean; 647 signoff?: boolean; 648 signCommit?: boolean; 649 empty?: boolean; 650} 651 652export interface PullOptions { 653 unshallow?: boolean; 654 tags?: boolean; 655 readonly cancellationToken?: CancellationToken; 656} 657 658export enum ForcePushMode { 659 Force, 660 ForceWithLease 661} 662 663export class Repository { 664 665 constructor( 666 private _git: Git, 667 private repositoryRoot: string, 668 readonly dotGit: string 669 ) { } 670 671 get git(): Git { 672 return this._git; 673 } 674 675 get root(): string { 676 return this.repositoryRoot; 677 } 678 679 // TODO@Joao: rename to exec 680 async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> { 681 return await this.git.exec(this.repositoryRoot, args, options); 682 } 683 684 stream(args: string[], options: SpawnOptions = {}): cp.ChildProcess { 685 return this.git.stream(this.repositoryRoot, args, options); 686 } 687 688 spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess { 689 return this.git.spawn(args, options); 690 } 691 692 async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> { 693 const args = ['config']; 694 695 if (scope) { 696 args.push('--' + scope); 697 } 698 699 args.push(key); 700 701 if (value) { 702 args.push(value); 703 } 704 705 const result = await this.run(args, options); 706 return result.stdout.trim(); 707 } 708 709 async getConfigs(scope: string): Promise<{ key: string; value: string; }[]> { 710 const args = ['config']; 711 712 if (scope) { 713 args.push('--' + scope); 714 } 715 716 args.push('-l'); 717 718 const result = await this.run(args); 719 const lines = result.stdout.trim().split(/\r|\r\n|\n/); 720 721 return lines.map(entry => { 722 const equalsIndex = entry.indexOf('='); 723 return { key: entry.substr(0, equalsIndex), value: entry.substr(equalsIndex + 1) }; 724 }); 725 } 726 727 async log(options?: LogOptions): Promise<Commit[]> { 728 const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32; 729 const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`]; 730 const gitResult = await this.run(args); 731 if (gitResult.exitCode) { 732 // An empty repo. 733 return []; 734 } 735 736 const s = gitResult.stdout; 737 const result: Commit[] = []; 738 let index = 0; 739 while (index < s.length) { 740 let nextIndex = s.indexOf('\x00\x00', index); 741 if (nextIndex === -1) { 742 nextIndex = s.length; 743 } 744 745 let entry = s.substr(index, nextIndex - index); 746 if (entry.startsWith('\n')) { 747 entry = entry.substring(1); 748 } 749 750 const commit = parseGitCommit(entry); 751 if (!commit) { 752 break; 753 } 754 755 result.push(commit); 756 index = nextIndex + 2; 757 } 758 759 return result; 760 } 761 762 async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> { 763 const stdout = await this.buffer(object); 764 765 if (autoGuessEncoding) { 766 encoding = detectEncoding(stdout) || encoding; 767 } 768 769 encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; 770 771 return iconv.decode(stdout, encoding); 772 } 773 774 async buffer(object: string): Promise<Buffer> { 775 const child = this.stream(['show', object]); 776 777 if (!child.stdout) { 778 return Promise.reject<Buffer>('Can\'t open file from git'); 779 } 780 781 const { exitCode, stdout, stderr } = await exec(child); 782 783 if (exitCode) { 784 const err = new GitError({ 785 message: 'Could not show object.', 786 exitCode 787 }); 788 789 if (/exists on disk, but not in/.test(stderr)) { 790 err.gitErrorCode = GitErrorCodes.WrongCase; 791 } 792 793 return Promise.reject<Buffer>(err); 794 } 795 796 return stdout; 797 } 798 799 async getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> { 800 if (!treeish) { // index 801 const elements = await this.lsfiles(path); 802 803 if (elements.length === 0) { 804 throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath }); 805 } 806 807 const { mode, object } = elements[0]; 808 const catFile = await this.run(['cat-file', '-s', object]); 809 const size = parseInt(catFile.stdout); 810 811 return { mode, object, size }; 812 } 813 814 const elements = await this.lstree(treeish, path); 815 816 if (elements.length === 0) { 817 throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath }); 818 } 819 820 const { mode, object, size } = elements[0]; 821 return { mode, object, size: parseInt(size) }; 822 } 823 824 async lstree(treeish: string, path: string): Promise<LsTreeElement[]> { 825 const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]); 826 return parseLsTree(stdout); 827 } 828 829 async lsfiles(path: string): Promise<LsFilesElement[]> { 830 const { stdout } = await this.run(['ls-files', '--stage', '--', path]); 831 return parseLsFiles(stdout); 832 } 833 834 async getGitRelativePath(ref: string, relativePath: string): Promise<string> { 835 const relativePathLowercase = relativePath.toLowerCase(); 836 const dirname = path.posix.dirname(relativePath) + '/'; 837 const elements: { file: string; }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname); 838 const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; 839 840 if (!element) { 841 throw new GitError({ message: 'Git relative path not found.' }); 842 } 843 844 return element.file; 845 } 846 847 async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> { 848 const child = await this.stream(['show', object]); 849 const buffer = await readBytes(child.stdout, 4100); 850 851 try { 852 child.kill(); 853 } catch (err) { 854 // noop 855 } 856 857 const encoding = detectUnicodeEncoding(buffer); 858 let isText = true; 859 860 if (encoding !== Encoding.UTF16be && encoding !== Encoding.UTF16le) { 861 for (let i = 0; i < buffer.length; i++) { 862 if (buffer.readInt8(i) === 0) { 863 isText = false; 864 break; 865 } 866 } 867 } 868 869 if (!isText) { 870 const result = filetype(buffer); 871 872 if (!result) { 873 return { mimetype: 'application/octet-stream' }; 874 } else { 875 return { mimetype: result.mime }; 876 } 877 } 878 879 if (encoding) { 880 return { mimetype: 'text/plain', encoding }; 881 } else { 882 // TODO@JOAO: read the setting OUTSIDE! 883 return { mimetype: 'text/plain' }; 884 } 885 } 886 887 async apply(patch: string, reverse?: boolean): Promise<void> { 888 const args = ['apply', patch]; 889 890 if (reverse) { 891 args.push('-R'); 892 } 893 894 try { 895 await this.run(args); 896 } catch (err) { 897 if (/patch does not apply/.test(err.stderr)) { 898 err.gitErrorCode = GitErrorCodes.PatchDoesNotApply; 899 } 900 901 throw err; 902 } 903 } 904 905 async diff(cached = false): Promise<string> { 906 const args = ['diff']; 907 908 if (cached) { 909 args.push('--cached'); 910 } 911 912 const result = await this.run(args); 913 return result.stdout; 914 } 915 916 diffWithHEAD(): Promise<Change[]>; 917 diffWithHEAD(path: string): Promise<string>; 918 diffWithHEAD(path?: string | undefined): Promise<string | Change[]>; 919 async diffWithHEAD(path?: string | undefined): Promise<string | Change[]> { 920 if (!path) { 921 return await this.diffFiles(false); 922 } 923 924 const args = ['diff', '--', path]; 925 const result = await this.run(args); 926 return result.stdout; 927 } 928 929 diffWith(ref: string): Promise<Change[]>; 930 diffWith(ref: string, path: string): Promise<string>; 931 diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>; 932 async diffWith(ref: string, path?: string): Promise<string | Change[]> { 933 if (!path) { 934 return await this.diffFiles(false, ref); 935 } 936 937 const args = ['diff', ref, '--', path]; 938 const result = await this.run(args); 939 return result.stdout; 940 } 941 942 diffIndexWithHEAD(): Promise<Change[]>; 943 diffIndexWithHEAD(path: string): Promise<string>; 944 diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>; 945 async diffIndexWithHEAD(path?: string): Promise<string | Change[]> { 946 if (!path) { 947 return await this.diffFiles(true); 948 } 949 950 const args = ['diff', '--cached', '--', path]; 951 const result = await this.run(args); 952 return result.stdout; 953 } 954 955 diffIndexWith(ref: string): Promise<Change[]>; 956 diffIndexWith(ref: string, path: string): Promise<string>; 957 diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>; 958 async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> { 959 if (!path) { 960 return await this.diffFiles(true, ref); 961 } 962 963 const args = ['diff', '--cached', ref, '--', path]; 964 const result = await this.run(args); 965 return result.stdout; 966 } 967 968 async diffBlobs(object1: string, object2: string): Promise<string> { 969 const args = ['diff', object1, object2]; 970 const result = await this.run(args); 971 return result.stdout; 972 } 973 974 diffBetween(ref1: string, ref2: string): Promise<Change[]>; 975 diffBetween(ref1: string, ref2: string, path: string): Promise<string>; 976 diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>; 977 async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> { 978 const range = `${ref1}...${ref2}`; 979 if (!path) { 980 return await this.diffFiles(false, range); 981 } 982 983 const args = ['diff', range, '--', path]; 984 const result = await this.run(args); 985 986 return result.stdout.trim(); 987 } 988 989 private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> { 990 const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR']; 991 if (cached) { 992 args.push('--cached'); 993 } 994 995 if (ref) { 996 args.push(ref); 997 } 998 999 const gitResult = await this.run(args); 1000 if (gitResult.exitCode) { 1001 return []; 1002 } 1003 1004 const entries = gitResult.stdout.split('\x00'); 1005 let index = 0; 1006 const result: Change[] = []; 1007 1008 entriesLoop: 1009 while (index < entries.length - 1) { 1010 const change = entries[index++]; 1011 const resourcePath = entries[index++]; 1012 if (!change || !resourcePath) { 1013 break; 1014 } 1015 1016 const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath)); 1017 let status: Status = Status.UNTRACKED; 1018 1019 // 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. 1020 switch (change[0]) { 1021 case 'M': 1022 status = Status.MODIFIED; 1023 break; 1024 1025 case 'A': 1026 status = Status.INDEX_ADDED; 1027 break; 1028 1029 case 'D': 1030 status = Status.DELETED; 1031 break; 1032 1033 // Rename contains two paths, the second one is what the file is renamed/copied to. 1034 case 'R': 1035 if (index >= entries.length) { 1036 break; 1037 } 1038 1039 const newPath = entries[index++]; 1040 if (!newPath) { 1041 break; 1042 } 1043 1044 const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath)); 1045 result.push({ 1046 uri, 1047 renameUri: uri, 1048 originalUri, 1049 status: Status.INDEX_RENAMED 1050 }); 1051 1052 continue; 1053 1054 default: 1055 // Unknown status 1056 break entriesLoop; 1057 } 1058 1059 result.push({ 1060 status, 1061 originalUri, 1062 uri: originalUri, 1063 renameUri: originalUri, 1064 }); 1065 } 1066 1067 return result; 1068 } 1069 1070 async getMergeBase(ref1: string, ref2: string): Promise<string> { 1071 const args = ['merge-base', ref1, ref2]; 1072 const result = await this.run(args); 1073 1074 return result.stdout.trim(); 1075 } 1076 1077 async hashObject(data: string): Promise<string> { 1078 const args = ['hash-object', '-w', '--stdin']; 1079 const result = await this.run(args, { input: data }); 1080 1081 return result.stdout.trim(); 1082 } 1083 1084 async add(paths: string[], opts?: { update?: boolean }): Promise<void> { 1085 const args = ['add']; 1086 1087 if (opts && opts.update) { 1088 args.push('-u'); 1089 } else { 1090 args.push('-A'); 1091 } 1092 1093 args.push('--'); 1094 1095 if (paths && paths.length) { 1096 args.push.apply(args, paths); 1097 } else { 1098 args.push('.'); 1099 } 1100 1101 await this.run(args); 1102 } 1103 1104 async rm(paths: string[]): Promise<void> { 1105 const args = ['rm', '--']; 1106 1107 if (!paths || !paths.length) { 1108 return; 1109 } 1110 1111 args.push(...paths); 1112 1113 await this.run(args); 1114 } 1115 1116 async stage(path: string, data: string): Promise<void> { 1117 const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] }); 1118 child.stdin.end(data, 'utf8'); 1119 1120 const { exitCode, stdout } = await exec(child); 1121 const hash = stdout.toString('utf8'); 1122 1123 if (exitCode) { 1124 throw new GitError({ 1125 message: 'Could not hash object.', 1126 exitCode: exitCode 1127 }); 1128 } 1129 1130 let mode: string; 1131 let add: string = ''; 1132 1133 try { 1134 const details = await this.getObjectDetails('HEAD', path); 1135 mode = details.mode; 1136 } catch (err) { 1137 if (err.gitErrorCode !== GitErrorCodes.UnknownPath) { 1138 throw err; 1139 } 1140 1141 mode = '100644'; 1142 add = '--add'; 1143 } 1144 1145 await this.run(['update-index', add, '--cacheinfo', mode, hash, path]); 1146 } 1147 1148 async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> { 1149 const args = ['checkout', '-q']; 1150 1151 if (opts.track) { 1152 args.push('--track'); 1153 } 1154 1155 if (treeish) { 1156 args.push(treeish); 1157 } 1158 1159 try { 1160 if (paths && paths.length > 0) { 1161 for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { 1162 await this.run([...args, '--', ...chunk]); 1163 } 1164 } else { 1165 await this.run(args); 1166 } 1167 } catch (err) { 1168 if (/Please,? commit your changes or stash them/.test(err.stderr || '')) { 1169 err.gitErrorCode = GitErrorCodes.DirtyWorkTree; 1170 } 1171 1172 throw err; 1173 } 1174 } 1175 1176 async commit(message: string, opts: CommitOptions = Object.create(null)): Promise<void> { 1177 const args = ['commit', '--quiet', '--allow-empty-message', '--file', '-']; 1178 1179 if (opts.all) { 1180 args.push('--all'); 1181 } 1182 1183 if (opts.amend) { 1184 args.push('--amend'); 1185 } 1186 1187 if (opts.signoff) { 1188 args.push('--signoff'); 1189 } 1190 1191 if (opts.signCommit) { 1192 args.push('-S'); 1193 } 1194 if (opts.empty) { 1195 args.push('--allow-empty'); 1196 } 1197 1198 try { 1199 await this.run(args, { input: message || '' }); 1200 } catch (commitErr) { 1201 await this.handleCommitError(commitErr); 1202 } 1203 } 1204 1205 async rebaseContinue(): Promise<void> { 1206 const args = ['rebase', '--continue']; 1207 1208 try { 1209 await this.run(args); 1210 } catch (commitErr) { 1211 await this.handleCommitError(commitErr); 1212 } 1213 } 1214 1215 private async handleCommitError(commitErr: any): Promise<void> { 1216 if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) { 1217 commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges; 1218 throw commitErr; 1219 } 1220 1221 try { 1222 await this.run(['config', '--get-all', 'user.name']); 1223 } catch (err) { 1224 err.gitErrorCode = GitErrorCodes.NoUserNameConfigured; 1225 throw err; 1226 } 1227 1228 try { 1229 await this.run(['config', '--get-all', 'user.email']); 1230 } catch (err) { 1231 err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured; 1232 throw err; 1233 } 1234 1235 throw commitErr; 1236 } 1237 1238 async branch(name: string, checkout: boolean, ref?: string): Promise<void> { 1239 const args = checkout ? ['checkout', '-q', '-b', name, '--no-track'] : ['branch', '-q', name]; 1240 1241 if (ref) { 1242 args.push(ref); 1243 } 1244 1245 await this.run(args); 1246 } 1247 1248 async deleteBranch(name: string, force?: boolean): Promise<void> { 1249 const args = ['branch', force ? '-D' : '-d', name]; 1250 await this.run(args); 1251 } 1252 1253 async renameBranch(name: string): Promise<void> { 1254 const args = ['branch', '-m', name]; 1255 await this.run(args); 1256 } 1257 1258 async setBranchUpstream(name: string, upstream: string): Promise<void> { 1259 const args = ['branch', '--set-upstream-to', upstream, name]; 1260 await this.run(args); 1261 } 1262 1263 async deleteRef(ref: string): Promise<void> { 1264 const args = ['update-ref', '-d', ref]; 1265 await this.run(args); 1266 } 1267 1268 async merge(ref: string): Promise<void> { 1269 const args = ['merge', ref]; 1270 1271 try { 1272 await this.run(args); 1273 } catch (err) { 1274 if (/^CONFLICT /m.test(err.stdout || '')) { 1275 err.gitErrorCode = GitErrorCodes.Conflict; 1276 } 1277 1278 throw err; 1279 } 1280 } 1281 1282 async tag(name: string, message?: string): Promise<void> { 1283 let args = ['tag']; 1284 1285 if (message) { 1286 args = [...args, '-a', name, '-m', message]; 1287 } else { 1288 args = [...args, name]; 1289 } 1290 1291 await this.run(args); 1292 } 1293 1294 async clean(paths: string[]): Promise<void> { 1295 const pathsByGroup = groupBy(paths, p => path.dirname(p)); 1296 const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]); 1297 1298 const limiter = new Limiter(5); 1299 const promises: Promise<any>[] = []; 1300 1301 for (const paths of groups) { 1302 for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { 1303 promises.push(limiter.queue(() => this.run(['clean', '-f', '-q', '--', ...chunk]))); 1304 } 1305 } 1306 1307 await Promise.all(promises); 1308 } 1309 1310 async undo(): Promise<void> { 1311 await this.run(['clean', '-fd']); 1312 1313 try { 1314 await this.run(['checkout', '--', '.']); 1315 } catch (err) { 1316 if (/did not match any file\(s\) known to git\./.test(err.stderr || '')) { 1317 return; 1318 } 1319 1320 throw err; 1321 } 1322 } 1323 1324 async reset(treeish: string, hard: boolean = false): Promise<void> { 1325 const args = ['reset', hard ? '--hard' : '--soft', treeish]; 1326 await this.run(args); 1327 } 1328 1329 async revert(treeish: string, paths: string[]): Promise<void> { 1330 const result = await this.run(['branch']); 1331 let args: string[]; 1332 1333 // In case there are no branches, we must use rm --cached 1334 if (!result.stdout) { 1335 args = ['rm', '--cached', '-r', '--']; 1336 } else { 1337 args = ['reset', '-q', treeish, '--']; 1338 } 1339 1340 if (paths && paths.length) { 1341 args.push.apply(args, paths); 1342 } else { 1343 args.push('.'); 1344 } 1345 1346 try { 1347 await this.run(args); 1348 } catch (err) { 1349 // In case there are merge conflicts to be resolved, git reset will output 1350 // some "needs merge" data. We try to get around that. 1351 if (/([^:]+: needs merge\n)+/m.test(err.stdout || '')) { 1352 return; 1353 } 1354 1355 throw err; 1356 } 1357 } 1358 1359 async addRemote(name: string, url: string): Promise<void> { 1360 const args = ['remote', 'add', name, url]; 1361 await this.run(args); 1362 } 1363 1364 async removeRemote(name: string): Promise<void> { 1365 const args = ['remote', 'rm', name]; 1366 await this.run(args); 1367 } 1368 1369 async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number } = {}): Promise<void> { 1370 const args = ['fetch']; 1371 1372 if (options.remote) { 1373 args.push(options.remote); 1374 1375 if (options.ref) { 1376 args.push(options.ref); 1377 } 1378 } else if (options.all) { 1379 args.push('--all'); 1380 } 1381 1382 if (options.prune) { 1383 args.push('--prune'); 1384 } 1385 1386 if (typeof options.depth === 'number') { 1387 args.push(`--depth=${options.depth}`); 1388 } 1389 1390 try { 1391 await this.run(args); 1392 } catch (err) { 1393 if (/No remote repository specified\./.test(err.stderr || '')) { 1394 err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified; 1395 } else if (/Could not read from remote repository/.test(err.stderr || '')) { 1396 err.gitErrorCode = GitErrorCodes.RemoteConnectionError; 1397 } 1398 1399 throw err; 1400 } 1401 } 1402 1403 async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> { 1404 const args = ['pull']; 1405 1406 if (options.tags) { 1407 args.push('--tags'); 1408 } 1409 1410 if (options.unshallow) { 1411 args.push('--unshallow'); 1412 } 1413 1414 if (rebase) { 1415 args.push('-r'); 1416 } 1417 1418 if (remote && branch) { 1419 args.push(remote); 1420 args.push(branch); 1421 } 1422 1423 try { 1424 await this.run(args, options); 1425 } catch (err) { 1426 if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) { 1427 err.gitErrorCode = GitErrorCodes.Conflict; 1428 } else if (/Please tell me who you are\./.test(err.stderr || '')) { 1429 err.gitErrorCode = GitErrorCodes.NoUserNameConfigured; 1430 } else if (/Could not read from remote repository/.test(err.stderr || '')) { 1431 err.gitErrorCode = GitErrorCodes.RemoteConnectionError; 1432 } else if (/Pull is not possible because you have unmerged files|Cannot pull with rebase: You have unstaged changes|Your local changes to the following files would be overwritten|Please, commit your changes before you can merge/i.test(err.stderr)) { 1433 err.stderr = err.stderr.replace(/Cannot pull with rebase: You have unstaged changes/i, 'Cannot pull with rebase, you have unstaged changes'); 1434 err.gitErrorCode = GitErrorCodes.DirtyWorkTree; 1435 } else if (/cannot lock ref|unable to update local ref/i.test(err.stderr || '')) { 1436 err.gitErrorCode = GitErrorCodes.CantLockRef; 1437 } else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) { 1438 err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches; 1439 } 1440 1441 throw err; 1442 } 1443 } 1444 1445 async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> { 1446 const args = ['push']; 1447 1448 if (forcePushMode === ForcePushMode.ForceWithLease) { 1449 args.push('--force-with-lease'); 1450 } else if (forcePushMode === ForcePushMode.Force) { 1451 args.push('--force'); 1452 } 1453 1454 if (setUpstream) { 1455 args.push('-u'); 1456 } 1457 1458 if (tags) { 1459 args.push('--follow-tags'); 1460 } 1461 1462 if (remote) { 1463 args.push(remote); 1464 } 1465 1466 if (name) { 1467 args.push(name); 1468 } 1469 1470 try { 1471 await this.run(args); 1472 } catch (err) { 1473 if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) { 1474 err.gitErrorCode = GitErrorCodes.PushRejected; 1475 } else if (/Could not read from remote repository/.test(err.stderr || '')) { 1476 err.gitErrorCode = GitErrorCodes.RemoteConnectionError; 1477 } else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) { 1478 err.gitErrorCode = GitErrorCodes.NoUpstreamBranch; 1479 } 1480 1481 throw err; 1482 } 1483 } 1484 1485 async blame(path: string): Promise<string> { 1486 try { 1487 const args = ['blame']; 1488 args.push(path); 1489 1490 let result = await this.run(args); 1491 1492 return result.stdout.trim(); 1493 } catch (err) { 1494 if (/^fatal: no such path/.test(err.stderr || '')) { 1495 err.gitErrorCode = GitErrorCodes.NoPathFound; 1496 } 1497 1498 throw err; 1499 } 1500 } 1501 1502 async createStash(message?: string, includeUntracked?: boolean): Promise<void> { 1503 try { 1504 const args = ['stash', 'push']; 1505 1506 if (includeUntracked) { 1507 args.push('-u'); 1508 } 1509 1510 if (message) { 1511 args.push('-m', message); 1512 } 1513 1514 await this.run(args); 1515 } catch (err) { 1516 if (/No local changes to save/.test(err.stderr || '')) { 1517 err.gitErrorCode = GitErrorCodes.NoLocalChanges; 1518 } 1519 1520 throw err; 1521 } 1522 } 1523 1524 async popStash(index?: number): Promise<void> { 1525 const args = ['stash', 'pop']; 1526 await this.popOrApplyStash(args, index); 1527 } 1528 1529 async applyStash(index?: number): Promise<void> { 1530 const args = ['stash', 'apply']; 1531 await this.popOrApplyStash(args, index); 1532 } 1533 1534 private async popOrApplyStash(args: string[], index?: number): Promise<void> { 1535 try { 1536 if (typeof index === 'number') { 1537 args.push(`stash@{${index}}`); 1538 } 1539 1540 await this.run(args); 1541 } catch (err) { 1542 if (/No stash found/.test(err.stderr || '')) { 1543 err.gitErrorCode = GitErrorCodes.NoStashFound; 1544 } else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) { 1545 err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten; 1546 } else if (/^CONFLICT/m.test(err.stdout || '')) { 1547 err.gitErrorCode = GitErrorCodes.StashConflict; 1548 } 1549 1550 throw err; 1551 } 1552 } 1553 1554 getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> { 1555 return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => { 1556 const parser = new GitStatusParser(); 1557 const env = { GIT_OPTIONAL_LOCKS: '0' }; 1558 const child = this.stream(['status', '-z', '-u'], { env }); 1559 1560 const onExit = (exitCode: number) => { 1561 if (exitCode !== 0) { 1562 const stderr = stderrData.join(''); 1563 return e(new GitError({ 1564 message: 'Failed to execute git', 1565 stderr, 1566 exitCode, 1567 gitErrorCode: getGitErrorCode(stderr), 1568 gitCommand: 'status' 1569 })); 1570 } 1571 1572 c({ status: parser.status, didHitLimit: false }); 1573 }; 1574 1575 const onStdoutData = (raw: string) => { 1576 parser.update(raw); 1577 1578 if (parser.status.length > limit) { 1579 child.removeListener('exit', onExit); 1580 child.stdout.removeListener('data', onStdoutData); 1581 child.kill(); 1582 1583 c({ status: parser.status.slice(0, limit), didHitLimit: true }); 1584 } 1585 }; 1586 1587 child.stdout.setEncoding('utf8'); 1588 child.stdout.on('data', onStdoutData); 1589 1590 const stderrData: string[] = []; 1591 child.stderr.setEncoding('utf8'); 1592 child.stderr.on('data', raw => stderrData.push(raw as string)); 1593 1594 child.on('error', cpErrorHandler(e)); 1595 child.on('exit', onExit); 1596 }); 1597 } 1598 1599 async getHEAD(): Promise<Ref> { 1600 try { 1601 const result = await this.run(['symbolic-ref', '--short', 'HEAD']); 1602 1603 if (!result.stdout) { 1604 throw new Error('Not in a branch'); 1605 } 1606 1607 return { name: result.stdout.trim(), commit: undefined, type: RefType.Head }; 1608 } catch (err) { 1609 const result = await this.run(['rev-parse', 'HEAD']); 1610 1611 if (!result.stdout) { 1612 throw new Error('Error parsing HEAD'); 1613 } 1614 1615 return { name: undefined, commit: result.stdout.trim(), type: RefType.Head }; 1616 } 1617 } 1618 1619 async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> { 1620 const result = await this.run(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']); 1621 return result.stdout.trim().split('\n') 1622 .map(line => line.trim().split('\0')) 1623 .filter(([_, upstream]) => upstream === upstreamBranch) 1624 .map(([ref]) => ({ name: ref, type: RefType.Head } as Branch)); 1625 } 1626 1627 async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate' }): Promise<Ref[]> { 1628 const args = ['for-each-ref', '--format', '%(refname) %(objectname)']; 1629 1630 if (opts && opts.sort && opts.sort !== 'alphabetically') { 1631 args.push('--sort', opts.sort); 1632 } 1633 1634 const result = await this.run(args); 1635 1636 const fn = (line: string): Ref | null => { 1637 let match: RegExpExecArray | null; 1638 1639 if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { 1640 return { name: match[1], commit: match[2], type: RefType.Head }; 1641 } else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { 1642 return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] }; 1643 } else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) { 1644 return { name: match[1], commit: match[2], type: RefType.Tag }; 1645 } 1646 1647 return null; 1648 }; 1649 1650 return result.stdout.trim().split('\n') 1651 .filter(line => !!line) 1652 .map(fn) 1653 .filter(ref => !!ref) as Ref[]; 1654 } 1655 1656 async getStashes(): Promise<Stash[]> { 1657 const result = await this.run(['stash', 'list']); 1658 const regex = /^stash@{(\d+)}:(.+)$/; 1659 const rawStashes = result.stdout.trim().split('\n') 1660 .filter(b => !!b) 1661 .map(line => regex.exec(line) as RegExpExecArray) 1662 .filter(g => !!g) 1663 .map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description })); 1664 1665 return rawStashes; 1666 } 1667 1668 async getRemotes(): Promise<Remote[]> { 1669 const result = await this.run(['remote', '--verbose']); 1670 const lines = result.stdout.trim().split('\n').filter(l => !!l); 1671 const remotes: MutableRemote[] = []; 1672 1673 for (const line of lines) { 1674 const parts = line.split(/\s/); 1675 const [name, url, type] = parts; 1676 1677 let remote = remotes.find(r => r.name === name); 1678 1679 if (!remote) { 1680 remote = { name, isReadOnly: false }; 1681 remotes.push(remote); 1682 } 1683 1684 if (/fetch/i.test(type)) { 1685 remote.fetchUrl = url; 1686 } else if (/push/i.test(type)) { 1687 remote.pushUrl = url; 1688 } else { 1689 remote.fetchUrl = url; 1690 remote.pushUrl = url; 1691 } 1692 1693 // https://github.com/Microsoft/vscode/issues/45271 1694 remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push'; 1695 } 1696 1697 return remotes; 1698 } 1699 1700 async getBranch(name: string): Promise<Branch> { 1701 if (name === 'HEAD') { 1702 return this.getHEAD(); 1703 } 1704 1705 let result = await this.run(['rev-parse', name]); 1706 1707 if (!result.stdout && /^@/.test(name)) { 1708 const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]); 1709 name = symbolicFullNameResult.stdout.trim(); 1710 1711 result = await this.run(['rev-parse', name]); 1712 } 1713 1714 if (!result.stdout) { 1715 return Promise.reject<Branch>(new Error('No such branch')); 1716 } 1717 1718 const commit = result.stdout.trim(); 1719 1720 try { 1721 const res2 = await this.run(['rev-parse', '--symbolic-full-name', name + '@{u}']); 1722 const fullUpstream = res2.stdout.trim(); 1723 const match = /^refs\/remotes\/([^/]+)\/(.+)$/.exec(fullUpstream); 1724 1725 if (!match) { 1726 throw new Error(`Could not parse upstream branch: ${fullUpstream}`); 1727 } 1728 1729 const upstream = { remote: match[1], name: match[2] }; 1730 const res3 = await this.run(['rev-list', '--left-right', name + '...' + fullUpstream]); 1731 1732 let ahead = 0, behind = 0; 1733 let i = 0; 1734 1735 while (i < res3.stdout.length) { 1736 switch (res3.stdout.charAt(i)) { 1737 case '<': ahead++; break; 1738 case '>': behind++; break; 1739 default: i++; break; 1740 } 1741 1742 while (res3.stdout.charAt(i++) !== '\n') { /* no-op */ } 1743 } 1744 1745 return { name, type: RefType.Head, commit, upstream, ahead, behind }; 1746 } catch (err) { 1747 return { name, type: RefType.Head, commit }; 1748 } 1749 } 1750 1751 async getCommitTemplate(): Promise<string> { 1752 try { 1753 const result = await this.run(['config', '--get', 'commit.template']); 1754 1755 if (!result.stdout) { 1756 return ''; 1757 } 1758 1759 // https://github.com/git/git/blob/3a0f269e7c82aa3a87323cb7ae04ac5f129f036b/path.c#L612 1760 const homedir = os.homedir(); 1761 let templatePath = result.stdout.trim() 1762 .replace(/^~([^\/]*)\//, (_, user) => `${user ? path.join(path.dirname(homedir), user) : homedir}/`); 1763 1764 if (!path.isAbsolute(templatePath)) { 1765 templatePath = path.join(this.repositoryRoot, templatePath); 1766 } 1767 1768 const raw = await readfile(templatePath, 'utf8'); 1769 return raw.replace(/\n?#.*/g, ''); 1770 1771 } catch (err) { 1772 return ''; 1773 } 1774 } 1775 1776 async getCommit(ref: string): Promise<Commit> { 1777 const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]); 1778 return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format'); 1779 } 1780 1781 async updateSubmodules(paths: string[]): Promise<void> { 1782 const args = ['submodule', 'update', '--']; 1783 1784 for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { 1785 await this.run([...args, ...chunk]); 1786 } 1787 } 1788 1789 async getSubmodules(): Promise<Submodule[]> { 1790 const gitmodulesPath = path.join(this.root, '.gitmodules'); 1791 1792 try { 1793 const gitmodulesRaw = await readfile(gitmodulesPath, 'utf8'); 1794 return parseGitmodules(gitmodulesRaw); 1795 } catch (err) { 1796 if (/ENOENT/.test(err.message)) { 1797 return []; 1798 } 1799 1800 throw err; 1801 } 1802 } 1803} 1804