feat(tool): Add ability to view all open PRs for repos other than freeCodeCamp in the Dashboard app (#40453)
* feat: show open boilerplate prs on dashboard fix: rest of boilerplate server changes fix: more fix: other * fix: update lib functions * fix: retrofitted one-off scripts * feat: added rateLimit for requests * fix: reduce time * fix: put limiter inside each route * fix: make client show when rated limited * fix: removed unused probot from app * fix: renamed folders * fix: consolidate config.js and constants.js * chore: update octokit to latest version * fix: remove invalid file * fix: refactored update-db.js * feat: add fcc logo * fix: logo url * fix: remove Home link * fix: change link colors * fix: added rate limiter to landing page * fix: ran npm install in client to create package-lock.json * fix: correct typo in doc Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> * fix: Replace favicon, Gitter => Discord Signed-off-by: nhcarrigan <nhcarrigan@gmail.com> * fix: add extra linting guidance to package.json * Ignore contributor app Signed-off-by: nhcarrigan <nhcarrigan@gmail.com> * fix: revert linting rules for client * fix: add skip_preflight_check=true for tests Co-authored-by: Kristofer Koishigawa <scissorsneedfoodtoo@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Kris Koishigawa <scissorsneedfoodtoo@gmail.com> Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com>
This commit is contained in:
parent
8324a5fcd7
commit
c8f6d15688
@ -4,4 +4,4 @@ client/static/**
|
||||
client/public/**
|
||||
api-server/public/**
|
||||
api-server/lib/**
|
||||
tools/dashboard/**
|
||||
tools/contributor/**
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
# Contribute
|
||||
|
||||
Tools to help maintain [freeCodeCamp.org](https://www.freecodecamp.org)'s Open Source Codebase on GitHub. We are currently working on creating a tools dashboard for helping you review PRs and translations. Hangout with us in the [contributor's chat room](https://gitter.im/FreeCodeCamp/Contributors) to learn more.
|
||||
Tools to help maintain [freeCodeCamp.org](https://www.freecodecamp.org)'s Open Source Codebase on GitHub. We are currently working on creating a tools dashboard for helping you review PRs and translations. Hangout with us in the [contributor's room on Discord](https://discord.gg/kgaE5dh) to learn more.
|
File diff suppressed because it is too large
Load Diff
@ -12,12 +12,9 @@
|
||||
"dev": "develop",
|
||||
"develop": "cross-env REACT_APP_HOST=local SKIP_PREFLIGHT_CHECK=true react-scripts start",
|
||||
"build": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test": "SKIP_PREFLIGHT_CHECK=true react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
BIN
tools/contributor/dashboard-app/client/public/favicon.ico
Normal file
BIN
tools/contributor/dashboard-app/client/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
@ -1,9 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import FreeCodeCampLogo from './assets/freeCodeCampLogo';
|
||||
import Tabs from './components/Tabs';
|
||||
import Search from './components/Search';
|
||||
import Pareto from './components/Pareto';
|
||||
import Repos from './components/Repos';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
import { ENDPOINT_INFO } from './constants';
|
||||
@ -48,7 +50,7 @@ const AppNavBar = styled.nav`
|
||||
}
|
||||
`;
|
||||
|
||||
const imgStyle = { paddingLeft: '30px' };
|
||||
const logoStyle = { paddingLeft: '30px' };
|
||||
|
||||
const titleStyle = { margin: '0', padding: '0' };
|
||||
|
||||
@ -76,7 +78,9 @@ class App extends Component {
|
||||
handleViewChange = ({ target: { id } }) => {
|
||||
const view = id.replace('tabs-', '');
|
||||
this.setState(prevState => ({ ...this.clearObj, view }));
|
||||
this.updateInfo();
|
||||
if (view === 'reports' || view === 'search') {
|
||||
this.updateInfo();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -91,18 +95,11 @@ class App extends Component {
|
||||
return (
|
||||
<>
|
||||
<AppNavBar>
|
||||
<a href="https://freecodecamp.org" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
style={imgStyle}
|
||||
src="https://discourse-user-assets.s3.dualstack.us-east-1.amazonaws.com/original/3X/e/d/ed1c70bda321aaeee9e6c20ab650ce8bc34899fa.svg"
|
||||
alt="Free Code Camp Logo"
|
||||
/>
|
||||
<a href="https://www.freecodecamp.org/" target="_blank" rel="noopener noreferrer" style={logoStyle}>
|
||||
<FreeCodeCampLogo />
|
||||
</a>
|
||||
<h1 style={titleStyle}>Contributor Tools</h1>
|
||||
<ul className="app-menu">
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/freeCodeCamp/freeCodeCamp" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</li>
|
||||
@ -114,6 +111,10 @@ class App extends Component {
|
||||
<Container>
|
||||
{view === 'search' && <Search />}
|
||||
{view === 'reports' && <Pareto />}
|
||||
{view === 'boilerplates' &&
|
||||
<Repos key='boilerplates' dataFilter={repo => repo._id.includes('boilerplate')} />}
|
||||
{view === 'other' &&
|
||||
<Repos key='other' dataFilter={repo => !repo._id.includes('boilerplate') && repo._id !== 'freeCodeCamp'} />}
|
||||
</Container>
|
||||
{footerInfo && <Footer footerInfo={footerInfo} />}
|
||||
</PageContainer>
|
@ -0,0 +1,114 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
|
||||
function freeCodeCampLogo() {
|
||||
return (
|
||||
<svg
|
||||
aria-label='[freeCodeCamp Logo]'
|
||||
height={24}
|
||||
role='math'
|
||||
version='1.1'
|
||||
viewBox='0 0 210 24'
|
||||
width={210}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
xmlnsXlink='http://www.w3.org/1999/xlink'
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
d='m35.42 5.56 0.43 0.05 0.42 0.08 0.39 0.09 0.37 0.12 0.36 0.14 0.32 0.16 0.31 0.18 0.28 0.21 0.27 0.22 0.24 0.24 0.22 0.27 0.2 0.28 0.18 0.31 0.16 0.33 0.13 0.35 0.12 0.37 0.09 0.39 0.08 0.41 0.05 0.44 0.03 0.45 0.01 0.47v0.12 0.11l-0.01 0.1-0.04 0.2-0.04 0.18-0.03 0.08-0.07 0.15-0.04 0.06-0.05 0.06-0.04 0.06-0.06 0.05-0.05 0.05-0.06 0.03-0.06 0.04-0.07 0.03-0.08 0.02-0.07 0.02-0.16 0.02h-8.9v-0.07h-0.02v1.84l0.01 0.24 0.03 0.24 0.03 0.23 0.06 0.22 0.07 0.2 0.09 0.2 0.1 0.17 0.12 0.18 0.13 0.15 0.15 0.16 0.17 0.13 0.18 0.13 0.2 0.11 0.21 0.11 0.23 0.09 0.24 0.09 0.27 0.06 0.27 0.07 0.3 0.05 0.31 0.03 0.32 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.13l0.12-0.01h0.13l0.24-0.02 0.23-0.02 0.11-0.01 0.11-0.02 0.21-0.03 0.1-0.01 0.1-0.02 0.29-0.06 0.09-0.03 0.09-0.02 0.08-0.03 0.09-0.03 0.08-0.03 0.05-0.01 0.15-0.06 0.06-0.03 0.06-0.02 0.12-0.06 0.21-0.11 0.08-0.04 0.07-0.05 0.17-0.09 0.08-0.05 0.09-0.05 0.09-0.06 0.19-0.13 0.1-0.06 0.1-0.07 0.11-0.07 0.12-0.12 0.13-0.1 0.06-0.05 0.05-0.04 0.06-0.04 0.05-0.05 0.09-0.07 0.1-0.06 0.04-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.03-0.01 0.04-0.01 0.02-0.01 0.05-0.01h0.09l0.1 0.01 0.15 0.03 0.04 0.02 0.07 0.04 0.04 0.03 0.09 0.09 0.03 0.04 0.04 0.08 0.01 0.05 0.02 0.05 0.01 0.06 0.02 0.1v0.07 0.07 0.06l-0.01 0.07-0.01 0.06-0.01 0.07-0.06 0.2-0.03 0.06-0.04 0.07-0.03 0.07-0.04 0.07-0.05 0.07-0.05 0.06-0.1 0.14-0.13 0.13-0.13 0.14-0.16 0.14-0.09 0.06-0.08 0.08-0.15 0.1-0.15 0.11-0.32 0.2-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.19 0.08-0.21 0.07-0.42 0.12-0.22 0.05-0.23 0.05-0.47 0.09-0.5 0.06-0.52 0.04-0.27 0.01-0.28 0.01h-0.28-0.48l-0.47-0.03-0.45-0.04-0.42-0.07-0.41-0.07-0.38-0.09-0.36-0.11-0.34-0.13-0.31-0.15-0.3-0.16-0.27-0.18-0.26-0.2-0.22-0.21-0.21-0.23-0.19-0.25-0.16-0.26-0.14-0.29-0.13-0.29-0.1-0.32-0.07-0.34-0.06-0.35-0.03-0.37-0.01-0.39v-4.71l0.01-0.14 0.01-0.12 0.01-0.13 0.04-0.26 0.04-0.13 0.03-0.12 0.08-0.24 0.1-0.24 0.12-0.23 0.06-0.11 0.07-0.12 0.08-0.1 0.08-0.11 0.08-0.1 0.09-0.11 0.19-0.2 0.23-0.23 0.3-0.24 0.15-0.11 0.17-0.11 0.17-0.1 0.17-0.09 0.18-0.09 0.38-0.16 0.2-0.07 0.42-0.12 0.22-0.05 0.23-0.05 0.22-0.04 0.24-0.03 0.24-0.04 0.24-0.02 0.25-0.02 0.52-0.02h0.27l0.47 0.01 0.47 0.03zm-2.04 1.6-0.41 0.07-0.39 0.1-0.38 0.13-0.37 0.15-0.35 0.18-0.32 0.25-0.27 0.26-0.23 0.28-0.19 0.3-0.13 0.31-0.08 0.32-0.03 0.35v0.96h8.19l-0.09-0.98-0.25-0.83-0.43-0.69-0.6-0.53-0.76-0.38-0.95-0.23-1.11-0.07-0.43 0.01-0.42 0.04z'
|
||||
id='k'
|
||||
/>
|
||||
<path
|
||||
d='m107.21 5.56 0.43 0.05 0.42 0.08 0.39 0.09 0.37 0.12 0.35 0.14 0.33 0.16 0.31 0.18 0.29 0.21 0.26 0.22 0.24 0.24 0.22 0.27 0.21 0.28 0.17 0.31 0.16 0.33 0.14 0.35 0.11 0.37 0.1 0.39 0.07 0.41 0.05 0.44 0.03 0.45 0.01 0.47v0.12l-0.01 0.11-0.02 0.2-0.02 0.1-0.02 0.09-0.03 0.09-0.02 0.08-0.03 0.07-0.04 0.08-0.04 0.06-0.1 0.12-0.1 0.1-0.13 0.07-0.13 0.05-0.08 0.02-0.16 0.02-8.92 0.01v1.76l0.01 0.24 0.02 0.24 0.04 0.23 0.06 0.22 0.07 0.2 0.08 0.2 0.11 0.17 0.11 0.18 0.14 0.15 0.15 0.16 0.17 0.13 0.18 0.13 0.19 0.11 0.22 0.11 0.23 0.09 0.24 0.09 0.26 0.06 0.28 0.07 0.3 0.05 0.31 0.03 0.32 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.13l0.25-0.01 0.24-0.02 0.22-0.02 0.12-0.01 0.11-0.02 0.31-0.04 0.2-0.04 0.19-0.04 0.09-0.03 0.09-0.02 0.09-0.03 0.08-0.03 0.13-0.04 0.04-0.02 0.06-0.02 0.05-0.02 0.06-0.03 0.05-0.02 0.07-0.03 0.2-0.1 0.14-0.08 0.08-0.05 0.08-0.04 0.08-0.05 0.18-0.1 0.18-0.12 0.2-0.13 0.1-0.07 0.11-0.07 0.06-0.06 0.07-0.06 0.05-0.05 0.07-0.05 0.05-0.05 0.06-0.04 0.11-0.09 0.05-0.03 0.05-0.04 0.04-0.03 0.05-0.03 0.04-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.12-0.04h0.02 0.09 0.05l0.06 0.01 0.09 0.02 0.05 0.01 0.12 0.06 0.12 0.12 0.03 0.04 0.04 0.08 0.01 0.05 0.02 0.05 0.02 0.11 0.01 0.05v0.07 0.13l-0.01 0.07-0.01 0.06-0.01 0.07-0.04 0.14-0.02 0.06-0.03 0.06-0.04 0.07-0.03 0.07-0.09 0.14-0.05 0.06-0.05 0.07-0.11 0.14-0.21 0.2-0.23 0.2-0.09 0.08-0.3 0.21-0.15 0.1-0.17 0.1-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.2 0.08-0.2 0.07-0.42 0.12-0.22 0.05-0.23 0.05-0.23 0.04-0.24 0.05-0.5 0.06-0.52 0.04-0.55 0.02h-0.28-0.49l-0.46-0.03-0.45-0.04-0.43-0.07-0.39-0.07-0.39-0.09-0.36-0.11-0.33-0.13-0.33-0.15-0.29-0.16-0.27-0.18-0.25-0.2-0.23-0.21-0.22-0.23-0.18-0.25-0.17-0.26-0.14-0.29-0.12-0.29-0.1-0.32-0.08-0.34-0.05-0.35-0.04-0.37-0.01-0.39v-4.58l0.01-0.13v-0.14l0.02-0.12 0.01-0.13 0.02-0.13 0.05-0.26 0.12-0.36 0.1-0.24 0.11-0.23 0.07-0.11 0.07-0.12 0.07-0.1 0.09-0.11 0.08-0.1 0.09-0.11 0.19-0.2 0.1-0.1 0.14-0.13 0.14-0.12 0.15-0.12 0.15-0.11 0.17-0.11 0.17-0.1 0.17-0.09 0.19-0.09 0.18-0.08 0.19-0.08 0.2-0.07 0.42-0.12 0.44-0.1 0.23-0.04 0.23-0.03 0.25-0.04 0.24-0.02 0.25-0.02 0.52-0.02h0.27l0.48 0.01 0.46 0.03zm-2.04 1.6-0.41 0.07-0.39 0.1-0.38 0.13-0.37 0.15-0.34 0.18-0.34 0.25-0.29 0.26-0.23 0.28-0.17 0.3-0.13 0.31-0.07 0.32-0.03 0.35v0.96h8.19l-0.09-0.98-0.25-0.83-0.43-0.69-0.6-0.53-0.76-0.38-0.95-0.23-1.11-0.07-0.43 0.01-0.42 0.04z'
|
||||
id='j'
|
||||
/>
|
||||
<path
|
||||
d='m203.57 0.17c-0.12 0.12-0.24 0.29-0.24 0.45 0 0.29 0.34 0.69 0.97 1.33 2.63 2.53 3.95 5.62 3.94 9.35-0.01 4.13-1.4 7.45-4.1 10.01-0.57 0.51-0.8 0.91-0.8 1.25 0 0.17 0.12 0.34 0.23 0.51 0.11 0.12 0.34 0.23 0.51 0.23 0.62 0 1.5-0.73 2.64-2.17 2.22-2.72 3.22-5.73 3.28-9.82 0.05-4.1-1.23-6.88-3.75-9.75-0.9-1.03-1.66-1.56-2.17-1.56-0.17 0-0.35 0.06-0.51 0.17z'
|
||||
id='b'
|
||||
/>
|
||||
<path
|
||||
d='m124.75 1.76c1.14 0.86 1.73 2.07 1.73 3.55 0 0.68-0.29 1.02-0.86 1.02-0.39 0-0.68-0.34-0.85-1.02-0.11-0.57-0.34-1.08-0.62-1.62-0.52-0.9-1.61-1.32-3.32-1.32-1.49 0-2.52 0.34-3.14 1.08-0.57 0.68-0.91 1.72-0.91 3.26v5.95c0 1.55 0.34 2.63 0.97 3.31 0.68 0.74 1.72 1.13 3.2 1.13 2.23 0 3.54-0.79 3.82-2.34 0.12-0.57 0.17-0.86 0.17-0.91 0.12-0.34 0.35-0.51 0.68-0.51 0.57 0 0.86 0.34 0.86 1.02 0 1.44-0.57 2.52-1.78 3.38-0.97 0.62-2.18 0.96-3.77 0.96-1.84 0-3.26-0.4-4.3-1.25-1.16-0.8-1.73-2.16-1.73-3.94v-7.16c0-3.77 1.95-5.61 5.95-5.61 1.61 0 2.86 0.34 3.9 1.02z'
|
||||
id='n'
|
||||
/>
|
||||
<path
|
||||
d='m14.21 6.57c0-0.56 0.34-0.79 1.02-0.79h3.32c0.57 0 0.85 0.51 0.85 1.44 1.02-1.08 2.12-1.73 3.26-1.73 0.96 0 1.72 0.29 2.23 0.86 0.57 0.57 0.8 1.38 0.8 2.29 0 0.63-0.29 0.97-0.8 0.97-0.34 0-0.57-0.23-0.68-0.63-0.23-0.8-0.34-1.19-0.4-1.25-0.22-0.39-0.68-0.62-1.25-0.62-0.62 0-1.25 0.23-1.78 0.68-0.34 0.23-0.8 0.74-1.38 1.49v7.67h3.08c0.68 0 1.03 0.29 1.03 0.8 0 0.57-0.35 0.86-1.03 0.86h-7.33c-0.68 0-1.02-0.29-1.02-0.8 0-0.57 0.34-0.8 1.02-0.8h2.52v-0.07h0.02v-9.57h-2.46c-0.68 0-1.02-0.28-1.02-0.8z'
|
||||
id='l'
|
||||
/>
|
||||
<path
|
||||
d='m96.68 0.04 0.06 0.02 0.06 0.03 0.05 0.03 0.06 0.04 0.13 0.13 0.03 0.06 0.04 0.06 0.03 0.07 0.02 0.07 0.04 0.16 0.04 0.18 0.01 0.2v0.1 16.84 0.08l-0.01 0.07v0.07l-0.04 0.13-0.01 0.05-0.03 0.06-0.02 0.05-0.06 0.09-0.07 0.08-0.05 0.03-0.04 0.03-0.05 0.03-0.1 0.04-0.06 0.02-0.12 0.02-0.07 0.01h-0.11l-0.13-0.02-0.04-0.01-0.04-0.02-0.04-0.01-0.03-0.02-0.03-0.01-0.03-0.03-0.03-0.02-0.06-0.05-0.02-0.03-0.07-0.12-0.01-0.04-0.02-0.05-0.01-0.04-0.02-0.05v-0.05-0.08-0.04-0.04l-0.01-0.04v-0.04l-0.01-0.04v-0.04-0.04l-0.01-0.05v-0.04-0.05l-0.01-0.09v-0.06l-0.01-0.04v-0.06-0.11l-0.01-0.06v-0.13l-0.13 0.09-0.13 0.08-0.13 0.09-0.24 0.14-0.11 0.08-0.12 0.07-0.21 0.12-0.1 0.06-0.09 0.06-0.1 0.05-0.25 0.14-0.14 0.08-0.14 0.06-0.12 0.05-0.05 0.02-0.09 0.04-0.09 0.03-0.09 0.02-0.19 0.06-0.4 0.08-0.21 0.03-0.22 0.03-0.11 0.02-0.23 0.02h-0.12l-0.12 0.01h-0.11l-0.12 0.01h-0.46l-0.21-0.01-0.2-0.01-0.2-0.02-0.19-0.01-0.2-0.03-0.18-0.02-0.37-0.07-0.34-0.08-0.18-0.05-0.32-0.1-0.32-0.12-0.15-0.07-0.15-0.06-0.42-0.24-0.13-0.08-0.14-0.1-0.28-0.22-0.12-0.11-0.12-0.13-0.11-0.12-0.11-0.13-0.1-0.14-0.09-0.14-0.17-0.29-0.07-0.15-0.07-0.16-0.06-0.16-0.05-0.16-0.05-0.18-0.04-0.18-0.03-0.17-0.02-0.19-0.03-0.19-0.01-0.19v-5.21l0.01-0.19 0.03-0.18 0.02-0.18 0.03-0.18 0.08-0.34 0.05-0.16 0.06-0.16 0.06-0.15 0.08-0.15 0.07-0.15 0.18-0.28 0.2-0.26 0.1-0.12 0.24-0.24 0.26-0.22 0.14-0.1 0.39-0.24 0.15-0.07 0.28-0.14 0.31-0.12 0.15-0.05 0.33-0.1 0.33-0.08 0.72-0.12 0.37-0.03 0.38-0.02h0.45l0.25 0.02h0.12 0.13l0.12 0.02 0.12 0.01 0.22 0.02 0.22 0.04 0.11 0.01 0.21 0.04 0.1 0.02 0.09 0.02 0.28 0.08 0.09 0.03 0.09 0.04 0.08 0.03 0.09 0.03 0.08 0.04 0.09 0.03 0.09 0.04 0.1 0.05 0.17 0.09 0.29 0.16 0.09 0.05 0.29 0.19 0.2 0.14 0.09 0.07 0.4 0.32v-5.89l0.01-0.1v-0.2l0.04-0.18 0.01-0.08 0.03-0.08 0.02-0.07 0.03-0.07 0.03-0.06 0.04-0.06 0.04-0.04 0.04-0.05 0.04-0.04 0.05-0.04 0.1-0.06 0.06-0.02 0.13-0.03 0.06-0.01h0.14l0.07 0.01 0.14 0.03zm-5.7 7.19-0.26 0.03-0.26 0.02-0.25 0.05-0.24 0.05-0.23 0.05-0.42 0.16-0.19 0.09-0.18 0.1-0.2 0.15-0.2 0.16-0.17 0.17-0.15 0.17-0.14 0.19-0.11 0.2-0.1 0.2-0.08 0.23-0.05 0.23-0.04 0.23-0.01 0.26v4.52l0.01 0.26 0.03 0.25 0.05 0.23 0.07 0.23 0.09 0.21 0.12 0.2 0.13 0.19 0.16 0.17 0.17 0.16 0.2 0.14 0.22 0.14 0.19 0.09 0.42 0.16 0.22 0.07 0.22 0.06 0.24 0.06 0.25 0.04 0.26 0.04 0.27 0.03 0.56 0.02 0.47-0.01 0.44-0.04 0.43-0.06 0.21-0.04 0.2-0.06 0.38-0.12 0.17-0.07 0.14-0.1 0.16-0.11 0.15-0.11 0.16-0.12 0.17-0.12 0.16-0.13 0.18-0.13 0.17-0.15 0.18-0.15 0.36-0.32v-6.59l-0.21-0.16-0.21-0.14-0.2-0.14-0.2-0.13-0.19-0.12-0.18-0.11-0.17-0.11-0.16-0.09-0.3-0.14-0.13-0.06-0.19-0.07-0.38-0.12-0.2-0.05-0.4-0.08-0.21-0.03-0.21-0.02-0.21-0.01-0.43-0.01h-0.28l-0.27 0.01z'
|
||||
id='c'
|
||||
/>
|
||||
<path
|
||||
d='m195.66 12.04c-0.99-0.25 3.06-5.03-4.13-10.75 0 0 0.94 3-3.81 9.69-4.76 6.68 2.11 10.66 2.11 10.66s-3.22-1.72 0.53-7.84c0.67-1.11 1.55-2.11 2.64-4.38 0 0 0.96 1.37 0.46 4.32-0.75 4.47 3.27 3.19 3.33 3.25 1.41 1.65-1.16 4.56-1.32 4.65s7.34-4.5 2.01-11.42c-0.36 0.36-0.83 2.08-1.82 1.82z'
|
||||
id='e'
|
||||
/>
|
||||
<path
|
||||
d='m135.26 5.37 0.19 0.01 0.18 0.02 0.18 0.01 0.18 0.02 0.34 0.04 0.16 0.02 0.16 0.04 0.15 0.02 0.14 0.04 0.15 0.03 0.28 0.08 0.26 0.08 0.36 0.15 0.12 0.06 0.11 0.05 0.1 0.06 0.11 0.06 0.12 0.08 0.1 0.09 0.11 0.09 0.2 0.2 0.18 0.22 0.16 0.24 0.07 0.12 0.14 0.26 0.12 0.29 0.05 0.14 0.04 0.15 0.05 0.16 0.04 0.15 0.03 0.17 0.02 0.16 0.04 0.36 0.01 0.18 0.02 0.18v6.1 0.08 0.08l0.02 0.28 0.01 0.07 0.02 0.12 0.02 0.06 0.01 0.06 0.03 0.11 0.02 0.05 0.06 0.14 0.03 0.04 0.02 0.04 0.03 0.04 0.06 0.07 0.08 0.08 0.02 0.01v0.01h0.03l0.01 0.02 0.05 0.02 0.02 0.01 0.05 0.02 0.03 0.02 0.04 0.01 0.11 0.04 0.12 0.05 0.1 0.04 0.04 0.01 0.05 0.02 0.05 0.03 0.09 0.03 0.07 0.03 0.02 0.02 0.03 0.02 0.05 0.04 0.02 0.02 0.02 0.03 0.02 0.02 0.04 0.06 0.03 0.06 0.01 0.04 0.01 0.03 0.02 0.08v0.04l0.01 0.04 0.01 0.05v0.05l0.01 0.05v0.21l-0.02 0.05v0.04l-0.01 0.05-0.01 0.04-0.02 0.04-0.01 0.04-0.02 0.03-0.03 0.04-0.05 0.06-0.02 0.03-0.04 0.02-0.11 0.06-0.08 0.04-0.1 0.02-0.1 0.01h-0.06-0.11-0.02-0.01l-0.03-0.01h-0.02-0.02l-0.1-0.02-0.03-0.02h-0.03l-0.06-0.02-0.11-0.03-0.12-0.03-0.05-0.01-0.04-0.01-0.05-0.02-0.21-0.07-0.07-0.02-0.07-0.03-0.24-0.08-0.1-0.04-0.05-0.01-0.04-0.02-0.05-0.01-0.04-0.01-0.04-0.02-0.03-0.01-0.07-0.02-0.07-0.03-0.03-0.01-0.01-0.01-0.02-0.01h-0.03-0.04l-0.03-0.02-0.03-0.01-0.04-0.01-0.02-0.02-0.06-0.04-0.03-0.03-0.02-0.03-0.03-0.03-0.02-0.03-0.02-0.04-0.03-0.04-0.02-0.04-0.01-0.04-0.03-0.04-0.02-0.05-0.03-0.09-0.06-0.21-0.02-0.06-0.23 0.16-0.22 0.15-0.1 0.07-0.2 0.12-0.1 0.07-0.34 0.22-0.08 0.04-0.07 0.05-0.21 0.12-0.06 0.04-0.16 0.08-0.05 0.02-0.09 0.04-0.04 0.01-0.06 0.04-0.07 0.02-0.15 0.05-0.4 0.1-0.08 0.01-0.18 0.03-0.09 0.01-0.09 0.02-0.28 0.03-0.2 0.02h-0.1l-0.11 0.01h-0.1-0.49-0.16l-0.16-0.01-0.31-0.02-0.3-0.03-0.15-0.02-0.15-0.03-0.28-0.05-0.27-0.06-0.14-0.04-0.13-0.04-0.12-0.04-0.13-0.05-0.24-0.09-0.12-0.06-0.11-0.05-0.11-0.06-0.11-0.07-0.11-0.06-0.1-0.07-0.24-0.16-0.22-0.19-0.1-0.1-0.09-0.1-0.1-0.1-0.24-0.33-0.14-0.24-0.06-0.12-0.06-0.13-0.05-0.12-0.05-0.13-0.08-0.27-0.06-0.28-0.04-0.3-0.02-0.3v-0.32l0.01-0.15 0.01-0.16 0.02-0.14 0.05-0.3 0.07-0.28 0.05-0.14 0.11-0.26 0.12-0.26 0.15-0.24 0.16-0.22 0.09-0.11 0.1-0.11 0.2-0.2 0.11-0.09 0.23-0.18 0.13-0.08 0.33-0.24 0.24-0.13 0.26-0.13 0.13-0.05 0.26-0.11 0.14-0.05 0.42-0.12 0.15-0.03 0.14-0.04 0.15-0.02 0.15-0.03 0.16-0.02 0.15-0.01 0.16-0.02 0.48-0.03h0.3l0.13 0.01h0.14l0.29 0.02 0.14 0.02 0.15 0.02 0.14 0.02 0.16 0.02 0.14 0.02 0.15 0.04 0.16 0.02 0.15 0.03 0.32 0.08 0.49 0.12 0.16 0.05 0.51 0.15 0.36 0.12 0.17 0.06v-2.01-0.12l-0.01-0.11-0.01-0.1-0.01-0.12-0.02-0.09-0.02-0.11-0.04-0.1-0.03-0.1-0.03-0.09-0.05-0.09-0.04-0.09-0.11-0.16-0.06-0.09-0.06-0.07-0.07-0.08-0.15-0.14-0.08-0.07-0.18-0.12-0.1-0.06-0.2-0.1-0.11-0.06-0.07-0.03-0.16-0.06-0.34-0.11-0.26-0.06-0.1-0.02-0.09-0.01-0.1-0.02-0.09-0.01-0.3-0.03-0.1-0.02h-0.1l-0.11-0.01-0.1-0.01h-0.11-0.1-0.47l-0.25 0.01-0.24 0.01-0.23 0.02-0.42 0.06-0.2 0.03-0.19 0.04-0.17 0.05-0.17 0.06-0.16 0.05-0.15 0.07-0.14 0.07-0.13 0.07-0.12 0.09-0.12 0.08-0.1 0.1-0.09 0.1-0.09 0.11-0.08 0.11-0.06 0.11-0.06 0.13-0.05 0.13-0.04 0.14-0.03 0.14-0.01 0.07-0.02 0.06-0.04 0.22-0.02 0.09-0.01 0.05-0.01 0.04-0.01 0.03v0.04l-0.02 0.06-0.01 0.06-0.01 0.04v0.02l-0.01 0.02v0.01 0.01 0.03l-0.02 0.03-0.03 0.07-0.01 0.02-0.05 0.06-0.01 0.02-0.03 0.02-0.05 0.05-0.02 0.01-0.02 0.02-0.03 0.01-0.03 0.02-0.09 0.03-0.04 0.01-0.06 0.02h-0.04-0.04l-0.03 0.01h-0.15l-0.06-0.01-0.13-0.02-0.06-0.01-0.15-0.06-0.1-0.06-0.04-0.04-0.04-0.03-0.08-0.08-0.03-0.05-0.05-0.09-0.03-0.06-0.01-0.06-0.04-0.12-0.01-0.06v-0.07l-0.01-0.08v-0.17l0.01-0.1v-0.1l0.02-0.1 0.01-0.1 0.04-0.2 0.05-0.2 0.03-0.1 0.03-0.09 0.08-0.2 0.05-0.1 0.09-0.19 0.18-0.28 0.06-0.09 0.07-0.09 0.22-0.27 0.08-0.09 0.1-0.11 0.22-0.2 0.12-0.09 0.26-0.18 0.14-0.08 0.15-0.08 0.16-0.08 0.15-0.06 0.17-0.07 0.18-0.06 0.18-0.05 0.38-0.1 0.2-0.04 0.42-0.08 0.44-0.05 0.46-0.04 0.24-0.01h0.25l0.25-0.01h0.2l0.2 0.01h0.2zm-2.41 7.31-0.68 0.24-0.54 0.35-0.38 0.44-0.23 0.54-0.07 0.64 0.02 0.34 0.06 0.31 0.11 0.3 0.15 0.27 0.2 0.25 0.25 0.2 0.29 0.17 0.25 0.13 0.28 0.12 0.3 0.09 0.32 0.08 0.34 0.05 0.36 0.03 0.38 0.01 0.34-0.01 0.32-0.02 0.32-0.04 0.31-0.07 0.3-0.07 0.3-0.11 0.29-0.13 0.2-0.12 0.22-0.15 0.25-0.16 0.26-0.18 0.28-0.21 0.3-0.22 0.31-0.23v-2.41l-0.54-0.16-0.53-0.14-0.53-0.11-0.52-0.09-0.51-0.07-0.49-0.04-0.48-0.01-0.98 0.05-0.83 0.14z'
|
||||
id='m'
|
||||
/>
|
||||
<path
|
||||
d='m0.97 5.8h1.84v-1.61c0-2.8 1.44-4.19 4.24-4.19 1.14 0 2.12 0.23 2.86 0.63 0.96 0.57 1.5 1.5 1.5 2.58 0 0.73-0.29 1.02-0.8 1.02-0.34 0-0.68-0.23-0.86-0.63-0.22-0.73-0.45-1.13-0.56-1.32-0.34-0.4-1.03-0.63-2.01-0.63-1.72 0-2.57 0.85-2.57 2.58v1.55h3.31c0.74 0 1.08 0.29 1.08 0.79 0 0.57-0.34 0.8-1.08 0.8h-3.31v10.48c0 0.62-0.29 0.96-0.8 0.96-0.57 0-0.8-0.34-0.8-0.96v-10.46h-2.04c-0.63 0-0.97-0.28-0.97-0.79 0-0.58 0.34-0.8 0.97-0.8z'
|
||||
id='a'
|
||||
/>
|
||||
<path
|
||||
d='m78.8 5.55 0.61 0.08 0.56 0.12 0.52 0.15 0.48 0.18 0.43 0.22 0.38 0.25 0.34 0.29 0.29 0.32 0.25 0.35 0.21 0.39 0.16 0.42 0.11 0.45 0.07 0.49 0.02 0.51v4.71l-0.02 0.52-0.07 0.48-0.11 0.46-0.15 0.42-0.2 0.38-0.24 0.35-0.29 0.32-0.33 0.29-0.38 0.25-0.42 0.22-0.47 0.19-0.51 0.15-0.55 0.11-0.6 0.09-0.64 0.05-0.68 0.02-0.72-0.01-0.68-0.04-0.63-0.08-0.58-0.11-0.53-0.15-0.48-0.18-0.43-0.22-0.39-0.25-0.34-0.29-0.3-0.32-0.25-0.36-0.21-0.38-0.15-0.43-0.11-0.46-0.07-0.49-0.02-0.53v-4.71l0.02-0.51 0.07-0.49 0.11-0.45 0.16-0.42 0.2-0.39 0.26-0.35 0.29-0.32 0.34-0.29 0.39-0.25 0.43-0.22 0.47-0.18 0.52-0.15 0.56-0.12 0.61-0.08 0.65-0.06 0.7-0.01 0.69 0.01 0.65 0.06zm-2.67 1.61-0.53 0.1-0.47 0.13-0.42 0.17-0.37 0.21-0.31 0.24-0.25 0.28-0.2 0.32-0.14 0.35-0.09 0.39-0.02 0.42v4.71l0.02 0.42 0.09 0.39 0.14 0.36 0.21 0.31 0.26 0.28 0.31 0.24 0.37 0.2 0.43 0.17 0.49 0.12 0.55 0.09 0.6 0.04h0.66 0.64l0.59-0.04 0.54-0.08 0.48-0.12 0.42-0.16 0.37-0.2 0.31-0.23 0.26-0.28 0.2-0.32 0.14-0.36 0.09-0.4 0.03-0.43v-4.71l-0.03-0.42-0.09-0.39-0.14-0.35-0.2-0.32-0.27-0.28-0.31-0.24-0.38-0.21-0.44-0.17-0.49-0.13-0.55-0.1-0.62-0.05-0.67-0.02-0.63 0.02-0.58 0.05z'
|
||||
id='i'
|
||||
/>
|
||||
<path
|
||||
d='m181.88 0.18c0.12 0.11 0.23 0.28 0.23 0.45 0 0.29-0.34 0.68-0.97 1.32-2.62 2.53-3.94 5.62-3.93 9.36 0.01 4.12 1.4 7.44 4.1 10.01 0.56 0.5 0.8 0.9 0.8 1.24 0 0.17-0.12 0.35-0.23 0.51-0.11 0.12-0.34 0.24-0.51 0.24-0.63 0-1.5-0.74-2.64-2.18-2.22-2.72-3.22-5.72-3.28-9.82-0.05-4.1 1.23-6.88 3.75-9.75 0.9-1.02 1.66-1.56 2.17-1.56 0.17 0 0.34 0.06 0.51 0.18z'
|
||||
id='f'
|
||||
/>
|
||||
<path
|
||||
d='m149.59 6.94c0.45-0.57 0.85-0.92 1.25-1.08 0.39-0.23 0.96-0.34 1.6-0.34 1.96 0 2.98 0.96 2.98 2.85v9.29c0 0.79-0.28 1.14-0.85 1.14s-0.8-0.35-0.8-1.14v-8.7c0-1.19-0.51-1.83-1.49-1.83-0.74 0-1.5 0.45-2.12 1.32v9.29c0 0.79-0.29 1.13-0.8 1.13s-0.8-0.34-0.8-1.13v-8.59c0-1.33-0.56-1.95-1.61-1.95-0.68 0-1.32 0.46-2 1.33v9.22c0 0.8-0.29 1.14-0.86 1.14s-0.79-0.34-0.79-1.14v-11.38c0-0.57 0.22-0.8 0.68-0.8 0.23 0 0.45 0.17 0.57 0.51 0.11 0.15 0.17 0.44 0.17 0.78 0.53-0.57 0.87-0.91 1.02-1.03 0.34-0.22 0.8-0.34 1.44-0.34 0.91 0 1.72 0.46 2.41 1.45z'
|
||||
id='g'
|
||||
/>
|
||||
<path
|
||||
d='m49.79 5.56 0.44 0.05 0.41 0.08 0.4 0.09 0.37 0.12 0.35 0.14 0.33 0.16 0.31 0.18 0.28 0.21 0.27 0.22 0.24 0.24 0.22 0.27 0.2 0.28 0.18 0.31 0.16 0.33 0.13 0.35 0.11 0.37 0.1 0.39 0.07 0.41 0.06 0.44 0.03 0.45 0.01 0.47v0.12l-0.01 0.11-0.01 0.1-0.03 0.2-0.02 0.09-0.03 0.09-0.02 0.08-0.08 0.15-0.03 0.06-0.1 0.12-0.05 0.05-0.06 0.05-0.05 0.03-0.07 0.04-0.07 0.03-0.14 0.04-0.08 0.01-0.09 0.01h-8.89v-0.07h-0.02v1.84l0.01 0.24 0.02 0.24 0.04 0.23 0.06 0.22 0.06 0.2 0.09 0.2 0.1 0.17 0.12 0.18 0.14 0.15 0.15 0.16 0.16 0.13 0.18 0.13 0.2 0.11 0.22 0.11 0.22 0.09 0.25 0.09 0.26 0.06 0.28 0.07 0.29 0.05 0.31 0.03 0.33 0.03 0.34 0.02 0.36 0.01h0.13l0.13-0.01h0.12l0.13-0.01h0.12l0.24-0.02 0.23-0.02 0.11-0.01 0.12-0.02 0.31-0.04 0.1-0.02 0.09-0.02 0.1-0.02 0.09-0.02 0.1-0.03 0.09-0.02 0.16-0.06 0.09-0.03 0.04-0.01 0.05-0.02 0.06-0.02 0.05-0.02 0.05-0.03 0.06-0.02 0.06-0.03 0.07-0.03 0.14-0.07 0.14-0.08 0.08-0.05 0.08-0.04 0.08-0.05 0.18-0.1 0.08-0.06 0.19-0.13 0.1-0.06 0.11-0.07 0.1-0.07 0.13-0.12 0.13-0.1 0.05-0.05 0.06-0.04 0.05-0.04 0.06-0.05 0.09-0.07 0.09-0.06 0.05-0.03 0.04-0.02 0.04-0.03 0.07-0.03 0.09-0.03 0.05-0.01h0.09l0.1 0.01 0.15 0.03 0.04 0.02 0.07 0.04 0.04 0.03 0.09 0.09 0.03 0.04 0.04 0.08 0.02 0.1 0.02 0.06v0.05l0.01 0.05v0.07l0.01 0.07-0.01 0.06v0.07l-0.01 0.06-0.01 0.07-0.07 0.2-0.03 0.06-0.06 0.14-0.09 0.14-0.05 0.06-0.11 0.14-0.12 0.13-0.14 0.14-0.16 0.14-0.08 0.06-0.08 0.08-0.15 0.1-0.15 0.11-0.32 0.2-0.17 0.09-0.18 0.09-0.18 0.08-0.19 0.07-0.2 0.08-0.2 0.07-0.21 0.06-0.22 0.06-0.44 0.1-0.47 0.09-0.5 0.06-0.52 0.04-0.55 0.02h-0.28-0.49l-0.47-0.03-0.44-0.04-0.43-0.07-0.4-0.07-0.38-0.09-0.36-0.11-0.34-0.13-0.32-0.15-0.29-0.16-0.27-0.18-0.26-0.2-0.23-0.21-0.21-0.23-0.19-0.25-0.16-0.26-0.14-0.29-0.12-0.29-0.1-0.32-0.08-0.34-0.06-0.35-0.03-0.37-0.01-0.39v-4.71l0.01-0.14 0.01-0.12 0.06-0.39 0.03-0.13 0.03-0.12 0.05-0.12 0.08-0.24 0.06-0.12 0.11-0.23 0.07-0.11 0.07-0.12 0.07-0.1 0.08-0.11 0.09-0.1 0.09-0.11 0.18-0.2 0.1-0.1 0.14-0.13 0.29-0.24 0.32-0.22 0.17-0.1 0.36-0.18 0.38-0.16 0.2-0.07 0.42-0.12 0.44-0.1 0.23-0.04 0.23-0.03 0.24-0.04 0.24-0.02 0.26-0.02 0.52-0.02h0.26l0.48 0.01 0.46 0.03zm-2.04 1.6-0.4 0.07-0.4 0.1-0.38 0.13-0.36 0.15-0.35 0.18-0.34 0.25-0.28 0.26-0.23 0.28-0.17 0.3-0.13 0.31-0.08 0.32-0.02 0.35v0.96h8.18l-0.08-0.98-0.26-0.83-0.42-0.69-0.6-0.53-0.77-0.38-0.94-0.23-1.11-0.07-0.44 0.01-0.42 0.04z'
|
||||
id='h'
|
||||
/>
|
||||
<path
|
||||
d='m67.34 1.76c1.14 0.86 1.73 2.07 1.73 3.55 0 0.68-0.29 1.02-0.86 1.02-0.4 0-0.68-0.34-0.85-1.02-0.12-0.57-0.34-1.08-0.62-1.62-0.52-0.9-1.61-1.32-3.32-1.32-1.5 0-2.52 0.34-3.14 1.08-0.57 0.68-0.92 1.72-0.92 3.26v5.95c0 1.55 0.35 2.63 0.97 3.31 0.68 0.74 1.73 1.13 3.21 1.13 2.23 0 3.54-0.79 3.82-2.34 0.11-0.57 0.17-0.86 0.17-0.91 0.11-0.34 0.34-0.51 0.68-0.51 0.57 0 0.86 0.34 0.86 1.02 0 1.44-0.58 2.52-1.79 3.38-0.96 0.62-2.18 0.96-3.77 0.96-1.83 0-3.25-0.4-4.3-1.25-1.21-0.8-1.72-2.16-1.72-3.94v-7.16c0-3.77 1.93-5.61 5.95-5.61 1.61 0 2.86 0.34 3.9 1.02z'
|
||||
id='o'
|
||||
/>
|
||||
<path
|
||||
d='m158.79 5.43 0.12 0.04 0.03 0.02 0.03 0.01 0.03 0.03 0.06 0.04 0.03 0.03 0.03 0.02 0.1 0.14 0.06 0.08 0.02 0.05 0.02 0.04 0.01 0.04 0.02 0.09 0.01 0.04 0.02 0.04 0.01 0.05 0.01 0.04 0.02 0.04 0.01 0.05 0.05 0.12 0.02 0.05 0.03 0.08 0.01 0.05 0.03 0.12 0.02 0.04 0.02 0.08 0.25-0.12 0.49-0.23 0.24-0.11 0.23-0.1 0.46-0.18 0.22-0.08 0.21-0.08 0.21-0.07 0.2-0.06 0.2-0.07 0.2-0.05 0.38-0.09 0.19-0.04 0.34-0.06 0.17-0.02 0.32-0.02h0.16l0.46 0.01 0.44 0.03 0.43 0.04 0.39 0.06 0.38 0.09 0.36 0.1 0.33 0.12 0.31 0.14 0.29 0.15 0.27 0.17 0.25 0.2 0.22 0.2 0.21 0.23 0.18 0.24 0.16 0.26 0.14 0.28 0.12 0.3 0.09 0.31 0.08 0.33 0.06 0.35 0.03 0.36 0.01 0.38v4.76l-0.01 0.19-0.01 0.18-0.01 0.17-0.03 0.18-0.02 0.17-0.08 0.32-0.05 0.16-0.05 0.15-0.12 0.3-0.08 0.14-0.07 0.14-0.09 0.14-0.09 0.13-0.09 0.12-0.1 0.13-0.11 0.12-0.12 0.12-0.11 0.12-0.26 0.22-0.24 0.18-0.26 0.16-0.14 0.07-0.13 0.07-0.15 0.07-0.15 0.06-0.15 0.05-0.32 0.11-0.33 0.08-0.17 0.04-0.36 0.06-0.36 0.04-0.19 0.02-0.2 0.01h-0.19-0.44l-0.24-0.01-0.24-0.02-0.23-0.01-0.46-0.06-0.22-0.04-0.44-0.09-0.21-0.06-0.21-0.05-0.21-0.07-0.21-0.08-0.4-0.16-0.2-0.1-0.19-0.1-0.2-0.1-0.19-0.11-0.19-0.12-0.36-0.26v5.49l-0.02 0.2-0.01 0.09-0.02 0.09-0.06 0.24-0.03 0.07-0.04 0.07-0.03 0.05-0.04 0.06-0.08 0.1-0.1 0.08-0.06 0.02-0.05 0.03-0.06 0.02-0.06 0.01-0.07 0.01h-0.14-0.06l-0.07-0.02-0.05-0.02-0.06-0.02-0.11-0.06-0.04-0.04-0.05-0.04-0.08-0.1-0.04-0.06-0.03-0.06-0.02-0.07-0.03-0.08-0.04-0.16-0.02-0.1-0.01-0.09-0.01-0.11v-0.1-16.31-0.1l0.01-0.09 0.01-0.1 0.01-0.09 0.01-0.08 0.02-0.08 0.03-0.07 0.02-0.08 0.05-0.12 0.04-0.06 0.03-0.05 0.04-0.05 0.04-0.03 0.05-0.04 0.04-0.03 0.05-0.03 0.11-0.03 0.06-0.01h0.09l0.03 0.01h0.03zm4.49 1.74-0.46 0.04-0.22 0.03-0.21 0.03-0.21 0.05-0.38 0.1-0.36 0.14-0.15 0.07-0.16 0.08-0.15 0.1-0.17 0.11-0.16 0.12-0.17 0.14-0.17 0.15-0.18 0.16-0.18 0.18-0.2 0.19-0.2 0.2v6.54h0.06v-0.03l0.47 0.28 0.45 0.25 0.45 0.24 0.42 0.19 0.41 0.18 0.4 0.14 0.38 0.12 0.36 0.09 0.35 0.07 0.34 0.04 0.32 0.02 0.28-0.01 0.27-0.01 0.26-0.03 0.24-0.03 0.25-0.04 0.23-0.05 0.22-0.06 0.21-0.07 0.2-0.08 0.19-0.09 0.17-0.1 0.21-0.13 0.18-0.15 0.17-0.15 0.14-0.16 0.12-0.17 0.1-0.19 0.08-0.19 0.06-0.2 0.05-0.22 0.02-0.22 0.01-0.23v-4.75l-0.01-0.28-0.03-0.26-0.05-0.23-0.07-0.23-0.09-0.21-0.11-0.2-0.14-0.18-0.15-0.17-0.18-0.15-0.2-0.15-0.22-0.12-0.18-0.09-0.19-0.09-0.2-0.06-0.21-0.07-0.22-0.05-0.24-0.05-0.24-0.04-0.26-0.03-0.27-0.02-0.28-0.01-0.29-0.01-0.25 0.01h-0.26z'
|
||||
id='d'
|
||||
/>
|
||||
</defs>
|
||||
<use fill='#ffffff' xlinkHref='#k' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#k' />
|
||||
<use fill='#ffffff' xlinkHref='#j' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#j' />
|
||||
<use fill='#ffffff' xlinkHref='#b' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#b' />
|
||||
<use fill='#ffffff' xlinkHref='#n' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#n' />
|
||||
<use fill='#ffffff' xlinkHref='#l' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#l' />
|
||||
<use fill='#ffffff' xlinkHref='#c' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#c' />
|
||||
<use fill='#ffffff' xlinkHref='#e' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#e' />
|
||||
<use fill='#ffffff' xlinkHref='#m' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#m' />
|
||||
<use fill='#ffffff' xlinkHref='#a' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#a' />
|
||||
<use fill='#ffffff' xlinkHref='#i' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#i' />
|
||||
<use fill='#ffffff' xlinkHref='#f' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#f' />
|
||||
<use fill='#ffffff' xlinkHref='#g' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#g' />
|
||||
<use fill='#ffffff' xlinkHref='#h' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#h' />
|
||||
<use fill='#ffffff' xlinkHref='#o' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#o' />
|
||||
<use fill='#ffffff' xlinkHref='#d' />
|
||||
<use fillOpacity={0} stroke='#000000' strokeOpacity={0} xlinkHref='#d' />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
freeCodeCampLogo.displayName = 'freeCodeCampLogo';
|
||||
|
||||
export default freeCodeCampLogo;
|
@ -13,7 +13,7 @@ const List = styled.div`
|
||||
|
||||
const filenameTitle = { fontWeight: '600' };
|
||||
|
||||
const FilenameResults = ({ searchValue, results }) => {
|
||||
const FilenameResults = ({ searchValue, results, rateLimitMessage }) => {
|
||||
const elements = results.map(result => {
|
||||
const { filename, prs: prObjects } = result;
|
||||
const prs = prObjects.map(({ number, username, title }, index) => {
|
||||
@ -31,11 +31,17 @@ const FilenameResults = ({ searchValue, results }) => {
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
|
||||
const showResults = () => {
|
||||
if (!rateLimitMessage) {
|
||||
return (results.length ? <h3>Results for: {searchValue}</h3> : null) && elements;
|
||||
} else {
|
||||
return rateLimitMessage;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
{results.length ? <h3>Results for: {searchValue}</h3> : null}
|
||||
{elements}
|
||||
{showResults()}
|
||||
</FullWidthDiv>
|
||||
);
|
||||
};
|
@ -16,8 +16,10 @@ const prNumStyle = { flex: 1 };
|
||||
const usernameStyle = { flex: 1 };
|
||||
const titleStyle = { flex: 3 };
|
||||
|
||||
const ListItem = ({ number, username, prTitle: title }) => {
|
||||
const prUrl = `https://github.com/freeCodeCamp/freeCodeCamp/pull/${number}`;
|
||||
const ListItem = ({ number, username, prTitle: title, prLink }) => {
|
||||
const prUrl = prLink
|
||||
? prLink
|
||||
: `https://github.com/freeCodeCamp/freeCodeCamp/pull/${number}`;
|
||||
return (
|
||||
<Container>
|
||||
<a
|
@ -26,13 +26,14 @@ class Pareto extends React.Component {
|
||||
all: [],
|
||||
selectedFileType: 'all',
|
||||
selectedLanguage: 'all',
|
||||
options: {}
|
||||
options: {},
|
||||
rateLimitMessage: ''
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
fetch(ENDPOINT_PARETO)
|
||||
.then(response => response.json())
|
||||
.then(({ ok, pareto }) => {
|
||||
.then(({ ok, rateLimitMessage, pareto }) => {
|
||||
if (ok) {
|
||||
if (!pareto.length) {
|
||||
pareto.push({
|
||||
@ -41,12 +42,16 @@ class Pareto extends React.Component {
|
||||
prs: []
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.setState(prevState => ({
|
||||
data: pareto,
|
||||
all: [...pareto],
|
||||
options: this.createOptions(pareto)
|
||||
}));
|
||||
} else if (rateLimitMessage) {
|
||||
this.setState(prevState => ({
|
||||
rateLimitMessage
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@ -74,8 +79,8 @@ class Pareto extends React.Component {
|
||||
handleFileTypeOptionChange = changeEvent => {
|
||||
let { all, selectedLanguage, options } = this.state;
|
||||
const selectedFileType = changeEvent.target.value;
|
||||
|
||||
let data = [ ...all ].filter(({ filename }) => {
|
||||
|
||||
let data = [...all].filter(({ filename }) => {
|
||||
const { articleType, language } = this.getFilenameOptions(filename);
|
||||
let condition;
|
||||
if (selectedFileType === 'all') {
|
||||
@ -86,7 +91,7 @@ class Pareto extends React.Component {
|
||||
condition = articleType === selectedFileType;
|
||||
} else if (!options[selectedFileType][selectedLanguage]) {
|
||||
condition = articleType === selectedFileType
|
||||
selectedLanguage = 'all';
|
||||
selectedLanguage = 'all';
|
||||
} else {
|
||||
condition = articleType === selectedFileType && language === selectedLanguage
|
||||
}
|
||||
@ -95,11 +100,11 @@ class Pareto extends React.Component {
|
||||
});
|
||||
this.setState(prevState => ({ data, selectedFileType, selectedLanguage }));
|
||||
}
|
||||
|
||||
|
||||
handleLanguageOptionChange = changeEvent => {
|
||||
const { all, selectedFileType } = this.state;
|
||||
const selectedLanguage = changeEvent.target.value;
|
||||
let data = [ ...all ].filter(({ filename }) => {
|
||||
let data = [...all].filter(({ filename }) => {
|
||||
const { articleType, language } = this.getFilenameOptions(filename);
|
||||
let condition;
|
||||
if (selectedLanguage === 'all') {
|
||||
@ -122,33 +127,34 @@ class Pareto extends React.Component {
|
||||
const [_, articleType, language] = filenameReplacement.match(regex) || [];
|
||||
return { articleType, language };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, options, selectedFileType, selectedLanguage } = this.state;
|
||||
|
||||
const elements = data.map(entry => {
|
||||
const { filename, count, prs } = entry;
|
||||
const prsList = prs.map(({ number, username, title }) => {
|
||||
return <ListItem key={number} number={number} username={username} prTitle={title} />;
|
||||
});
|
||||
const fileOnMaster = `https://github.com/freeCodeCamp/freeCodeCamp/blob/master/${filename}`;
|
||||
return (
|
||||
<Result key={filename}>
|
||||
<span style={filenameTitle}>{filename}</span>{' '}
|
||||
<a href={fileOnMaster} rel="noopener noreferrer" target="_blank">
|
||||
(File on Master)
|
||||
render() {
|
||||
const { data, options, selectedFileType, selectedLanguage, rateLimitMessage } = this.state;
|
||||
const elements = rateLimitMessage
|
||||
? rateLimitMessage
|
||||
: data.map(entry => {
|
||||
const { filename, count, prs } = entry;
|
||||
const prsList = prs.map(({ number, username, title }) => {
|
||||
return <ListItem key={number} number={number} username={username} prTitle={title} />;
|
||||
});
|
||||
const fileOnMaster = `https://github.com/freeCodeCamp/freeCodeCamp/blob/master/${filename}`;
|
||||
return (
|
||||
<Result key={filename}>
|
||||
<span style={filenameTitle}>{filename}</span>{' '}
|
||||
<a href={fileOnMaster} rel="noopener noreferrer" target="_blank">
|
||||
(File on Master)
|
||||
</a>
|
||||
<br />
|
||||
<details style={detailsStyle}>
|
||||
<summary># of PRs: {count}</summary>
|
||||
<List>{prsList}</List>
|
||||
</details>
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
|
||||
<br />
|
||||
<details style={detailsStyle}>
|
||||
<summary># of PRs: {count}</summary>
|
||||
<List>{prsList}</List>
|
||||
</details>
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
|
||||
let fileTypeOptions = Object.keys(options).map(articleType => articleType);
|
||||
const typeOptions = [ 'all', ...fileTypeOptions].map((type) => (
|
||||
const typeOptions = ['all', ...fileTypeOptions].map((type) => (
|
||||
<FilterOption
|
||||
key={type}
|
||||
name="filetype"
|
||||
@ -156,13 +162,13 @@ class Pareto extends React.Component {
|
||||
onOptionChange={this.handleFileTypeOptionChange}
|
||||
selectedOption={selectedFileType}
|
||||
>
|
||||
{type.charAt().toUpperCase() + type.slice(1)}
|
||||
{type.charAt().toUpperCase() + type.slice(1)}
|
||||
</FilterOption>
|
||||
));
|
||||
|
||||
|
||||
let languageOptions = null;
|
||||
if (selectedFileType !== 'all') {
|
||||
let languages = Object.keys(options[selectedFileType]);
|
||||
let languages = Object.keys(options[selectedFileType]);
|
||||
languages = ['all', ...languages.sort()];
|
||||
languageOptions = languages.map(language => (
|
||||
<FilterOption
|
||||
@ -172,9 +178,9 @@ class Pareto extends React.Component {
|
||||
onOptionChange={this.handleLanguageOptionChange}
|
||||
selectedOption={selectedLanguage}
|
||||
>
|
||||
{language.charAt().toUpperCase() + language.slice(1)}
|
||||
{language.charAt().toUpperCase() + language.slice(1)}
|
||||
</FilterOption>
|
||||
));
|
||||
));
|
||||
}
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
@ -198,7 +204,11 @@ class Pareto extends React.Component {
|
||||
</fieldset>
|
||||
}
|
||||
</Options>
|
||||
{data.length ? elements : 'Report Loading...'}
|
||||
{rateLimitMessage
|
||||
? rateLimitMessage
|
||||
: data.length
|
||||
? elements
|
||||
: 'Report Loading...'}
|
||||
</FullWidthDiv>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ const List = styled.ul`
|
||||
margin: 3px;
|
||||
`;
|
||||
|
||||
const PrResults = ({ searchValue, results }) => {
|
||||
const PrResults = ({ searchValue, results, rateLimitMessage }) => {
|
||||
const elements = results.map((result, idx) => {
|
||||
const { number, filenames, username, title } = result;
|
||||
const files = filenames.map((filename, index) => {
|
||||
@ -32,10 +32,17 @@ const PrResults = ({ searchValue, results }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const showResults = () => {
|
||||
if (!rateLimitMessage) {
|
||||
return (results.length ? <h3>Results for PR# {searchValue}</h3> : null) && elements;
|
||||
} else {
|
||||
return rateLimitMessage;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FullWidthDiv style={{ width: '100%' }}>
|
||||
{results.length ? <h3>Results for PR# {searchValue}</h3> : null}
|
||||
{elements}
|
||||
{showResults()}
|
||||
</FullWidthDiv>
|
||||
);
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
import FullWidthDiv from './FullWidthDiv';
|
||||
import Result from './Result';
|
||||
import { ENDPOINT_ALL_REPOS } from '../constants';
|
||||
|
||||
const List = styled.div`
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const detailsStyle = { padding: '3px' };
|
||||
const filenameTitle = { fontWeight: '600' };
|
||||
|
||||
class Repos extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: [],
|
||||
rateLimitMessage: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch(ENDPOINT_ALL_REPOS)
|
||||
.then(response => response.json())
|
||||
.then(({ ok, rateLimitMessage, allRepos }) => {
|
||||
if (ok) {
|
||||
const repos = allRepos.filter(this.props.dataFilter);
|
||||
if (!repos.length) {
|
||||
repos.push({
|
||||
repoName: 'No repos with open PRs',
|
||||
prs: []
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(prevState => ({
|
||||
data: repos
|
||||
}));
|
||||
} else if (rateLimitMessage) {
|
||||
this.setState(prevState => ({
|
||||
rateLimitMessage
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const repos = [
|
||||
{ repoName: 'No repos with open PRs', prs: [] }
|
||||
];
|
||||
this.setState(prevState => ({ data: repos }));
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data, rateLimitMessage } = this.state;
|
||||
|
||||
const elements = rateLimitMessage
|
||||
? rateLimitMessage
|
||||
: data.map(entry => {
|
||||
const { _id: repoName, prs } = entry;
|
||||
const prsList = prs.map(({ _id: number, username, title, prLink }) => {
|
||||
return <ListItem key={number} number={number} username={username} prTitle={title} prLink={prLink} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Result key={repoName}>
|
||||
<span style={filenameTitle}>{repoName}</span>
|
||||
<br />
|
||||
<details style={detailsStyle}>
|
||||
<summary># of PRs: {prs.length}</summary>
|
||||
<List>{prsList}</List>
|
||||
</details>
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
{rateLimitMessage
|
||||
? rateLimitMessage
|
||||
: data.length
|
||||
? elements
|
||||
: 'Report Loading...'}
|
||||
</FullWidthDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Repos;
|
@ -11,7 +11,7 @@ class Search extends Component {
|
||||
searchValue: '',
|
||||
selectedOption: 'pr',
|
||||
results: [],
|
||||
message: ''
|
||||
message: '',
|
||||
};
|
||||
|
||||
clearObj = { searchValue: '', results: [] };
|
||||
@ -64,9 +64,13 @@ class Search extends Component {
|
||||
|
||||
fetch(fetchUrl)
|
||||
.then(response => response.json())
|
||||
.then(({ ok, message, results }) => {
|
||||
.then(({ ok, message, results, rateLimitMessage }) => {
|
||||
if (ok) {
|
||||
this.setState(prevState => ({ message, results }));
|
||||
} else if (rateLimitMessage) {
|
||||
this.setState(prevState => ({
|
||||
rateLimitMessage
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@ -86,7 +90,7 @@ class Search extends Component {
|
||||
handleOptionChange,
|
||||
state
|
||||
} = this;
|
||||
const { searchValue, message, results, selectedOption } = state;
|
||||
const { searchValue, message, results, selectedOption, rateLimitMessage } = state;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -114,10 +118,10 @@ class Search extends Component {
|
||||
<button onClick={handleButtonClick}>Search</button>
|
||||
{message}
|
||||
{selectedOption === 'pr' && (
|
||||
<PrResults searchValue={searchValue} results={results} />
|
||||
<PrResults searchValue={searchValue} results={results} rateLimitMessage={rateLimitMessage} />
|
||||
)}
|
||||
{selectedOption === 'filename' && (
|
||||
<FilenameResults searchValue={searchValue} results={results} />
|
||||
<FilenameResults searchValue={searchValue} results={results} rateLimitMessage={rateLimitMessage} />
|
||||
)}
|
||||
</>
|
||||
);
|
@ -20,8 +20,8 @@ const Tab = styled.div`
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #eeeeee;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
background: ${({ theme }) => theme.primary};
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@ -42,6 +42,12 @@ const Tabs = ({ view, onViewChange }) => {
|
||||
<Tab id="tabs-reports" onClick={onViewChange} active={view === 'reports'}>
|
||||
Pareto
|
||||
</Tab>
|
||||
<Tab id="tabs-boilerplates" onClick={onViewChange} active={view === 'boilerplates'}>
|
||||
Boilerplate PRs
|
||||
</Tab>
|
||||
<Tab id="tabs-other" onClick={onViewChange} active={view === 'other'}>
|
||||
Other Repos' PRs
|
||||
</Tab>
|
||||
</Container>
|
||||
);
|
||||
};
|
@ -1,12 +1,14 @@
|
||||
require('dotenv').config();
|
||||
|
||||
let API_HOST = process.env.REACT_APP_HOST === 'local'
|
||||
? 'http://localhost:3001'
|
||||
: '';
|
||||
API_HOST += '/dashboard';
|
||||
|
||||
const ENDPOINT_INFO = API_HOST + '/info';
|
||||
const ENDPOINT_PARETO = API_HOST + '/pareto';
|
||||
const ENDPOINT_ALL_REPOS = API_HOST + '/all-repos';
|
||||
const ENDPOINT_PR = API_HOST + '/pr';
|
||||
const ENDPOINT_SEARCH = API_HOST + '/search';
|
||||
export {
|
||||
API_HOST, ENDPOINT_INFO, ENDPOINT_PARETO, ENDPOINT_PR, ENDPOINT_SEARCH
|
||||
API_HOST, ENDPOINT_INFO, ENDPOINT_PARETO, ENDPOINT_PR, ENDPOINT_SEARCH, ENDPOINT_ALL_REPOS
|
||||
};
|
@ -47,13 +47,13 @@ body {
|
||||
}
|
||||
|
||||
.app-menu li a:hover {
|
||||
color: #006400;
|
||||
background: #eee;
|
||||
color: #0a0a23;
|
||||
background: white;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #006400;
|
||||
color: #0a0a23;
|
||||
}
|
||||
|
||||
a:visited {
|
@ -1,5 +1,5 @@
|
||||
const theme = {
|
||||
primary: '#006400'
|
||||
primary: '#0a0a23'
|
||||
};
|
||||
|
||||
export default theme;
|
69
tools/contributor/dashboard-app/server/index.js
Normal file
69
tools/contributor/dashboard-app/server/index.js
Normal file
@ -0,0 +1,69 @@
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const config = require('../../lib/config');
|
||||
const { pareto, pr, search, info, allRepos } = require('./routes');
|
||||
const { reqLimiter } = require('./req-limiter');
|
||||
|
||||
// May need to uncomment the following to get rateLimit to work properly since we are using reverse-proxy
|
||||
// app.set('trust proxy', 1);
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../client/build')));
|
||||
|
||||
app.use((request, response, next) => {
|
||||
response.header('Access-Control-Allow-Origin', '*');
|
||||
response.header(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Origin, X-Requested-With, Content-Type, Accept'
|
||||
);
|
||||
response.header('Access-Control-Allow-Methods', 'GET');
|
||||
next();
|
||||
});
|
||||
|
||||
const landingPage = path.join(__dirname, '../client/build/index.html');
|
||||
app.get('/', reqLimiter, (req, res) => res.sendFile(landingPage));
|
||||
|
||||
app.use('/pr', pr);
|
||||
app.use('/search', search);
|
||||
app.use('/pareto', pareto);
|
||||
app.use('/info', info);
|
||||
app.use('/all-repos', allRepos);
|
||||
|
||||
// 404
|
||||
app.use(function(req, res) {
|
||||
const message = 'Route' + req.url + ' Not found.';
|
||||
console.log(message);
|
||||
return res.status(404).send({ message });
|
||||
});
|
||||
|
||||
// 500 - Any server error
|
||||
app.use(function(err, req, res) {
|
||||
console.log('error: ' + err);
|
||||
return res.status(500).send({ error: err });
|
||||
});
|
||||
|
||||
if (mongoose.connection.readyState === 0) {
|
||||
// connect to mongo db
|
||||
const mongoUri = config.mongo.host;
|
||||
|
||||
const promise = mongoose.connect(mongoUri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
});
|
||||
promise
|
||||
.then(() => {
|
||||
console.log('MongoDB is connected');
|
||||
const portNum = process.env.PORT || 3000;
|
||||
app.listen(portNum, () => {
|
||||
console.log(`server listening on port ${portNum}`);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.log('MongoDB connection unsuccessful');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = app;
|
@ -14,11 +14,30 @@ const info = new mongoose.Schema({
|
||||
prRange: String
|
||||
});
|
||||
|
||||
const allRepos = new mongoose.Schema({
|
||||
_id: String,
|
||||
prs: [
|
||||
{
|
||||
_id: Number,
|
||||
title: String,
|
||||
username: String,
|
||||
prLink: String
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const dbCollections = {
|
||||
pr: 'openprs',
|
||||
info: 'info'
|
||||
pr: 'openprs',
|
||||
info: 'info',
|
||||
boilerplate: 'boilerplate',
|
||||
otherPrs: 'otherPrs'
|
||||
};
|
||||
|
||||
const PR = mongoose.model('PR', pr, dbCollections['pr']);
|
||||
const INFO = mongoose.model('INFO', info, dbCollections['info']);
|
||||
module.exports = { PR, INFO, dbCollections };
|
||||
const ALL_REPOS = mongoose.model(
|
||||
'ALL_REPOS',
|
||||
allRepos,
|
||||
dbCollections['allRepos']
|
||||
);
|
||||
module.exports = { PR, INFO, ALL_REPOS, dbCollections };
|
File diff suppressed because it is too large
Load Diff
@ -1,46 +1,39 @@
|
||||
{
|
||||
"name": "@freecodecamp/contribute",
|
||||
"name": "@freecodecamp/contributer-tools",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": "https://github.com/freecodecamp/contribute.git",
|
||||
"bugs": "https://github.com/freeCodeCamp/contribute/issues",
|
||||
"repository": "https://github.com/freecodecamp/dashboard.git",
|
||||
"bugs": "https://github.com/freeCodeCamp/freeCodeCamp/issues",
|
||||
"keywords": [
|
||||
"probot",
|
||||
"freeCodeCamp",
|
||||
"github",
|
||||
"probot-app"
|
||||
"contributer tools"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "develop",
|
||||
"develop": "nodemon",
|
||||
"start": "probot run esm ./index.js",
|
||||
"start-probot": "cross-env TEST_ENV=false probot run ./index.js",
|
||||
"start": "node index.js",
|
||||
"lint": "standard --fix",
|
||||
"test": "cross-env TEST_ENV=true jest --forceExit && standard",
|
||||
"test:watch": "jest --watch --notify --notifyMode=change --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.3",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"body-parser": "^1.18.3",
|
||||
"cli-progress": "^2.1.1",
|
||||
"cross-env": "^5.2.0",
|
||||
"dotenv": "^6.2.0",
|
||||
"express": "^4.16.3",
|
||||
"http-status": "^1.3.1",
|
||||
"joi": "^14.3.1",
|
||||
"mongoose": "^5.4.1",
|
||||
"node-fetch": "^2.3.0",
|
||||
"probot": "^7.2.0"
|
||||
"express-rate-limit": "^5.2.3",
|
||||
"mongoose": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect": "^23.6.0",
|
||||
"jest": "^22.4.3",
|
||||
"nock": "^10.0.0",
|
||||
"nodemon": "^1.17.2",
|
||||
"smee-client": "^1.0.2",
|
||||
"standard": "^10.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3.0"
|
||||
"node": ">= 12.18.3"
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
23
tools/contributor/dashboard-app/server/req-limiter.js
Normal file
23
tools/contributor/dashboard-app/server/req-limiter.js
Normal file
@ -0,0 +1,23 @@
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const limitHandler = (req, res) => {
|
||||
res
|
||||
.status(429)
|
||||
.json({
|
||||
ok: false,
|
||||
rateLimitMessage: 'You have accessed this app\'s pages too quickly. Please try again in 5 minutes.'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const rateLimitOptions = {
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
max: 100,
|
||||
message: 'rate limit activated',
|
||||
handler: limitHandler,
|
||||
onLimitReached: limitHandler
|
||||
};
|
||||
|
||||
const reqLimiter = rateLimit(rateLimitOptions);
|
||||
|
||||
module.exports = { reqLimiter };
|
20
tools/contributor/dashboard-app/server/routes/all-repos.js
Normal file
20
tools/contributor/dashboard-app/server/routes/all-repos.js
Normal file
@ -0,0 +1,20 @@
|
||||
const router = require('express').Router();
|
||||
const { ALL_REPOS } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
router.get('/', reqLimiter, async (request, response) => {
|
||||
let allRepos = await ALL_REPOS.find({}).then(data => data);
|
||||
allRepos.sort((a, b) => a._id - b._id);
|
||||
allRepos = allRepos.reduce((allReposArr, aRepo) => {
|
||||
const { _id, prs } = aRepo;
|
||||
if (prs.length) {
|
||||
prs.sort((a, b) => a._id - b._id);
|
||||
return allReposArr.concat({ _id, prs });
|
||||
}
|
||||
return allRepos;
|
||||
}, []);
|
||||
|
||||
response.json({ ok: true, allRepos });
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -2,5 +2,6 @@ const pareto = require('./pareto');
|
||||
const pr = require('./pr');
|
||||
const search = require('./search');
|
||||
const info = require('./info');
|
||||
const allRepos = require('./all-repos');
|
||||
|
||||
module.exports = { pareto, pr, search, info };
|
||||
module.exports = { pareto, pr, search, info, allRepos };
|
@ -1,7 +1,8 @@
|
||||
const router = require('express').Router();
|
||||
const { INFO } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
router.get('/', reqLimiter, async (request, response) => {
|
||||
const [{ lastUpdate, numPRs, prRange }] = await INFO.find({});
|
||||
response.json({ ok: true, lastUpdate, numPRs, prRange });
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
const createPareto = reportObj =>
|
||||
Object.keys(reportObj)
|
||||
@ -12,7 +13,7 @@ const createPareto = reportObj =>
|
||||
}, [])
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
router.get('/', async (reqeust, response) => {
|
||||
router.get('/', reqLimiter, async (reqeust, response) => {
|
||||
const prs = await PR.find({}).then(data => data);
|
||||
prs.sort((a, b) => a._id - b._id);
|
||||
const reportObj = prs.reduce((obj, pr) => {
|
@ -1,7 +1,8 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
router.get('/:number', async (request, response) => {
|
||||
router.get('/:number', reqLimiter, async (request, response) => {
|
||||
const prs = await PR.find({}).then(data => data);
|
||||
prs.sort((a, b) => a._id - b._id);
|
||||
const indices = prs.reduce((obj, { _id }, index) => {
|
@ -1,7 +1,8 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
router.get('/', reqLimiter, async (request, response) => {
|
||||
const prs = await PR.find({}).then(data => data);
|
||||
prs.sort((a, b) => a._id - b._id);
|
||||
const indices = prs.reduce((obj, { _id }, index) => {
|
27
tools/contributor/dashboard-app/server/tools/get-prs.js
Normal file
27
tools/contributor/dashboard-app/server/tools/get-prs.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../../../lib/config');
|
||||
|
||||
const getPRs = async () => {
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const methodProps = {
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
base: defaultBase,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const openPRs = await octokit.paginate(octokit.pulls.list, methodProps);
|
||||
// const openPRs = await octokit.pulls.list(methodProps);
|
||||
// console.log(openPRs[0])
|
||||
return openPRs;
|
||||
};
|
||||
|
||||
module.exports = getPRs;
|
54
tools/contributor/dashboard-app/server/tools/get-repos.js
Normal file
54
tools/contributor/dashboard-app/server/tools/get-repos.js
Normal file
@ -0,0 +1,54 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const {
|
||||
github: { owner, secret }
|
||||
} = require('../../../lib/config');
|
||||
|
||||
const getRepos = async () => {
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const methodProps = {
|
||||
org: owner,
|
||||
sort: 'full_name',
|
||||
direction: 'asc',
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const repos = await octokit.paginate(octokit.repos.listForOrg, methodProps);
|
||||
const otherRepos = repos
|
||||
.filter(repo => !repo.archived && repo.name !== owner)
|
||||
.map(repo => repo.name);
|
||||
|
||||
const reposToAdd = [];
|
||||
for (let repo of otherRepos) {
|
||||
const methodProps = {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
page: 1,
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const openPRs = await octokit.paginate(octokit.pulls.list, methodProps);
|
||||
|
||||
if (openPRs.length) {
|
||||
const prsToAdd = [];
|
||||
for (let pr of openPRs) {
|
||||
const {
|
||||
number,
|
||||
title,
|
||||
user: { login: username },
|
||||
html_url: prLink
|
||||
} = pr;
|
||||
prsToAdd.push({ _id: number, title, username, prLink });
|
||||
}
|
||||
reposToAdd.push({ _id: repo, prs: prsToAdd });
|
||||
}
|
||||
}
|
||||
return reposToAdd;
|
||||
};
|
||||
|
||||
module.exports = getRepos;
|
27
tools/contributor/dashboard-app/server/tools/getFilenames.js
Normal file
27
tools/contributor/dashboard-app/server/tools/getFilenames.js
Normal file
@ -0,0 +1,27 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../../../lib/config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const getFiles = async number => {
|
||||
/* eslint-disable camelcase */
|
||||
const methodProps = {
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
pull_number: number,
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const files = await octokit.paginate(octokit.pulls.listFiles, methodProps);
|
||||
return files;
|
||||
};
|
||||
|
||||
const getFilenames = async number => {
|
||||
const files = await getFiles(number);
|
||||
return files.map(({ filename }) => filename);
|
||||
};
|
||||
|
||||
module.exports = getFilenames;
|
@ -1,34 +1,37 @@
|
||||
const config = require('../../../config');
|
||||
// config should be imported before importing any other file
|
||||
const mongoose = require('mongoose');
|
||||
const getRepos = require('./get-repos');
|
||||
const getPRs = require('./get-prs');
|
||||
const getFilenames = require('./getFilenames');
|
||||
const { PR, INFO, ALL_REPOS } = require('../models');
|
||||
|
||||
const { mongo } = require('../../../lib/config');
|
||||
|
||||
// added to prevent deprecation warning when findOneAndUpdate is used
|
||||
mongoose.set('useFindAndModify', false);
|
||||
|
||||
// connect to mongo db
|
||||
const mongoUri = config.mongo.host;
|
||||
const db = mongoose.connect(
|
||||
mongoUri,
|
||||
{ useNewUrlParser: true }
|
||||
);
|
||||
|
||||
const { PR, INFO } = require('../models');
|
||||
const { getPRs, getUserInput, getFilenames } = require('../../../lib/get-prs');
|
||||
const { rateLimiter } = require('../../../lib/utils');
|
||||
const mongoUri = mongo.host;
|
||||
const db = mongoose.connect(mongoUri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
});
|
||||
|
||||
const lastUpdate = new Date();
|
||||
|
||||
db.then(async () => {
|
||||
const reposToAdd = await getRepos();
|
||||
await ALL_REPOS.deleteMany();
|
||||
await ALL_REPOS.insertMany(reposToAdd);
|
||||
|
||||
// update PRs for freeCodeCamp repo
|
||||
const oldPRs = await PR.find({}).then(data => data);
|
||||
const oldIndices = oldPRs.reduce((obj, { _id }, index) => {
|
||||
obj[_id] = index;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
|
||||
const prPropsToGet = ['number', 'user', 'title', 'updated_at'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
let count = 0;
|
||||
const openPRs = await getPRs();
|
||||
|
||||
const newIndices = {};
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
const {
|
||||
@ -44,25 +47,20 @@ db.then(async () => {
|
||||
if (!oldIndices.hasOwnProperty(number)) {
|
||||
// insert a new pr
|
||||
const filenames = await getFilenames(number);
|
||||
count++;
|
||||
await PR.create({ _id: number, updatedAt, title, username, filenames });
|
||||
console.log('added PR# ' + number);
|
||||
} else if (updatedAt > oldUpdatedAt) {
|
||||
// update an existing pr
|
||||
const filenames = await getFilenames(number);
|
||||
count++;
|
||||
await PR.findOneAndUpdate(
|
||||
{ _id: number },
|
||||
{ updatedAt, title, username, filenames }
|
||||
);
|
||||
console.log('updated PR #' + number);
|
||||
}
|
||||
if (count > 4500) {
|
||||
await rateLimiter(4500);
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < oldPRs.length; j++) {
|
||||
const { _id: number } = oldPRs[j];
|
||||
for (let pr of oldPRs) {
|
||||
const { _id: number } = pr;
|
||||
if (!newIndices.hasOwnProperty(number)) {
|
||||
// delete pr because it is no longer open
|
||||
await PR.deleteOne({ _id: number });
|
||||
@ -72,25 +70,24 @@ db.then(async () => {
|
||||
})
|
||||
.then(async () => {
|
||||
// update info collection
|
||||
const [ { firstPR, lastPR }] = await PR.aggregate(
|
||||
[{
|
||||
const [{ firstPR, lastPR }] = await PR.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
firstPR: { $min: '$_id' },
|
||||
lastPR: { $max: '$_id' }
|
||||
}
|
||||
}]
|
||||
);
|
||||
}
|
||||
]);
|
||||
const numPRs = await PR.countDocuments();
|
||||
const info = {
|
||||
lastUpdate,
|
||||
numPRs,
|
||||
prRange: `${firstPR}-${lastPR}`
|
||||
};
|
||||
await INFO.updateOne({}, info, { upsert: true })
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
await INFO.updateOne({}, info, { upsert: true }).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
mongoose.connection.close();
|
||||
})
|
||||
.catch(err => {
|
64
tools/contributor/docs/README.md
Normal file
64
tools/contributor/docs/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
## Local Setup
|
||||
- Follow the steps below to get this running on your local machine
|
||||
|
||||
### 1. Copy .env
|
||||
- Copy the `sample.env` file into `.env`. The command below will do that in the terminal if your CWD(current working directory) is the `contribute` folder.
|
||||
```bash
|
||||
cp sample.env .env
|
||||
```
|
||||
- If you do not want to populate the database with the freeCodeCamp PR's you can [skip to step 5](#5-start-the-app-in-developemnt-mode)
|
||||
|
||||
### 2. Update .env
|
||||
- Use your GitHub username as the `GITHUB_USERNAME` variable of the `.env` file
|
||||
- Use your GitHub Personal Access Token as the `GITHUB_ACCESS_TOKEN` variable of the `.env` file
|
||||
|
||||
### 3. Run mongoDB
|
||||
- Make sure a mongoDB instance is running by running the command below in the terminal.
|
||||
```bash
|
||||
mongod —dbpath=./database_folder
|
||||
```
|
||||
|
||||
### 4. Update the Database
|
||||
- Run the command below to populate your local database with PR’s from the freeCodeCamp repo. Note that you must have mongoDB running.
|
||||
```bash
|
||||
node dashboard-app/server/tools/update-db.js
|
||||
```
|
||||
- This will take a while. If it stops running partway through, it's probably a timeout error. Run the command again and it should finish
|
||||
|
||||
### 5. Start the app in development mode
|
||||
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
|
||||
```bash
|
||||
npm ci
|
||||
npm run develop
|
||||
```
|
||||
|
||||
### 6. Start the app in production mode
|
||||
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
## Caveats & Notes
|
||||
|
||||
### Local Ports when developing locally
|
||||
Using `npm run develop` will start both the api server and the Create React App(Dashboard) in development mode. The app server runs on port 3001 and the React app runs on port 3000.
|
||||
|
||||
### The one-off scripts will error out on actions performed by repository admins
|
||||
For example, if an admin removes a label from a Pull Request, the script can not add that label back. This is usually because the script is acting on behalf of a non-admin user with write access.
|
||||
This is usually the case with the use of access tokens for scripts.
|
||||
|
||||
### Setting up Cron jobs for Sweeper Scripts
|
||||
For updating dashboard data we use PM2 like so:
|
||||
```bash
|
||||
pm2 start --no-autorestart dashboard-app/server/tools/update-db.js --cron "*/10 * * * *"
|
||||
```
|
||||
This will start the script in the "no restart" mode and re-run it every 10 minutes.
|
||||
A useful link to calculate a Cron expression: <https://crontab.guru/every-10-minutes>
|
||||
|
||||
### Starting the express server (via probot)
|
||||
```bash
|
||||
pm2 start "npm start" --name "contribute-app"
|
||||
```
|
||||
**Note:** Start only one instance of this app, you can't have multiple probot apps running. Starting multiple instances will crash the app.
|
4
tools/contributor/lerna.json
Normal file
4
tools/contributor/lerna.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"packages": ["dashboard-app", "dashboard-app/client", "dashboard-app/server", "lib", "one-off-scripts"],
|
||||
"version": "independent"
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
const Joi = require('joi');
|
||||
const path = require('path');
|
||||
// require and configure dotenv, will load vars in .env in PROCESS.ENV
|
||||
const envPath = path.resolve(__dirname, '../probot/.env');
|
||||
require('dotenv').config({ path: envPath });
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// define validation for all the env vars
|
||||
const envVarsSchema = Joi.object({
|
||||
@ -13,16 +11,12 @@ const envVarsSchema = Joi.object({
|
||||
MONGO_HOST: Joi.string()
|
||||
.required()
|
||||
.description('Mongo DB host url'),
|
||||
MONGO_PORT: Joi.number().default(27017),
|
||||
GITHUB_USERNAME: Joi.string().required(),
|
||||
GITHUB_ACCESS_TOKEN: Joi.string().required(),
|
||||
REPOSITORY_OWNER: Joi.string().required(),
|
||||
REPOSITORY: Joi.string().required(),
|
||||
PRODUCTION_RUN: Joi.boolean()
|
||||
.default(false),
|
||||
WEBHOOK_PROXY_URL: Joi.string().required(),
|
||||
APP_ID: Joi.number().required(),
|
||||
WEBHOOK_SECRET: Joi.string().required()
|
||||
DEFAULT_BASE: Joi.string().required(),
|
||||
PRODUCTION_RUN: Joi.boolean().default(false)
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
@ -35,19 +29,14 @@ if (error) {
|
||||
const config = {
|
||||
env: envVars.NODE_ENV,
|
||||
mongo: {
|
||||
host: envVars.MONGO_HOST,
|
||||
port: envVars.MONGO_PORT
|
||||
host: envVars.MONGO_HOST
|
||||
},
|
||||
github: {
|
||||
id: envVars.GITHUB_USERNAME,
|
||||
secret: envVars.GITHUB_ACCESS_TOKEN,
|
||||
owner: envVars.REPOSITORY_OWNER,
|
||||
repo: envVars.REPOSITORY,
|
||||
probot: {
|
||||
webhookUrl: envVars.WEBHOOK_PROXY_URL,
|
||||
webhookSecret: envVars.WEBHOOK_SECRET,
|
||||
appID: envVars.APP_ID
|
||||
}
|
||||
freeCodeCampRepo: envVars.REPOSITORY,
|
||||
defaultBase: envVars.DEFAULT_BASE
|
||||
},
|
||||
oneoff: {
|
||||
productionRun: envVars.PRODUCTION_RUN
|
@ -1,30 +1,37 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const _cliProgress = require('cli-progress');
|
||||
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
|
||||
const {
|
||||
github: { owner, secret }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const { getRange, getCount } = require('./pr-stats');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
|
||||
const methodProps = {
|
||||
owner,
|
||||
/* eslint-disable camelcase */
|
||||
const prsPaginate = async (
|
||||
repo,
|
||||
state: 'open',
|
||||
base: 'master',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
page: 1,
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const prsPaginate = async(
|
||||
base,
|
||||
method,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet,
|
||||
progressBar
|
||||
prPropsToGet
|
||||
) => {
|
||||
let methodProps = {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
page: 1,
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
if (base) {
|
||||
methodProps = { ...methodProps, base };
|
||||
}
|
||||
|
||||
const prFilter = (prs, first, last, prPropsToGet) => {
|
||||
const filtered = [];
|
||||
for (let pr of prs) {
|
||||
@ -46,26 +53,31 @@ const prsPaginate = async(
|
||||
// will be true when lastPR is seen in paginated results
|
||||
let done = false;
|
||||
let response = await method(methodProps);
|
||||
console.log('x-ratelimit-remaining:', response.meta['x-ratelimit-remaining']);
|
||||
let { data } = response;
|
||||
data = prFilter(data, firstPR, lastPR, prPropsToGet);
|
||||
while (octokit.hasNextPage(response) && !done) {
|
||||
response = await octokit.getNextPage(response);
|
||||
console.log(
|
||||
'x-ratelimit-remaining:',
|
||||
response.meta['x-ratelimit-remaining']
|
||||
);
|
||||
let dataFiltered = prFilter(response.data, firstPR, lastPR, prPropsToGet);
|
||||
data = data.concat(dataFiltered);
|
||||
progressBar.increment(dataFiltered.length);
|
||||
// progressBar.increment(dataFiltered.length);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getUserInput = async(rangeType = '') => {
|
||||
const getUserInput = async (repo, base, rangeType = '') => {
|
||||
let data, firstPR, lastPR;
|
||||
if (rangeType === 'all') {
|
||||
data = await getRange().then(data => data);
|
||||
data = await getRange(repo, base).then(data => data);
|
||||
firstPR = data[0];
|
||||
lastPR = data[1];
|
||||
} else {
|
||||
let [type, start, end] = process.argv.slice(2);
|
||||
data = await getRange().then(data => data);
|
||||
data = await getRange(repo, base).then(data => data);
|
||||
firstPR = data[0];
|
||||
lastPR = data[1];
|
||||
if (type !== 'all' && type !== 'range') {
|
||||
@ -90,11 +102,15 @@ const getUserInput = async(rangeType = '') => {
|
||||
lastPR = end;
|
||||
}
|
||||
}
|
||||
const totalPRs = await getCount().then(data => data);
|
||||
// A null value for firstPR or lastPR indicates the repo had no open PRs
|
||||
if (firstPR === null || lastPR === null) {
|
||||
return { totalPRs: 0, firstPR, lastPR };
|
||||
}
|
||||
const totalPRs = await getCount(repo, base).then(data => data);
|
||||
return { totalPRs, firstPR, lastPR };
|
||||
};
|
||||
|
||||
const getPRs = async (totalPRs, firstPR, lastPR, prPropsToGet) => {
|
||||
const getPRs = async (repo, base, totalPRs, firstPR, lastPR, prPropsToGet) => {
|
||||
let progressText = `Retrieve PRs (${firstPR}-${lastPR}) [{bar}] `;
|
||||
progressText += '{percentage}% | Elapsed Time: {duration_formatted} ';
|
||||
progressText += '| ETA: {eta_formatted}';
|
||||
@ -105,24 +121,39 @@ const getPRs = async (totalPRs, firstPR, lastPR, prPropsToGet) => {
|
||||
},
|
||||
_cliProgress.Presets.shades_classic
|
||||
);
|
||||
getPRsBar.start(totalPRs, 0);
|
||||
// getPRsBar.start(totalPRs, 0);
|
||||
let openPRs = await prsPaginate(
|
||||
octokit.pullRequests.list,
|
||||
repo,
|
||||
base,
|
||||
octokit.pulls.list,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet,
|
||||
getPRsBar
|
||||
);
|
||||
getPRsBar.update(totalPRs);
|
||||
getPRsBar.stop();
|
||||
// getPRsBar.update(totalPRs);
|
||||
// getPRsBar.stop();
|
||||
console.log(`# of PRs retrieved: ${openPRs.length}`);
|
||||
return { firstPR, lastPR, openPRs };
|
||||
};
|
||||
|
||||
const filesPaginate = async (repo, number) => {
|
||||
let methodProps = {
|
||||
owner,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
page: 1,
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const filesPaginate = async number => {
|
||||
let response = await octokit.pullRequests.listFiles({
|
||||
number, ...methodProps
|
||||
if (repo) {
|
||||
methodProps = { ...methodProps, repo };
|
||||
}
|
||||
|
||||
let response = await octokit.pulls.listFiles({
|
||||
number,
|
||||
...methodProps
|
||||
});
|
||||
|
||||
let { data } = response;
|
||||
@ -134,10 +165,9 @@ const filesPaginate = async number => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const getFiles = async number => await filesPaginate(number);
|
||||
const getFiles = async (repo, number) => await filesPaginate(repo, number);
|
||||
|
||||
const getFilenames = async number => (
|
||||
await getFiles(number)
|
||||
).map(({ filename }) => filename);
|
||||
const getFilenames = async (repo, number) =>
|
||||
(await getFiles(repo, number)).map(({ filename }) => filename);
|
||||
|
||||
module.exports = { getPRs, getUserInput, getFiles, getFilenames };
|
54
tools/contributor/lib/get-prs/pr-stats.js
Normal file
54
tools/contributor/lib/get-prs/pr-stats.js
Normal file
@ -0,0 +1,54 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const {
|
||||
github: { owner, secret }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const getCount = async (repo, base) => {
|
||||
const baseStr = base ? `+base:${base}` : '';
|
||||
/* eslint-disable camelcase */
|
||||
const {
|
||||
data: { total_count: count }
|
||||
} = await octokit.search
|
||||
.issues({
|
||||
q: `repo:${owner}/${repo}+is:open+type:pr${baseStr}`,
|
||||
sort: 'created',
|
||||
order: 'asc',
|
||||
page: 1,
|
||||
per_page: 1
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
return count;
|
||||
};
|
||||
|
||||
const getRange = async (repo, base) => {
|
||||
let methodProps = {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
page: 1,
|
||||
per_page: 1
|
||||
};
|
||||
if (base) {
|
||||
methodProps = { ...methodProps, base };
|
||||
}
|
||||
let response = await octokit.pulls.list({
|
||||
direction: 'asc',
|
||||
...methodProps
|
||||
});
|
||||
// In the case there are no open PRs for repo
|
||||
if (!response.data.length) {
|
||||
return [null, null];
|
||||
}
|
||||
const firstPR = response.data[0].number;
|
||||
response = await octokit.pulls.list({
|
||||
direction: 'desc',
|
||||
...methodProps
|
||||
});
|
||||
const lastPR = response.data[0].number;
|
||||
return [firstPR, lastPR];
|
||||
};
|
||||
|
||||
module.exports = { getCount, getRange };
|
1825
tools/contributor/lib/package-lock.json
generated
Normal file
1825
tools/contributor/lib/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "freecodecampcontributelib",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^15.18.0",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"cli-progress": "^2.1.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"dedent": "^0.7.0",
|
@ -1,14 +1,16 @@
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../config');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const addComment = async (number, comment) => {
|
||||
const result = await octokit.issues
|
||||
.createComment({
|
||||
owner,
|
||||
repo,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
body: comment
|
||||
})
|
@ -1,11 +1,13 @@
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../config');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const addLabels = (number, labels, log) => {
|
||||
octokit.issues
|
||||
.addLabels({ owner, repo, number, labels })
|
||||
.addLabels({ owner, repo: freeCodeCampRepo, number, labels })
|
||||
.then(() => {
|
||||
console.log(`PR #${number} added ${JSON.stringify(labels)}\n`);
|
||||
})
|
@ -1,29 +1,31 @@
|
||||
const { addComment } = require('./add-comment');
|
||||
const { rateLimiter } = require('../utils');
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
/* closes and reopens an open PR with applicable comment */
|
||||
const closeOpen = async number => {
|
||||
await octokit.pullRequests
|
||||
await octokit.pulls
|
||||
.update({
|
||||
owner,
|
||||
repo,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
state: 'closed',
|
||||
base: 'master'
|
||||
base: defaultBase
|
||||
})
|
||||
.then(async () => {
|
||||
await rateLimiter(5000);
|
||||
return octokit.pullRequests.update({
|
||||
return octokit.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
state: 'open',
|
||||
base: 'master'
|
||||
base: defaultBase
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
@ -1,13 +1,9 @@
|
||||
const config = require('../../config');
|
||||
const config = require('../../lib/config');
|
||||
const { validLabels } = require('../validation');
|
||||
const { addLabels } = require('./add-labels');
|
||||
const { rateLimiter } = require('../utils');
|
||||
|
||||
const labeler = async(
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels
|
||||
) => {
|
||||
const labeler = async (number, prFiles, currentLabels) => {
|
||||
// holds potential labels to add based on file path
|
||||
const labelsToAdd = {};
|
||||
const existingLabels = currentLabels.map(({ name }) => name);
|
||||
@ -20,7 +16,9 @@ const labeler = async(
|
||||
);
|
||||
const regex = /^(docs|curriculum)(?:\/)(english|arabic|chinese|portuguese|russian|spanish)?\/?/;
|
||||
// need an array to pass to labelsAdder
|
||||
const [_, articleType, language] = filenameReplacement.match(regex) || [];
|
||||
const match = filenameReplacement.match(regex) || [];
|
||||
const articleType = match[1];
|
||||
const language = match[2];
|
||||
if (articleType && validLabels[articleType]) {
|
||||
labelsToAdd[validLabels[articleType]] = 1;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
const config = require('../../config');
|
||||
const config = require('../../lib/config');
|
||||
const formatDate = require('date-fns/format');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
@ -4,10 +4,7 @@ a comment and "status: needs update" label to any PR with guide articles which
|
||||
have frontmatter issues.
|
||||
*/
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
|
||||
const { addLabels, addComment } = require('../lib/pr-tasks');
|
||||
const { rateLimiter, ProcessingLog } = require('../lib/utils');
|
||||
@ -18,6 +15,11 @@ const {
|
||||
createErrorMsg
|
||||
} = require('../lib/validation/guide-folder-checks/create-error-msg');
|
||||
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../config');
|
||||
|
||||
const allowedLangDirNames = [
|
||||
'arabic',
|
||||
'chinese',
|
||||
@ -29,7 +31,7 @@ const allowedLangDirNames = [
|
||||
|
||||
const log = new ProcessingLog('all-frontmatter-checks');
|
||||
|
||||
const labeler = async(
|
||||
const labeler = async (
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels,
|
||||
@ -47,7 +49,7 @@ const labeler = async(
|
||||
return !existingLabels.includes(label);
|
||||
});
|
||||
if (newLabels.length) {
|
||||
if (config.oneoff.productionRun) {
|
||||
if (productionRun) {
|
||||
addLabels(number, newLabels);
|
||||
await rateLimiter();
|
||||
}
|
||||
@ -68,11 +70,11 @@ const checkPath = (fullPath, fileContent) => {
|
||||
return errorMsgs.concat(frontMatterErrMsgs);
|
||||
};
|
||||
|
||||
const guideFolderChecks = async(number, prFiles, user) => {
|
||||
const guideFolderChecks = async (number, prFiles, user) => {
|
||||
let prErrors = [];
|
||||
for (let { filename: fullPath, raw_url: fileUrl } of prFiles) {
|
||||
let newErrors;
|
||||
if ((/^guide\//).test(fullPath)) {
|
||||
if (/^guide\//.test(fullPath)) {
|
||||
const response = await fetch(fileUrl);
|
||||
const fileContent = await response.text();
|
||||
newErrors = checkPath(fullPath, fileContent);
|
||||
@ -84,7 +86,7 @@ const guideFolderChecks = async(number, prFiles, user) => {
|
||||
|
||||
if (prErrors.length) {
|
||||
const comment = createErrorMsg(prErrors, user);
|
||||
if (config.oneoff.productionRun) {
|
||||
if (productionRun) {
|
||||
await addComment(number, comment);
|
||||
await rateLimiter();
|
||||
}
|
||||
@ -94,10 +96,20 @@ const guideFolderChecks = async(number, prFiles, user) => {
|
||||
}
|
||||
};
|
||||
|
||||
(async() => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
log.start();
|
||||
console.log('Starting frontmatter checks process...');
|
||||
@ -109,8 +121,8 @@ const guideFolderChecks = async(number, prFiles, user) => {
|
||||
labels: currentLabels,
|
||||
user: { login: username }
|
||||
} = openPRs[count];
|
||||
|
||||
const prFiles = await getFiles(number);
|
||||
|
||||
const prFiles = await getFiles(freeCodeCampRepo, number);
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
@ -5,6 +5,9 @@ To run the script for a specific range,
|
||||
run `node sweeper.js range startingPrNumber endingPrNumber`
|
||||
*/
|
||||
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
|
||||
const { ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
const { labeler } = require('../lib/pr-tasks');
|
||||
@ -13,24 +16,30 @@ const log = new ProcessingLog('add-language-labels');
|
||||
|
||||
log.start();
|
||||
console.log('Curriculum File language labeler started...');
|
||||
(async() => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
let count = 0;
|
||||
if (openPRs.length) {
|
||||
console.log('Processing PRs...');
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
let { number, labels: currentLabels } = openPRs[i];
|
||||
|
||||
const prFiles = await getFiles(number);
|
||||
const prFiles = await getFiles(freeCodeCampRepo, number);
|
||||
count++;
|
||||
|
||||
const labelsAdded = await labeler(
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels
|
||||
);
|
||||
const labelsAdded = await labeler(number, prFiles, currentLabels);
|
||||
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
|
||||
|
||||
log.add(number, { number, labels: labelLogVal });
|
@ -4,7 +4,10 @@ This is a one-off script to run on all open PRs to add the
|
||||
"scope: learn" label on it.
|
||||
*/
|
||||
|
||||
const config = require('../config');
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../lib/config');
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { addLabels } = require('../lib/pr-tasks');
|
||||
@ -13,9 +16,19 @@ const { rateLimiter, ProcessingLog } = require('../lib/utils');
|
||||
const log = new ProcessingLog('all-locally-tested-labels');
|
||||
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput();
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
if (openPRs.length) {
|
||||
log.start();
|
||||
@ -36,7 +49,7 @@ const log = new ProcessingLog('all-locally-tested-labels');
|
||||
|
||||
if (newLabels.length) {
|
||||
log.add(number, { number, labels: newLabels });
|
||||
if (config.oneoff.productionRun) {
|
||||
if (productionRun) {
|
||||
addLabels(number, newLabels, log);
|
||||
await rateLimiter();
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
const config = require('../config');
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../lib/config');
|
||||
|
||||
const { closeOpen } = require('../lib/pr-tasks');
|
||||
const { openJSONFile, ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
|
||||
@ -21,14 +25,14 @@ const getUserInput = async () => {
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const { prs } = await getUserInput();
|
||||
const { prs } = await getUserInput(freeCodeCampRepo, defaultBase);
|
||||
return prs;
|
||||
})()
|
||||
.then(async prs => {
|
||||
for (let { number, errorDesc } of prs) {
|
||||
if (errorDesc !== 'unknown error') {
|
||||
log.add(number, { number, closedOpened: true, errorDesc });
|
||||
if (config.oneoff.productionRun) {
|
||||
if (productionRun) {
|
||||
await closeOpen(number);
|
||||
await rateLimiter(90000);
|
||||
}
|
@ -3,16 +3,18 @@ This is a one-off script to find all open PRs which have one of the
|
||||
console.error descriptions in the failuresToFind.json file.
|
||||
*/
|
||||
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { savePrData, ProcessingLog } = require('../lib/utils');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
|
||||
const log = new ProcessingLog('find-failures-script');
|
||||
|
||||
const errorsToFind = [
|
||||
@ -23,9 +25,19 @@ const errorsToFind = [
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput();
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'head'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
if (openPRs.length) {
|
||||
savePrData(openPRs, firstPR, lastPR);
|
||||
@ -46,7 +58,7 @@ const errorsToFind = [
|
||||
) {
|
||||
const { data: statuses } = await octokit.repos.listStatusesForRef({
|
||||
owner,
|
||||
repo,
|
||||
repo: freeCodeCampRepo,
|
||||
ref
|
||||
});
|
||||
if (statuses.length) {
|
@ -9,15 +9,17 @@ Note: If any PR displayed in the console shows "unknown", you will need to rerun
|
||||
the script again.
|
||||
*/
|
||||
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
const { validLabels } = require('../lib/validation/valid-labels');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
|
||||
let languageLabel;
|
||||
let [languageArg] = process.argv.slice(2);
|
||||
if (languageArg) {
|
||||
@ -32,26 +34,49 @@ if (languageLabel) {
|
||||
const log = new ProcessingLog('unknown-repo-prs-with-merge-conflicts');
|
||||
log.start();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
'all'
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user', 'head'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
if (openPRs.length) {
|
||||
let count = 0;
|
||||
let mergeConflictCount = 0;
|
||||
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
let { labels, number, head: { repo: headRepo } } = openPRs[i];
|
||||
let {
|
||||
labels,
|
||||
number,
|
||||
head: { repo: headRepo }
|
||||
} = openPRs[i];
|
||||
|
||||
const hasLanguage = languageLabel && labels.some(
|
||||
({ name }) => languageLabel === name
|
||||
);
|
||||
const hasLanguage =
|
||||
languageLabel && labels.some(({ name }) => languageLabel === name);
|
||||
|
||||
if (headRepo === null && (!languageLabel || hasLanguage )) {
|
||||
let data = await octokit.pullRequests.get({ owner, repo, number });
|
||||
if (headRepo === null && (!languageLabel || hasLanguage)) {
|
||||
let data = await octokit.pulls.get({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number
|
||||
});
|
||||
let mergeableState = data.data.mergeable_state;
|
||||
if (mergeableState === 'unknown') {
|
||||
await rateLimiter(4000); // allow time to let GitHub determine status
|
||||
data = await octokit.pullRequests.get({ owner, repo, number });
|
||||
// allow time to let GitHub determine status
|
||||
await rateLimiter(4000);
|
||||
data = await octokit.pulls.get({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number
|
||||
});
|
||||
mergeableState = data.data.mergeable_state;
|
||||
}
|
||||
count++;
|
||||
@ -66,7 +91,9 @@ log.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} unknown repos received from GitHub`);
|
||||
console.log(
|
||||
`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} unknown repos received from GitHub`
|
||||
);
|
||||
} else {
|
||||
throw 'There were no open PRs received from Github';
|
||||
}
|
236
tools/contributor/one-off-scripts/package-lock.json
generated
Normal file
236
tools/contributor/one-off-scripts/package-lock.json
generated
Normal file
@ -0,0 +1,236 @@
|
||||
{
|
||||
"name": "freecodecampcontributeoneoffscripts",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz",
|
||||
"integrity": "sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q==",
|
||||
"requires": {
|
||||
"@octokit/types": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/core": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.4.tgz",
|
||||
"integrity": "sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg==",
|
||||
"requires": {
|
||||
"@octokit/auth-token": "^2.4.4",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@octokit/request": "^5.4.12",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"before-after-hook": "^2.1.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.10.tgz",
|
||||
"integrity": "sha512-9+Xef8nT7OKZglfkOMm7IL6VwxXUQyR7DUSU0LH/F7VNqs8vyd7es5pTfz9E7DwUIx7R3pGscxu1EBhYljyu7Q==",
|
||||
"requires": {
|
||||
"@octokit/types": "^6.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.8.tgz",
|
||||
"integrity": "sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.3.0",
|
||||
"@octokit/types": "^6.0.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/openapi-types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
|
||||
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw=="
|
||||
},
|
||||
"@octokit/plugin-paginate-rest": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.6.2.tgz",
|
||||
"integrity": "sha512-3Dy7/YZAwdOaRpGQoNHPeT0VU1fYLpIUdPyvR37IyFLgd6XSij4j9V/xN/+eSjF2KKvmfIulEh9LF1tRPjIiDA==",
|
||||
"requires": {
|
||||
"@octokit/types": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-request-log": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz",
|
||||
"integrity": "sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg=="
|
||||
},
|
||||
"@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz",
|
||||
"integrity": "sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA==",
|
||||
"requires": {
|
||||
"@octokit/types": "^6.1.0",
|
||||
"deprecation": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz",
|
||||
"integrity": "sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg==",
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^6.0.1",
|
||||
"@octokit/request-error": "^2.0.0",
|
||||
"@octokit/types": "^6.0.3",
|
||||
"deprecation": "^2.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.4.tgz",
|
||||
"integrity": "sha512-LjkSiTbsxIErBiRh5wSZvpZqT4t0/c9+4dOe0PII+6jXR+oj/h66s7E4a/MghV7iT8W9ffoQ5Skoxzs96+gBPA==",
|
||||
"requires": {
|
||||
"@octokit/types": "^6.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/rest": {
|
||||
"version": "18.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz",
|
||||
"integrity": "sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw==",
|
||||
"requires": {
|
||||
"@octokit/core": "^3.2.3",
|
||||
"@octokit/plugin-paginate-rest": "^2.6.2",
|
||||
"@octokit/plugin-request-log": "^1.0.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "4.4.1"
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz",
|
||||
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==",
|
||||
"requires": {
|
||||
"@octokit/openapi-types": "^2.0.0",
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz",
|
||||
"integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
|
||||
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
|
||||
},
|
||||
"cli-progress": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-2.1.1.tgz",
|
||||
"integrity": "sha512-TSJw3LY9ZRSis7yYzQ7flIdtQMbacd9oYoiFphJhI4SzgmqF0zErO+uNv0lbUjk1L4AGfHQJ4OVYYzW+JV66KA==",
|
||||
"requires": {
|
||||
"colors": "^1.1.2",
|
||||
"string-width": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
|
||||
},
|
||||
"deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
|
||||
"requires": {
|
||||
"process": "^0.11.1",
|
||||
"util": "^0.10.3"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||
"requires": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "freecodecampcontributeoneoffscripts",
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^15.18.0",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"cli-progress": "^2.1.0",
|
||||
"dedent": "^0.7.0",
|
||||
"path": "^0.12.7"
|
@ -1,88 +1,110 @@
|
||||
/*
|
||||
This script was created to find all open PRs that have merge
|
||||
conflicts and then add a the 'status: merge conflict' label to any PR
|
||||
which does not already have the label.
|
||||
|
||||
To run the script for a specific language, call the script with the language
|
||||
name as the first argument.
|
||||
|
||||
Note: It is possible that it could take more than 4 seconds for GitHub to
|
||||
determine if a PR is mergeable. If that happens, the PR will not be labeled.
|
||||
*/
|
||||
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../lib/constants');
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
const { addLabels } = require('../lib/pr-tasks');
|
||||
const { validLabels } = require('../lib/validation/valid-labels');
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
|
||||
let languageLabel;
|
||||
let [languageArg] = process.argv.slice(2);
|
||||
if (languageArg) {
|
||||
languageArg = languageArg.toLowerCase();
|
||||
languageLabel = validLabels[languageArg] ? validLabels[languageArg] : null;
|
||||
}
|
||||
|
||||
if (languageLabel) {
|
||||
console.log(`finding PRs with label = ${languageLabel}`);
|
||||
}
|
||||
|
||||
const log = new ProcessingLog('prs-with-merge-conflicts');
|
||||
log.start();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput('all');
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
if (openPRs.length) {
|
||||
let count = 0;
|
||||
let mergeConflictCount = 0;
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
let { labels, number } = openPRs[i];
|
||||
|
||||
const hasLanguage = languageLabel && labels.some(
|
||||
({ name }) => languageLabel === name
|
||||
);
|
||||
|
||||
const hasMergeConflictLabel = labels.some(
|
||||
({ name }) => "status: merge conflict" === name
|
||||
);
|
||||
|
||||
if (!languageLabel || hasLanguage) {
|
||||
let data = await octokit.pullRequests.get({ owner, repo, number });
|
||||
let mergeableState = data.data.mergeable_state;
|
||||
count++;
|
||||
if (mergeableState === 'unknown') {
|
||||
await rateLimiter(4000);
|
||||
data = await octokit.pullRequests.get({ owner, repo, number });
|
||||
mergeableState = data.data.mergeable_state;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (mergeableState === 'dirty' && !hasMergeConflictLabel) {
|
||||
mergeConflictCount++;
|
||||
addLabels(number, ['status: merge conflict'], log);
|
||||
await rateLimiter();
|
||||
}
|
||||
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} PRs received from GitHub`);
|
||||
} else {
|
||||
throw 'There were no open PRs received from Github';
|
||||
}
|
||||
})()
|
||||
.then(async () => {
|
||||
log.finish();
|
||||
console.log('Finished finding PRs with merge conflicts');
|
||||
})
|
||||
.catch(err => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
||||
/*
|
||||
This script was created to find all open PRs that have merge
|
||||
conflicts and then add a the 'status: merge conflict' label to any PR
|
||||
which does not already have the label.
|
||||
|
||||
To run the script for a specific language, call the script with the language
|
||||
name as the first argument.
|
||||
|
||||
Note: It is possible that it could take more than 4 seconds for GitHub to
|
||||
determine if a PR is mergeable. If that happens, the PR will not be labeled.
|
||||
*/
|
||||
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
const { addLabels } = require('../lib/pr-tasks');
|
||||
const { validLabels } = require('../lib/validation/valid-labels');
|
||||
|
||||
let languageLabel;
|
||||
let [languageArg] = process.argv.slice(2);
|
||||
if (languageArg) {
|
||||
languageArg = languageArg.toLowerCase();
|
||||
languageLabel = validLabels[languageArg] ? validLabels[languageArg] : null;
|
||||
}
|
||||
|
||||
if (languageLabel) {
|
||||
console.log(`finding PRs with label = ${languageLabel}`);
|
||||
}
|
||||
|
||||
const log = new ProcessingLog('prs-with-merge-conflicts');
|
||||
log.start();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
'all'
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
if (openPRs.length) {
|
||||
let count = 0;
|
||||
let mergeConflictCount = 0;
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
let { labels, number } = openPRs[i];
|
||||
|
||||
const hasLanguage =
|
||||
languageLabel && labels.some(({ name }) => languageLabel === name);
|
||||
|
||||
const hasMergeConflictLabel = labels.some(
|
||||
({ name }) => 'status: merge conflict' === name
|
||||
);
|
||||
|
||||
if (!languageLabel || hasLanguage) {
|
||||
let data = await octokit.pulls.get({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number
|
||||
});
|
||||
let mergeableState = data.data.mergeable_state;
|
||||
count++;
|
||||
if (mergeableState === 'unknown') {
|
||||
await rateLimiter(4000);
|
||||
data = await octokit.pulls.get({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number
|
||||
});
|
||||
mergeableState = data.data.mergeable_state;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (mergeableState === 'dirty' && !hasMergeConflictLabel) {
|
||||
mergeConflictCount++;
|
||||
addLabels(number, ['status: merge conflict'], log);
|
||||
await rateLimiter();
|
||||
}
|
||||
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`There were ${mergeConflictCount} PRs with potential merge conflicts out of ${count} PRs received from GitHub`
|
||||
);
|
||||
} else {
|
||||
throw 'There were no open PRs received from Github';
|
||||
}
|
||||
})()
|
||||
.then(async () => {
|
||||
log.finish();
|
||||
console.log('Finished finding PRs with merge conflicts');
|
||||
})
|
||||
.catch(err => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -10,6 +10,9 @@ To run the script for a specific range,
|
||||
run `node sweeper.js range startingPrNumber endingPrNumber`
|
||||
*/
|
||||
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
const { getPRs, getUserInput, getFiles } = require('../lib/get-prs');
|
||||
const { ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
const { labeler } = require('../lib/pr-tasks');
|
||||
@ -19,25 +22,28 @@ const log = new ProcessingLog('sweeper');
|
||||
log.start();
|
||||
console.log('Sweeper started...');
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput();
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(totalPRs, firstPR, lastPR, prPropsToGet);
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
let count = 0;
|
||||
if (openPRs.length) {
|
||||
console.log('Processing PRs...');
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
let {
|
||||
number,
|
||||
labels: currentLabels,
|
||||
} = openPRs[i];
|
||||
const prFiles = await getFiles(number);
|
||||
let { number, labels: currentLabels } = openPRs[i];
|
||||
const prFiles = await getFiles(freeCodeCampRepo, number);
|
||||
count++;
|
||||
|
||||
const labelsAdded = await labeler(
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels,
|
||||
);
|
||||
const labelsAdded = await labeler(number, prFiles, currentLabels);
|
||||
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
|
||||
|
||||
log.add(number, { number, labels: labelLogVal });
|
File diff suppressed because it is too large
Load Diff
@ -1,34 +1,27 @@
|
||||
{
|
||||
"name": "@freecodecamp/contribute",
|
||||
"name": "@freecodecamp/contributer-tools",
|
||||
"description": "Tools to help maintain freecodecamp.org's Open Source Codebase on GitHub",
|
||||
"bin": {
|
||||
"presolver": "probot/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap --ci",
|
||||
"build": "lerna run build",
|
||||
"postinstall": "npm run bootstrap",
|
||||
"test": "run-p test:*",
|
||||
"test:probot": "cd ./probot && npm run test",
|
||||
"test:client": "cd ./probot/client && npm run test",
|
||||
"test:client": "cd ./dashboard-app/client && npm run test",
|
||||
"format": "prettier --write es5 ./**/*.{js,json} && npm run lint",
|
||||
"lint": "eslint ./**/*.js --fix",
|
||||
"start": "cd probot && npm start && cd ../",
|
||||
"start": "cd dashboard-app/server && npm start",
|
||||
"develop": "run-p develop:*",
|
||||
"develop:client": "cd ./probot/client && npm run develop",
|
||||
"develop:server": "cd ./probot/ && npm run develop"
|
||||
"develop:client": "cd ./dashboard-app/client && npm run develop",
|
||||
"develop:server": "cd ./dashboard-app/server && npm run develop"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^6.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^5.9.0",
|
||||
"eslint-config-freecodecamp": "^1.1.1",
|
||||
"esm": "^3.0.84",
|
||||
"joi": "^14.3.1",
|
||||
"lerna": "^3.5.1",
|
||||
"mocha": "^5.2.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"presolver": "file:probot",
|
||||
"prettier": "^1.15.2"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
@ -38,11 +31,6 @@
|
||||
"."
|
||||
]
|
||||
},
|
||||
"probot": {
|
||||
"apps": [
|
||||
"presolver"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^6.6.1",
|
||||
"ajv-keywords": "^3.2.0"
|
9
tools/contributor/sample.env
Normal file
9
tools/contributor/sample.env
Normal file
@ -0,0 +1,9 @@
|
||||
GITHUB_USERNAME='camperbot'
|
||||
GITHUB_ACCESS_TOKEN=
|
||||
REPOSITORY_OWNER='freeCodeCamp'
|
||||
REPOSITORY='freeCodeCamp'
|
||||
DEFAULT_BASE='master'
|
||||
PORT=3001
|
||||
MONGO_HOST=mongodb://localhost/contributor-tools
|
||||
MONGO_PORT=27017
|
||||
PRODUCTION_RUN=false
|
@ -1,81 +0,0 @@
|
||||
## Local Setup
|
||||
- Follow the steps below to get this running on your local machine
|
||||
|
||||
### 1. Copy .env
|
||||
- Copy the `sample.env` file into `.env`. The command below will do that in the terminal if your CWD(current working directory) is the `contribute` folder.
|
||||
```bash
|
||||
cp probot/sample.env probot/.env
|
||||
```
|
||||
- If you do not want to populate the database with the freeCodeCamp PR's you can [skip to step 6](#6-start-the-program)
|
||||
|
||||
### 2. Update .env
|
||||
- Put your GitHub username in the `GITHUB_USERNAME` field of the `.env` file
|
||||
|
||||
### 3. Obtain GitHub Personal access token
|
||||
- While on Github, click your profile icon > `Settings`
|
||||
- Then click `Developer settings` > `Personal access tokens`
|
||||
- Click `Generate new token`
|
||||
- On the next page, give the token a name so you know what it’s for
|
||||
- Scroll down to the bottom and click `Generate token`
|
||||
- Copy the token string and paste it into the `GITHUB_ACCESS_TOKEN` field in the `.env` file. Note that you will not be able to see this token string on GitHub again
|
||||
|
||||
### 4. Create your own GitHub app
|
||||
- While on GitHub, click your profile icon > `Settings`
|
||||
- Then click `developer settings` > `New GitHub app`
|
||||
- Fill in the `name` field with a name of your choice
|
||||
- Fill in the `Homepage URL` field with the URL to the GitHub repository for your app. e.g. `https://github.com/username/contribute`
|
||||
- In a new tab, go to https://smee.io/
|
||||
- Click `Start a new channel` and copy the URL they give you. You can ignore the rest of the instructions on the `smee.io` page
|
||||
- Paste the URL you copied into the `WEBHOOK_PROXY_URL` field of the `.env` file and the `Webhook URL` field of your new GitHub app
|
||||
- Fill in the `Webhook secret` field on the GitHub app with a secret of your choice
|
||||
- Put the same secret you just used in the `WEBHOOK_SECRET` field of the `.env` file
|
||||
- Scroll to the bottom of your GitHub app page and click `Create GitHub App`
|
||||
- On the next page, copy the `App ID` and paste it into the `APP_ID` field of the `.env` file
|
||||
- Scroll to the bottom of this page and click `Generate Private Key`
|
||||
- A popup menu will allow you to download the key file. Download it and put it in the `probot` folder
|
||||
|
||||
### 5. Run mongoDB
|
||||
- Make sure a mongoDB instance is running by running the command below in the terminal.
|
||||
```bash
|
||||
mongod —dbpath=./database_folder
|
||||
```
|
||||
|
||||
### 6. Start the program
|
||||
- In a new terminal window or tab, run these three commands to start the program. Wait for one command to finish running before starting the next.
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm run develop
|
||||
```
|
||||
- If you skipped steps 2-5, you can ignore the rest
|
||||
|
||||
### 7. Update the Database
|
||||
- Run the command below to populate your local database with PR’s from the freeCodeCamp repo. Note that you must have the program running and mongoDB running.
|
||||
```bash
|
||||
node probot/server/tools/update-db.js
|
||||
```
|
||||
- This will take a while. If it stops running partway through, it's probably a timeout error. Run the command again and it should finish
|
||||
|
||||
## Caveats & Notes
|
||||
|
||||
### Local Ports when developing locally
|
||||
Using `npm run develop` will start both the api server and the Create React App(Dashboard) in development mode. The api server runs on port 3001 and the React app runs on port 3000.
|
||||
|
||||
### The one-off scripts will error out on actions performed by repository admins
|
||||
For example, if an admin removes a label from a Pull Request, the script can not add that label back. This is usually because the script is acting on behalf of a non-admin user with write access.
|
||||
This is usually the case with the use of access tokens for scripts.
|
||||
|
||||
### Setting up Cron jobs for Sweeper Scripts
|
||||
For updating dashboard data we use PM2 like so:
|
||||
```bash
|
||||
pm2 start --no-autorestart probot/server/tools/update-db.js --cron "*/10 * * * *"
|
||||
```
|
||||
This will start the script in the "no restart" mode and re-run it every 10 minutes.
|
||||
A useful link to calculate a Cron expression: <https://crontab.guru/every-10-minutes>
|
||||
|
||||
### Starting the express server (via probot)
|
||||
```bash
|
||||
pm2 start "npm start" --name "contribute-app"
|
||||
```
|
||||
**Note:** Start only one instance of this app, you can't have multiple probot apps running. Starting multiple instances will crash the app.
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"packages": ["probot", "probot/client", "lib", "one-off-scripts"],
|
||||
"version": "independent"
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
const config = require('../config');
|
||||
|
||||
const owner = config.github.owner;
|
||||
const repo = config.github.repo;
|
||||
const fccBaseUrl = `https://github.com/${owner}/${repo}/`;
|
||||
const prBaseUrl = `${fccBaseUrl}pull/`;
|
||||
|
||||
const octokitConfig = {
|
||||
// 0 means no request timeout
|
||||
timeout: 0,
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3+json',
|
||||
// v1.2.3 will be current version
|
||||
'user-agent': 'octokit/rest.js v1.2.3'
|
||||
},
|
||||
// custom GitHub Enterprise URL
|
||||
baseUrl: 'https://api.github.com',
|
||||
// Node only: advanced request options can be passed as http(s) agent
|
||||
/* eslint-disable no-undefined */
|
||||
agent: undefined
|
||||
/* eslint-enable no-undefined */
|
||||
};
|
||||
|
||||
const octokitAuth = {
|
||||
type: 'basic',
|
||||
username: config.github.id,
|
||||
password: config.github.secret
|
||||
};
|
||||
|
||||
module.exports = { owner, repo, prBaseUrl, octokitConfig, octokitAuth };
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
labelPRConflict: {
|
||||
name: 'PR: potential-conflict',
|
||||
color: 'c2e0c6'
|
||||
}
|
||||
};
|
@ -1,41 +0,0 @@
|
||||
const { owner, repo, octokitConfig, octokitAuth } = require('../constants');
|
||||
|
||||
const octokit = require('@octokit/rest')(octokitConfig);
|
||||
|
||||
octokit.authenticate(octokitAuth);
|
||||
|
||||
let methodProps = {
|
||||
owner, repo, state: 'open',
|
||||
base: 'master', sort: 'created',
|
||||
page: 1, per_page: 1
|
||||
};
|
||||
|
||||
const getCount = async() => {
|
||||
const { data: { total_count: count } } = await octokit.search.issues({
|
||||
q: `repo:${owner}/${repo}+is:open+type:pr+base:master`,
|
||||
sort: 'created', order: 'asc', page: 1, per_page: 1
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
return count;
|
||||
};
|
||||
|
||||
const getFirst = async() => {
|
||||
const response = await octokit.pullRequests.list({
|
||||
direction: 'asc', ...methodProps
|
||||
});
|
||||
return response.data[0].number;
|
||||
};
|
||||
|
||||
const getRange = async() => {
|
||||
let response = await octokit.pullRequests.list({
|
||||
direction: 'asc', ...methodProps
|
||||
});
|
||||
const firstPR = response.data[0].number;
|
||||
response = await octokit.pullRequests.list({
|
||||
direction: 'desc', ...methodProps }
|
||||
);
|
||||
const lastPR = response.data[0].number;
|
||||
return [ firstPR, lastPR ];
|
||||
};
|
||||
|
||||
module.exports = { getCount, getFirst, getRange };
|
1105
tools/dashboard/lib/package-lock.json
generated
1105
tools/dashboard/lib/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
318
tools/dashboard/one-off-scripts/package-lock.json
generated
318
tools/dashboard/one-off-scripts/package-lock.json
generated
@ -1,318 +0,0 @@
|
||||
{
|
||||
"name": "freecodecampcontributeoneoffscripts",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@octokit/rest": {
|
||||
"version": "15.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.18.1.tgz",
|
||||
"integrity": "sha512-g2tecjp2TEtYV8bKAFvfQtu+W29HM7ektmWmw8zrMy9/XCKDEYRErR2YvvhN9+IxkLC4O3lDqYP4b6WgsL6Utw==",
|
||||
"requires": {
|
||||
"before-after-hook": "^1.1.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"debug": "^3.1.0",
|
||||
"http-proxy-agent": "^2.1.0",
|
||||
"https-proxy-agent": "^2.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"node-fetch": "^2.1.1",
|
||||
"universal-user-agent": "^2.0.0",
|
||||
"url-template": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
|
||||
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-1.3.1.tgz",
|
||||
"integrity": "sha512-BIjg60OP/sQvG7Q2L9Xkc77gyyFw1B4T73LIfZVQtXbutJinC1+t2HRl4qeR3EWAmY+tA6z9vpRi02q6ZXyluQ=="
|
||||
},
|
||||
"btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
|
||||
},
|
||||
"cli-progress": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-2.1.1.tgz",
|
||||
"integrity": "sha512-TSJw3LY9ZRSis7yYzQ7flIdtQMbacd9oYoiFphJhI4SzgmqF0zErO+uNv0lbUjk1L4AGfHQJ4OVYYzW+JV66KA==",
|
||||
"requires": {
|
||||
"colors": "^1.1.2",
|
||||
"string-width": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz",
|
||||
"integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
"semver": "^5.5.0",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz",
|
||||
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
|
||||
"integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||
},
|
||||
"http-proxy-agent": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
|
||||
"integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
|
||||
"requires": {
|
||||
"agent-base": "4",
|
||||
"debug": "3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
|
||||
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
|
||||
"requires": {
|
||||
"agent-base": "^4.1.0",
|
||||
"debug": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
|
||||
},
|
||||
"macos-release": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.0.0.tgz",
|
||||
"integrity": "sha512-iCM3ZGeqIzlrH7KxYK+fphlJpCCczyHXc+HhRVbEu9uNTCrzYJjvvtefzeKTCVHd5AP/aD/fzC80JZ4ZP+dQ/A=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
|
||||
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
|
||||
"requires": {
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"os-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.0.0.tgz",
|
||||
"integrity": "sha512-7c74tib2FsdFbQ3W+qj8Tyd1R3Z6tuVRNNxXjJcZ4NgjIEQU9N/prVMqcW29XZPXGACqaXN3jq58/6hoaoXH6g==",
|
||||
"requires": {
|
||||
"macos-release": "^2.0.0",
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"path": {
|
||||
"version": "0.12.7",
|
||||
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
|
||||
"integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
|
||||
"requires": {
|
||||
"process": "^0.11.1",
|
||||
"util": "^0.10.3"
|
||||
}
|
||||
},
|
||||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
|
||||
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
|
||||
"requires": {
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
|
||||
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-2.0.2.tgz",
|
||||
"integrity": "sha512-nOwvHWLH3dBazyuzbECPA5uVFNd7AlgviXRHgR4yf48QqitIvpdncRrxMbZNMpPPEfgz30I9ubd1XmiJiqsTrg==",
|
||||
"requires": {
|
||||
"os-name": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"url-template": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||
"integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE="
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
|
||||
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
|
||||
"requires": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"windows-release": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.1.0.tgz",
|
||||
"integrity": "sha512-hBb7m7acFgQPQc222uEQTmdcGLeBmQLNLFIh0rDk3CwFOBrfjefLzEfEfmpMq8Af/n/GnFf3eYf203FY1PmudA==",
|
||||
"requires": {
|
||||
"execa": "^0.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
# This is a GitHub App Manifest. These settings will be used by default when
|
||||
# initially configuring your GitHub App.
|
||||
#
|
||||
# NOTE: changing this file will not update your GitHub App settings.
|
||||
# You must visit github.com/settings/apps/your-app-name to edit them.
|
||||
#
|
||||
# Read more about configuring your GitHub App:
|
||||
# https://probot.github.io/docs/development/#configuring-a-github-app
|
||||
#
|
||||
# Read more about GitHub App Manifests:
|
||||
# https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/
|
||||
|
||||
# The list of events the GitHub App subscribes to.
|
||||
# Uncomment the event names below to enable them.
|
||||
default_events:
|
||||
# - check_run
|
||||
# - check_suite
|
||||
# - commit_comment
|
||||
# - create
|
||||
# - delete
|
||||
# - deployment
|
||||
# - deployment_status
|
||||
# - fork
|
||||
# - gollum
|
||||
# - issue_comment
|
||||
- issues
|
||||
# - label
|
||||
# - milestone
|
||||
# - member
|
||||
# - membership
|
||||
# - org_block
|
||||
# - organization
|
||||
# - page_build
|
||||
# - project
|
||||
# - project_card
|
||||
# - project_column
|
||||
# - public
|
||||
- pull_request
|
||||
- pull_request_review
|
||||
- pull_request_review_comment
|
||||
# - push
|
||||
# - release
|
||||
- repository
|
||||
- repository_import
|
||||
# - status
|
||||
# - team
|
||||
# - team_add
|
||||
# - watch
|
||||
|
||||
# The set of permissions needed by the GitHub App. The format of the object uses
|
||||
# the permission name for the key (for example, issues) and the access type for
|
||||
# the value (for example, write).
|
||||
# Valid values are `read`, `write`, and `none`
|
||||
default_permissions:
|
||||
# Repository creation, deletion, settings, teams, and collaborators.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-administration
|
||||
administration: read
|
||||
|
||||
# Checks on code.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-checks
|
||||
checks: read
|
||||
|
||||
# Repository contents, commits, branches, downloads, releases, and merges.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-contents
|
||||
contents: read
|
||||
|
||||
# Deployments and deployment statuses.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-deployments
|
||||
# deployments: read
|
||||
|
||||
# Issues and related comments, assignees, labels, and milestones.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-issues
|
||||
issues: write
|
||||
|
||||
# Search repositories, list collaborators, and access repository metadata.
|
||||
# https://developer.github.com/v3/apps/permissions/#metadata-permissions
|
||||
metadata: read
|
||||
|
||||
# Retrieve Pages statuses, configuration, and builds, as well as create new builds.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-pages
|
||||
# pages: read
|
||||
|
||||
# Pull requests and related comments, assignees, labels, milestones, and merges.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests
|
||||
pull_requests: write
|
||||
|
||||
# Manage the post-receive hooks for a repository.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks
|
||||
repository_hooks: read
|
||||
|
||||
# Manage repository projects, columns, and cards.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects
|
||||
# repository_projects: read
|
||||
|
||||
# Retrieve security vulnerability alerts.
|
||||
# https://developer.github.com/v4/object/repositoryvulnerabilityalert/
|
||||
# vulnerability_alerts: read
|
||||
|
||||
# Commit statuses.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-statuses
|
||||
statuses: read
|
||||
|
||||
# Organization members and teams.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-members
|
||||
# members: read
|
||||
|
||||
# View and manage users blocked by the organization.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking
|
||||
# organization_user_blocking: read
|
||||
|
||||
# Manage organization projects, columns, and cards.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects
|
||||
# organization_projects: read
|
||||
|
||||
# Manage team discussions and related comments.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions
|
||||
# team_discussions: read
|
||||
|
||||
# Manage the post-receive hooks for an organization.
|
||||
# https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks
|
||||
organization_hooks: read
|
||||
|
||||
# Get notified of, and update, content references.
|
||||
# https://developer.github.com/v3/apps/permissions/
|
||||
# organization_administration: read
|
||||
|
||||
|
||||
# The name of the GitHub App. Defaults to the name specified in package.json
|
||||
# name: My Probot App
|
||||
|
||||
# The homepage of your GitHub App.
|
||||
# url: https://example.com/
|
||||
|
||||
# A description of the GitHub App.
|
||||
# description: A description of my awesome app
|
||||
|
||||
# Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app.
|
||||
# Default: true
|
||||
# public: false
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -1,92 +0,0 @@
|
||||
const debug = require('debug')('probot:presolver');
|
||||
const Presolver = require('./server/presolver');
|
||||
const config = require('../config');
|
||||
// config should be imported before importing any other file
|
||||
const mongoose = require('mongoose');
|
||||
const path = require('path');
|
||||
|
||||
async function probotPlugin(robot) {
|
||||
const events = [
|
||||
'pull_request.opened',
|
||||
'pull_request.edited',
|
||||
'pull_request.synchronize',
|
||||
'pull_request.reopened',
|
||||
'pull_request.labeled',
|
||||
'pull_request.closed'
|
||||
];
|
||||
|
||||
robot.on(events, presolve.bind(null, robot));
|
||||
|
||||
// const prOpened = require('./test/payloads/events/pullRequests.opened');
|
||||
// robot.receive({
|
||||
// name: 'pull_request.opened',
|
||||
// payload: prOpened
|
||||
// });
|
||||
const redirect = robot.route('/');
|
||||
redirect.get('/', (req, res) => res.redirect('/home'));
|
||||
const landingPage = robot.route('/home');
|
||||
landingPage.use(require('express').static('public'));
|
||||
const app = robot.route('/dashboard');
|
||||
const { pareto, pr, search, info } = require('./server/routes');
|
||||
|
||||
const staticPath = path.join(__dirname, '.', 'client', 'build');
|
||||
app.use(require('express').static(staticPath));
|
||||
|
||||
app.use((request, response, next) => {
|
||||
response.header('Access-Control-Allow-Origin', '*');
|
||||
response.header(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Origin, X-Requested-With, Content-Type, Accept'
|
||||
);
|
||||
response.header('Access-Control-Allow-Methods', 'GET');
|
||||
next();
|
||||
});
|
||||
|
||||
const landingHtml = path.join(__dirname, './public/index.html');
|
||||
landingPage.get('/', (req, res) => res.sendFile(landingHtml));
|
||||
|
||||
const htmlpath = path.join(__dirname, './client/build/index.html');
|
||||
app.get('/', (request, response) => response.sendFile(htmlpath));
|
||||
|
||||
app.use('/pr', pr);
|
||||
app.use('/search', search);
|
||||
app.use('/pareto', pareto);
|
||||
app.use('/info', info);
|
||||
|
||||
app.use(function(err, req, res) {
|
||||
res.status(err.status || 500).send(err.message);
|
||||
});
|
||||
if (mongoose.connection.readyState === 0) {
|
||||
// connect to mongo db
|
||||
const mongoUri = config.mongo.host;
|
||||
|
||||
const promise = mongoose.connect(
|
||||
mongoUri, { useNewUrlParser: true }
|
||||
);
|
||||
promise
|
||||
.then(() => {
|
||||
console.log('MongoDB is connected');
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
console.log('MongoDB connection unsuccessful');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function presolve(app, context) {
|
||||
const presolver = forRepository(context);
|
||||
const pullRequest = getPullRequest(context);
|
||||
return presolver.presolve(pullRequest);
|
||||
}
|
||||
|
||||
function forRepository(context) {
|
||||
const config = { ...context.repo({ logger: debug }) };
|
||||
return new Presolver(context, config);
|
||||
}
|
||||
|
||||
function getPullRequest(context) {
|
||||
return context.payload.pull_request || context.payload.review.pull_request;
|
||||
}
|
||||
|
||||
module.exports = probotPlugin;
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,145 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="X5tHeKjV-jMLyp4VMoUhW9PAYaOjtPslV250" name="csrf-token">
|
||||
<title>Contribute to freeCodeCamp | Learn to code with free online courses, programming projects, and interview preparation for developer jobs.</title>
|
||||
<link href="https://www.freecodecamp.org" rel="canonical">
|
||||
<meta charset="utf-8">
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="X5tHeKjV-jMLyp4VMoUhW9PAYaOjtPslV250" name="csrf-token">
|
||||
<meta content="Contribute to freeCodeCamp | Learn to code with free online courses, programming projects, and interview preparation for developer jobs." property="og:title">
|
||||
<meta content="freeCodeCamp" property="og:site_name">
|
||||
<meta content="on" name="twitter:widgets:csp">
|
||||
<meta content="d0bc047a482c03c24f1168004c2a216a" name="p:domain_verify">
|
||||
<meta content="https://www.freecodecamp.org" property="og:url">
|
||||
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." property="og:description">
|
||||
<meta content="https://s3.amazonaws.com/freecodecamp/curriculum-diagram-full.jpg" property="og:image">
|
||||
<meta content="article" property="og:type">
|
||||
<meta content="https://www.facebook.com/freecodecamp" property="article:publisher">
|
||||
<meta content="Responsive" property="article:section">
|
||||
<link href="https://plus.google.com/+Freecodecamp" rel="publisher">
|
||||
<link href="https://plus.google.com/+Freecodecamp" rel="author">
|
||||
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." name="description">
|
||||
<meta content="@freecodecamp" name="twitter:creator">
|
||||
<meta content="https://www.freecodecamp.org" name="twitter:url">
|
||||
<meta content="@freecodecamp" name="twitter:site">
|
||||
<meta content="summary_large_image" name="twitter:card">
|
||||
<meta content="https://s3.amazonaws.com/freecodecamp/curriculum-diagram-full.jpg" name="twitter:image:src">
|
||||
<meta content="Learn to code and help nonprofits" name="twitter:title">
|
||||
<meta content="Learn to code with free online courses, programming projects, and interview preparation for developer jobs." name="twitter:description">
|
||||
<meta content="a40ee5d5dba3bb091ad783ebd2b1383f" name="p:domain_verify">
|
||||
<meta content="#FFFFFF" name="msapplication-TileColor">
|
||||
<meta content="/" name="msapplication-TileImage">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-144x144.png" rel="android-chrome" sizes="144x144">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-192x192.png" rel="android-chrome" sizes="192x192">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-36x36.png" rel="android-chrome" sizes="36x36">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-48x48.png" rel="android-chrome" sizes="48x48">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-72x72.png" rel="android-chrome" sizes="72x72">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-96x96.png" rel="android-chrome" sizes="96x96">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/android-chrome-manifest.json" rel="android-chrome-manifest">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-114x114.png" rel="apple-touch-icon" sizes="114x114">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-144x144.png" rel="apple-touch-icon" sizes="144x144">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png" rel="apple-touch-icon" sizes="180x180">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-57x57.png" rel="apple-touch-icon" sizes="57x57">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-60x60.png" rel="apple-touch-icon" sizes="60x60">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-72x72.png" rel="apple-touch-icon" sizes="72x72">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-76x76.png" rel="apple-touch-icon" sizes="76x76">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-precomposed.png" rel="apple-touch-icon-precomposed">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon.png" rel="apple-touch-icon">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-16x16.png" rel="favicon" sizes="16x16">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-32x32.png" rel="favicon" sizes="32x32">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon-96x96.png" rel="favicon" sizes="96x96">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-144x144.png" rel="mstile" sizes="144x144">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-150x150.png" rel="mstile" sizes="150x150">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-310x310.png" rel="mstile" sizes="310x310">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-310x150.png" rel="mstile" sizes="310x150">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/mstile-70x70.png" rel="mstile" sizes="70x70">
|
||||
<link href="https://s3.amazonaws.com/freecodecamp/favicons/favicon.ico" rel="favicon">
|
||||
<link href="//s3.amazonaws.com/freecodecamp/favicons/favicon.ico" rel="shortcut icon">
|
||||
<link rel='stylesheet' href='https://d33wubrfki0l68.cloudfront.net/css/1d4e5eca38a921b4701240a97afaf40185de57c2/style.css'>
|
||||
<link href="./style.css" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="top-and-bottom-margins">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.min.js">
|
||||
</script>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){ i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', 'UA-55446531-1', 'auto');
|
||||
ga('require', 'displayfeatures');
|
||||
ga('send', 'pageview');
|
||||
</script><!-- Leave the below lines alone!-->
|
||||
<nav class="navbar navbar-default navbar-fixed-top nav-height">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="https://www.freecodecamp.org"><img alt="learn to code javascript at freeCodeCamp logo" class="img-responsive nav-logo" src="https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg"></a>
|
||||
</div>
|
||||
<div class="return-to-free-code-camp">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="return-to-free-code-camp">
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
</li>
|
||||
<li class="return-to-free-code-camp">
|
||||
<a href="https://github.com/freeCodeCamp/freeCodeCamp/" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div class="row flashMessage negative-30">
|
||||
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row main-content">
|
||||
<div class="col-xs-12 col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
|
||||
<h1 class="text-center">Contribute to the Community</h1>
|
||||
<hr>
|
||||
<h4>freeCodeCamp is possible thanks to thousands of kind volunteers like you.</p>
|
||||
<h4>Here are 5 fun quick ways you can help the community right now:</p>
|
||||
<div class="spacer"></div>
|
||||
<table class="table link-table">
|
||||
<tr>
|
||||
<td class="text-center"><i class="fa fa-question"></i></td>
|
||||
<td>
|
||||
<a href="https://forum.freecodecamp.org/" target="_blank" rel="noopener noreferrer">Help campers by answering their questions</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center"><i class="fa fa-comments"></i></td>
|
||||
<td>
|
||||
<a href="https://forum.freecodecamp.org/c/project-feedback/" target="_blank" rel="noopener noreferrer">Give feedback on camper projects</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center"><i class="fa fa-youtube"></i></td>
|
||||
<td>
|
||||
<a href="https://www.youtube.com/timedtext_cs_panel?c=UC8butISFwT-Wl7EV0hUK0BQ&tab=2" rel="noopener noreferrer"target="_blank">Add subtitles to YouTube videos</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center"><i class="fa fa-book"></i></td>
|
||||
<td>
|
||||
<a href="https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md#research-write-and-update-our-guide-articles" rel="noopener noreferrer" target="_blank">Research and write guide articles</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center"><i class="fa fa-github"></i></td>
|
||||
<td>
|
||||
<a href="https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md" rel="noopener noreferrer" target="_blank">Contribute to the codebase</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
@media (max-width: 991px) {
|
||||
.main-content {
|
||||
margin-top: 65px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.return-to-free-code-camp {
|
||||
padding: 0!important;
|
||||
margin: -9px 0!important;
|
||||
height: 50px!important;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user