Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/gh-got
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2c5a24e120f7c28ccfd4fd4396346e78390a6ed2
Choose a base ref
...
head repository: sindresorhus/gh-got
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 718356ed3c6246c07e7dcd2ea82cf6da0f2e82ee
Choose a head ref

Commits on Nov 22, 2017

  1. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    fb8f3fa View commit details
  2. 7.0.0

    sindresorhus committed Nov 22, 2017
    Copy the full SHA
    33ad432 View commit details
  3. Copy the full SHA
    9c7597c View commit details

Commits on Aug 23, 2018

  1. Copy the full SHA
    a9b31de View commit details
  2. 7.1.0

    sindresorhus committed Aug 23, 2018
    Copy the full SHA
    a73bca8 View commit details
  3. Copy the full SHA
    402f07d View commit details
  4. 8.0.0

    sindresorhus committed Aug 23, 2018
    Copy the full SHA
    d20f61c View commit details

Commits on Aug 27, 2018

  1. Copy the full SHA
    c5491da View commit details
  2. 8.0.1

    sindresorhus committed Aug 27, 2018
    Copy the full SHA
    e59a7ba View commit details

Commits on Aug 31, 2018

  1. Remove duplicated code (#32)

    Because `got` has it implemented already.
    szmarczak authored and sindresorhus committed Aug 31, 2018
    Copy the full SHA
    a66fa03 View commit details

Commits on Dec 31, 2018

  1. Meta tweaks

    sindresorhus committed Dec 31, 2018
    Copy the full SHA
    11e621b View commit details

Commits on Jan 1, 2019

  1. Copy the full SHA
    6c418ab View commit details
  2. 8.1.0

    sindresorhus committed Jan 1, 2019
    Copy the full SHA
    bf13dc2 View commit details

Commits on May 28, 2019

  1. Create funding.yml

    sindresorhus authored May 28, 2019
    Copy the full SHA
    1e062bd View commit details

Commits on Feb 17, 2020

  1. Update to Got 10 (#38)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    szmarczak and sindresorhus authored Feb 17, 2020
    Copy the full SHA
    e17687a View commit details
  2. Require Node.js 10

    sindresorhus committed Feb 17, 2020
    Copy the full SHA
    5c4a263 View commit details
  3. 9.0.0

    sindresorhus committed Feb 17, 2020
    Copy the full SHA
    3d68bf6 View commit details

Commits on Oct 8, 2020

  1. Mention official packages in the readme (#40)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    fregante and sindresorhus authored Oct 8, 2020
    Copy the full SHA
    a9020db View commit details

Commits on Jan 2, 2021

  1. Copy the full SHA
    2902a01 View commit details

Commits on Oct 18, 2022

  1. Require Node.js 14 and move to ESM

    Fixes #42
    sindresorhus committed Oct 18, 2022
    Copy the full SHA
    24abafe View commit details
  2. 10.0.0

    sindresorhus committed Oct 18, 2022
    Copy the full SHA
    718356e View commit details
Showing with 289 additions and 4,047 deletions.
  1. +1 −2 .gitattributes
  2. +22 −0 .github/workflows/main.yml
  3. +1 −0 .gitignore
  4. +1 −0 .npmrc
  5. +0 −5 .travis.yml
  6. +77 −54 index.js
  7. +1 −1 license
  8. +0 −3,892 package-lock.json
  9. +43 −46 package.json
  10. +73 −27 readme.md
  11. +70 −20 test.js
3 changes: 1 addition & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
* text=auto
*.js text eol=lf
* text=auto eol=lf
22 changes: 22 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI
on:
- push
- pull_request
jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- 18
- 16
- 14
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
yarn.lock
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
5 changes: 0 additions & 5 deletions .travis.yml

This file was deleted.

131 changes: 77 additions & 54 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,91 @@
'use strict';
const got = require('got');
const isPlainObj = require('is-plain-obj');
import process from 'node:process';
import got from 'got';

function ghGot(path, opts) {
if (typeof path !== 'string') {
return Promise.reject(new TypeError(`Expected \`path\` to be a string, got ${typeof path}`));
}
const getRateLimit = headers => ({
limit: Number.parseInt(headers['x-ratelimit-limit'], 10),
remaining: Number.parseInt(headers['x-ratelimit-remaining'], 10),
reset: new Date(Number.parseInt(headers['x-ratelimit-reset'], 10) * 1000),
});

const env = process.env;
const create = () => got.extend({
prefixUrl: process.env.GITHUB_ENDPOINT || 'https://api.github.com',
headers: {
accept: 'application/vnd.github.v3+json',
'user-agent': 'https://github.com/sindresorhus/gh-got',
},
responseType: 'json',
context: {
token: process.env.GITHUB_TOKEN,
},
hooks: {
init: [
(raw, options) => {
// TODO: This should be fixed in Got.
// TODO: This doesn't seem to have any effect.
if (typeof options.url === 'string' && options.url.startsWith('/')) {
options.url = options.url.slice(1);
}

opts = Object.assign({
json: true,
token: env.GITHUB_TOKEN,
endpoint: env.GITHUB_ENDPOINT ? env.GITHUB_ENDPOINT.replace(/[^/]$/, '$&/') : 'https://api.github.com/'
}, opts);
if ('token' in raw) {
options.context.token = raw.token;
delete raw.token;
}
},
],
},
handlers: [
(options, next) => {
// TODO: This should be fixed in Got
// TODO: This doesn't seem to have any effect.
if (typeof options.url === 'string' && options.url.startsWith('/')) {
options.url = options.url.slice(1);
}

opts.headers = Object.assign({
accept: 'application/vnd.github.v3+json',
'user-agent': 'https://github.com/sindresorhus/gh-got'
}, opts.headers);
// Authorization
const {token} = options.context;
if (token && !options.headers.authorization) {
options.headers.authorization = `token ${token}`;
}

if (opts.token) {
opts.headers.authorization = `token ${opts.token}`;
}
// Don't touch streams
if (options.isStream) {
return next(options);
}

// https://developer.github.com/v3/#http-verbs
if (opts.method && opts.method.toLowerCase() === 'put' && !opts.body) {
opts.headers['content-length'] = 0;
}
// Magic begins
return (async () => {
try {
const response = await next(options);

const url = /^https?/.test(path) ? path : opts.endpoint + path;
// Rate limit for the Response object
response.rateLimit = getRateLimit(response.headers);

if (opts.stream) {
return got.stream(url, opts);
}
return response;
} catch (error) {
const {response} = error;

return got(url, opts).catch(err => {
if (err.response && isPlainObj(err.response.body)) {
err.name = 'GitHubError';
err.message = `${err.response.body.message} (${err.statusCode})`;
}
// Nicer errors
if (response && response.body) {
error.name = 'GitHubError';
error.message = `${response.body.message} (${response.statusCode})`;
}

throw err;
});
}
// Rate limit for errors
if (response) {
error.rateLimit = getRateLimit(response.headers);
}

const helpers = [
'get',
'post',
'put',
'patch',
'head',
'delete'
];
throw error;
}
})();
},
],
});

ghGot.stream = (url, opts) => ghGot(url, Object.assign({}, opts, {
json: false,
stream: true
}));
const ghGot = create();

for (const x of helpers) {
const method = x.toUpperCase();
ghGot[x] = (url, opts) => ghGot(url, Object.assign({}, opts, {method}));
ghGot.stream[x] = (url, opts) => ghGot.stream(url, Object.assign({}, opts, {method}));
}
export default ghGot;

module.exports = ghGot;
if (process.env.NODE_ENV === 'test') {
ghGot.recreate = create;
}
2 changes: 1 addition & 1 deletion license
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

3,892 changes: 0 additions & 3,892 deletions package-lock.json

This file was deleted.

89 changes: 43 additions & 46 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,48 +1,45 @@
{
"name": "gh-got",
"version": "6.0.0",
"description": "Convenience wrapper for `got` to interact with the GitHub API",
"license": "MIT",
"repository": "sindresorhus/gh-got",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
"engines": {
"node": ">=4"
},
"scripts": {
"test": "xo && ava"
},
"files": [
"index.js"
],
"keywords": [
"got",
"gh",
"github",
"api",
"request",
"http",
"https",
"get",
"url",
"uri",
"util",
"utility"
],
"dependencies": {
"got": "^7.0.0",
"is-plain-obj": "^1.1.0"
},
"devDependencies": {
"ava": "*",
"get-stream": "^2.0.0",
"nock": "^8.0.0",
"xo": "*"
},
"xo": {
"esnext": true
}
"name": "gh-got",
"version": "10.0.0",
"description": "Convenience wrapper for Got to interact with the GitHub API",
"license": "MIT",
"repository": "sindresorhus/gh-got",
"funding": "https://github.com/sindresorhus/got?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=14.16"
},
"scripts": {
"test": "xo && ava"
},
"files": [
"index.js"
],
"keywords": [
"got",
"gh",
"github",
"api",
"request",
"http",
"https",
"get",
"url",
"utility"
],
"dependencies": {
"got": "^12.5.2"
},
"devDependencies": {
"ava": "^4.3.3",
"get-stream": "^6.0.1",
"nock": "^13.2.9",
"xo": "^0.52.4"
}
}
100 changes: 73 additions & 27 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,134 @@
# gh-got [![Build Status](https://travis-ci.org/sindresorhus/gh-got.svg?branch=master)](https://travis-ci.org/sindresorhus/gh-got)
# gh-got

> Convenience wrapper for [`got`](https://github.com/sindresorhus/got) to interact with the [GitHub API](https://developer.github.com/v3/)
> Convenience wrapper for [Got](https://github.com/sindresorhus/got) to interact with the [GitHub API](https://developer.github.com/v3/)
Unless you're already using Got, you should probably use GitHub's own [@octokit/rest.js](https://github.com/octokit/rest.js) or [@octokit/graphql.js](https://github.com/octokit/graphql.js) packages instead.

## Install

```sh
npm install gh-got
```
$ npm install --save gh-got
```


## Usage

Instead of:

```js
const got = require('got');
import got from 'got';

const token = 'foo';

got('https://api.github.com/users/sindresorhus', {
const {body} = await got('https://api.github.com/users/sindresorhus', {
json: true,
headers: {
'accept': 'application/vnd.github.v3+json',
'authorization': `token ${token}`
}
}).then(res => {
console.log(res.body.login);
//=> 'sindresorhus'
});

console.log(body.login);
//=> 'sindresorhus'
```

You can do:

```js
const ghGot = require('gh-got');
import ghGot from 'gh-got';

ghGot('users/sindresorhus', {token: 'foo'}).then(res => {
console.log(res.body.login);
//=> 'sindresorhus'
const {body} = await ghGot('users/sindresorhus', {
context: {
token: 'foo'
}
});

console.log(body.login);
//=> 'sindresorhus'
```

Or:

```js
const ghGot = require('gh-got');
import ghGot from 'gh-got';

ghGot('https://api.github.com/users/sindresorhus', {token: 'foo'}).then(res => {
console.log(res.body.login);
//=> 'sindresorhus'
const {body} = await ghGot('https://api.github.com/users/sindresorhus', {
context: {
token: 'foo'
}
});
```

console.log(body.login);
//=> 'sindresorhus'
```

## API

Same as [`got`](https://github.com/sindresorhus/got) (including the stream API and aliases), but with some additional options below.
Same API as [`got`](https://github.com/sindresorhus/got), including options, the stream API, aliases, pagination, etc, but with some additional options below.

Errors are improved by using the custom GitHub error messages. Doesn't apply to the stream API.

### token
### `gh-got` specific options

#### token

Type: `string`

GitHub [access token](https://github.com/settings/tokens/new).

Can be set globally with the `GITHUB_TOKEN` environment variable.

### endpoint
#### prefixUrl

Type: `string`<br>
Type: `string`\
Default: `https://api.github.com/`

To support [GitHub Enterprise](https://enterprise.github.com).

Can be set globally with the `GITHUB_ENDPOINT` environment variable.

### body
#### body

Type: `Object`
Type: `object`

Can be specified as a plain object and will be serialized as JSON with the appropriate headers set.

## Rate limit

Responses and errors have a `.rateLimit` property with info about the current [rate limit](https://developer.github.com/v3/#rate-limiting). *(This is not yet implemented for the stream API)*

```js
import ghGot from 'gh-got';

const {rateLimit} = await ghGot('users/sindresorhus');

console.log(rateLimit);
//=> {limit: 5000, remaining: 4899, reset: [Date 2018-12-31T20:45:20.000Z]}
```

## Authorization

Authorization for GitHub uses the following logic:

1. If `options.headers.authorization` is passed to `gh-got`, then this will be used as first preference.
2. If `options.token` is provided, then the `authorization` header will be set to `token <options.token>`.
3. If `options.headers.authorization` and `options.token` are not provided, then the `authorization` header will be set to `token <process.env.GITHUB_TOKEN>`

In most cases, this means you can simply set `GITHUB_TOKEN`, but it also allows it to be overridden by setting `options.token` or `options.headers.authorization` explicitly. For example, if [authenticating as a GitHub App](https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app), you could do the following:

```js
import ghGot from 'gh-got';

const options = {
headers: {
authorization: `Bearer ${jwt}`
}
};
const {body} = await ghGot('app', options);

console.log(body.name);
//=> 'MyApp'
```

## License
## Pagination

MIT © [Sindre Sorhus](https://sindresorhus.com)
See the [Got docs](https://github.com/sindresorhus/got/blob/main/documentation/4-pagination.md).
90 changes: 70 additions & 20 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,115 @@
import process from 'node:process';
import test from 'ava';
import nock from 'nock';
import getStream from 'get-stream';
import m from '.';
import ghGot from './index.js';

const token = process.env.GITHUB_TOKEN;

test('default', async t => {
t.is((await m('users/sindresorhus')).body.login, 'sindresorhus');
const {body} = await ghGot('users/sindresorhus');
t.is(body.login, 'sindresorhus');
});

test('full path', async t => {
t.is((await m('https://api.github.com/users/sindresorhus')).body.login, 'sindresorhus');
const {body} = await ghGot('https://api.github.com/users/sindresorhus', {prefixUrl: ''});
t.is(body.login, 'sindresorhus');
});

test('accepts options', async t => {
t.is((await m('users/sindresorhus', {})).body.login, 'sindresorhus');
const {body} = await ghGot('users/sindresorhus', {});
t.is(body.login, 'sindresorhus');
});

test('accepts options.prefixUrl without trailing slash', async t => {
const {body} = await ghGot('users/sindresorhus', {prefixUrl: 'https://api.github.com'});
t.is(body.login, 'sindresorhus');
});

test.failing('dedupes slashes', async t => {
const {body} = await ghGot('/users/sindresorhus', {prefixUrl: 'https://api.github.com/'});
t.is(body.login, 'sindresorhus');
});

test.serial('global token option', async t => {
process.env.GITHUB_TOKEN = 'fail';
await t.throws(m('users/sindresorhus'), 'Bad credentials (401)');

await t.throwsAsync(
ghGot.recreate()('users/sindresorhus'),
{
message: 'Bad credentials (401)',
},
);

process.env.GITHUB_TOKEN = token;
});

test('token option', async t => {
await t.throws(m('users/sindresorhus', {token: 'fail'}), 'Bad credentials (401)');
await t.throwsAsync(ghGot('users/sindresorhus', {context: {token: 'fail'}}), {
message: 'Bad credentials (401)',
});
});

test.serial('global endpoint option', async t => {
process.env.GITHUB_ENDPOINT = 'fail';
await t.throws(m('users/sindresorhus', {retries: 1}), /ENOTFOUND/);

await t.throwsAsync(ghGot.recreate()('users/sindresorhus', {retries: 1}), {
message: /Invalid URL/,
});

delete process.env.GITHUB_ENDPOINT;
});

test.serial('endpoint option', async t => {
process.env.GITHUB_ENDPOINT = 'https://api.github.com/';
await t.throws(m('users/sindresorhus', {
endpoint: 'fail',
retries: 1
}), /ENOTFOUND/);

await t.throwsAsync(ghGot.recreate()('users/sindresorhus', {
prefixUrl: 'fail',
retries: 1,
}), {
message: /Invalid URL/,
});

delete process.env.GITHUB_ENDPOINT;
});

test('stream interface', async t => {
t.is(JSON.parse(await getStream(m.stream('users/sindresorhus'))).login, 'sindresorhus');
t.is(JSON.parse(await getStream(m.stream.get('users/sindresorhus'))).login, 'sindresorhus');
const string1 = await getStream(ghGot.stream('users/sindresorhus'));
t.is(JSON.parse(string1).login, 'sindresorhus');

const string2 = await getStream(ghGot.stream.get('users/sindresorhus'));
t.is(JSON.parse(string2).login, 'sindresorhus');
});

test('json body', async t => {
const endpoint = 'http://mock-endpoint';
const body = {test: [1, 3, 3, 7]};
const prefixUrl = 'http://mock-endpoint';
const postBody = {test: [1, 3, 3, 7]};
const reply = {ok: true};

const scope = nock(endpoint).post('/test', body).reply(200, reply);
const scope = nock(prefixUrl).post('/test', postBody).reply(200, reply);

t.deepEqual((await m('/test', {endpoint, body})).body, reply);
const {body} = await ghGot.post('test', {prefixUrl, json: postBody});
t.deepEqual(body, reply);
t.truthy(scope.isDone());
});

test('custom error', async t => {
const err = await t.throws(m('users/sindresorhus', {token: 'fail'}));
t.is(err.name, 'GitHubError');
t.is(err.message, 'Bad credentials (401)');
await t.throwsAsync(ghGot('users/sindresorhus', {context: {token: 'fail'}}), {
name: 'GitHubError',
message: 'Bad credentials (401)',
});
});

test('.rateLimit response property', async t => {
const {rateLimit} = await ghGot('users/sindresorhus');
t.is(typeof rateLimit.limit, 'number');
t.is(typeof rateLimit.remaining, 'number');
t.true(rateLimit.reset instanceof Date);
});

test('.rateLimit error property', async t => {
const {rateLimit} = await t.throwsAsync(ghGot('users/sindresorhus', {context: {token: 'fail'}}));
t.is(typeof rateLimit.limit, 'number');
t.is(typeof rateLimit.remaining, 'number');
t.true(rateLimit.reset instanceof Date);
});