Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
CC: clang
CXX: clang++
npm_config_clang: 1
- os: windows-latest
- os: windows-2022
env: {}
- os: macos-latest
env:
Expand Down
21 changes: 0 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,6 @@ remote tracking branch exists.
Returns an object with `ahead` and `behind` keys pointing to integer values
that will always be >= 0.

### Repository.getCommitCount(fromCommit, toCommit)

Get the number of commits between `fromCommit` and `toCommit`.

`fromCommit` - The string commit SHA-1 to start the rev walk at.

`toCommit` - The string commit SHA-1 to end the rev walk at.

Returns the number of commits between the two, always >= 0.

### Repository.getConfigValue(key)

Get the config value of the given key.
Expand Down Expand Up @@ -170,17 +160,6 @@ object has `oldStart`, `oldLines`, `newStart`, `newLines`, `oldLineNumber` and
`newLineNumber` keys pointing to integer values, and a `line` key pointing to the
respective line content. May be `null` if the diff fails.

### Repository.getMergeBase(commit1, commit2)

Get the merge base of two commits.

`commit1` - The string SHA-1 of the first commit.

`commit2` - The string SHA-1 of the second commit.

Returns the string SHA-1 of the merge base of `commit1` and `commit2` or `null`
if there isn't one.

### Repository.getPath()

