From ee2319893874833ac3cdfd6de4a1fe1ce1937b9f Mon Sep 17 00:00:00 2001 From: Kristoffer K Date: Wed, 30 Nov 2022 14:22:45 +0100 Subject: [PATCH] fix(git): normalize repo url without legacy `url.parse` (#5100) --- .yarn/versions/c2cb7806.yml | 25 +++++++ packages/plugin-git/sources/gitUtils.ts | 29 +++----- .../sources/hosted-git-info-parse.ts | 67 +++++++++++++++++++ .../tests/__snapshots__/gitUtils.test.ts.snap | 4 +- 4 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 .yarn/versions/c2cb7806.yml create mode 100644 packages/plugin-git/sources/hosted-git-info-parse.ts diff --git a/.yarn/versions/c2cb7806.yml b/.yarn/versions/c2cb7806.yml new file mode 100644 index 000000000000..59e71eacc93f --- /dev/null +++ b/.yarn/versions/c2cb7806.yml @@ -0,0 +1,25 @@ +releases: + "@yarnpkg/cli": patch + "@yarnpkg/plugin-git": patch + +declined: + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-npm-cli" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/core" + - "@yarnpkg/doctor" diff --git a/packages/plugin-git/sources/gitUtils.ts b/packages/plugin-git/sources/gitUtils.ts index 6a637be1addc..d1345e41a6f7 100644 --- a/packages/plugin-git/sources/gitUtils.ts +++ b/packages/plugin-git/sources/gitUtils.ts @@ -5,7 +5,8 @@ import GitUrlParse import capitalize from 'lodash/capitalize'; import querystring from 'querystring'; import semver from 'semver'; -import urlLib from 'url'; + +import {tryParseGitURL} from './hosted-git-info-parse'; function makeGitEnvironment() { return { @@ -140,27 +141,13 @@ export function normalizeRepoUrl(url: string, {git = false}: {git?: boolean} = { url = url.replace(/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/, `https://github.com/$1/$2.git#$3`); if (git) { - // The `git+` prefix doesn't mean anything at all for Git - url = url.replace(/^git\+([^:]+):/, `$1:`); - - // The `ssh://` prefix should be removed because so URLs won't work in Git: - // ssh://git@github.com:yarnpkg/berry.git - // git@github.com/yarnpkg/berry.git - // Git only allows: - // git@github.com:yarnpkg/berry.git (no ssh) - // ssh://git@github.com/yarnpkg/berry.git (no colon) - // So we should cut `ssh://`, but only in URLs that contain colon after the hostname - - let parsedUrl: urlLib.UrlWithStringQuery | null; - try { - parsedUrl = urlLib.parse(url); - } catch { - parsedUrl = null; - } + // Try to normalize the URL in a way that git accepts. + const parsedUrl = tryParseGitURL(url); + if (parsedUrl) + url = parsedUrl.href; - if (parsedUrl && parsedUrl.protocol === `ssh:` && parsedUrl.path?.startsWith(`/:`)) { - url = url.replace(/^ssh:\/\//, ``); - } + // The `git+` prefix doesn't mean anything at all for Git. + url = url.replace(/^git\+([^:]+):/, `$1:`); } return url; diff --git a/packages/plugin-git/sources/hosted-git-info-parse.ts b/packages/plugin-git/sources/hosted-git-info-parse.ts new file mode 100644 index 000000000000..aafe3453e2aa --- /dev/null +++ b/packages/plugin-git/sources/hosted-git-info-parse.ts @@ -0,0 +1,67 @@ +// Based on https://github.com/npm/hosted-git-info/blob/cf8115d6fa056fbfb0d63d4d13bde6116b2a02e0/lib/parse-url.js + +/** + @license + Copyright (c) 2015, Rebecca Turner + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + */ + +function lastIndexOfBefore(str: string, char: string, beforeChar: string) { + const startPosition = str.indexOf(beforeChar); + return str.lastIndexOf(char, startPosition > -1 ? startPosition : Infinity); +} + +function safeURL(url: string) { + try { + return new URL(url); + } catch { + return undefined; + } +} + +// attempt to correct an scp style url so that it will parse with `new URL()` +function correctURL(gitURL: string) { + // ignore @ that come after the first hash since the denotes the start + // of a committish which can contain @ characters + const firstAt = lastIndexOfBefore(gitURL, `@`, `#`); + // ignore colons that come after the hash since that could include colons such as: + // git@github.com:user/package-2#semver:^1.0.0 + const lastColonBeforeHash = lastIndexOfBefore(gitURL, `:`, `#`); + + if (lastColonBeforeHash > firstAt) + // the last : comes after the first @ (or there is no @) + // like it would in: + // proto://hostname.com:user/repo + // username@hostname.com:user/repo + // :password@hostname.com:user/repo + // username:password@hostname.com:user/repo + // proto://username@hostname.com:user/repo + // proto://:password@hostname.com:user/repo + // proto://username:password@hostname.com:user/repo + // then we replace the last : with a / to create a valid path + gitURL = `${gitURL.slice(0, lastColonBeforeHash)}/${gitURL.slice(lastColonBeforeHash + 1)}`; + + if (lastIndexOfBefore(gitURL, `:`, `#`) === -1 && gitURL.indexOf(`//`) === -1) + // we have no : at all + // as it would be in: + // username@hostname.com/user/repo + // then we prepend a protocol + gitURL = `ssh://${gitURL}`; + + return gitURL; +} + +export function tryParseGitURL(gitURL: string) { + return safeURL(gitURL) || safeURL(correctURL(gitURL)); +} diff --git a/packages/plugin-git/tests/__snapshots__/gitUtils.test.ts.snap b/packages/plugin-git/tests/__snapshots__/gitUtils.test.ts.snap index aa04a2034c3d..370f9fbc43da 100644 --- a/packages/plugin-git/tests/__snapshots__/gitUtils.test.ts.snap +++ b/packages/plugin-git/tests/__snapshots__/gitUtils.test.ts.snap @@ -46,7 +46,7 @@ exports[`gitUtils should properly normalize git+ssh://git@github.com/TooTallNate exports[`gitUtils should properly normalize git+ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: false }) 1`] = `"git+ssh://git@github.com:yarnpkg/berry.git#v2.1.1"`; -exports[`gitUtils should properly normalize git+ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: true }) 1`] = `"git@github.com:yarnpkg/berry.git#v2.1.1"`; +exports[`gitUtils should properly normalize git+ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: true }) 1`] = `"ssh://git@github.com/yarnpkg/berry.git#v2.1.1"`; exports[`gitUtils should properly normalize git://github.com/TooTallNate/util-deprecate.git#v1.0.1 ({ git: false }) 1`] = `"git://github.com/TooTallNate/util-deprecate.git#v1.0.1"`; @@ -86,7 +86,7 @@ exports[`gitUtils should properly normalize ssh://git@github.com/TooTallNate/uti exports[`gitUtils should properly normalize ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: false }) 1`] = `"ssh://git@github.com:yarnpkg/berry.git#v2.1.1"`; -exports[`gitUtils should properly normalize ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: true }) 1`] = `"git@github.com:yarnpkg/berry.git#v2.1.1"`; +exports[`gitUtils should properly normalize ssh://git@github.com:yarnpkg/berry.git#v2.1.1 ({ git: true }) 1`] = `"ssh://git@github.com/yarnpkg/berry.git#v2.1.1"`; exports[`gitUtils should properly split GitHubOrg/foo-bar.js 1`] = ` Object {