Compare commits
1 Commits
server-202
...
increase-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afd23abd1d |
14
.github/actions/close-bot/package-lock.json
generated
vendored
14
.github/actions/close-bot/package-lock.json
generated
vendored
@@ -9,14 +9,14 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/core": "^1.8.2",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
@@ -226,9 +226,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-5pbM693Ih59ZdUhgk+fts+bUWTnIdHV3kwOSr+QIoFHMLg7Gzhwm0cifDY/AG68ekEJAkHnQVpcy4f6GjmzBCA==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.2.tgz",
|
||||
"integrity": "sha512-FXcBL7nyik8K5ODeCKlxi+vts7torOkoDAKfeh61EAkAy1HAvwn9uVzZBY0f15YcQTcZZ2/iSGBFHEuioZWfDA==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
|
||||
2
.github/actions/close-bot/package.json
vendored
2
.github/actions/close-bot/package.json
vendored
@@ -10,7 +10,7 @@
|
||||
"author": "chris48s",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.9.0",
|
||||
"@actions/core": "^1.8.2",
|
||||
"@actions/github": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v1
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -4,28 +4,6 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2022-07-01
|
||||
|
||||
- change routes for [gitlab] license and contributors badges [#8140](https://github.com/badges/shields/issues/8140)
|
||||
- ci: support new circle PR url variable [#8135](https://github.com/badges/shields/issues/8135)
|
||||
- increase cache length on AUR version badge, run [AUR] [#8110](https://github.com/badges/shields/issues/8110)
|
||||
- Use GraphQL to fix GitHub file count badges [github] [#8112](https://github.com/badges/shields/issues/8112)
|
||||
- update targets for [bower] service tests [#8107](https://github.com/badges/shields/issues/8107)
|
||||
- accept version with suffix in [ore] service tests [#8106](https://github.com/badges/shields/issues/8106)
|
||||
- style: unified self-managed gitlab instance name [#8105](https://github.com/badges/shields/issues/8105)
|
||||
- feat: add [gitlab] contributors service [#8084](https://github.com/badges/shields/issues/8084)
|
||||
- fix: display greasy fork dd correctly [#8088](https://github.com/badges/shields/issues/8088)
|
||||
- [greasyfork] Add Greasy Fork service badges [#8080](https://github.com/badges/shields/issues/8080)
|
||||
- Fix typos in endpoint badge docs [#8085](https://github.com/badges/shields/issues/8085)
|
||||
- fix: gitlab licence service docs and example [#8083](https://github.com/badges/shields/issues/8083)
|
||||
- add docstrings for endpoint-common service [#8079](https://github.com/badges/shields/issues/8079)
|
||||
- fix(gitlab service test): fix gitlab pipeline & tag(nested group) service test, run [gitlabtag gitlabpipeline] [#8076](https://github.com/badges/shields/issues/8076)
|
||||
- Add [gitlablicense] services [#8024](https://github.com/badges/shields/issues/8024)
|
||||
- [Spack] Package Manager: Update Domain [#8046](https://github.com/badges/shields/issues/8046)
|
||||
- Docstrings for dynamic-common service [#8027](https://github.com/badges/shields/issues/8027)
|
||||
- switch [jitpack] to use latestOk endpoint [#8041](https://github.com/badges/shields/issues/8041)
|
||||
- Dependency updates
|
||||
|
||||
## server-2022-06-01
|
||||
|
||||
- Update GitLab logo (2022) [#7984](https://github.com/badges/shields/issues/7984)
|
||||
|
||||
@@ -59,9 +59,7 @@ function _inferPullRequestFromTravisEnv(env) {
|
||||
}
|
||||
|
||||
function _inferPullRequestFromCircleEnv(env) {
|
||||
return parseGithubPullRequestUrl(
|
||||
env.CI_PULL_REQUEST || env.CIRCLE_PULL_REQUEST
|
||||
)
|
||||
return parseGithubPullRequestUrl(env.CI_PULL_REQUEST)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
</p>
|
||||
<p>
|
||||
The endpoint badge is a better alternative than redirecting to the
|
||||
static badge endpoint or generating SVG on your server:
|
||||
static badge enpoint or generating SVG on your server:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -142,7 +142,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
Content and presentation are separate.
|
||||
</a>{' '}
|
||||
The service provider authors the badge, and Shields takes input from
|
||||
the user to format it. As a service provider, you author the badge
|
||||
the user to format it. As a service provider you author the badge
|
||||
but don't have to concern yourself with styling. You don't even have
|
||||
to pass the formatting options through to Shields.
|
||||
</li>
|
||||
@@ -152,12 +152,12 @@ export default function EndpointPage(): JSX.Element {
|
||||
</li>
|
||||
<li>
|
||||
A JSON response is easy to implement; easier than an HTTP redirect.
|
||||
It is trivial in almost any framework and is more compatible with
|
||||
It is trivial in almost any framework, and is more compatible with
|
||||
hosting environments such as{' '}
|
||||
<a href="https://runkit.com/docs/endpoint">RunKit endpoints</a>.
|
||||
</li>
|
||||
<li>
|
||||
As a service provider, you can rely on the Shields CDN. There's no
|
||||
As a service provider you can rely on the Shields CDN. There's no
|
||||
need to study the HTTP headers. Adjusting cache behavior is as
|
||||
simple as setting a property in the JSON response.
|
||||
</li>
|
||||
@@ -197,7 +197,7 @@ export default function EndpointPage(): JSX.Element {
|
||||
<dd>
|
||||
Default: <code>false</code>. <code>true</code> to treat this as an
|
||||
error badge. This prevents the user from overriding the color. In the
|
||||
future, it may affect cache behavior.
|
||||
future it may affect cache behavior.
|
||||
</dd>
|
||||
<dt>namedLogo</dt>
|
||||
<dd>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as originalSimpleIcons from 'simple-icons/icons'
|
||||
import originalSimpleIcons from 'simple-icons'
|
||||
import { svg2base64 } from './svg-helpers.js'
|
||||
|
||||
function loadSimpleIcons() {
|
||||
@@ -14,10 +14,10 @@ function loadSimpleIcons() {
|
||||
// https://github.com/badges/shields/issues/4273
|
||||
Object.keys(originalSimpleIcons).forEach(key => {
|
||||
const icon = originalSimpleIcons[key]
|
||||
const { title, slug, hex } = icon
|
||||
|
||||
const title = icon.title.toLowerCase()
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
icon.base64 = {
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${hex}"`)),
|
||||
default: svg2base64(icon.svg.replace('<svg', `<svg fill="#${icon.hex}"`)),
|
||||
light: svg2base64(icon.svg.replace('<svg', `<svg fill="whitesmoke"`)),
|
||||
dark: svg2base64(icon.svg.replace('<svg', `<svg fill="#333"`)),
|
||||
}
|
||||
@@ -26,17 +26,14 @@ function loadSimpleIcons() {
|
||||
// (e.g. 'Hive'). If a by-title reference we generate for
|
||||
// backwards compatibility collides with a proper slug from Simple Icons
|
||||
// then do nothing, so that the proper slug will always map to the correct icon.
|
||||
// Starting in v7, the exported object with the full icon set has updated the keys
|
||||
// to include a lowercase `si` prefix, and utilizes proper case naming conventions.
|
||||
if (!(`si${title}` in originalSimpleIcons)) {
|
||||
simpleIcons[title.toLowerCase()] = icon
|
||||
if (!(title in originalSimpleIcons)) {
|
||||
simpleIcons[title] = icon
|
||||
}
|
||||
const legacyTitle = title.replace(/ /g, '-')
|
||||
if (!(`si${legacyTitle}` in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle.toLowerCase()] = icon
|
||||
if (!(legacyTitle in originalSimpleIcons)) {
|
||||
simpleIcons[legacyTitle] = icon
|
||||
}
|
||||
|
||||
simpleIcons[slug] = icon
|
||||
simpleIcons[key] = icon
|
||||
})
|
||||
return simpleIcons
|
||||
}
|
||||
|
||||
572
package-lock.json
generated
572
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -24,7 +24,7 @@
|
||||
"@fontsource/lato": "^4.5.8",
|
||||
"@fontsource/lekton": "^4.5.9",
|
||||
"@renovate/pep440": "^1.0.0",
|
||||
"@sentry/node": "^7.2.0",
|
||||
"@sentry/node": "^7.1.1",
|
||||
"@shields_io/camp": "^18.1.1",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
@@ -61,7 +61,7 @@
|
||||
"qs": "^6.10.5",
|
||||
"query-string": "^7.1.1",
|
||||
"semver": "~7.3.7",
|
||||
"simple-icons": "7.2.0",
|
||||
"simple-icons": "6.23.0",
|
||||
"webextension-store-meta": "^1.0.5",
|
||||
"xmldom": "~0.6.0",
|
||||
"xpath": "~0.0.32"
|
||||
@@ -141,7 +141,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/register": "7.17.7",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
@@ -155,7 +155,7 @@
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-select": "^4.0.17",
|
||||
"@types/styled-components": "5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"babel-preset-gatsby": "^2.14.0",
|
||||
@@ -167,8 +167,8 @@
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"concurrently": "^7.2.2",
|
||||
"cypress": "^10.2.0",
|
||||
"concurrently": "^7.2.1",
|
||||
"cypress": "^10.0.3",
|
||||
"danger": "^11.0.7",
|
||||
"danger-plugin-no-test-shortcuts": "^2.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
@@ -180,13 +180,13 @@
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.3",
|
||||
"eslint-plugin-jsdoc": "^39.3.2",
|
||||
"eslint-plugin-mocha": "^10.0.5",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.2.0",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react": "^7.30.0",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-sort-class-members": "^1.14.1",
|
||||
"fetch-ponyfill": "^7.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -203,7 +203,7 @@
|
||||
"is-svg": "^4.3.2",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdoc": "^3.6.10",
|
||||
"lint-staged": "^13.0.2",
|
||||
"lint-staged": "^13.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.6",
|
||||
@@ -211,13 +211,13 @@
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.0.2",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.2.7",
|
||||
"nock": "13.2.6",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"nodemon": "^2.0.18",
|
||||
"nodemon": "^2.0.16",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^7.0.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"prettier": "2.7.1",
|
||||
"prettier": "2.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-overlay": "^6.0.11",
|
||||
@@ -236,8 +236,8 @@
|
||||
"start-server-and-test": "1.14.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tsd": "^0.21.0",
|
||||
"typescript": "^4.7.4",
|
||||
"tsd": "^0.20.0",
|
||||
"typescript": "^4.7.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -108,7 +108,9 @@ class AurVotes extends BaseAurService {
|
||||
|
||||
class AurVersion extends BaseAurService {
|
||||
static category = 'version'
|
||||
|
||||
static route = { base: 'aur/version', pattern: ':packageName' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'AUR version',
|
||||
@@ -117,8 +119,6 @@ class AurVersion extends BaseAurService {
|
||||
},
|
||||
]
|
||||
|
||||
static _cacheLength = 3600
|
||||
|
||||
static render({ version, outOfDate }) {
|
||||
const color = outOfDate === null ? 'blue' : 'orange'
|
||||
return { message: addv(version), color }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
isVPlusDottedVersionAtLeastOne,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import Joi from 'joi'
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'BowerVersion',
|
||||
@@ -9,17 +7,21 @@ export const t = new ServiceTester({
|
||||
pathPrefix: '/bower',
|
||||
})
|
||||
|
||||
t.create('version').timeout(10000).get('/v/angular.json').expectBadge({
|
||||
const isBowerPrereleaseVersion = Joi.string().regex(
|
||||
/^v\d+(\.\d+)?(\.\d+)?(-?[.\w\d])+?$/
|
||||
)
|
||||
|
||||
t.create('version').timeout(10000).get('/v/bootstrap.json').expectBadge({
|
||||
label: 'bower',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('pre version')
|
||||
t.create('pre version') // e.g. bower|v0.2.5-alpha-rc-pre
|
||||
.timeout(10000)
|
||||
.get('/v/angular.json?include_prereleases')
|
||||
.get('/v/bootstrap.json?include_prereleases')
|
||||
.expectBadge({
|
||||
label: 'bower',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
message: isBowerPrereleaseVersion,
|
||||
})
|
||||
|
||||
t.create('Version for Invalid Package')
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* Common functions and utilities for tasks related to endpoint badges.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import Joi from 'joi'
|
||||
import validate from '../core/base-service/validate.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
@@ -20,11 +14,6 @@ const optionalNumberWhenAnyLogoPresent = Joi.alternatives()
|
||||
.conditional('namedLogo', { is: Joi.string().required(), then: Joi.number() })
|
||||
.conditional('logoSvg', { is: Joi.string().required(), then: Joi.number() })
|
||||
|
||||
/**
|
||||
* Joi schema for validating endpoint.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
const endpointSchema = Joi.object({
|
||||
schemaVersion: 1,
|
||||
label: Joi.string().allow('').required(),
|
||||
@@ -44,18 +33,9 @@ const endpointSchema = Joi.object({
|
||||
.oxor('namedLogo', 'logoSvg')
|
||||
.required()
|
||||
|
||||
/**
|
||||
* Strictly validate the data according to the endpoint schema.
|
||||
* This rejects unknown/invalid keys.
|
||||
* Optionally it prints those keys in the message to provide detailed feedback.
|
||||
*
|
||||
* @param {object} data Object containing the data for validation
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} [attrs.prettyErrorMessage] If provided then error message is set to this value
|
||||
* @param {boolean} [attrs.includeKeys] If true then includes error details in error message, defaults to false
|
||||
* @throws {InvalidResponse|Error} Error if Joi validation fails due to invalid or no schema
|
||||
* @returns {object} Value if Joi validation is success
|
||||
*/
|
||||
// Strictly validate according to the endpoint schema. This rejects unknown /
|
||||
// invalid keys. Optionally it prints those keys in the message in order to
|
||||
// provide detailed feedback.
|
||||
function validateEndpointData(
|
||||
data,
|
||||
{ prettyErrorMessage = 'invalid response data', includeKeys = false } = {}
|
||||
@@ -76,17 +56,6 @@ function validateEndpointData(
|
||||
|
||||
const anySchema = Joi.any()
|
||||
|
||||
/**
|
||||
* Fetches data from the endpoint and validates the data.
|
||||
*
|
||||
* @param {object} serviceInstance Instance of Endpoint class
|
||||
* @param {object} attrs Refer to individual attributes
|
||||
* @param {string} attrs.url Endpoint URL
|
||||
* @param {object} attrs.errorMessages Object containing error messages for different error codes
|
||||
* @param {string} attrs.validationPrettyErrorMessage If provided then the error message is set to this value
|
||||
* @param {boolean} attrs.includeKeys If true then includes error details in error message
|
||||
* @returns {object} Data fetched from endpoint
|
||||
*/
|
||||
async function fetchEndpointData(
|
||||
serviceInstance,
|
||||
{ url, errorMessages, validationPrettyErrorMessage, includeKeys }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from 'path'
|
||||
import Joi from 'joi'
|
||||
import gql from 'graphql-tag'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { InvalidParameter } from '../index.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
import { ConditionalGithubAuthV3Service } from './github-auth-service.js'
|
||||
import {
|
||||
documentation as commonDocumentation,
|
||||
transformErrors,
|
||||
errorMessagesFor,
|
||||
} from './github-helpers.js'
|
||||
|
||||
const documentation = `${commonDocumentation}
|
||||
@@ -22,29 +22,28 @@ const documentation = `${commonDocumentation}
|
||||
</p>
|
||||
`
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
repository: Joi.object({
|
||||
object: Joi.object({
|
||||
entries: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required(),
|
||||
extension: Joi.string().allow('').required(),
|
||||
})
|
||||
),
|
||||
const schema = Joi.alternatives(
|
||||
/*
|
||||
alternative empty object schema to provide a custom error message
|
||||
in the event a file path is provided by the user instead of a directory
|
||||
*/
|
||||
Joi.object({}).required(),
|
||||
Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
path: Joi.string().required(),
|
||||
type: Joi.string().required(),
|
||||
})
|
||||
.allow(null)
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
)
|
||||
.required()
|
||||
)
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
type: Joi.any().valid('dir', 'file'),
|
||||
extension: Joi.string(),
|
||||
})
|
||||
|
||||
export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
export default class GithubDirectoryFileCount extends ConditionalGithubAuthV3Service {
|
||||
static category = 'size'
|
||||
|
||||
static route = {
|
||||
@@ -104,7 +103,7 @@ export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
title: 'GitHub repo file count (file extension)',
|
||||
pattern: ':user/:repo/:path',
|
||||
namedParams: { user: 'badges', repo: 'shields', path: 'services' },
|
||||
queryParams: { type: 'file', extension: 'js' },
|
||||
queryParams: { extension: 'js' },
|
||||
staticPreview: this.render({ count: 1 }),
|
||||
documentation,
|
||||
},
|
||||
@@ -119,25 +118,10 @@ export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
}
|
||||
|
||||
async fetch({ user, repo, path = '' }) {
|
||||
const expression = `HEAD:${path}`
|
||||
return this._requestGraphql({
|
||||
query: gql`
|
||||
query RepoFiles($user: String!, $repo: String!, $expression: String!) {
|
||||
repository(owner: $user, name: $repo) {
|
||||
object(expression: $expression) {
|
||||
... on Tree {
|
||||
entries {
|
||||
type
|
||||
extension
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { user, repo, expression },
|
||||
return this._requestJson({
|
||||
url: `/repos/${user}/${repo}/contents/${path}`,
|
||||
schema,
|
||||
transformErrors,
|
||||
errorMessages: errorMessagesFor('repo or directory not found'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,12 +137,11 @@ export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
}
|
||||
|
||||
if (type) {
|
||||
const objectType = type === 'dir' ? 'tree' : 'blob'
|
||||
files = files.filter(file => file.type === objectType)
|
||||
files = files.filter(file => file.type === type)
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
files = files.filter(file => file.extension === `.${extension}`)
|
||||
files = files.filter(file => path.extname(file.path) === `.${extension}`)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,13 +150,7 @@ export default class GithubDirectoryFileCount extends GithubAuthV4Service {
|
||||
}
|
||||
|
||||
async handle({ user, repo, path }, { type, extension }) {
|
||||
const json = await this.fetch({ user, repo, path })
|
||||
if (json.data.repository.object === null) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'directory not found',
|
||||
})
|
||||
}
|
||||
const content = json.data.repository.object.entries
|
||||
const content = await this.fetch({ user, repo, path })
|
||||
const { count } = this.constructor.transform(content, { type, extension })
|
||||
return this.constructor.render({ count })
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import GithubDirectoryFileCount from './github-directory-file-count.service.js'
|
||||
|
||||
describe('GithubDirectoryFileCount', function () {
|
||||
const contents = [
|
||||
{ extension: '', type: 'tree' },
|
||||
{ extension: '', type: 'tree' },
|
||||
{ extension: '.js', type: 'blob' },
|
||||
{ extension: '.js', type: 'blob' },
|
||||
{ extension: '.txt', type: 'blob' },
|
||||
{ extension: '', type: 'commit' },
|
||||
{ path: 'a', type: 'dir' },
|
||||
{ path: 'b', type: 'dir' },
|
||||
{ path: 'c.js', type: 'file' },
|
||||
{ path: 'd.js', type: 'file' },
|
||||
{ path: 'e.txt', type: 'file' },
|
||||
{ path: 'f', type: 'submodule' },
|
||||
]
|
||||
|
||||
test(GithubDirectoryFileCount.transform, () => {
|
||||
|
||||
@@ -14,18 +14,11 @@ t.create('directory file count (custom path)')
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('directory file count (repo not found)')
|
||||
.get('/badges/not_existing_repository.json')
|
||||
.expectBadge({
|
||||
label: 'files',
|
||||
message: 'repo not found',
|
||||
})
|
||||
|
||||
t.create('directory file count (directory not found)')
|
||||
.get('/badges/shields/not_existing_directory.json')
|
||||
.expectBadge({
|
||||
label: 'files',
|
||||
message: 'directory not found',
|
||||
message: 'repo or directory not found',
|
||||
})
|
||||
|
||||
t.create('directory file count (not a directory)')
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { redirector } from '../index.js'
|
||||
|
||||
// https://github.com/badges/shields/issues/8138
|
||||
export default redirector({
|
||||
category: 'build',
|
||||
route: {
|
||||
base: 'gitlab/v/contributor',
|
||||
pattern: ':project+',
|
||||
},
|
||||
transformPath: ({ project }) => `/gitlab/contributors/${project}`,
|
||||
dateAdded: new Date('2022-06-29'),
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Contributors redirect')
|
||||
.get('/gitlab-org/gitlab', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader('Location', '/gitlab/contributors/gitlab-org/gitlab.svg')
|
||||
@@ -1,76 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { optionalUrl, nonNegativeInteger } from '../validators.js'
|
||||
import { renderContributorBadge } from '../contributor-count.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
|
||||
const schema = Joi.object({ 'x-total': nonNegativeInteger }).required()
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
gitlab_url: optionalUrl,
|
||||
}).required()
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
You may use your GitLab Project Id (e.g. 278964) or your Project Path (e.g. gitlab-org/gitlab )
|
||||
</p>
|
||||
`
|
||||
|
||||
const customDocumentation = `
|
||||
<p>
|
||||
Note that only network-accessible jihulab.com and other self-managed GitLab instances are supported.
|
||||
You may use your GitLab Project Id (e.g. 13953) or your Project Path (e.g. gitlab-cn/gitlab ) in <a href="https://jihulab.com">https://jihulab.com</a>
|
||||
</p>
|
||||
`
|
||||
|
||||
export default class GitlabContributors extends GitLabBase {
|
||||
static category = 'activity'
|
||||
static route = {
|
||||
base: 'gitlab/contributors',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab contributors',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
staticPreview: this.render({ contributorCount: 418 }),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab (self-managed) contributors',
|
||||
queryParams: { gitlab_url: 'https://jihulab.com' },
|
||||
namedParams: {
|
||||
project: 'gitlab-cn/gitlab',
|
||||
},
|
||||
staticPreview: this.render({ contributorCount: 415 }),
|
||||
documentation: customDocumentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'contributors' }
|
||||
|
||||
static render({ contributorCount }) {
|
||||
return renderContributorBadge({ contributorCount })
|
||||
}
|
||||
|
||||
async handle({ project }, { gitlab_url: baseUrl = 'https://gitlab.com' }) {
|
||||
// https://docs.gitlab.com/ee/api/repositories.html#contributors
|
||||
const { res } = await this._request({
|
||||
url: `${baseUrl}/api/v4/projects/${encodeURIComponent(
|
||||
project
|
||||
)}/repository/contributors`,
|
||||
options: { searchParams: { page: '1', per_page: '1' } },
|
||||
errorMessages: {
|
||||
404: 'project not found',
|
||||
},
|
||||
})
|
||||
const data = this.constructor._validate(res.headers, schema)
|
||||
// The total number of contributors is in the `x-total` field in the headers.
|
||||
// https://docs.gitlab.com/ee/api/index.html#other-pagination-headers
|
||||
const contributorCount = data['x-total']
|
||||
return this.constructor.render({ contributorCount })
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric } from '../test-validators.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Contributors')
|
||||
.get('/guoxudong.io/shields-test/licenced-test.json')
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: isMetric,
|
||||
})
|
||||
|
||||
t.create('Contributors (repo not found)')
|
||||
.get('/guoxudong.io/shields-test/do-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: 'project not found',
|
||||
})
|
||||
|
||||
t.create('Mocking the missing x-total header')
|
||||
.get('/group/project.json')
|
||||
.intercept(nock =>
|
||||
nock('https://gitlab.com')
|
||||
.get(
|
||||
'/api/v4/projects/group%2Fproject/repository/contributors?page=1&per_page=1'
|
||||
)
|
||||
.reply(200)
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'contributors',
|
||||
message: 'invalid response data',
|
||||
})
|
||||
@@ -68,14 +68,14 @@ export default class GitlabCoverage extends BaseSvgScrapingService {
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab code coverage (self-managed)',
|
||||
title: 'Gitlab code coverage (self-hosted)',
|
||||
namedParams: { user: 'GNOME', repo: 'at-spi2-core', branch: 'master' },
|
||||
queryParams: { gitlab_url: 'https://gitlab.gnome.org' },
|
||||
staticPreview: this.render({ coverage: 93 }),
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab code coverage (self-managed, specific job)',
|
||||
title: 'Gitlab code coverage (self-hosted, specific job)',
|
||||
namedParams: { user: 'GNOME', repo: 'libhandy', branch: 'master' },
|
||||
queryParams: {
|
||||
gitlab_url: 'https://gitlab.gnome.org',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { redirector } from '../index.js'
|
||||
|
||||
// https://github.com/badges/shields/issues/8138
|
||||
export default redirector({
|
||||
category: 'build',
|
||||
route: {
|
||||
base: 'gitlab/v/license',
|
||||
pattern: ':project+',
|
||||
},
|
||||
transformPath: ({ project }) => `/gitlab/license/${project}`,
|
||||
dateAdded: new Date('2022-06-29'),
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('License redirect')
|
||||
.get('/gitlab-org/gitlab', {
|
||||
followRedirect: false,
|
||||
})
|
||||
.expectStatus(301)
|
||||
.expectHeader('Location', '/gitlab/license/gitlab-org/gitlab.svg')
|
||||
@@ -15,22 +15,21 @@ const queryParamSchema = Joi.object({
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
You may use your GitLab Project Id (e.g. 278964) or your Project Path (e.g. gitlab-org/gitlab )
|
||||
</p>
|
||||
`
|
||||
|
||||
const customDocumentation = `
|
||||
<p>
|
||||
Note that only internet-accessible GitLab instances are supported, for example https://jihulab.com, https://gitlab.gnome.org, or https://gitlab.com/.
|
||||
You may use your GitLab Project Id (e.g. 13953) or your Project Path (e.g. gitlab-cn/gitlab ) in <a href="https://jihulab.com">https://jihulab.com</a>
|
||||
You may use your GitLab Project Id (e.g. 13083) or your Project Path (e.g. gitlab-org/gitlab-foss )
|
||||
</p>
|
||||
`
|
||||
const commonProps = {
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab-foss',
|
||||
},
|
||||
documentation,
|
||||
}
|
||||
|
||||
export default class GitlabLicense extends GitLabBase {
|
||||
static category = 'license'
|
||||
|
||||
static route = {
|
||||
base: 'gitlab/license',
|
||||
base: 'gitlab/v/license',
|
||||
pattern: ':project+',
|
||||
queryParamSchema,
|
||||
}
|
||||
@@ -38,28 +37,22 @@ export default class GitlabLicense extends GitLabBase {
|
||||
static examples = [
|
||||
{
|
||||
title: 'GitLab',
|
||||
namedParams: {
|
||||
project: 'gitlab-org/gitlab',
|
||||
},
|
||||
...commonProps,
|
||||
staticPreview: {
|
||||
label: 'license',
|
||||
message: 'MIT License',
|
||||
color: 'green',
|
||||
},
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'GitLab (self-managed)',
|
||||
namedParams: {
|
||||
project: 'gitlab-cn/gitlab',
|
||||
},
|
||||
title: 'GitLab (custom server)',
|
||||
...commonProps,
|
||||
queryParams: { gitlab_url: 'https://jihulab.com' },
|
||||
staticPreview: {
|
||||
label: 'license',
|
||||
message: 'MIT License',
|
||||
color: 'green',
|
||||
},
|
||||
documentation: customDocumentation,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class GitlabPipelineStatus extends BaseSvgScrapingService {
|
||||
documentation,
|
||||
},
|
||||
{
|
||||
title: 'Gitlab pipeline status (self-managed)',
|
||||
title: 'Gitlab pipeline status (self-hosted)',
|
||||
namedParams: { project: 'GNOME/pango' },
|
||||
queryParams: { gitlab_url: 'https://gitlab.gnome.org', branch: 'master' },
|
||||
staticPreview: this.render({ status: 'passed' }),
|
||||
|
||||
@@ -15,7 +15,7 @@ t.create('Pipeline status')
|
||||
|
||||
t.create('Pipeline status (nested groups)')
|
||||
.get(
|
||||
'/pipeline-status/megabyte-labs/docker/ci-pipeline/ansible.json?branch=master'
|
||||
'/pipeline-status/megabyte-labs/dockerfile/ci-pipeline/ansible-lint.json?branch=master'
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'build',
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class GitLabRelease extends GitLabBase {
|
||||
staticPreview: renderVersionBadge({ version: 'v5.0.0-beta.1' }),
|
||||
},
|
||||
{
|
||||
title: 'GitLab Release (self-managed)',
|
||||
title: 'GitLab Release (custom instance)',
|
||||
namedParams: {
|
||||
project: 'GNOME/librsvg',
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ t.create('Release (release display name)')
|
||||
.get('/gitlab-org/gitlab.json?display_name=release')
|
||||
.expectBadge({ label: 'release', message: isGitLabDisplayVersion })
|
||||
|
||||
t.create('Release (custom instance)')
|
||||
t.create('Release (custom instance')
|
||||
.get('/GNOME/librsvg.json?gitlab_url=https://gitlab.gnome.org')
|
||||
.expectBadge({ label: 'release', message: isSemver, color: 'blue' })
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class GitlabTag extends GitLabBase {
|
||||
staticPreview: this.render({ version: 'v5.0.0-beta.1', sort: 'semver' }),
|
||||
},
|
||||
{
|
||||
title: 'GitLab tag (self-managed)',
|
||||
title: 'GitLab tag (custom instance)',
|
||||
namedParams: {
|
||||
project: 'GNOME/librsvg',
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ t.create('Tag (latest by date)')
|
||||
.expectBadge({ label: 'tag', message: 'v2.0.0', color: 'blue' })
|
||||
|
||||
t.create('Tag (nested groups)')
|
||||
.get('/megabyte-labs/docker/ci-pipeline/ansible.json')
|
||||
.get('/megabyte-labs/docker/ci-pipeline/ansible-lint.json')
|
||||
.expectBadge({ label: 'tag', message: isSemver, color: 'blue' })
|
||||
|
||||
t.create('Tag (project id latest by date)')
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
daily_installs: nonNegativeInteger,
|
||||
total_installs: nonNegativeInteger,
|
||||
good_ratings: nonNegativeInteger,
|
||||
ok_ratings: nonNegativeInteger,
|
||||
bad_ratings: nonNegativeInteger,
|
||||
version: Joi.string().required(),
|
||||
license: Joi.string().allow(null).required(),
|
||||
}).required()
|
||||
|
||||
export default class BaseGreasyForkService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'greasy fork' }
|
||||
|
||||
async fetch({ scriptId }) {
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://greasyfork.org/scripts/${scriptId}.json`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkInstalls extends BaseGreasyForkService {
|
||||
static category = 'downloads'
|
||||
static route = { base: 'greasyfork', pattern: ':variant(dt|dd)/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
pattern: 'dd/:scriptId',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderDownloadsBadge({ downloads: 17, interval: 'day' }),
|
||||
},
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
pattern: 'dt/:scriptId',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderDownloadsBadge({ downloads: 3420 }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'installs' }
|
||||
|
||||
async handle({ variant, scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
if (variant === 'dd') {
|
||||
const downloads = data.daily_installs
|
||||
const interval = 'day'
|
||||
return renderDownloadsBadge({ downloads, interval })
|
||||
}
|
||||
const downloads = data.total_installs
|
||||
return renderDownloadsBadge({ downloads })
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Daily Installs')
|
||||
.get('/dd/407466.json')
|
||||
.expectBadge({ label: 'installs', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('Daily Installs (not found)')
|
||||
.get('/dd/000000.json')
|
||||
.expectBadge({ label: 'installs', message: 'not found' })
|
||||
|
||||
t.create('Total Installs')
|
||||
.get('/dt/407466.json')
|
||||
.expectBadge({ label: 'installs', message: isMetric })
|
||||
|
||||
t.create('Total Installs (not found)')
|
||||
.get('/dt/000000.json')
|
||||
.expectBadge({ label: 'installs', message: 'not found' })
|
||||
@@ -1,34 +0,0 @@
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkLicense extends BaseGreasyForkService {
|
||||
static category = 'license'
|
||||
static route = { base: 'greasyfork', pattern: 'l/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderLicenseBadge({ licenses: ['MIT'] }),
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'license' }
|
||||
|
||||
transform({ data }) {
|
||||
if (data.license === null) {
|
||||
throw new InvalidResponse({
|
||||
prettyMessage: 'license not found',
|
||||
})
|
||||
}
|
||||
// remove suffix " License" from data.license
|
||||
return { license: data.license.replace(/ License$/, '') }
|
||||
}
|
||||
|
||||
async handle({ scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
const { license } = this.transform({ data })
|
||||
return renderLicenseBadge({ licenses: [license] })
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('License (valid)').get('/l/407466.json').expectBadge({
|
||||
label: 'license',
|
||||
message: 'MIT',
|
||||
})
|
||||
|
||||
t.create('License (not found)')
|
||||
.get('/l/000000.json')
|
||||
.expectBadge({ label: 'license', message: 'not found' })
|
||||
@@ -1,20 +0,0 @@
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import BaseGreasyForkService from './greasyfork-base.js'
|
||||
|
||||
export default class GreasyForkVersion extends BaseGreasyForkService {
|
||||
static category = 'version'
|
||||
static route = { base: 'greasyfork', pattern: 'v/:scriptId' }
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'Greasy Fork',
|
||||
namedParams: { scriptId: '407466' },
|
||||
staticPreview: renderVersionBadge({ version: '3.9.3' }),
|
||||
},
|
||||
]
|
||||
|
||||
async handle({ scriptId }) {
|
||||
const data = await this.fetch({ scriptId })
|
||||
return renderVersionBadge({ version: data.version })
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Version').get('/v/407466.json').expectBadge({
|
||||
label: 'greasy fork',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('Version (not found)')
|
||||
.get('/v/000000.json')
|
||||
.expectBadge({ label: 'greasy fork', message: 'not found' })
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isVPlusDottedVersionNClausesWithOptionalSuffix } from '../test-validators.js'
|
||||
import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Nucleus (pluginId nucleus)').get('/nucleus.json').expectBadge({
|
||||
label: 'version',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('Invalid Plugin (pluginId 1)').get('/1.json').expectBadge({
|
||||
|
||||
@@ -62,7 +62,10 @@ t.create('valid repo -- unregistered')
|
||||
color: COLOR_MAP.unregistered,
|
||||
})
|
||||
|
||||
t.create('invalid repo').get('/github.com/repo/invalid-repo.json').expectBadge({
|
||||
label: 'reuse',
|
||||
message: 'Not a Git repository',
|
||||
})
|
||||
t.create('invalid repo')
|
||||
.timeout(10000)
|
||||
.get('/github.com/repo/invalid-repo.json')
|
||||
.expectBadge({
|
||||
label: 'reuse',
|
||||
message: 'Not a Git repository',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user