import type { UrlOpening } from './systems' // GitHub's names (github.md §1): its locators, remotes and what opening a // pull request by its URL declares (integrations §3), and the account (auth). // Exporting a review's types are in reviews.ts. Tokens never leave the main // process; the renderer sees only these. /** Where the GitHub token comes from: `gh auth token`, or a token the user saved in the OS keychain. */ export type GithubAuthSource = 'gh' | 'keychain' export interface GithubAuthStatus { source: GithubAuthSource host: string /** The `gh` binary found, when there is one. */ ghPath: string | null /** A token is available from the chosen source. */ ready: boolean /** The account the token belongs to, when checked. */ login?: string /** Why the chosen source has no usable token. */ problem?: string /** The keychain holds a token for this host (whichever source is chosen). */ keychainHasToken: boolean } // ---------------------------------------------------------------- locators const LOCATOR = /^([A-Za-z0-9._-]{1,100})\/([A-Za-z0-9._-]{1,100})\/pull\/([0-9]{1,12})$/ /** GraphQL's Int is 32-bit signed. */ const MAX_NUMBER = 2 ** 31 - 1 export interface PullRequestRef { owner: string name: string number: number } /** * `//pull/` in canonical form: owner and name of * `[A-Za-z0-9._-]` (not only dots), lower case (GitHub's names are * case-insensitive), a decimal number without leading zeros. Null otherwise. */ export function canonicalLocator(locator: string): string | null { const ref = parseLocator(locator) return ref ? `${ref.owner}/${ref.name}/pull/${ref.number}` : null } export function parseLocator(locator: string): PullRequestRef | null { if (typeof locator !== 'string') return null const m = LOCATOR.exec(locator) if (!m) return null const [, owner, name, num] = m as unknown as [string, string, string, string] if (/^\.+$/.test(owner) || /^\.+$/.test(name)) return null const number = Number(num) if (!Number.isSafeInteger(number) || number < 1 || number > MAX_NUMBER) return null return { owner: owner.toLowerCase(), name: name.toLowerCase(), number } } /** github.com, and hosts that look like GitHub Enterprise. */ const isGithubHost = (host: string): boolean => host === 'github.com' || /(^|\.)github\./.test(host) /** owner/name on a GitHub host, from a remote URL (https, ssh or scp-like). */ export function parseGithubRemote(url: string): { host: string; owner: string; name: string } | null { const u = url.trim() const m = /^(?:https?|ssh|git):\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(u) ?? /^(?:[^@/]+@)?([^/:]+):([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(u) if (!m) return null const host = m[1]!.toLowerCase().replace(/^ssh\./, '') if (!isGithubHost(host)) return null return { host, owner: m[2]!, name: m[3]! } } // ---------------------------------------------------------------- opening by URL (github.md §1) const TRACKING = /^pull\/([0-9]{1,12})$/ const number = (locator: string): number => { const ref = parseLocator(locator) if (!ref) throw new Error('Not a GitHub pull request locator') return ref.number } export const githubOpening: UrlOpening = { system: 'github', names: { system: 'GitHub', noun: 'pull request', kind: 'pr', example: 'https://github.com/owner/name/pull/123' }, short: (locator) => `#${number(locator)}`, /** `https://github.com/org/repo/pull/42` (and `/files`, `#…` and the like after it). */ parseUrl(text) { const m = /^(?:https?:\/\/)?([a-z0-9.-]+(?::\d{1,5})?)\/([\w.-]+)\/([\w.-]+)\/pull\/(\d{1,9})(?:[/?#].*)?$/i.exec(text.trim()) if (!m) return null const host = m[1]!.toLowerCase() if (!isGithubHost(host)) return null const locator = canonicalLocator(`${m[2]}/${m[3]!.replace(/\.git$/, '')}/pull/${m[4]}`) return locator ? { host, locator } : null }, canonicalUrl: (host, locator) => `https://${host}/${canonicalLocator(locator) ?? locator}`, cloneUrl(host, locator) { const ref = parseLocator(locator) if (!ref) throw new Error('Not a GitHub pull request locator') return `https://${host}/${ref.owner}/${ref.name}.git` }, isRepository(host, locator, remoteUrl) { const g = parseGithubRemote(remoteUrl) const ref = parseLocator(locator) return !!g && !!ref && g.host === host && g.owner.toLowerCase() === ref.owner && g.name.toLowerCase() === ref.name }, refs: { // Forks' pull requests have refs/pull//head too. head: (locator) => ({ ref: `refs/pull/${number(locator)}/head`, tracking: `pull/${number(locator)}` }), base: (branch) => ({ ref: `refs/heads/${branch}`, tracking: branch }), headOf: (tracking) => { const m = TRACKING.exec(tracking) return m ? `refs/pull/${m[1]}/head` : null } }, destinationOf(remoteUrl, tracking) { const g = parseGithubRemote(remoteUrl) const n = TRACKING.exec(tracking)?.[1] const locator = g && n ? canonicalLocator(`${g.owner}/${g.name.replace(/\.git$/, '')}/pull/${n}`) : null return g && locator ? { host: g.host, locator } : null } }