Skip to content
This repository was archived by the owner on Mar 10, 2022. It is now read-only.
Merged
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
5 changes: 5 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"reveal": "never",
},
},
{
"type": "npm",
"script": "build",
"label": "build",
},
],
}
11 changes: 8 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ async function openCommand(): Promise<void> {
if (!editor) {
throw new Error('No active editor')
}
const [remoteURL, branch, fileRelative] = await repoInfo(editor.document.uri.fsPath)
if (remoteURL === '') {
const repositoryInfo = await repoInfo(editor.document.uri.fsPath)
if (!repositoryInfo) {
return
}
const { remoteURL, branch, fileRelative } = repositoryInfo

// Open in browser.
await open(
Expand All @@ -59,7 +60,11 @@ async function searchCommand(): Promise<void> {
if (!editor) {
throw new Error('No active editor')
}
const [remoteURL, branch, fileRelative] = await repoInfo(editor.document.uri.fsPath)
const repositoryInfo = await repoInfo(editor.document.uri.fsPath)
if (!repositoryInfo) {
return
}
const { remoteURL, branch, fileRelative } = repositoryInfo

const query = editor.document.getText(editor.selection)
if (query === '') {
Expand Down
128 changes: 66 additions & 62 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,12 @@ import { log } from './log'
import { getRemoteUrlReplacements } from './config'

/**
* Returns [remote, upstream branch].
* Empty remote is returned if the upstream branch points to a local branch.
* Empty upstream branch is returned if there is no upstream branch.
* Returns the repository root directory for any directory within the
* repository.
*/
async function gitRemoteBranch(repoDirectory: string): Promise<[string, string]> {
try {
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD@{upstream}'], { cwd: repoDirectory })
const remoteAndBranch = stdout.split('/')
if (remoteAndBranch.length === 2) {
const [remote, branch] = remoteAndBranch
return [remote, branch]
}
if (remoteAndBranch.length === 1) {
// The upstream branch points to a local branch.
return ['', remoteAndBranch[0]]
}
return ['', '']
} catch {
return ['', '']
}
async function gitRootDirectory(repoDirectory: string): Promise<string> {
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], { cwd: repoDirectory })
return stdout
}

/**
Expand Down Expand Up @@ -51,74 +37,92 @@ async function gitRemoteURL(repoDirectory: string, remoteName: string): Promise<
return stdout
}

interface RemoteName {
/**
* Remote name of the upstream repository,
* or the first found remote name if no upstream is found
*/
remoteName: string
}

interface Branch {
/**
* Remote branch name, or 'HEAD' if it isn't found because
* e.g. detached HEAD state, upstream branch points to a local branch
*/
branch: string
}

/**
* Returns the remote URL.
* Returns the remote name and branch
*
* @param repoDirectory the repository root directory
*/
async function gitDefaultRemoteURL(repoDirectory: string): Promise<string> {
const [remote] = await gitRemoteBranch(repoDirectory)
if (remote !== '') {
return gitRemoteURL(repoDirectory, remote)
async function gitRemoteNameAndBranch(repoDirectory: string): Promise<RemoteName & Branch> {
let remoteName: string | undefined
let branch = 'HEAD'

const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD@{upstream}'], { cwd: repoDirectory })
const remoteAndBranch = stdout.split('/')

if (remoteAndBranch.length === 1) {
// The upstream branch points to a local branch.
;[remoteName] = remoteAndBranch
}
if (remoteAndBranch.length === 2) {
;[remoteName, branch] = remoteAndBranch
}

// If we cannot find the remote name deterministically, we use the first
// Git remote found.
const remotes = await gitRemotes(repoDirectory)
if (remotes.length === 0) {
throw new Error('no configured git remotes')
if (!remoteName) {
const remotes = await gitRemotes(repoDirectory)
if (remotes.length > 1) {
log.appendLine(`using first git remote: ${remotes[0]}`)
remoteName = remotes[0]
}
}
if (remotes.length > 1) {
log.appendLine(`using first git remote: ${remotes[0]}`)

// Throw if a remote still isn't found
if (!remoteName) {
throw new Error('no configured git remotes')
}
return gitRemoteURL(repoDirectory, remotes[0])
}

/**
* Returns the repository root directory for any directory within the
* repository.
*/
async function gitRootDirectory(repoDirectory: string): Promise<string> {
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], { cwd: repoDirectory })
return stdout
return { remoteName, branch }
}

/**
* Returns either the current remote branch name of the repository OR in all
* other cases (e.g. detached HEAD state, upstream branch points to a local
* branch), it returns "HEAD".
*/
async function gitBranch(repoDirectory: string): Promise<string> {
const [origin, branch] = await gitRemoteBranch(repoDirectory)
if (origin !== '') {
// The remote branch exists.
return branch
}
return 'HEAD'
interface RepositoryInfo extends Branch {
/** Git repository remote URL */
remoteURL: string

/** File path relative to the repository root */
fileRelative: string
}

/**
* Returns the Git repository remote URL, the current branch, and the file path
* relative to the repository root. Empty strings are returned if this cannot be
* determined.
* relative to the repository root. Returns undefined if no remote is found
*/
export async function repoInfo(filePath: string): Promise<[string, string, string]> {
let remoteURL = ''
let branch = ''
let fileRelative = ''
export async function repoInfo(filePath: string): Promise<RepositoryInfo | undefined> {
try {
// Determine repository root directory.
const fileDirectory = path.dirname(filePath)
const repoRoot = await gitRootDirectory(fileDirectory)

// Determine file path, relative to repository root.
fileRelative = filePath.slice(repoRoot.length + 1)
remoteURL = await gitDefaultRemoteURL(repoRoot)
branch = await gitBranch(repoRoot)
// Determine file path relative to repository root.
let fileRelative = filePath.slice(repoRoot.length + 1)

const remoteNameAndBranch = await gitRemoteNameAndBranch(repoRoot)
const { branch, remoteName } = remoteNameAndBranch
const remoteURL = await gitRemoteURL(repoRoot, remoteName)

if (process.platform === 'win32') {
fileRelative = fileRelative.replace(/\\/g, '/')
}
log.appendLine(`repoInfo(${filePath}): remoteURL="${remoteURL}" branch="${branch}" fileRel="${fileRelative}"`)
return { remoteURL, branch, fileRelative }
} catch (error) {
log.appendLine(`repoInfo(${filePath}): ${error as string}`)
return undefined
}
log.appendLine(`repoInfo(${filePath}): remoteURL="${remoteURL}" branch="${branch}" fileRel="${fileRelative}"`)
return [remoteURL, branch, fileRelative]
}