Get the path of the repository.
Expand Down
9 changes: 5 additions & 4 deletions spec/git-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ const _ = require('underscore')
describe('git', () => {
let repo

afterEach(() => {
if (repo) repo.release()
})
afterEach(() => repo?.release())

describe('.open(path)', () => {
describe('when the path is a repository', () => {
Expand Down Expand Up @@ -1030,6 +1028,7 @@ describe('git', () => {
repo.caseInsensitiveFs = true
workingDirectory = repo.getWorkingDirectory()

expect(repo.isWorkingDirectory(repoDirectory)).toBe(true)
expect(repo.isWorkingDirectory(workingDirectory.toUpperCase())).toBe(true)
})
})
Expand Down Expand Up @@ -1116,7 +1115,9 @@ describe('git', () => {
expect(repo.submoduleForPath('sub1')).toBe(null)

let submoduleRepoPath = path.join(repo.getPath(), 'modules', 'sub/')
if (process.platform === 'win32') { submoduleRepoPath = submoduleRepoPath.replace(/\\/g, '/') }
if (process.platform === 'win32') {
submoduleRepoPath = submoduleRepoPath.replace(/\\/g, '/')
}

expect(repo.submoduleForPath('sub').getPath()).toBe(submoduleRepoPath)
expect(repo.submoduleForPath('sub/').getPath()).toBe(submoduleRepoPath)
Expand Down
239 changes: 160 additions & 79 deletions src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,77 @@ const indexStatusFlags =
statusIndexRenamed |
statusIndexTypeChange


const IS_WINDOWS = process.platform === 'win32'

// Given a path on disk (real or hypothetical), attempt to normalize it by
// (optionally) resolving `realpath` and (if on Windows) converting all path
// separators to forward slashes.
function normalizePath (filePath, useRealpath = true) {
if (typeof filePath !== 'string') return filePath

if (useRealpath) {
filePath = realpath(filePath)
}
if (!IS_WINDOWS) return filePath
return realpath(filePath).replace(/\\/g, '/')
}

// Compare two paths to determine whether they resolve to the same file or
// directory on disk.
//
// This is more complicated than it sounds — not just because of symlinks but
// also because of files/directories on Windows possibly having both a short
// name and a long name.
function pathsAreEqual (pathA, pathB, caseInsensitive = false, useRealpath = true) {
if (typeof pathA !== 'string' || typeof pathB !== 'string') {
return false
}

pathA = normalizePath(pathA, useRealpath)
pathB = normalizePath(pathB, useRealpath)

if (IS_WINDOWS || caseInsensitive) {
pathA = pathA.toLowerCase()
pathB = pathB.toLowerCase()
}

let result = pathA === pathB
if (result || !IS_WINDOWS) return result
if (!pathA.includes('~') && !pathB.includes('~')) {
return result
}

// If we get this far, we're on Windows and comparing two paths, at least one
// of which contains an 8.3 short name. The only obvious and reliable way to
// address this is to `statSync` both paths and verify their IDs are the
// same.
if (!fs.existsSync(pathA) || !fs.existsSync(pathB)) {
return result
}
let statA = fs.statSync(pathA)
let statB = fs.statSync(pathB)

return statA.ino === statB.ino && statA.dev === statB.dev
}

// Returns whether `pathA` starts with `pathB` — i.e., whether `pathB` is equal
// to `pathA` or else one of its ancestor directories.
function pathStartsWith (pathA, pathB, caseInsensitive = false, useRealpath = true) {
if (IS_WINDOWS) {
pathA = normalizePath(pathA, useRealpath)
pathB = normalizePath(pathB, useRealpath)
}
if (caseInsensitive) {
pathA = pathA.toLowerCase()
pathB = pathB.toLowerCase()
}
if (!pathB.endsWith(`/`)) {
pathB = `${pathB}/`
}
return pathA.startsWith(pathB)
}

Repository.prototype.release = function () {
for (let submodulePath in this.submodules) {
const submoduleRepo = this.submodules[submodulePath]
Expand Down Expand Up @@ -130,7 +201,7 @@ Repository.prototype.getAheadBehindCount = function (branch = 'HEAD') {
return this.compareCommits(headCommit, upstreamCommit)
}

Repository.prototype.getAheadBehindCountAsync = function (branch = 'HEAD') {
Repository.prototype.getAheadBehindCountAsync = async function (branch = 'HEAD') {
if (branch !== 'HEAD' && !branch.startsWith('refs/heads/')) {
branch = `refs/heads/${branch}`
}
Expand Down Expand Up @@ -159,89 +230,66 @@ Repository.prototype.checkoutReference = function (branch, create) {
Repository.prototype.relativize = function (filePath) {
let workingDirectory
if (!filePath) return filePath
filePath = realpathRecursive(filePath)

if (process.platform === 'win32') {
filePath = filePath.replace(/\\/g, '/')
} else {
if (filePath[0] !== '/') return filePath
if (!IS_WINDOWS && filePath[0] !== '/') {
return filePath
}

if (this.caseInsensitiveFs) {
const lowerCasePath = filePath.toLowerCase()

workingDirectory = this.getWorkingDirectory()
if (workingDirectory) {
workingDirectory = workingDirectory.toLowerCase()
if (lowerCasePath.startsWith(`${workingDirectory}/`)) {
return filePath.substring(workingDirectory.length + 1)
} else if (lowerCasePath === workingDirectory) {
return ''
}
}

if (this.openedWorkingDirectory) {
workingDirectory = this.openedWorkingDirectory.toLowerCase()
if (lowerCasePath.startsWith(`${workingDirectory}/`)) {
return filePath.substring(workingDirectory.length + 1)
} else if (lowerCasePath === workingDirectory) {
return ''
}
}
} else {
workingDirectory = this.getWorkingDirectory()
if (workingDirectory) {
if (filePath.startsWith(`${workingDirectory}/`)) {
return filePath.substring(workingDirectory.length + 1)
} else if (filePath === workingDirectory) {
return ''
}
workingDirectory = this.getWorkingDirectory()
if (workingDirectory) {
if (pathStartsWith(filePath, workingDirectory, this.caseInsensitiveFs, false)) {
return filePath.substring(workingDirectory.length + 1)
} else if (pathsAreEqual(filePath, workingDirectory, this.caseInsensitiveFs, false)) {
return ''
}
}

if (this.openedWorkingDirectory) {
if (filePath.startsWith(`${this.openedWorkingDirectory}/`)) {
return filePath.substring(this.openedWorkingDirectory.length + 1)
} else if (filePath === this.openedWorkingDirectory) {
return ''
}
if (this.openedWorkingDirectory) {
workingDirectory = this.openedWorkingDirectory
if (pathStartsWith(filePath, workingDirectory, this.caseInsensitiveFs, false)) {
return filePath.substring(workingDirectory.length + 1)
} else if (pathsAreEqual(filePath, workingDirectory, this.caseInsensitiveFs, false)) {
return ''
}
}

return filePath
}

Repository.prototype.submoduleForPath = function (path) {
path = this.relativize(path)
if (!path) return null
Repository.prototype.submoduleForPath = function (filePath) {
filePath = this.relativize(filePath)
if (!filePath) return null

for (let submodulePath in this.submodules) {
const submoduleRepo = this.submodules[submodulePath]
if (path === submodulePath) {
if (filePath === submodulePath) {
return submoduleRepo
} else if (path.startsWith(`${submodulePath}/`)) {
path = path.substring(submodulePath.length + 1)
return submoduleRepo.submoduleForPath(path) || submoduleRepo
} else if (filePath.startsWith(`${submodulePath}/`)) {
filePath = filePath.substring(submodulePath.length + 1)
return submoduleRepo.submoduleForPath(filePath) || submoduleRepo
}
}

return null
}

Repository.prototype.isWorkingDirectory = function (path) {
if (!path) return false
Repository.prototype.isWorkingDirectory = function (dirPath) {
if (!dirPath) return false
dirPath = normalizePath(dirPath)

if (process.platform === 'win32') {
path = path.replace(/\\/g, '/')
} else {
if (path[0] !== '/') return false
if (!IS_WINDOWS && dirPath[0] !== '/') {
return false
}

if (this.caseInsensitiveFs) {
const lowerCasePath = path.toLowerCase()
const workingDirectory = this.getWorkingDirectory()
if (workingDirectory && workingDirectory.toLowerCase() === lowerCasePath) return true
if (this.openedWorkingDirectory && this.openedWorkingDirectory.toLowerCase() === lowerCasePath) return true
} else {
return path === this.getWorkingDirectory() || path === this.openedWorkingDirectory
let workingDirectory = this.getWorkingDirectory()
if (workingDirectory && pathsAreEqual(workingDirectory, dirPath, this.caseInsensitiveFs)) {
return true
}

let openedWorkingDirectory = this.openedWorkingDirectory
if (openedWorkingDirectory && pathsAreEqual(openedWorkingDirectory, dirPath, this.caseInsensitiveFs)) {
return true
}

return false
Expand All @@ -258,9 +306,9 @@ Repository.prototype.getStatusForPaths = function (paths) {
}
}

Repository.prototype.getStatus = function (path) {
if (typeof path === 'string') {
return getStatusForPath.call(this, path)
Repository.prototype.getStatus = function (filePath) {
if (typeof filePath === 'string') {
return getStatusForPath.call(this, filePath)
} else {
return getStatus.call(this)
}
Expand Down Expand Up @@ -295,46 +343,79 @@ function promisify (fn) {
)
}

// Given `unrealPath` — which may or may not exist on disk in its current form
// — resolve to a real path on disk, if possible.
//
// This is done by traversing upward to the first directory that _does_ exist,
// then getting its `realpath` and appending the rest back on.
function realpathRecursive (unrealPath) {
let currentPath = unrealPath
let result = unrealPath
let remainder = ''
if (!path.isAbsolute(unrealPath)) {
return realpath(unrealPath, true)
}
while (!isRootPath(currentPath)) {
try {
result = fs.realpathSync.native(currentPath)
break
} catch (e) {
if (e.message.includes('ENOENT')) {
currentPath = path.resolve(currentPath, '..')
remainder = path.relative(currentPath, unrealPath)
} else {
return unrealPath
}
}
}
if (isRootPath(currentPath)) {
return unrealPath
}
let finalResult = trimPath(`${result}/${remainder}`)
return normalizePath(finalResult)
}

function trimPath (filePath) {
if (!filePath.endsWith('/')) return filePath
return filePath.replace(/\/$/, '')
}

// Attempts to resolve a path to its real path on disk; if it fails, returns
// the original path.
function realpath (unrealPath) {
try {
// `fs.realpathSync.native` somehow is the only thing that can consistently
// normalize 8.3 "short names" in Windows to their long equivalents.
if (typeof fs.realpathSync.native === 'function') {
return fs.realpathSync.native(unrealPath)
}
return fs.realpathSync(unrealPath)
} catch (e) {
return unrealPath
}
}

// Returns whether the path has no parent directory.
function isRootPath (repositoryPath) {
if (process.platform === 'win32') {
if (IS_WINDOWS) {
return /^[a-zA-Z]+:[\\/]$/.test(repositoryPath)
} else {
return repositoryPath === path.sep
}
}

function openRepository (repositoryPath, search) {
if (!fs.existsSync(repositoryPath)) return null
const symlink = realpath(repositoryPath) !== repositoryPath
repositoryPath = normalizePath(repositoryPath, false)

if (process.platform === 'win32') {
repositoryPath = repositoryPath.replace(/\\/g, '/')
}
const repository = new Repository(repositoryPath, search)
if (repository.exists()) {
repository.caseInsensitiveFs = fs.isCaseInsensitive()
if (symlink) {
const workingDirectory = repository.getWorkingDirectory()
// On Windows, normalize both sides through realpath so that 8.3 short
// names (e.g., RUNNER~1) and path separator differences don't prevent
// the comparison from matching. Compare case-insensitively because
// Windows paths are case-insensitive.
const normalizedWorkingDir = process.platform === 'win32'
? realpath(workingDirectory).replace(/\\/g, '/').toLowerCase()
: workingDirectory
while (!isRootPath(repositoryPath)) {
let realpathResult = realpath(repositoryPath)
if (process.platform === 'win32') {
realpathResult = realpathResult.replace(/\\/g, '/').toLowerCase()
}
if (realpathResult === normalizedWorkingDir) {
if (pathsAreEqual(repositoryPath, workingDirectory, fs.isCaseInsensitive())) {
repository.openedWorkingDirectory = repositoryPath
break
}
Expand Down
Loading