Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/util.ts
5225 views
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
6
import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env, SourceControlHistoryItem } from 'vscode';
7
import { dirname, normalize, sep, relative } from 'path';
8
import { Readable } from 'stream';
9
import { promises as fs, createReadStream } from 'fs';
10
import byline from 'byline';
11
import { Stash } from './git';
12
13
export const isMacintosh = process.platform === 'darwin';
14
export const isWindows = process.platform === 'win32';
15
export const isRemote = env.remoteName !== undefined;
16
export const isLinux = process.platform === 'linux';
17
export const isLinuxSnap = isLinux && !!process.env['SNAP'] && !!process.env['SNAP_REVISION'];
18
19
export type Mutable<T> = {
20
-readonly [P in keyof T]: T[P]
21
};
22
23
export function log(...args: any[]): void {
24
console.log.apply(console, ['git:', ...args]);
25
}
26
27
export interface IDisposable {
28
dispose(): void;
29
}
30
31
export function dispose<T extends IDisposable>(disposables: T[]): T[] {
32
disposables.forEach(d => d.dispose());
33
return [];
34
}
35
36
export function toDisposable(dispose: () => void): IDisposable {
37
return { dispose };
38
}
39
40
export function combinedDisposable(disposables: IDisposable[]): IDisposable {
41
return toDisposable(() => dispose(disposables));
42
}
43
44
export const EmptyDisposable = toDisposable(() => null);
45
46
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
47
return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables);
48
}
49
50
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
51
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
52
}
53
54
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T) => any, initial: T): IDisposable;
55
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any): IDisposable;
56
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any, initial?: T): IDisposable {
57
handler(initial);
58
return event(e => handler(e));
59
}
60
61
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
62
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
63
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
64
65
disposables?.push(result);
66
67
return result;
68
};
69
}
70
71
export function done<T>(promise: Promise<T>): Promise<void> {
72
return promise.then<void>(() => undefined);
73
}
74
75
export function onceEvent<T>(event: Event<T>): Event<T> {
76
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
77
const result = event(e => {
78
result.dispose();
79
return listener.call(thisArgs, e);
80
}, null, disposables);
81
82
return result;
83
};
84
}
85
86
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
87
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
88
let timer: NodeJS.Timeout;
89
return event(e => {
90
clearTimeout(timer);
91
timer = setTimeout(() => listener.call(thisArgs, e), delay);
92
}, null, disposables);
93
};
94
}
95
96
export function eventToPromise<T>(event: Event<T>): Promise<T> {
97
return new Promise<T>(c => onceEvent(event)(c));
98
}
99
100
export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
101
const didRun = false;
102
103
return (...args) => {
104
if (didRun) {
105
return;
106
}
107
108
return fn(...args);
109
};
110
}
111
112
export function assign<T>(destination: T, ...sources: any[]): T {
113
for (const source of sources) {
114
Object.keys(source).forEach(key =>
115
(destination as Record<string, unknown>)[key] = source[key]);
116
}
117
118
return destination;
119
}
120
121
export function uniqBy<T>(arr: T[], fn: (el: T) => string): T[] {
122
const seen = Object.create(null);
123
124
return arr.filter(el => {
125
const key = fn(el);
126
127
if (seen[key]) {
128
return false;
129
}
130
131
seen[key] = true;
132
return true;
133
});
134
}
135
136
export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[] } {
137
return arr.reduce((result, el) => {
138
const key = fn(el);
139
result[key] = [...(result[key] || []), el];
140
return result;
141
}, Object.create(null));
142
}
143
144
export function coalesce<T>(array: ReadonlyArray<T | undefined>): T[] {
145
return array.filter((e): e is T => !!e);
146
}
147
148
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
149
const mkdir = async () => {
150
try {
151
await fs.mkdir(path, mode);
152
} catch (err) {
153
if (err.code === 'EEXIST') {
154
const stat = await fs.stat(path);
155
156
if (stat.isDirectory()) {
157
return;
158
}
159
160
throw new Error(`'${path}' exists and is not a directory.`);
161
}
162
163
throw err;
164
}
165
};
166
167
// is root?
168
if (path === dirname(path)) {
169
return true;
170
}
171
172
try {
173
await mkdir();
174
} catch (err) {
175
if (err.code !== 'ENOENT') {
176
throw err;
177
}
178
179
await mkdirp(dirname(path), mode);
180
await mkdir();
181
}
182
183
return true;
184
}
185
186
export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
187
const seen: { [key: string]: boolean } = Object.create(null);
188
189
return element => {
190
const key = keyFn(element);
191
192
if (seen[key]) {
193
return false;
194
}
195
196
seen[key] = true;
197
return true;
198
};
199
}
200
201
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
202
let result: T | undefined = undefined;
203
204
array.some(e => {
205
if (fn(e)) {
206
result = e;
207
return true;
208
}
209
210
return false;
211
});
212
213
return result;
214
}
215
216
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
217
return new Promise<boolean>((c, e) => {
218
const fileStream = createReadStream(filename, { encoding: 'utf8' });
219
const stream = byline(fileStream);
220
stream.on('data', (line: string) => {
221
if (pattern.test(line)) {
222
fileStream.close();
223
c(true);
224
}
225
});
226
227
stream.on('error', e);
228
stream.on('end', () => c(false));
229
});
230
}
231
232
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
233
return new Promise<Buffer>((complete, error) => {
234
let done = false;
235
const buffer = Buffer.allocUnsafe(bytes);
236
let bytesRead = 0;
237
238
stream.on('data', (data: Buffer) => {
239
const bytesToRead = Math.min(bytes - bytesRead, data.length);
240
data.copy(buffer, bytesRead, 0, bytesToRead);
241
bytesRead += bytesToRead;
242
243
if (bytesRead === bytes) {
244
stream.destroy(); // Will trigger the close event eventually
245
}
246
});
247
248
stream.on('error', (e: Error) => {
249
if (!done) {
250
done = true;
251
error(e);
252
}
253
});
254
255
stream.on('close', () => {
256
if (!done) {
257
done = true;
258
complete(buffer.slice(0, bytesRead));
259
}
260
});
261
});
262
}
263
264
export const enum Encoding {
265
UTF8 = 'utf8',
266
UTF16be = 'utf16be',
267
UTF16le = 'utf16le'
268
}
269
270
export function detectUnicodeEncoding(buffer: Buffer): Encoding | null {
271
if (buffer.length < 2) {
272
return null;
273
}
274
275
const b0 = buffer.readUInt8(0);
276
const b1 = buffer.readUInt8(1);
277
278
if (b0 === 0xFE && b1 === 0xFF) {
279
return Encoding.UTF16be;
280
}
281
282
if (b0 === 0xFF && b1 === 0xFE) {
283
return Encoding.UTF16le;
284
}
285
286
if (buffer.length < 3) {
287
return null;
288
}
289
290
const b2 = buffer.readUInt8(2);
291
292
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
293
return Encoding.UTF8;
294
}
295
296
return null;
297
}
298
299
export function truncate(value: string, maxLength = 20, ellipsis = true): string {
300
return value.length <= maxLength ? value : `${value.substring(0, maxLength)}${ellipsis ? '\u2026' : ''}`;
301
}
302
303
export function subject(value: string): string {
304
const index = value.indexOf('\n');
305
return index === -1 ? value : truncate(value, index, false);
306
}
307
308
function normalizePath(path: string): string {
309
// Windows & Mac are currently being handled
310
// as case insensitive file systems in VS Code.
311
if (isWindows || isMacintosh) {
312
path = path.toLowerCase();
313
}
314
315
// Trailing separator
316
if (/[/\\]$/.test(path)) {
317
// Remove trailing separator
318
path = path.substring(0, path.length - 1);
319
}
320
321
// Normalize the path
322
return normalize(path);
323
}
324
325
export function isDescendant(parent: string, descendant: string): boolean {
326
if (parent === descendant) {
327
return true;
328
}
329
330
// Normalize the paths
331
parent = normalizePath(parent);
332
descendant = normalizePath(descendant);
333
334
// Ensure parent ends with separator
335
if (parent.charAt(parent.length - 1) !== sep) {
336
parent += sep;
337
}
338
339
return descendant.startsWith(parent);
340
}
341
342
export function pathEquals(a: string, b: string): boolean {
343
return normalizePath(a) === normalizePath(b);
344
}
345
346
/**
347
* Given the `repository.root` compute the relative path while trying to preserve
348
* the casing of the resource URI. The `repository.root` segment of the path can
349
* have a casing mismatch if the folder/workspace is being opened with incorrect
350
* casing which is why we attempt to use substring() before relative().
351
*/
352
export function relativePath(from: string, to: string): string {
353
return relativePathWithNoFallback(from, to) ?? relative(from, to);
354
}
355
356
export function relativePathWithNoFallback(from: string, to: string): string | undefined {
357
// There are cases in which the `from` path may contain a trailing separator at
358
// the end (ex: "C:\", "\\server\folder\" (Windows) or "/" (Linux/macOS)) which
359
// is by design as documented in https://github.com/nodejs/node/issues/1765. If
360
// the trailing separator is missing, we add it.
361
if (from.charAt(from.length - 1) !== sep) {
362
from += sep;
363
}
364
365
if (isDescendant(from, to) && from.length < to.length) {
366
return to.substring(from.length);
367
}
368
369
return undefined;
370
}
371
372
export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator<string[]> {
373
let current: string[] = [];
374
let length = 0;
375
376
for (const value of array) {
377
let newLength = length + value.length;
378
379
if (newLength > maxChunkLength && current.length > 0) {
380
yield current;
381
current = [];
382
newLength = value.length;
383
}
384
385
current.push(value);
386
length = newLength;
387
}
388
389
if (current.length > 0) {
390
yield current;
391
}
392
}
393
394
/**
395
* @returns whether the provided parameter is defined.
396
*/
397
export function isDefined<T>(arg: T | null | undefined): arg is T {
398
return !isUndefinedOrNull(arg);
399
}
400
401
/**
402
* @returns whether the provided parameter is undefined or null.
403
*/
404
export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
405
return (isUndefined(obj) || obj === null);
406
}
407
408
/**
409
* @returns whether the provided parameter is undefined.
410
*/
411
export function isUndefined(obj: unknown): obj is undefined {
412
return (typeof obj === 'undefined');
413
}
414
415
interface ILimitedTaskFactory<T> {
416
factory: () => Promise<T>;
417
c: (value: T | Promise<T>) => void;
418
e: (error?: any) => void;
419
}
420
421
export class Limiter<T> {
422
423
private runningPromises: number;
424
private maxDegreeOfParalellism: number;
425
private outstandingPromises: ILimitedTaskFactory<T>[];
426
427
constructor(maxDegreeOfParalellism: number) {
428
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
429
this.outstandingPromises = [];
430
this.runningPromises = 0;
431
}
432
433
queue(factory: () => Promise<T>): Promise<T> {
434
return new Promise<T>((c, e) => {
435
this.outstandingPromises.push({ factory, c, e });
436
this.consume();
437
});
438
}
439
440
private consume(): void {
441
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
442
const iLimitedTask = this.outstandingPromises.shift()!;
443
this.runningPromises++;
444
445
const promise = iLimitedTask.factory();
446
promise.then(iLimitedTask.c, iLimitedTask.e);
447
promise.then(() => this.consumed(), () => this.consumed());
448
}
449
}
450
451
private consumed(): void {
452
this.runningPromises--;
453
454
if (this.outstandingPromises.length > 0) {
455
this.consume();
456
}
457
}
458
}
459
460
type Completion<T> = { success: true; value: T } | { success: false; err: any };
461
462
export class PromiseSource<T> {
463
464
private _onDidComplete = new EventEmitter<Completion<T>>();
465
466
private _promise: Promise<T> | undefined;
467
get promise(): Promise<T> {
468
if (this._promise) {
469
return this._promise;
470
}
471
472
return eventToPromise(this._onDidComplete.event).then(completion => {
473
if (completion.success) {
474
return completion.value;
475
} else {
476
throw completion.err;
477
}
478
});
479
}
480
481
resolve(value: T): void {
482
if (!this._promise) {
483
this._promise = Promise.resolve(value);
484
this._onDidComplete.fire({ success: true, value });
485
}
486
}
487
488
reject(err: any): void {
489
if (!this._promise) {
490
this._promise = Promise.reject(err);
491
this._onDidComplete.fire({ success: false, err });
492
}
493
}
494
}
495
496
export namespace Versions {
497
declare type VersionComparisonResult = -1 | 0 | 1;
498
499
export interface Version {
500
major: number;
501
minor: number;
502
patch: number;
503
pre?: string;
504
}
505
506
export function compare(v1: string | Version, v2: string | Version): VersionComparisonResult {
507
if (typeof v1 === 'string') {
508
v1 = fromString(v1);
509
}
510
if (typeof v2 === 'string') {
511
v2 = fromString(v2);
512
}
513
514
if (v1.major > v2.major) { return 1; }
515
if (v1.major < v2.major) { return -1; }
516
517
if (v1.minor > v2.minor) { return 1; }
518
if (v1.minor < v2.minor) { return -1; }
519
520
if (v1.patch > v2.patch) { return 1; }
521
if (v1.patch < v2.patch) { return -1; }
522
523
if (v1.pre === undefined && v2.pre !== undefined) { return 1; }
524
if (v1.pre !== undefined && v2.pre === undefined) { return -1; }
525
526
if (v1.pre !== undefined && v2.pre !== undefined) {
527
return v1.pre.localeCompare(v2.pre) as VersionComparisonResult;
528
}
529
530
return 0;
531
}
532
533
export function from(major: string | number, minor: string | number, patch?: string | number, pre?: string): Version {
534
return {
535
major: typeof major === 'string' ? parseInt(major, 10) : major,
536
minor: typeof minor === 'string' ? parseInt(minor, 10) : minor,
537
patch: patch === undefined || patch === null ? 0 : typeof patch === 'string' ? parseInt(patch, 10) : patch,
538
pre: pre,
539
};
540
}
541
542
export function fromString(version: string): Version {
543
const [ver, pre] = version.split('-');
544
const [major, minor, patch] = ver.split('.');
545
return from(major, minor, patch, pre);
546
}
547
}
548
549
export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): {
550
added: SourceControlHistoryItemRef[];
551
modified: SourceControlHistoryItemRef[];
552
removed: SourceControlHistoryItemRef[];
553
} {
554
if (before.length === 0) {
555
return { added: after, modified: [], removed: [] };
556
}
557
558
const added: SourceControlHistoryItemRef[] = [];
559
const modified: SourceControlHistoryItemRef[] = [];
560
const removed: SourceControlHistoryItemRef[] = [];
561
562
let beforeIdx = 0;
563
let afterIdx = 0;
564
565
while (true) {
566
if (beforeIdx === before.length) {
567
added.push(...after.slice(afterIdx));
568
break;
569
}
570
if (afterIdx === after.length) {
571
removed.push(...before.slice(beforeIdx));
572
break;
573
}
574
575
const beforeElement = before[beforeIdx];
576
const afterElement = after[afterIdx];
577
578
const result = beforeElement.id.localeCompare(afterElement.id);
579
580
if (result === 0) {
581
if (beforeElement.revision !== afterElement.revision) {
582
// modified
583
modified.push(afterElement);
584
}
585
586
beforeIdx += 1;
587
afterIdx += 1;
588
} else if (result < 0) {
589
// beforeElement is smaller -> before element removed
590
removed.push(beforeElement);
591
592
beforeIdx += 1;
593
} else if (result > 0) {
594
// beforeElement is greater -> after element added
595
added.push(afterElement);
596
597
afterIdx += 1;
598
}
599
}
600
601
return { added, modified, removed };
602
}
603
604
const minute = 60;
605
const hour = minute * 60;
606
const day = hour * 24;
607
const week = day * 7;
608
const month = day * 30;
609
const year = day * 365;
610
611
/**
612
* Create a l10n.td difference of the time between now and the specified date.
613
* @param date The date to generate the difference from.
614
* @param appendAgoLabel Whether to append the " ago" to the end.
615
* @param useFullTimeWords Whether to use full words (eg. seconds) instead of
616
* shortened (eg. secs).
617
* @param disallowNow Whether to disallow the string "now" when the difference
618
* is less than 30 seconds.
619
*/
620
export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string {
621
if (typeof date !== 'number') {
622
date = date.getTime();
623
}
624
625
const seconds = Math.round((new Date().getTime() - date) / 1000);
626
if (seconds < -30) {
627
return l10n.t('in {0}', fromNow(new Date().getTime() + seconds * 1000, false));
628
}
629
630
if (!disallowNow && seconds < 30) {
631
return l10n.t('now');
632
}
633
634
let value: number;
635
if (seconds < minute) {
636
value = seconds;
637
638
if (appendAgoLabel) {
639
if (value === 1) {
640
return useFullTimeWords
641
? l10n.t('{0} second ago', value)
642
: l10n.t('{0} sec ago', value);
643
} else {
644
return useFullTimeWords
645
? l10n.t('{0} seconds ago', value)
646
: l10n.t('{0} secs ago', value);
647
}
648
} else {
649
if (value === 1) {
650
return useFullTimeWords
651
? l10n.t('{0} second', value)
652
: l10n.t('{0} sec', value);
653
} else {
654
return useFullTimeWords
655
? l10n.t('{0} seconds', value)
656
: l10n.t('{0} secs', value);
657
}
658
}
659
}
660
661
if (seconds < hour) {
662
value = Math.floor(seconds / minute);
663
if (appendAgoLabel) {
664
if (value === 1) {
665
return useFullTimeWords
666
? l10n.t('{0} minute ago', value)
667
: l10n.t('{0} min ago', value);
668
} else {
669
return useFullTimeWords
670
? l10n.t('{0} minutes ago', value)
671
: l10n.t('{0} mins ago', value);
672
}
673
} else {
674
if (value === 1) {
675
return useFullTimeWords
676
? l10n.t('{0} minute', value)
677
: l10n.t('{0} min', value);
678
} else {
679
return useFullTimeWords
680
? l10n.t('{0} minutes', value)
681
: l10n.t('{0} mins', value);
682
}
683
}
684
}
685
686
if (seconds < day) {
687
value = Math.floor(seconds / hour);
688
if (appendAgoLabel) {
689
if (value === 1) {
690
return useFullTimeWords
691
? l10n.t('{0} hour ago', value)
692
: l10n.t('{0} hr ago', value);
693
} else {
694
return useFullTimeWords
695
? l10n.t('{0} hours ago', value)
696
: l10n.t('{0} hrs ago', value);
697
}
698
} else {
699
if (value === 1) {
700
return useFullTimeWords
701
? l10n.t('{0} hour', value)
702
: l10n.t('{0} hr', value);
703
} else {
704
return useFullTimeWords
705
? l10n.t('{0} hours', value)
706
: l10n.t('{0} hrs', value);
707
}
708
}
709
}
710
711
if (seconds < week) {
712
value = Math.floor(seconds / day);
713
if (appendAgoLabel) {
714
return value === 1
715
? l10n.t('{0} day ago', value)
716
: l10n.t('{0} days ago', value);
717
} else {
718
return value === 1
719
? l10n.t('{0} day', value)
720
: l10n.t('{0} days', value);
721
}
722
}
723
724
if (seconds < month) {
725
value = Math.floor(seconds / week);
726
if (appendAgoLabel) {
727
if (value === 1) {
728
return useFullTimeWords
729
? l10n.t('{0} week ago', value)
730
: l10n.t('{0} wk ago', value);
731
} else {
732
return useFullTimeWords
733
? l10n.t('{0} weeks ago', value)
734
: l10n.t('{0} wks ago', value);
735
}
736
} else {
737
if (value === 1) {
738
return useFullTimeWords
739
? l10n.t('{0} week', value)
740
: l10n.t('{0} wk', value);
741
} else {
742
return useFullTimeWords
743
? l10n.t('{0} weeks', value)
744
: l10n.t('{0} wks', value);
745
}
746
}
747
}
748
749
if (seconds < year) {
750
value = Math.floor(seconds / month);
751
if (appendAgoLabel) {
752
if (value === 1) {
753
return useFullTimeWords
754
? l10n.t('{0} month ago', value)
755
: l10n.t('{0} mo ago', value);
756
} else {
757
return useFullTimeWords
758
? l10n.t('{0} months ago', value)
759
: l10n.t('{0} mos ago', value);
760
}
761
} else {
762
if (value === 1) {
763
return useFullTimeWords
764
? l10n.t('{0} month', value)
765
: l10n.t('{0} mo', value);
766
} else {
767
return useFullTimeWords
768
? l10n.t('{0} months', value)
769
: l10n.t('{0} mos', value);
770
}
771
}
772
}
773
774
value = Math.floor(seconds / year);
775
if (appendAgoLabel) {
776
if (value === 1) {
777
return useFullTimeWords
778
? l10n.t('{0} year ago', value)
779
: l10n.t('{0} yr ago', value);
780
} else {
781
return useFullTimeWords
782
? l10n.t('{0} years ago', value)
783
: l10n.t('{0} yrs ago', value);
784
}
785
} else {
786
if (value === 1) {
787
return useFullTimeWords
788
? l10n.t('{0} year', value)
789
: l10n.t('{0} yr', value);
790
} else {
791
return useFullTimeWords
792
? l10n.t('{0} years', value)
793
: l10n.t('{0} yrs', value);
794
}
795
}
796
}
797
798
export function getCommitShortHash(scope: Uri, hash: string): string {
799
const config = workspace.getConfiguration('git', scope);
800
const shortHashLength = config.get<number>('commitShortHashLength', 7);
801
return hash.substring(0, shortHashLength);
802
}
803
804
export function getHistoryItemDisplayName(historyItem: SourceControlHistoryItem): string {
805
return historyItem.references?.length
806
? historyItem.references[0].name
807
: historyItem.displayId ?? historyItem.id;
808
}
809
810
export type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint' | 'none';
811
812
export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity {
813
return value === 'error'
814
? DiagnosticSeverity.Error
815
: value === 'warning'
816
? DiagnosticSeverity.Warning
817
: value === 'information'
818
? DiagnosticSeverity.Information
819
: DiagnosticSeverity.Hint;
820
}
821
822
export function extractFilePathFromArgs(argv: string[], startIndex: number): string {
823
// Argument doesn't start with a quote
824
const firstArg = argv[startIndex];
825
if (!firstArg.match(/^["']/)) {
826
return firstArg.replace(/^["']+|["':]+$/g, '');
827
}
828
829
// If it starts with a quote, we need to find the matching closing
830
// quote which might be in a later argument if the path contains
831
// spaces
832
const quote = firstArg[0];
833
834
// If the first argument ends with the same quote, it's complete
835
if (firstArg.endsWith(quote) && firstArg.length > 1) {
836
return firstArg.slice(1, -1);
837
}
838
839
// Concatenate arguments until we find the closing quote
840
let path = firstArg;
841
for (let i = startIndex + 1; i < argv.length; i++) {
842
path = `${path} ${argv[i]}`;
843
if (argv[i].endsWith(quote)) {
844
// Found the matching quote
845
return path.slice(1, -1);
846
}
847
}
848
849
// If no closing quote was found, remove
850
// leading quote and return the path as-is
851
return path.slice(1);
852
}
853
854
export function getStashDescription(stash: Stash): string | undefined {
855
if (!stash.commitDate && !stash.branchName) {
856
return undefined;
857
}
858
859
const descriptionSegments: string[] = [];
860
if (stash.commitDate) {
861
descriptionSegments.push(fromNow(stash.commitDate));
862
}
863
if (stash.branchName) {
864
descriptionSegments.push(stash.branchName);
865
}
866
867
return descriptionSegments.join(' \u2022 ');
868
}
869
870
export function isCopilotWorktree(path: string): boolean {
871
const lastSepIndex = path.lastIndexOf(sep);
872
873
return lastSepIndex !== -1
874
? path.substring(lastSepIndex + 1).startsWith('copilot-worktree-')
875
: path.startsWith('copilot-worktree-');
876
}
877
878