Skip to content
This repository was archived by the owner on May 20, 2020. It is now read-only.

Commit 9acdaae

Browse files
author
Tak Tran
authored
Add topics management (#58)
Add topics management
2 parents 2ff9276 + 73359d1 commit 9acdaae

15 files changed

+898
-5
lines changed

README.md

+28-5
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@
44

55
<sup>1</sup> It's script-friendly too.
66

7-
![image](https://user-images.githubusercontent.com/224547/58557351-1a153680-8216-11e9-9ebc-f25471405ea3.png)
7+
![asari command](https://user-images.githubusercontent.com/79451/59847137-986a8180-9359-11e9-84b9-ac60e1b205fa.png)
88

99
> Above: Use `npx asari` in your shell to manage your work in GitHub.
1010
1111
"Asari" (あさり) is [Japanese for "clam"](https://translate.google.com/#view=home&op=translate&sl=en&tl=ja&text=clam). Like a clam, `asari` is happiest when it's inside a shell.
1212

1313
🐚 `asari` lets you work with GitHub from your command line, and is delicious when lightly fried with garlic and spices.
1414

15-
![image](https://user-images.githubusercontent.com/224547/58558164-010d8500-8218-11e9-9279-9b93307989a7.png)
15+
![asari command line tool help](https://user-images.githubusercontent.com/79451/59781793-7965e400-92b4-11e9-9646-bfc60a9927a2.png)
1616

1717
_Above: Running `npx asari` from your command line shows you the top level of options and commands._
1818

19-
![image](https://user-images.githubusercontent.com/224547/58558212-213d4400-8218-11e9-8b46-42be9ea9d0e9.png)
19+
![asari issues command help](https://user-images.githubusercontent.com/224547/58558212-213d4400-8218-11e9-8b46-42be9ea9d0e9.png)
2020

21-
![image](https://user-images.githubusercontent.com/224547/58558194-15518200-8218-11e9-8d48-0832558413ad.png)
21+
![asari projects command help](https://user-images.githubusercontent.com/224547/58558194-15518200-8218-11e9-8d48-0832558413ad.png)
22+
23+
![asari pulls command help](https://user-images.githubusercontent.com/224547/58558179-0c60b080-8218-11e9-8e17-d5823b55c5ad.png)
24+
25+
![asari repos command help](https://user-images.githubusercontent.com/79451/59847162-aa4c2480-9359-11e9-9807-61a1c572bc69.png)
2226

23-
![image](https://user-images.githubusercontent.com/224547/58558179-0c60b080-8218-11e9-8e17-d5823b55c5ad.png)
2427

2528
_Above: Running `npx asari <command>` shows you options for working with GitHub [issues](#working-with-github-issues), [projects](#working-with-github-projects) and [pull requests](#working-with-github-pull-requests)._
2629

@@ -182,6 +185,26 @@ npx asari pulls open <github-url>
182185
# Set the state of an existing pull request to `open`.
183186
```
184187

188+
### Working with GitHub Repositories
189+
190+
```bash
191+
npx asari repos list-topics <github-url>
192+
193+
# List all topics.
194+
```
195+
196+
```bash
197+
npx asari repos <add-topics|remove-topics> <github-url> --topic new-app
198+
199+
# Add/Remove a topic
200+
201+
npx asari repos <add-topics|remove-topics> <github-url> --topic new-app --topic good-one
202+
npx asari repos <add-topics|remove-topics> <github-url> --topics new-app,good-one
203+
npx asari repos <add-topics|remove-topics> <github-url> --topics "new-app, good-one"
204+
205+
# Add/Remove multiple topics
206+
```
207+
185208
### Global Options
186209

187210
```bash

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
"@adambraimbridge/prettierrc-2019-05": "^1.0.0",
2121
"@octokit/plugin-throttling": "^2.6.0",
2222
"@octokit/rest": "^16.16.3",
23+
"lodash.flatmap": "^4.5.0",
2324
"lodash.flow": "^3.5.0",
25+
"lodash.isequal": "^4.5.0",
26+
"lodash.uniq": "^4.5.0",
27+
"lodash.without": "^4.4.0",
2428
"update-notifier": "^3.0.0",
2529
"yargs": "^13.2.1"
2630
},

src/commands/repos.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* This project follows the example provided in the Yargs documentation for command hierarchy and directory structure.
3+
* @see: https://github.com/yargs/yargs/blob/master/docs/advanced.md#commanddirdirectory-opts
4+
*/
5+
exports.command = 'repos <subcommand> [...options]'
6+
exports.desc = 'Manage GitHub repositories'
7+
exports.builder = function(yargs) {
8+
return yargs.commandDir('repos')
9+
}
10+
exports.handler = function() {}

src/commands/repos/add-topics.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @see: https://octokit.github.io/rest.js/#octokit-routes-repos-replace-topics
3+
* Add topics to a repository.
4+
* const result = await octokit.repos.replaceTopics()
5+
*/
6+
const flow = require('lodash.flow')
7+
const { withGitHubUrl, withTopics } = require('../../lib/common-yargs')
8+
const printOutput = require('../../lib/print-output')
9+
const { addTopics } = require('../../lib/topics')
10+
11+
/**
12+
* yargs builder function.
13+
*
14+
* @param {import('yargs').Yargs} yargs - Instance of yargs
15+
*/
16+
const builder = yargs => {
17+
const baseOptions = flow([
18+
withGitHubUrl({
19+
describe: 'The URL of the GitHub repository to list the topics of.',
20+
}),
21+
withTopics({
22+
required: true,
23+
}),
24+
])
25+
return baseOptions(yargs).example('github-url', 'Pattern: https://github.com/[owner]/[repository]')
26+
}
27+
28+
/**
29+
* Add topics to a repository.
30+
*
31+
* @param {object} argv - argv parsed and filtered by yargs
32+
* @param {string} argv.token
33+
* @param {string} argv.json
34+
* @param {object} argv.githubUrl - The GitHub url parsed in the withGitHubUrl() yarg option into appropriate properties, such as `owner` and `repo`.
35+
* @param {array} argv.topics - The topics to add
36+
*/
37+
const handler = async ({ token, json, githubUrl, topics: topicsToAdd }) => {
38+
try {
39+
const topics = await addTopics({ githubUrl, token, topics: topicsToAdd })
40+
if (json) {
41+
printOutput({ json, resource: topics })
42+
} else {
43+
const output = topics.join('\n')
44+
console.log(output)
45+
}
46+
} catch (error) {
47+
printOutput({ json, error })
48+
}
49+
}
50+
51+
module.exports = {
52+
command: 'add-topics <github-url>',
53+
desc: 'Add topics to a repository.',
54+
builder,
55+
handler,
56+
}

src/commands/repos/list-topics.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @see: https://octokit.github.io/rest.js/#octokit-routes-repos-list-topics
3+
* List all topics of a repository.
4+
* const result = await octokit.repos.listTopics()
5+
*/
6+
const flow = require('lodash.flow')
7+
const commonYargs = require('../../lib/common-yargs')
8+
const printOutput = require('../../lib/print-output')
9+
const { getTopics } = require('../../lib/topics')
10+
11+
/**
12+
* yargs builder function.
13+
*
14+
* @param {import('yargs').Yargs} yargs - Instance of yargs
15+
*/
16+
const builder = yargs => {
17+
const baseOptions = flow([
18+
commonYargs.withGitHubUrl({
19+
describe: 'The URL of the GitHub repository to list the topics of.',
20+
}),
21+
])
22+
return baseOptions(yargs).example('github-url', 'Pattern: https://github.com/[owner]/[repository]')
23+
}
24+
25+
/**
26+
* List all issues for a repository.
27+
*
28+
* @param {object} argv - argv parsed and filtered by yargs
29+
* @param {string} argv.token
30+
* @param {string} argv.json
31+
* @param {object} argv.githubUrl - The GitHub url parsed in the withGitHubUrl() yarg option into appropriate properties, such as `owner` and `repo`.
32+
*/
33+
const handler = async args => {
34+
const { token, json, githubUrl } = args
35+
try {
36+
const topics = await getTopics({ githubUrl, token })
37+
if (json) {
38+
printOutput({ json, resource: topics })
39+
} else {
40+
const output = topics.join('\n')
41+
console.log(output)
42+
}
43+
} catch (error) {
44+
printOutput({ json, error })
45+
}
46+
}
47+
48+
module.exports = {
49+
command: 'list-topics <github-url>',
50+
desc: 'List all topics of a repository.',
51+
builder,
52+
handler,
53+
}

src/commands/repos/remove-topics.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @see: https://octokit.github.io/rest.js/#octokit-routes-repos-replace-topics
3+
* Remove topics from a repository.
4+
* const result = await octokit.repos.replaceTopics()
5+
*/
6+
const flow = require('lodash.flow')
7+
const { withGitHubUrl, withTopics } = require('../../lib/common-yargs')
8+
const printOutput = require('../../lib/print-output')
9+
const { removeTopics } = require('../../lib/topics')
10+
11+
/**
12+
* yargs builder function.
13+
*
14+
* @param {import('yargs').Yargs} yargs - Instance of yargs
15+
*/
16+
const builder = yargs => {
17+
const baseOptions = flow([
18+
withGitHubUrl({
19+
describe: 'The URL of the GitHub repository to list the topics of.',
20+
}),
21+
withTopics({
22+
required: true,
23+
}),
24+
])
25+
return baseOptions(yargs).example('github-url', 'Pattern: https://github.com/[owner]/[repository]')
26+
}
27+
28+
/**
29+
* Remove topics from a repository.
30+
*
31+
* @param {object} argv - argv parsed and filtered by yargs
32+
* @param {string} argv.token
33+
* @param {string} argv.json
34+
* @param {object} argv.githubUrl - The GitHub url parsed in the withGitHubUrl() yarg option into appropriate properties, such as `owner` and `repo`.
35+
* @param {array} argv.topics - The topics to remove
36+
*/
37+
const handler = async ({ token, json, githubUrl, topics: topicsToRemove }) => {
38+
try {
39+
const topics = await removeTopics({ githubUrl, token, topics: topicsToRemove })
40+
if (json) {
41+
printOutput({ json, resource: topics })
42+
} else {
43+
const output = topics.join('\n')
44+
console.log(output)
45+
}
46+
} catch (error) {
47+
printOutput({ json, error })
48+
}
49+
}
50+
51+
module.exports = {
52+
command: 'remove-topics <github-url>',
53+
desc: 'Remove topics from a repository.',
54+
builder,
55+
handler,
56+
}

src/lib/common-yargs.js

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const fs = require('fs')
22
const parseGitHubURL = require('./parse-github-url')
3+
const convertToFlattenedArrayOfValues = require('./convert-to-flattened-array-of-values')
34

45
const withToken = options => yargs =>
56
yargs
@@ -179,6 +180,22 @@ const withGitHubUrl = options => yargs => {
179180
)
180181
}
181182

183+
const withTopics = options => yargs => {
184+
return yargs
185+
.option(
186+
'topics',
187+
Object.assign(
188+
{
189+
alias: ['topic'],
190+
type: 'string',
191+
describe: 'GitHub topics to add. To add more than one, add multiple options or a comma separated list',
192+
},
193+
options
194+
)
195+
)
196+
.coerce('topic', convertToFlattenedArrayOfValues)
197+
}
198+
182199
module.exports = {
183200
withToken,
184201
withJson,
@@ -188,4 +205,5 @@ module.exports = {
188205
withReviewers,
189206
withTeamReviewers,
190207
withGitHubUrl,
208+
withTopics,
191209
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const flatMap = require('lodash.flatmap')
2+
3+
const expandCommaSeparatedValues = array => flatMap(array, (item = '') => item.split(','))
4+
5+
/**
6+
* Converts strings, strings of comma separated values and arrays of these
7+
* into a flattened array of values
8+
*
9+
* @param {string|array} input - The input string or array of values
10+
* @return {array} Array of values
11+
*/
12+
const convertToFlattenedArrayOfValues = input => {
13+
const values = Array.isArray(input) ? input : [input]
14+
15+
return expandCommaSeparatedValues(values).map(value => value.trim())
16+
}
17+
18+
module.exports = convertToFlattenedArrayOfValues

src/lib/octokit.js

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ module.exports = async ({ personalAccessToken }) => {
2020
* @see https://developer.github.com/v3/projects
2121
*/
2222
'inertia-preview',
23+
/**
24+
* Access Topics API while it is under preview
25+
*
26+
* @see https://developer.github.com/v3/repos/#list-all-topics-for-a-repository
27+
*/
28+
`mercy-preview`,
2329
],
2430
/**
2531
* Authenticate GitHub API calls using GitHub personal access token

src/lib/topics.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const authenticatedOctokit = require('./octokit')
2+
const uniq = require('lodash.uniq')
3+
const isEqual = require('lodash.isequal')
4+
const without = require('lodash.without')
5+
6+
const getTopics = async ({ githubUrl, token }) => {
7+
const { owner, repo } = githubUrl
8+
const octokit = await authenticatedOctokit({ personalAccessToken: token })
9+
10+
const request = octokit.repos.listTopics({ owner, repo })
11+
const { data: { names: topics = [] } = {} } = await request
12+
13+
return topics
14+
}
15+
16+
const addTopics = async ({ githubUrl, token, topics: newTopics }) => {
17+
const initialTopics = await getTopics({ githubUrl, token })
18+
const { owner, repo } = githubUrl
19+
20+
const octokit = await authenticatedOctokit({ personalAccessToken: token })
21+
const topics = uniq(initialTopics.concat(newTopics))
22+
23+
if (isEqual(initialTopics, topics)) {
24+
return initialTopics
25+
}
26+
27+
const request = octokit.repos.replaceTopics({ owner, repo, names: topics })
28+
const { data: { names: allTopics = [] } = {} } = await request
29+
30+
return allTopics
31+
}
32+
33+
const removeTopics = async ({ githubUrl, token, topics: topicsToRemove }) => {
34+
const initialTopics = await getTopics({ githubUrl, token })
35+
const { owner, repo } = githubUrl
36+
37+
const octokit = await authenticatedOctokit({ personalAccessToken: token })
38+
const topics = without(initialTopics, ...topicsToRemove)
39+
40+
if (isEqual(initialTopics, topics)) {
41+
return initialTopics
42+
}
43+
44+
const request = octokit.repos.replaceTopics({ owner, repo, names: topics })
45+
const { data: { names: allTopics = [] } = {} } = await request
46+
47+
return allTopics
48+
}
49+
50+
module.exports = {
51+
getTopics,
52+
addTopics,
53+
removeTopics,
54+
}

0 commit comments

Comments
 (0)