diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index b8ec3382b63..913627c8922 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -14,7 +14,7 @@ import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri } from './uri'; -import { grep, isDescendant, logTimestamp, pathEquals } from './util'; +import { grep, isDescendant, logTimestamp, pathEquals, relativePath } from './util'; import { Log, LogLevel } from './log'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; @@ -803,7 +803,7 @@ export class CommandCenter { return; } - const from = path.relative(repository.root, fromUri.fsPath); + const from = relativePath(repository.root, fromUri.fsPath); let to = await window.showInputBox({ value: from, valueSelection: [from.length - path.basename(from).length, from.length] diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index a2e8fd1a800..6b2205ddcb7 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -14,7 +14,7 @@ import { debounce, memoize, throttle } from './decorators'; import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, logTimestamp, onceEvent } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, logTimestamp, onceEvent, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { Log, LogLevel } from './log'; import { IPushErrorHandlerRegistry } from './pushError'; @@ -1161,8 +1161,8 @@ export class Repository implements Disposable { } async stage(resource: Uri, contents: string): Promise { - const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/'); - await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents)); + const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); + await this.run(Operation.Stage, () => this.repository.stage(path, contents)); this._onDidChangeOriginalResource.fire(resource); } @@ -1545,16 +1545,16 @@ export class Repository implements Disposable { async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { - const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); + const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); try { - return await this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { - const gitRelativePath = await this.repository.getGitRelativePath(ref, relativePath); + const gitRelativePath = await this.repository.getGitRelativePath(ref, path); return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); } @@ -1565,8 +1565,8 @@ export class Repository implements Disposable { async buffer(ref: string, filePath: string): Promise { return this.run(Operation.Show, () => { - const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.buffer(`${ref}:${relativePath}`); + const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); + return this.repository.buffer(`${ref}:${path}`); }); } @@ -1610,7 +1610,7 @@ export class Repository implements Disposable { return await this.run(Operation.Ignore, async () => { const ignoreFile = `${this.repository.root}${path.sep}.gitignore`; const textToAppend = files - .map(uri => path.relative(this.repository.root, uri.fsPath).replace(/\\/g, '/')) + .map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/')) .join('\n'); const document = await new Promise(c => fs.exists(ignoreFile, c)) diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 7631dbe4727..a8ae8fff98a 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Disposable, EventEmitter } from 'vscode'; -import { dirname, sep } from 'path'; +import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import * as byline from 'byline'; @@ -287,6 +287,16 @@ export function detectUnicodeEncoding(buffer: Buffer): Encoding | null { return null; } +function normalizePath(path: string): string { + // Windows & Mac are currently being handled + // as case insensitive file systems in VS Code. + if (isWindows || isMacintosh) { + return path.toLowerCase(); + } + + return path; +} + export function isDescendant(parent: string, descendant: string): boolean { if (parent === descendant) { return true; @@ -296,25 +306,26 @@ export function isDescendant(parent: string, descendant: string): boolean { parent += sep; } - // Windows & Mac are currently being handled - // as case insensitive file systems in VS Code. - if (isWindows || isMacintosh) { - parent = parent.toLowerCase(); - descendant = descendant.toLowerCase(); - } - - return descendant.startsWith(parent); + return normalizePath(descendant).startsWith(normalizePath(parent)); } export function pathEquals(a: string, b: string): boolean { - // Windows & Mac are currently being handled - // as case insensitive file systems in VS Code. - if (isWindows || isMacintosh) { - a = a.toLowerCase(); - b = b.toLowerCase(); + return normalizePath(a) === normalizePath(b); +} + +/** + * Given the `repository.root` compute the relative path while trying to preserve + * the casing of the resource URI. The `repository.root` segment of the path can + * have a casing mismatch if the folder/workspace is being opened with incorrect + * casing. + */ +export function relativePath(from: string, to: string): string { + if (isDescendant(from, to) && from.length < to.length) { + return to.substring(from.length + 1); } - return a === b; + // Fallback to `path.relative` + return relative(from, to); } export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator {