-part parmater can be found in the url of blog
+
## Visual Studio Code Marketplace
diff --git a/docs/programming.md b/docs/programming.md
index 4b8a08c58b..25bd7f29b1 100644
--- a/docs/programming.md
+++ b/docs/programming.md
@@ -127,6 +127,10 @@ GitHub 官方也提供了一些 RSS:
+### 仓库 Contirbutors
+
+
+
## GitLab
### Explore
diff --git a/lib/router.js b/lib/router.js
index fa50024903..dd3f0e183d 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -274,6 +274,8 @@ router.get('/github/search/:query/:sort?/:order?', require('./routes/github/sear
router.get('/github/branches/:user/:repo', require('./routes/github/branches'));
router.get('/github/file/:user/:repo/:branch/:filepath+', require('./routes/github/file'));
router.get('/github/starred_repos/:user', require('./routes/github/starred_repos'));
+router.get('/github/contributors/:user/:repo/:order?/:anon?', require('./routes/github/contributors'));
+
// f-droid
router.get('/fdroid/apprelease/:app', require('./routes/fdroid/apprelease'));
diff --git a/lib/routes/github/contributors.js b/lib/routes/github/contributors.js
new file mode 100644
index 0000000000..6d84bf7d2a
--- /dev/null
+++ b/lib/routes/github/contributors.js
@@ -0,0 +1,98 @@
+const got = require('@/utils/got');
+const config = require('@/config').value;
+
+module.exports = async (ctx) => {
+ const { user, repo, order, anon } = ctx.params;
+
+ const host = `https://github.com/${user}/${repo}`;
+ const url = `https://api.github.com/repos/${user}/${repo}/contributors?` + (anon ? 'anon=1' : '');
+
+ // Use token if available
+ const headers = {};
+ if (config.github && config.github.access_token) {
+ headers.Authorization = `token ${config.github.access_token}`;
+ }
+
+ // First page
+ const response = await got({
+ method: 'get',
+ url,
+ headers,
+ });
+ let data = response.data;
+
+ try {
+ // Get total page number
+ const last_page_link = response.headers.link.split(',').find(function(elem) {
+ return elem.includes('"last"');
+ });
+ const url_base = last_page_link.match(/<(.*)page=\d*/)[1];
+ const page_count = Number(last_page_link.match(/page=(\d*)/)[1]);
+
+ const generate_array = (n) => [...Array(n - 1)].map((_, index) => index + 2);
+ const page_array = generate_array(page_count);
+
+ // Get everypage
+ const tasks = page_array.map(async (page) => {
+ const response = await got({
+ method: 'get',
+ url: `${url_base}page=${page}`,
+ headers,
+ });
+ data = data.concat(response.data);
+ });
+ await Promise.all(tasks);
+ } catch (err) {
+ // If only one page
+
+ // Other errors
+ if (!(err instanceof TypeError)) {
+ throw err;
+ }
+ }
+
+ // Sort by commits
+ data.sort((a, b) => a.contributions - b.contributions);
+ if (order !== 'asc') {
+ data.reverse();
+ }
+
+ let title, description, link, guid;
+
+ const items = [];
+ let index = 0;
+
+ // Generate RSS
+ data.forEach(function(item) {
+ const time = new Date();
+ time.setMinutes(time.getMinutes() - index);
+ index++;
+
+ // Distiguished between acounts
+ if (item.type === 'Anonymous') {
+ title = `Contributor: ${item.name}`;
+ description = `Anonymous contributor
Name: ${item.name}
E-mail: ${item.email}
Contributions: ${item.contributions}
`;
+ link = '';
+ guid = `anon-${item.name}`;
+ } else {
+ title = `Contributor: ${item.login}`;
+ description = `
${item.login}
Contributions: ${item.contributions}
`;
+ link = item.html_url;
+ guid = item.id;
+ }
+
+ items.push({
+ title: title,
+ description: description,
+ guid: guid,
+ link: link,
+ pubDate: time,
+ });
+ }),
+ (ctx.state.data = {
+ title: `${user}/${repo} Contributors`,
+ link: `${host}/graphs/contributors`,
+ description: `New contributors for ${user}/${repo}`,
+ item: items,
+ });
+};