Socket
Socket
Sign inDemoInstall

hosted-git-info

Package Overview
Dependencies
Maintainers
5
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

hosted-git-info - npm Package Compare versions

Comparing version 5.1.0 to 6.0.0

lib/from-url.js

326

lib/index.js
'use strict'
const url = require('url')
const gitHosts = require('./git-host-info.js')
const GitHost = module.exports = require('./git-host.js')
const LRU = require('lru-cache')
const hosts = require('./hosts.js')
const fromUrl = require('./from-url.js')
const cache = new LRU({ max: 1000 })
const protocolToRepresentationMap = {
'git+ssh:': 'sshurl',
'git+https:': 'https',
'ssh:': 'sshurl',
'git:': 'git',
}
class GitHost {
constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) {
Object.assign(this, GitHost.#gitHosts[type], {
type,
user,
auth,
project,
committish,
default: defaultRepresentation,
opts,
})
}
function protocolToRepresentation (protocol) {
return protocolToRepresentationMap[protocol] || protocol.slice(0, -1)
}
static #gitHosts = { byShortcut: {}, byDomain: {} }
static #protocols = {
'git+ssh:': { name: 'sshurl' },
'ssh:': { name: 'sshurl' },
'git+https:': { name: 'https', auth: true },
'git:': { auth: true },
'http:': { auth: true },
'https:': { auth: true },
'git+http:': { auth: true },
}
const authProtocols = {
'git:': true,
'https:': true,
'git+https:': true,
'http:': true,
'git+http:': true,
}
const knownProtocols = Object.keys(gitHosts.byShortcut)
.concat(['http:', 'https:', 'git:', 'git+ssh:', 'git+https:', 'ssh:'])
module.exports.fromUrl = function (giturl, opts) {
if (typeof giturl !== 'string') {
return
static addHost (name, host) {
GitHost.#gitHosts[name] = host
GitHost.#gitHosts.byDomain[host.domain] = name
GitHost.#gitHosts.byShortcut[`${name}:`] = name
GitHost.#protocols[`${name}:`] = { name }
}
const key = giturl + JSON.stringify(opts || {})
static fromUrl (giturl, opts) {
if (typeof giturl !== 'string') {
return
}
if (!cache.has(key)) {
cache.set(key, fromUrl(giturl, opts))
}
const key = giturl + JSON.stringify(opts || {})
return cache.get(key)
}
if (!cache.has(key)) {
const hostArgs = fromUrl(giturl, opts, {
gitHosts: GitHost.#gitHosts,
protocols: GitHost.#protocols,
})
cache.set(key, hostArgs ? new GitHost(...hostArgs) : undefined)
}
function fromUrl (giturl, opts) {
if (!giturl) {
return
return cache.get(key)
}
const correctedUrl = isGitHubShorthand(giturl) ? 'github:' + giturl : correctProtocol(giturl)
const parsed = parseGitUrl(correctedUrl)
if (!parsed) {
return parsed
}
#fill (template, opts) {
if (typeof template !== 'function') {
return null
}
const gitHostShortcut = gitHosts.byShortcut[parsed.protocol]
const gitHostDomain =
gitHosts.byDomain[parsed.hostname.startsWith('www.') ?
parsed.hostname.slice(4) :
parsed.hostname]
const gitHostName = gitHostShortcut || gitHostDomain
if (!gitHostName) {
return
}
const options = { ...this, ...this.opts, ...opts }
const gitHostInfo = gitHosts[gitHostShortcut || gitHostDomain]
let auth = null
if (authProtocols[parsed.protocol] && (parsed.username || parsed.password)) {
auth = `${parsed.username}${parsed.password ? ':' + parsed.password : ''}`
}
// the path should always be set so we don't end up with 'undefined' in urls
if (!options.path) {
options.path = ''
}
let committish = null
let user = null
let project = null
let defaultRepresentation = null
// template functions will insert the leading slash themselves
if (options.path.startsWith('/')) {
options.path = options.path.slice(1)
}
try {
if (gitHostShortcut) {
let pathname = parsed.pathname.startsWith('/') ? parsed.pathname.slice(1) : parsed.pathname
const firstAt = pathname.indexOf('@')
// we ignore auth for shortcuts, so just trim it out
if (firstAt > -1) {
pathname = pathname.slice(firstAt + 1)
}
if (options.noCommittish) {
options.committish = null
}
const lastSlash = pathname.lastIndexOf('/')
if (lastSlash > -1) {
user = decodeURIComponent(pathname.slice(0, lastSlash))
// we want nulls only, never empty strings
if (!user) {
user = null
}
project = decodeURIComponent(pathname.slice(lastSlash + 1))
} else {
project = decodeURIComponent(pathname)
}
const result = template(options)
return options.noGitPlus && result.startsWith('git+') ? result.slice(4) : result
}
if (project.endsWith('.git')) {
project = project.slice(0, -4)
}
hash () {
return this.committish ? `#${this.committish}` : ''
}
if (parsed.hash) {
committish = decodeURIComponent(parsed.hash.slice(1))
}
ssh (opts) {
return this.#fill(this.sshtemplate, opts)
}
defaultRepresentation = 'shortcut'
} else {
if (!gitHostInfo.protocols.includes(parsed.protocol)) {
return
}
sshurl (opts) {
return this.#fill(this.sshurltemplate, opts)
}
const segments = gitHostInfo.extract(parsed)
if (!segments) {
return
}
browse (path, ...args) {
// not a string, treat path as opts
if (typeof path !== 'string') {
return this.#fill(this.browsetemplate, path)
}
user = segments.user && decodeURIComponent(segments.user)
project = decodeURIComponent(segments.project)
committish = decodeURIComponent(segments.committish)
defaultRepresentation = protocolToRepresentation(parsed.protocol)
if (typeof args[0] !== 'string') {
return this.#fill(this.browsetreetemplate, { ...args[0], path })
}
} catch (err) {
/* istanbul ignore else */
if (err instanceof URIError) {
return
} else {
throw err
}
return this.#fill(this.browsetreetemplate, { ...args[1], fragment: args[0], path })
}
return new GitHost(gitHostName, user, auth, project, committish, defaultRepresentation, opts)
}
// If the path is known to be a file, then browseFile should be used. For some hosts
// the url is the same as browse, but for others like GitHub a file can use both `/tree/`
// and `/blob/` in the path. When using a default committish of `HEAD` then the `/tree/`
// path will redirect to a specific commit. Using the `/blob/` path avoids this and
// does not redirect to a different commit.
browseFile (path, ...args) {
if (typeof args[0] !== 'string') {
return this.#fill(this.browseblobtemplate, { ...args[0], path })
}
// accepts input like git:github.com:user/repo and inserts the // after the first :
const correctProtocol = (arg) => {
const firstColon = arg.indexOf(':')
const proto = arg.slice(0, firstColon + 1)
if (knownProtocols.includes(proto)) {
return arg
return this.#fill(this.browseblobtemplate, { ...args[1], fragment: args[0], path })
}
const firstAt = arg.indexOf('@')
if (firstAt > -1) {
if (firstAt > firstColon) {
return `git+ssh://${arg}`
} else {
return arg
}
docs (opts) {
return this.#fill(this.docstemplate, opts)
}
const doubleSlash = arg.indexOf('//')
if (doubleSlash === firstColon + 1) {
return arg
bugs (opts) {
return this.#fill(this.bugstemplate, opts)
}
return arg.slice(0, firstColon + 1) + '//' + arg.slice(firstColon + 1)
}
https (opts) {
return this.#fill(this.httpstemplate, opts)
}
// look for github shorthand inputs, such as npm/cli
const isGitHubShorthand = (arg) => {
// it cannot contain whitespace before the first #
// it cannot start with a / because that's probably an absolute file path
// but it must include a slash since repos are username/repository
// it cannot start with a . because that's probably a relative file path
// it cannot start with an @ because that's a scoped package if it passes the other tests
// it cannot contain a : before a # because that tells us that there's a protocol
// a second / may not exist before a #
const firstHash = arg.indexOf('#')
const firstSlash = arg.indexOf('/')
const secondSlash = arg.indexOf('/', firstSlash + 1)
const firstColon = arg.indexOf(':')
const firstSpace = /\s/.exec(arg)
const firstAt = arg.indexOf('@')
git (opts) {
return this.#fill(this.gittemplate, opts)
}
const spaceOnlyAfterHash = !firstSpace || (firstHash > -1 && firstSpace.index > firstHash)
const atOnlyAfterHash = firstAt === -1 || (firstHash > -1 && firstAt > firstHash)
const colonOnlyAfterHash = firstColon === -1 || (firstHash > -1 && firstColon > firstHash)
const secondSlashOnlyAfterHash = secondSlash === -1 || (firstHash > -1 && secondSlash > firstHash)
const hasSlash = firstSlash > 0
// if a # is found, what we really want to know is that the character
// immediately before # is not a /
const doesNotEndWithSlash = firstHash > -1 ? arg[firstHash - 1] !== '/' : !arg.endsWith('/')
const doesNotStartWithDot = !arg.startsWith('.')
shortcut (opts) {
return this.#fill(this.shortcuttemplate, opts)
}
return spaceOnlyAfterHash && hasSlash && doesNotEndWithSlash &&
doesNotStartWithDot && atOnlyAfterHash && colonOnlyAfterHash &&
secondSlashOnlyAfterHash
}
path (opts) {
return this.#fill(this.pathtemplate, opts)
}
// attempt to correct an scp style url so that it will parse with `new URL()`
const correctUrl = (giturl) => {
const firstAt = giturl.indexOf('@')
const lastHash = giturl.lastIndexOf('#')
let firstColon = giturl.indexOf(':')
let lastColon = giturl.lastIndexOf(':', lastHash > -1 ? lastHash : Infinity)
let corrected
if (lastColon > 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
corrected = giturl.slice(0, lastColon) + '/' + giturl.slice(lastColon + 1)
// // and we find our new : positions
firstColon = corrected.indexOf(':')
lastColon = corrected.lastIndexOf(':')
tarball (opts) {
return this.#fill(this.tarballtemplate, { ...opts, noCommittish: false })
}
if (firstColon === -1 && giturl.indexOf('//') === -1) {
// we have no : at all
// as it would be in:
// username@hostname.com/user/repo
// then we prepend a protocol
corrected = `git+ssh://${corrected}`
file (path, opts) {
return this.#fill(this.filetemplate, { ...opts, path })
}
return corrected
}
// try to parse the url as its given to us, if that throws
// then we try to clean the url and parse that result instead
// THIS FUNCTION SHOULD NEVER THROW
const parseGitUrl = (giturl) => {
let result
try {
result = new url.URL(giturl)
} catch {
// this fn should never throw
edit (path, opts) {
return this.#fill(this.edittemplate, { ...opts, path })
}
if (result) {
return result
getDefaultRepresentation () {
return this.default
}
const correctedUrl = correctUrl(giturl)
try {
result = new url.URL(correctedUrl)
} catch {
// this fn should never throw
toString (opts) {
if (this.default && typeof this[this.default] === 'function') {
return this[this.default](opts)
}
return this.sshurl(opts)
}
}
return result
for (const [name, host] of Object.entries(hosts)) {
GitHost.addHost(name, host)
}
module.exports = GitHost
{
"name": "hosted-git-info",
"version": "5.1.0",
"version": "6.0.0",
"description": "Provides metadata and conversions from repository urls for GitHub, Bitbucket and GitLab",

@@ -24,5 +24,2 @@ "main": "./lib/index.js",

"posttest": "npm run lint",
"postversion": "npm publish",
"prepublishOnly": "git push origin --follow-tags",
"preversion": "npm test",
"snap": "tap",

@@ -41,3 +38,3 @@ "test": "tap",

"@npmcli/eslint-config": "^3.0.1",
"@npmcli/template-oss": "3.5.0",
"@npmcli/template-oss": "4.5.1",
"tap": "^16.0.1"

@@ -50,12 +47,16 @@ },

"engines": {
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
},
"tap": {
"color": 1,
"coverage": true
"coverage": true,
"nyc-arg": [
"--exclude",
"tap-snapshots/**"
]
},
"templateOSS": {
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
"version": "3.5.0"
"version": "4.5.1"
}
}

@@ -10,4 +10,4 @@ # hosted-git-info

```javascript
var hostedGitInfo = require("hosted-git-info")
var info = hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git", opts)
const hostedGitInfo = require("hosted-git-info")
const info = hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git", opts)
/* info looks like:

@@ -56,3 +56,3 @@ {

### var info = hostedGitInfo.fromUrl(gitSpecifier[, options])
### const info = hostedGitInfo.fromUrl(gitSpecifier[, options])

@@ -73,3 +73,3 @@ * *gitSpecifer* is a URL of a git repository or a SCP-style specifier of one.

directly fetching it from the githost. If no committish was set then
`master` will be used as the default.
`HEAD` will be used as the default.

@@ -135,3 +135,3 @@ For example `hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git#v1.0.0").file("package.json")`

Currently this supports GitHub, Bitbucket and GitLab. Pull requests for
additional hosts welcome.
Currently this supports GitHub (including Gists), Bitbucket, GitLab and Sourcehut.
Pull requests for additional hosts welcome.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc