chore: remove contributor tools (#44368)
They're being stored in freeCodeCamp/tools now
This commit is contained in:
committed by
GitHub
parent
8751f55bc7
commit
0cbb499e9c
110
tools/contributor/.gitignore
vendored
110
tools/contributor/.gitignore
vendored
@ -1,110 +0,0 @@
|
||||
#-----
|
||||
# Node
|
||||
#-----
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
#-----------------
|
||||
# Create React App
|
||||
#-----------------
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
# ------------
|
||||
# Custom Files
|
||||
# ------------
|
||||
|
||||
work-logs
|
||||
# work-logs
|
||||
#
|
||||
|
||||
# ------------
|
||||
# Probot Files
|
||||
# ------------
|
||||
|
||||
*.pem
|
||||
*.vscode
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "@freecodecamp/dashboard-client",
|
||||
"version": "0.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"scripts": {
|
||||
"build": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts build",
|
||||
"dev": "develop",
|
||||
"develop": "cross-env REACT_APP_HOST=local SKIP_PREFLIGHT_CHECK=true react-scripts start",
|
||||
"eject": "react-scripts eject",
|
||||
"test": "SKIP_PREFLIGHT_CHECK=true react-scripts test"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
],
|
||||
"dependencies": {
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-scripts": "2.1.8",
|
||||
"styled-components": "4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "5.2.1"
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta
|
||||
content="https://cdn.freecodecamp.org/platform/universal/fcc_meta_1920x1080-indigo.png"
|
||||
name="og:image"
|
||||
/>
|
||||
<meta content="freeCodeCamp Contributor Tools" name="twitter:title" />
|
||||
<meta
|
||||
content="Tools for contributors working on the freeCodeCamp curriculum."
|
||||
name="og:description"
|
||||
/>
|
||||
<meta content="a40ee5d5dba3bb091ad783ebd2b1383f" name="p:domain_verify" />
|
||||
<meta content="#FFFFFF" name="msapplication-TileColor" />
|
||||
<meta
|
||||
content="https://cdn.freecodecamp.org/universal/favicons/browserconfig.xml"
|
||||
rel="msapplication-config"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/android-chrome-192x192.png"
|
||||
rel="android-chrome"
|
||||
sizes="192x192"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/android-chrome-384x384.png"
|
||||
rel="android-chrome"
|
||||
sizes="384x384"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/site.webmanifest"
|
||||
rel="manifest"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/apple-touch-icon.png"
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/favicon-16x16.png"
|
||||
rel="favicon"
|
||||
sizes="16x16"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/favicon-32x32.png"
|
||||
rel="favicon"
|
||||
sizes="32x32"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.freecodecamp.org/universal/favicons/favicon.ico"
|
||||
rel="icon"
|
||||
/>
|
||||
<title>freeCodeCamp Contributor Tools</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
@ -1,145 +0,0 @@
|
||||
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';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
margin-top: 70px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@media (max-width: 991px) {
|
||||
margin-top: 135px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 960px;
|
||||
width: 90vw;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 4px 0 #777;
|
||||
`;
|
||||
|
||||
const AppNavBar = styled.nav`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: white;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.primary};
|
||||
@media (max-width: 991px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const logoStyle = { paddingLeft: '30px' };
|
||||
|
||||
const titleStyle = { margin: '0', padding: '0' };
|
||||
|
||||
class App extends Component {
|
||||
state = {
|
||||
view: 'search',
|
||||
footerInfo: null
|
||||
};
|
||||
|
||||
updateInfo() {
|
||||
fetch(ENDPOINT_INFO)
|
||||
.then((response) => response.json())
|
||||
.then(({ ok, numPRs, prRange, lastUpdate }) => {
|
||||
if (ok) {
|
||||
const footerInfo = { numPRs, prRange, lastUpdate };
|
||||
this.setState((prevState) => ({ footerInfo }));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
|
||||
handleViewChange = ({ target: { id } }) => {
|
||||
const view = id.replace('tabs-', '');
|
||||
this.setState((prevState) => ({ ...this.clearObj, view }));
|
||||
if (view === 'reports' || view === 'search') {
|
||||
this.updateInfo();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleViewChange,
|
||||
state: { view, footerInfo }
|
||||
} = this;
|
||||
return (
|
||||
<>
|
||||
<AppNavBar>
|
||||
<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="https://github.com/freeCodeCamp/freeCodeCamp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</AppNavBar>
|
||||
<PageContainer>
|
||||
<Tabs view={view} onViewChange={handleViewChange} />
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default App;
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
@ -1,114 +0,0 @@
|
||||
/* 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;
|
@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
import FullWidthDiv from './FullWidthDiv';
|
||||
import Result from './Result';
|
||||
|
||||
const List = styled.div`
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const filenameTitle = { fontWeight: '600' };
|
||||
|
||||
const FilenameResults = ({ searchValue, results, rateLimitMessage }) => {
|
||||
const elements = results.map((result) => {
|
||||
const { filename, prs: prObjects } = result;
|
||||
const prs = prObjects.map(({ number, username, title }, index) => {
|
||||
return <ListItem number={number} username={username} prTitle={title} />;
|
||||
});
|
||||
|
||||
const fileOnMain = `https://github.com/freeCodeCamp/freeCodeCamp/blob/main/${filename}`;
|
||||
return (
|
||||
<Result key={filename}>
|
||||
<span style={filenameTitle}>{filename}</span>{' '}
|
||||
<a href={fileOnMain} rel="noopener noreferrer" target="_blank">
|
||||
(File on Main)
|
||||
</a>
|
||||
<List>{prs}</List>
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
const showResults = () => {
|
||||
if (!rateLimitMessage) {
|
||||
return (
|
||||
(results.length ? <h3>Results for: {searchValue}</h3> : null) &&
|
||||
elements
|
||||
);
|
||||
} else {
|
||||
return rateLimitMessage;
|
||||
}
|
||||
};
|
||||
|
||||
return <FullWidthDiv>{showResults()}</FullWidthDiv>;
|
||||
};
|
||||
|
||||
export default FilenameResults;
|
@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.label`
|
||||
@media (max-width: 600px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterOption = ({
|
||||
group,
|
||||
children,
|
||||
value,
|
||||
selectedOption,
|
||||
onOptionChange
|
||||
}) => {
|
||||
return (
|
||||
<Container>
|
||||
<input
|
||||
name={group}
|
||||
type="radio"
|
||||
value={value}
|
||||
checked={selectedOption === value}
|
||||
onChange={onOptionChange}
|
||||
/>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default FilterOption;
|
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Info = styled.div`
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
`;
|
||||
|
||||
const Footer = (props) => {
|
||||
const localTime = (lastUpdate) => {
|
||||
const newTime = new Date(lastUpdate);
|
||||
return newTime.toLocaleString();
|
||||
};
|
||||
|
||||
const {
|
||||
footerInfo: { numPRs, prRange, lastUpdate }
|
||||
} = props;
|
||||
return (
|
||||
lastUpdate && (
|
||||
<Container>
|
||||
<Info>Last Update: {localTime(lastUpdate)}</Info>
|
||||
<Info>
|
||||
# of open PRs: {numPRs} ({prRange})
|
||||
</Info>
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FullWidthDiv = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default FullWidthDiv;
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.input`
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const Input = React.forwardRef((props, ref) => (
|
||||
<Container
|
||||
type="text"
|
||||
onChange={props.onInputEvent}
|
||||
onKeyPress={props.onInputEvent}
|
||||
value={props.value}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
export default Input;
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
@media (max-width: 600px) {
|
||||
margin-top: 1em;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const prNumStyle = { flex: 1 };
|
||||
const usernameStyle = { flex: 1 };
|
||||
const titleStyle = { flex: 3 };
|
||||
|
||||
const ListItem = ({ number, username, prTitle: title, prLink }) => {
|
||||
const prUrl = prLink
|
||||
? prLink
|
||||
: `https://github.com/freeCodeCamp/freeCodeCamp/pull/${number}`;
|
||||
return (
|
||||
<Container>
|
||||
<a
|
||||
style={prNumStyle}
|
||||
href={prUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
#{number}
|
||||
</a>
|
||||
<span style={usernameStyle}>{username}</span>
|
||||
<span style={titleStyle}>{title}</span>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
@ -1,237 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
import FullWidthDiv from './FullWidthDiv';
|
||||
import Result from './Result';
|
||||
import FilterOption from './FilterOption';
|
||||
import { ENDPOINT_PARETO } from '../constants';
|
||||
|
||||
const List = styled.div`
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Options = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const detailsStyle = { padding: '3px' };
|
||||
const filenameTitle = { fontWeight: '600' };
|
||||
|
||||
class Pareto extends React.Component {
|
||||
state = {
|
||||
data: [],
|
||||
all: [],
|
||||
selectedFileType: 'all',
|
||||
selectedLanguage: 'all',
|
||||
options: {},
|
||||
rateLimitMessage: ''
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
fetch(ENDPOINT_PARETO)
|
||||
.then((response) => response.json())
|
||||
.then(({ ok, rateLimitMessage, pareto }) => {
|
||||
if (ok) {
|
||||
if (!pareto.length) {
|
||||
pareto.push({
|
||||
filename: 'Nothing to show in Pareto Report',
|
||||
count: 0,
|
||||
prs: []
|
||||
});
|
||||
}
|
||||
|
||||
this.setState((prevState) => ({
|
||||
data: pareto,
|
||||
all: [...pareto],
|
||||
options: this.createOptions(pareto)
|
||||
}));
|
||||
} else if (rateLimitMessage) {
|
||||
this.setState((prevState) => ({
|
||||
rateLimitMessage
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const pareto = [
|
||||
{ filename: 'Nothing to show in Pareto Report', count: 0, prs: [] }
|
||||
];
|
||||
this.setState((prevState) => ({ data: pareto }));
|
||||
});
|
||||
}
|
||||
|
||||
createOptions = (data) => {
|
||||
const options = data.reduce((seen, { filename }) => {
|
||||
const { articleType, language } = this.getFilenameOptions(filename);
|
||||
if (articleType && language) {
|
||||
if (!seen.hasOwnProperty(articleType)) {
|
||||
seen[articleType] = {};
|
||||
}
|
||||
seen[articleType][language] = true;
|
||||
}
|
||||
return seen;
|
||||
}, {});
|
||||
return options;
|
||||
};
|
||||
|
||||
handleFileTypeOptionChange = (changeEvent) => {
|
||||
let { all, selectedLanguage, options } = this.state;
|
||||
const selectedFileType = changeEvent.target.value;
|
||||
|
||||
let data = [...all].filter(({ filename }) => {
|
||||
const { articleType, language } = this.getFilenameOptions(filename);
|
||||
let condition;
|
||||
if (selectedFileType === 'all') {
|
||||
condition = true;
|
||||
selectedLanguage = 'all';
|
||||
} else {
|
||||
if (selectedLanguage === 'all') {
|
||||
condition = articleType === selectedFileType;
|
||||
} else if (!options[selectedFileType][selectedLanguage]) {
|
||||
condition = articleType === selectedFileType;
|
||||
selectedLanguage = 'all';
|
||||
} else {
|
||||
condition =
|
||||
articleType === selectedFileType && language === selectedLanguage;
|
||||
}
|
||||
}
|
||||
return condition;
|
||||
});
|
||||
this.setState((prevState) => ({
|
||||
data,
|
||||
selectedFileType,
|
||||
selectedLanguage
|
||||
}));
|
||||
};
|
||||
|
||||
handleLanguageOptionChange = (changeEvent) => {
|
||||
const { all, selectedFileType } = this.state;
|
||||
const selectedLanguage = changeEvent.target.value;
|
||||
let data = [...all].filter(({ filename }) => {
|
||||
const { articleType, language } = this.getFilenameOptions(filename);
|
||||
let condition;
|
||||
if (selectedLanguage === 'all') {
|
||||
condition = articleType === selectedFileType;
|
||||
} else {
|
||||
condition =
|
||||
language === selectedLanguage && articleType === selectedFileType;
|
||||
}
|
||||
return condition;
|
||||
});
|
||||
this.setState((prevState) => ({ data, selectedLanguage }));
|
||||
};
|
||||
|
||||
getFilenameOptions = (filename) => {
|
||||
const filenameReplacement = filename.replace(
|
||||
/^curriculum\/challenges\//,
|
||||
'curriculum/'
|
||||
);
|
||||
const regex =
|
||||
/^(docs|curriculum|guide)(?:\/)(english|arabic|chinese|portuguese|russian|spanish)?\/?/;
|
||||
// need an array to pass to labelsAdder
|
||||
// eslint-disable-next-line
|
||||
const [_, articleType, language] = filenameReplacement.match(regex) || [];
|
||||
return { articleType, language };
|
||||
};
|
||||
|
||||
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 fileOnMain = `https://github.com/freeCodeCamp/freeCodeCamp/blob/main/${filename}`;
|
||||
return (
|
||||
<Result key={filename}>
|
||||
<span style={filenameTitle}>{filename}</span>{' '}
|
||||
<a href={fileOnMain} rel="noopener noreferrer" target="_blank">
|
||||
(File on Main)
|
||||
</a>
|
||||
<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) => (
|
||||
<FilterOption
|
||||
key={type}
|
||||
name="filetype"
|
||||
value={type}
|
||||
onOptionChange={this.handleFileTypeOptionChange}
|
||||
selectedOption={selectedFileType}
|
||||
>
|
||||
{type.charAt().toUpperCase() + type.slice(1)}
|
||||
</FilterOption>
|
||||
));
|
||||
|
||||
let languageOptions = null;
|
||||
if (selectedFileType !== 'all') {
|
||||
let languages = Object.keys(options[selectedFileType]);
|
||||
languages = ['all', ...languages.sort()];
|
||||
languageOptions = languages.map((language) => (
|
||||
<FilterOption
|
||||
key={language}
|
||||
name="language"
|
||||
value={language}
|
||||
onOptionChange={this.handleLanguageOptionChange}
|
||||
selectedOption={selectedLanguage}
|
||||
>
|
||||
{language.charAt().toUpperCase() + language.slice(1)}
|
||||
</FilterOption>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
{fileTypeOptions.length > 0 && <strong>Filter Options</strong>}
|
||||
<Options>
|
||||
{fileTypeOptions.length > 0 && (
|
||||
<>
|
||||
<fieldset>
|
||||
<legend>File Type:</legend>
|
||||
<div>{typeOptions}</div>
|
||||
</fieldset>
|
||||
</>
|
||||
)}
|
||||
{languageOptions && (
|
||||
<fieldset>
|
||||
<legend>Language:</legend>
|
||||
<div>{languageOptions}</div>
|
||||
</fieldset>
|
||||
)}
|
||||
</Options>
|
||||
{rateLimitMessage
|
||||
? rateLimitMessage
|
||||
: data.length
|
||||
? elements
|
||||
: 'Report Loading...'}
|
||||
</FullWidthDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Pareto;
|
@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
import FullWidthDiv from './FullWidthDiv';
|
||||
import Result from './Result';
|
||||
|
||||
const List = styled.ul`
|
||||
margin: 3px;
|
||||
`;
|
||||
|
||||
const PrResults = ({ searchValue, results, rateLimitMessage }) => {
|
||||
const elements = results.map((result, idx) => {
|
||||
const { number, filenames, username, title } = result;
|
||||
const files = filenames.map((filename, index) => {
|
||||
const fileOnMain = `https://github.com/freeCodeCamp/freeCodeCamp/blob/main/${filename}`;
|
||||
return (
|
||||
<li key={`${number}-${index}`}>
|
||||
{filename}{' '}
|
||||
<a href={fileOnMain} rel="noopener noreferrer" target="_blank">
|
||||
(File on Main)
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Result key={`${number}-${idx}`}>
|
||||
<ListItem number={number} username={username} prTitle={title} />
|
||||
<List>{files}</List>
|
||||
</Result>
|
||||
);
|
||||
});
|
||||
|
||||
const showResults = () => {
|
||||
if (!rateLimitMessage) {
|
||||
return (
|
||||
(results.length ? <h3>Results for PR# {searchValue}</h3> : null) &&
|
||||
elements
|
||||
);
|
||||
} else {
|
||||
return rateLimitMessage;
|
||||
}
|
||||
};
|
||||
|
||||
return <FullWidthDiv style={{ width: '100%' }}>{showResults()}</FullWidthDiv>;
|
||||
};
|
||||
|
||||
export default PrResults;
|
@ -1,100 +0,0 @@
|
||||
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;
|
@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Result = styled.div`
|
||||
border: 1px solid #aaa;
|
||||
margin: 10px 0;
|
||||
&:nth-child(odd) {
|
||||
background: #eee;
|
||||
}
|
||||
padding: 3px;
|
||||
`;
|
||||
|
||||
export default Result;
|
@ -1,140 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Input from './Input';
|
||||
import PrResults from './PrResults';
|
||||
import FilenameResults from './FilenameResults';
|
||||
import SearchOption from './SearchOption';
|
||||
|
||||
import { ENDPOINT_PR, ENDPOINT_SEARCH } from '../constants';
|
||||
class Search extends Component {
|
||||
state = {
|
||||
searchValue: '',
|
||||
selectedOption: 'pr',
|
||||
results: [],
|
||||
message: ''
|
||||
};
|
||||
|
||||
clearObj = { searchValue: '', results: [] };
|
||||
|
||||
inputRef = React.createRef();
|
||||
|
||||
handleInputEvent = (event) => {
|
||||
const {
|
||||
type,
|
||||
key,
|
||||
target: { value: searchValue }
|
||||
} = event;
|
||||
|
||||
if (type === 'change') {
|
||||
if (this.state.selectedOption === 'pr') {
|
||||
if (Number(searchValue) || searchValue === '') {
|
||||
this.setState((prevState) => ({ searchValue, results: [] }));
|
||||
}
|
||||
} else {
|
||||
this.setState((prevState) => ({ searchValue, results: [] }));
|
||||
}
|
||||
} else if (type === 'keypress' && key === 'Enter') {
|
||||
this.searchPRs(searchValue);
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonClick = () => {
|
||||
const { searchValue } = this.state;
|
||||
if (searchValue) {
|
||||
this.searchPRs(searchValue);
|
||||
} else {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleOptionChange = (changeEvent) => {
|
||||
const selectedOption = changeEvent.target.value;
|
||||
|
||||
this.setState((prevState) => ({ selectedOption, ...this.clearObj }));
|
||||
this.inputRef.current.focus();
|
||||
};
|
||||
|
||||
searchPRs = (value) => {
|
||||
const { selectedOption } = this.state;
|
||||
|
||||
const fetchUrl =
|
||||
selectedOption === 'pr'
|
||||
? `${ENDPOINT_PR}/${value}`
|
||||
: `${ENDPOINT_SEARCH}/?value=${value}`;
|
||||
|
||||
fetch(fetchUrl)
|
||||
.then((response) => response.json())
|
||||
.then(({ ok, message, results, rateLimitMessage }) => {
|
||||
if (ok) {
|
||||
this.setState((prevState) => ({ message, results }));
|
||||
} else if (rateLimitMessage) {
|
||||
this.setState((prevState) => ({
|
||||
rateLimitMessage
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState((prevState) => this.clearObj);
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
handleButtonClick,
|
||||
handleInputEvent,
|
||||
inputRef,
|
||||
handleOptionChange,
|
||||
state
|
||||
} = this;
|
||||
const { searchValue, message, results, selectedOption, rateLimitMessage } =
|
||||
state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<SearchOption
|
||||
value="pr"
|
||||
onOptionChange={handleOptionChange}
|
||||
selectedOption={selectedOption}
|
||||
>
|
||||
PR #
|
||||
</SearchOption>
|
||||
<SearchOption
|
||||
value="filename"
|
||||
onOptionChange={handleOptionChange}
|
||||
selectedOption={selectedOption}
|
||||
>
|
||||
Filename
|
||||
</SearchOption>
|
||||
</div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={searchValue}
|
||||
onInputEvent={handleInputEvent}
|
||||
/>
|
||||
<button onClick={handleButtonClick}>Search</button>
|
||||
{message}
|
||||
{selectedOption === 'pr' && (
|
||||
<PrResults
|
||||
searchValue={searchValue}
|
||||
results={results}
|
||||
rateLimitMessage={rateLimitMessage}
|
||||
/>
|
||||
)}
|
||||
{selectedOption === 'filename' && (
|
||||
<FilenameResults
|
||||
searchValue={searchValue}
|
||||
results={results}
|
||||
rateLimitMessage={rateLimitMessage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const SearchOption = ({ children, value, selectedOption, onOptionChange }) => (
|
||||
<label>
|
||||
<input
|
||||
name="searchType"
|
||||
type="radio"
|
||||
value={value}
|
||||
checked={selectedOption === value}
|
||||
onChange={onOptionChange}
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
|
||||
export default SearchOption;
|
@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
margin: 20px;
|
||||
`;
|
||||
|
||||
const Tab = styled.div`
|
||||
background: ${({ active, theme }) => (active ? theme.primary : 'white')};
|
||||
color: ${({ active, theme }) => (active ? 'white' : theme.primary)};
|
||||
font-size: 18px;
|
||||
padding: 5px;
|
||||
border: 2px solid ${({ theme }) => theme.primary};
|
||||
border-left: none;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: ${({ theme }) => theme.primary};
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-left: 2px solid ${({ theme }) => theme.primary};
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
width: auto;
|
||||
min-width: 100px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Tabs = ({ view, onViewChange }) => {
|
||||
return (
|
||||
<Container>
|
||||
<Tab id="tabs-search" onClick={onViewChange} active={view === 'search'}>
|
||||
Search
|
||||
</Tab>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
@ -1,18 +0,0 @@
|
||||
require('dotenv').config();
|
||||
|
||||
let API_HOST =
|
||||
process.env.REACT_APP_HOST === 'local' ? 'http://localhost:3001' : '';
|
||||
|
||||
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,
|
||||
ENDPOINT_ALL_REPOS
|
||||
};
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,67 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url(./fonts/Lato-Regular.ttf) format('truetype');
|
||||
font-display: fallback;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lato Light';
|
||||
src: url(./fonts/Lato-Light.ttf) format('truetype');
|
||||
font-display: fallback;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lato Bold';
|
||||
src: url(./fonts/Lato-Bold.ttf) format('truetype');
|
||||
font-display: fallback;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Lato, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
margin: 0;
|
||||
padding: 13px 15px;
|
||||
margin-right: 15px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.app-menu li a {
|
||||
padding: 13px 15px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-menu li a:hover {
|
||||
color: #0a0a23;
|
||||
background: white;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0a0a23;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
color: purple;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './index.css';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import theme from './theme';
|
||||
|
||||
import App from './App';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
ReactDOM.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
<App />
|
||||
</ThemeProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
serviceWorker.unregister();
|
@ -1,135 +0,0 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read http://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit http://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
const theme = {
|
||||
primary: '#0a0a23'
|
||||
};
|
||||
|
||||
export default theme;
|
@ -1,69 +0,0 @@
|
||||
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;
|
@ -1,43 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const pr = new mongoose.Schema({
|
||||
_id: Number,
|
||||
updatedAt: String,
|
||||
username: String,
|
||||
title: String,
|
||||
filenames: [String]
|
||||
});
|
||||
|
||||
const info = new mongoose.Schema({
|
||||
lastUpdate: Date,
|
||||
numPRs: Number,
|
||||
prRange: String
|
||||
});
|
||||
|
||||
const allRepos = new mongoose.Schema({
|
||||
_id: String,
|
||||
prs: [
|
||||
{
|
||||
_id: Number,
|
||||
title: String,
|
||||
username: String,
|
||||
prLink: String
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const dbCollections = {
|
||||
pr: 'openprs',
|
||||
info: 'info',
|
||||
boilerplate: 'boilerplate',
|
||||
otherPrs: 'otherPrs'
|
||||
};
|
||||
|
||||
const PR = mongoose.model('PR', pr, dbCollections['pr']);
|
||||
const INFO = mongoose.model('INFO', info, dbCollections['info']);
|
||||
const ALL_REPOS = mongoose.model(
|
||||
'ALL_REPOS',
|
||||
allRepos,
|
||||
dbCollections['allRepos']
|
||||
);
|
||||
module.exports = { PR, INFO, ALL_REPOS, dbCollections };
|
@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "@freecodecamp/dashboard-server",
|
||||
"version": "0.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://tools.freecodecamp.org",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"scripts": {
|
||||
"dev": "develop",
|
||||
"develop": "nodemon",
|
||||
"lint": "standard --fix",
|
||||
"start": "node index.js",
|
||||
"test": "cross-env TEST_ENV=true jest --forceExit && standard",
|
||||
"test:watch": "jest --watch --notify --notifyMode=change --coverage"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "npm start",
|
||||
"watch": [
|
||||
".env",
|
||||
"."
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "18.5.2",
|
||||
"body-parser": "1.19.0",
|
||||
"cross-env": "5.2.1",
|
||||
"express": "4.17.1",
|
||||
"express-rate-limit": "5.2.3",
|
||||
"mongoose": "5.11.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect": "23.6.0",
|
||||
"jest": "22.4.4",
|
||||
"nodemon": "1.19.4",
|
||||
"smee-client": "1.2.2",
|
||||
"standard": "10.0.3"
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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 };
|
@ -1,20 +0,0 @@
|
||||
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;
|
@ -1,7 +0,0 @@
|
||||
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, allRepos };
|
@ -1,10 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { INFO } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
router.get('/', reqLimiter, async (request, response) => {
|
||||
const [{ lastUpdate, numPRs, prRange }] = await INFO.find({});
|
||||
response.json({ ok: true, lastUpdate, numPRs, prRange });
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -1,38 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
const createPareto = (reportObj) =>
|
||||
Object.keys(reportObj)
|
||||
.reduce((arr, filename) => {
|
||||
const { count, prs } = reportObj[filename];
|
||||
if (count > 1) {
|
||||
arr.push({ filename, count, prs });
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
router.get('/', reqLimiter, async (request, response) => {
|
||||
const prs = await PR.find({}).then((data) => data);
|
||||
prs.sort((a, b) => a._id - b._id);
|
||||
const reportObj = prs.reduce((obj, pr) => {
|
||||
const { _id: number, filenames, username, title } = pr;
|
||||
filenames.forEach((filename) => {
|
||||
if (obj[filename]) {
|
||||
const { count, prs } = obj[filename];
|
||||
obj[filename] = {
|
||||
count: count + 1,
|
||||
prs: prs.concat({ number, username, title })
|
||||
};
|
||||
} else {
|
||||
obj[filename] = { count: 1, prs: [{ number, username, title }] };
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
response.json({ ok: true, pareto: createPareto(reportObj) });
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -1,47 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
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) => {
|
||||
obj[_id] = index;
|
||||
return obj;
|
||||
}, {});
|
||||
const { number: refNumber } = request.params;
|
||||
const index = indices[refNumber];
|
||||
|
||||
if (!index && index !== 0) {
|
||||
response.json({
|
||||
ok: true,
|
||||
message: `Unable to find an open PR with #${refNumber}.`,
|
||||
results: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = prs[index];
|
||||
const results = [];
|
||||
const { filenames: refFilenames } = pr;
|
||||
|
||||
prs.forEach(({ _id: number, filenames, username, title }) => {
|
||||
if (number !== +refNumber) {
|
||||
const matchedFilenames = filenames.filter((filename) => {
|
||||
return refFilenames.includes(filename);
|
||||
});
|
||||
if (matchedFilenames.length) {
|
||||
results.push({ number, filenames: matchedFilenames, username, title });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!results.length) {
|
||||
let msg = `No other open PRs with matching filenames of PR #${refNumber}`;
|
||||
response.json({ ok: true, message: msg, results: [] });
|
||||
return;
|
||||
}
|
||||
response.json({ ok: true, results });
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -1,48 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const { PR } = require('../models');
|
||||
const { reqLimiter } = require('../req-limiter');
|
||||
|
||||
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) => {
|
||||
obj[_id] = index;
|
||||
return obj;
|
||||
}, {});
|
||||
const value = request.query.value;
|
||||
|
||||
if (value) {
|
||||
const filesFound = {};
|
||||
prs.forEach(({ _id: number, filenames, username, title }) => {
|
||||
filenames.forEach((filename) => {
|
||||
if (filename.toLowerCase().includes(value.toLowerCase())) {
|
||||
const fileCount = prs[indices[number]].filenames.length;
|
||||
const prObj = { number, fileCount, username, title };
|
||||
|
||||
if (filesFound.hasOwnProperty(filename)) {
|
||||
filesFound[filename].push(prObj);
|
||||
} else {
|
||||
filesFound[filename] = [prObj];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let results = Object.keys(filesFound)
|
||||
.map((filename) => ({ filename, prs: filesFound[filename] }))
|
||||
.sort((a, b) => {
|
||||
if (a.filename === b.filename) {
|
||||
return 0;
|
||||
} else {
|
||||
return a.filename < b.filename ? -1 : 1;
|
||||
}
|
||||
});
|
||||
if (!results.length) {
|
||||
response.json({ ok: true, message: 'No matching results.', results: [] });
|
||||
return;
|
||||
}
|
||||
response.json({ ok: true, results });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -1,24 +0,0 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../../../lib/config');
|
||||
|
||||
const getPRs = async () => {
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const methodProps = {
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
per_page: 100
|
||||
};
|
||||
|
||||
const openPRs = await octokit.paginate(octokit.pulls.list, methodProps);
|
||||
return openPRs;
|
||||
};
|
||||
|
||||
module.exports = getPRs;
|
@ -1,54 +0,0 @@
|
||||
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;
|
@ -1,27 +0,0 @@
|
||||
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,96 +0,0 @@
|
||||
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 = 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 openPRs = await getPRs();
|
||||
|
||||
const newIndices = {};
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
const {
|
||||
number,
|
||||
updated_at: updatedAt,
|
||||
title,
|
||||
user: { login: username }
|
||||
} = openPRs[i];
|
||||
|
||||
newIndices[number] = i;
|
||||
let oldPrData = oldPRs[oldIndices[number]];
|
||||
const oldUpdatedAt = oldPrData ? oldPrData.updatedAt : null;
|
||||
if (!oldIndices.hasOwnProperty(number)) {
|
||||
// insert a new pr
|
||||
const filenames = await getFilenames(number);
|
||||
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);
|
||||
await PR.findOneAndUpdate(
|
||||
{ _id: number },
|
||||
{ updatedAt, title, username, filenames }
|
||||
);
|
||||
console.log('updated PR #' + number);
|
||||
}
|
||||
}
|
||||
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 });
|
||||
console.log('deleted PR #' + number);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(async () => {
|
||||
// update info collection
|
||||
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);
|
||||
});
|
||||
mongoose.connection.close();
|
||||
})
|
||||
.catch((err) => {
|
||||
mongoose.connection.close();
|
||||
throw err;
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Todo
|
||||
|
||||
## Usage
|
||||
|
||||
Todo
|
@ -1,85 +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 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.
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"packages": [
|
||||
"dashboard-app",
|
||||
"dashboard-app/client",
|
||||
"dashboard-app/server",
|
||||
"lib",
|
||||
"one-off-scripts"
|
||||
],
|
||||
"version": "independent"
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
const Joi = require('joi');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// define validation for all the env vars
|
||||
const envVarsSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.allow(['development', 'production', 'test', 'provision'])
|
||||
.default('development'),
|
||||
PORT: Joi.number().default(3001),
|
||||
MONGO_HOST: Joi.string().required().description('Mongo DB host url'),
|
||||
GITHUB_USERNAME: Joi.string().required(),
|
||||
GITHUB_ACCESS_TOKEN: Joi.string().required(),
|
||||
REPOSITORY_OWNER: Joi.string().required(),
|
||||
REPOSITORY: Joi.string().required(),
|
||||
DEFAULT_BASE: Joi.string().required(),
|
||||
PRODUCTION_RUN: Joi.boolean().default(false)
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
|
||||
const { error, value: envVars } = Joi.validate(process.env, envVarsSchema);
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
||||
const config = {
|
||||
env: envVars.NODE_ENV,
|
||||
mongo: {
|
||||
host: envVars.MONGO_HOST
|
||||
},
|
||||
github: {
|
||||
id: envVars.GITHUB_USERNAME,
|
||||
secret: envVars.GITHUB_ACCESS_TOKEN,
|
||||
owner: envVars.REPOSITORY_OWNER,
|
||||
freeCodeCampRepo: envVars.REPOSITORY,
|
||||
defaultBase: envVars.DEFAULT_BASE
|
||||
},
|
||||
oneoff: {
|
||||
productionRun: envVars.PRODUCTION_RUN
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
@ -1,173 +0,0 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const _cliProgress = require('cli-progress');
|
||||
|
||||
const {
|
||||
github: { owner, secret }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const { getRange, getCount } = require('./pr-stats');
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const prsPaginate = async (
|
||||
repo,
|
||||
base,
|
||||
method,
|
||||
firstPR,
|
||||
lastPR,
|
||||
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) {
|
||||
if (pr.number >= first && pr.number <= last) {
|
||||
const propsObj = prPropsToGet.reduce((obj, prop) => {
|
||||
obj[prop] = pr[prop];
|
||||
return obj;
|
||||
}, {});
|
||||
filtered.push(propsObj);
|
||||
}
|
||||
if (pr.number >= last) {
|
||||
done = true;
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getUserInput = async (repo, base, rangeType = '') => {
|
||||
let data, firstPR, lastPR;
|
||||
if (rangeType === 'all') {
|
||||
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(repo, base).then((data) => data);
|
||||
firstPR = data[0];
|
||||
lastPR = data[1];
|
||||
if (type !== 'all' && type !== 'range') {
|
||||
throw 'Please specify either all or range for 1st arg.';
|
||||
}
|
||||
if (type === 'range') {
|
||||
start = parseInt(start, 10);
|
||||
end = parseInt(end, 10);
|
||||
if (!start || !end) {
|
||||
throw 'Specify both a starting PR # (2nd arg) and ending PR # (3rd arg).';
|
||||
}
|
||||
if (start > end) {
|
||||
throw 'Starting PR # must be less than or equal to end PR #.';
|
||||
}
|
||||
if (start < firstPR) {
|
||||
throw `Starting PR # can not be less than first open PR # (${firstPR})`;
|
||||
}
|
||||
firstPR = start;
|
||||
if (end > lastPR) {
|
||||
throw `Ending PR # can not be greater than last open PR # (${lastPR})`;
|
||||
}
|
||||
lastPR = end;
|
||||
}
|
||||
}
|
||||
// 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 (repo, base, totalPRs, firstPR, lastPR, prPropsToGet) => {
|
||||
let progressText = `Retrieve PRs (${firstPR}-${lastPR}) [{bar}] `;
|
||||
progressText += '{percentage}% | Elapsed Time: {duration_formatted} ';
|
||||
progressText += '| ETA: {eta_formatted}';
|
||||
const getPRsBar = new _cliProgress.Bar(
|
||||
{
|
||||
format: progressText,
|
||||
etaBuffer: 50
|
||||
},
|
||||
_cliProgress.Presets.shades_classic
|
||||
);
|
||||
// getPRsBar.start(totalPRs, 0);
|
||||
let openPRs = await prsPaginate(
|
||||
repo,
|
||||
base,
|
||||
octokit.pulls.list,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet,
|
||||
getPRsBar
|
||||
);
|
||||
// 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
|
||||
};
|
||||
|
||||
if (repo) {
|
||||
methodProps = { ...methodProps, repo };
|
||||
}
|
||||
|
||||
let response = await octokit.pulls.listFiles({
|
||||
number,
|
||||
...methodProps
|
||||
});
|
||||
|
||||
let { data } = response;
|
||||
while (octokit.hasNextPage(response)) {
|
||||
response = await octokit.getNextPage(response);
|
||||
let { data: moreData } = response;
|
||||
data = data.concat(moreData);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getFiles = async (repo, number) => await filesPaginate(repo, number);
|
||||
|
||||
const getFilenames = async (repo, number) =>
|
||||
(await getFiles(repo, number)).map(({ filename }) => filename);
|
||||
|
||||
module.exports = { getPRs, getUserInput, getFiles, getFilenames };
|
@ -1,54 +0,0 @@
|
||||
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 };
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "@freecodecamp/contributor-tools-lib",
|
||||
"version": "0.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "18.5.2",
|
||||
"cli-progress": "3.6.1",
|
||||
"date-fns": "1.30.1",
|
||||
"dedent": "0.7.0",
|
||||
"dotenv": "6.2.0",
|
||||
"form-data": "2.5.1",
|
||||
"gray-matter": "4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"path": "0.12.7",
|
||||
"readdirp-walk": "1.7.0",
|
||||
"travis-ci": "2.2.0",
|
||||
"util": "0.11.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const addComment = async (number, comment) => {
|
||||
const result = await octokit.issues
|
||||
.createComment({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
body: comment
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`PR #${number} had an error when trying to add a comment\n`);
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log(`PR #${number} successfully added a comment\n`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = { addComment };
|
@ -1,25 +0,0 @@
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo }
|
||||
} = require('../config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const addLabels = (number, labels, log) => {
|
||||
octokit.issues
|
||||
.addLabels({ owner, repo: freeCodeCampRepo, number, labels })
|
||||
.then(() => {
|
||||
console.log(`PR #${number} added ${JSON.stringify(labels)}\n`);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(
|
||||
`PR #${number} had an error when trying to labels: ${JSON.stringify(
|
||||
labels
|
||||
)}\n`
|
||||
);
|
||||
console.log(err);
|
||||
log.finish();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { addLabels };
|
@ -1,57 +0,0 @@
|
||||
const { addComment } = require('./add-comment');
|
||||
const { rateLimiter } = require('../utils');
|
||||
const {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../config');
|
||||
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
/* closes and reopens an open PR with applicable comment */
|
||||
const closeOpen = async (number) => {
|
||||
await octokit.pulls
|
||||
.update({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
state: 'closed',
|
||||
base: defaultBase
|
||||
})
|
||||
.then(async () => {
|
||||
await rateLimiter(5000);
|
||||
return octokit.pulls.update({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number,
|
||||
state: 'open',
|
||||
base: defaultBase
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
||||
await rateLimiter(1000);
|
||||
const msg = 'Closed/Reopened to resolve a specific Travis build failure.';
|
||||
await addComment(number, msg);
|
||||
})
|
||||
.catch(async (err) => {
|
||||
// Octokit stores message as a stringified object
|
||||
const { errorMg } = JSON.parse(err.message);
|
||||
if (
|
||||
errorMg ===
|
||||
'state cannot be changed. The repository that submitted this pull request has been deleted.'
|
||||
) {
|
||||
await rateLimiter(1000);
|
||||
await addComment(
|
||||
number,
|
||||
"This PR was closed because user's repo was deleted."
|
||||
);
|
||||
console.log(
|
||||
`PR #${number} was closed because user's repo was deleted.`
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { closeOpen };
|
@ -1,6 +0,0 @@
|
||||
const { addComment } = require('./add-comment');
|
||||
const { addLabels } = require('./add-labels');
|
||||
const { closeOpen } = require('./close-open');
|
||||
const { labeler } = require('./labeler');
|
||||
|
||||
module.exports = { addComment, addLabels, closeOpen, labeler };
|
@ -1,47 +0,0 @@
|
||||
const config = require('../../lib/config');
|
||||
const { validLabels } = require('../validation');
|
||||
const { addLabels } = require('./add-labels');
|
||||
const { rateLimiter } = require('../utils');
|
||||
|
||||
const labeler = async (number, prFiles, currentLabels) => {
|
||||
// holds potential labels to add based on file path
|
||||
const labelsToAdd = {};
|
||||
const existingLabels = currentLabels.map(({ name }) => name);
|
||||
prFiles.forEach(({ filename }) => {
|
||||
/* remove '/challenges' from filename so
|
||||
language variable hold the language */
|
||||
const filenameReplacement = filename.replace(
|
||||
/^curriculum\/challenges\//,
|
||||
'curriculum/'
|
||||
);
|
||||
const regex =
|
||||
/^(docs|curriculum)(?:\/)(english|arabic|chinese|portuguese|russian|spanish)?\/?/;
|
||||
// need an array to pass to labelsAdder
|
||||
const match = filenameReplacement.match(regex) || [];
|
||||
const articleType = match[1];
|
||||
const language = match[2];
|
||||
if (articleType && validLabels[articleType]) {
|
||||
labelsToAdd[validLabels[articleType]] = 1;
|
||||
}
|
||||
if (language && validLabels[language]) {
|
||||
labelsToAdd[validLabels[language]] = 1;
|
||||
}
|
||||
if (articleType === 'curriculum') {
|
||||
labelsToAdd['status: need to test locally'] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// only adds needed labels which are NOT currently on the PR
|
||||
const newLabels = Object.keys(labelsToAdd).filter((label) => {
|
||||
return !existingLabels.includes(label);
|
||||
});
|
||||
if (newLabels.length) {
|
||||
if (config.oneoff.productionRun) {
|
||||
addLabels(number, newLabels);
|
||||
await rateLimiter(1000);
|
||||
}
|
||||
}
|
||||
return newLabels;
|
||||
};
|
||||
|
||||
module.exports = { labeler };
|
@ -1,13 +0,0 @@
|
||||
const { rateLimiter } = require('./rate-limiter');
|
||||
const { savePrData } = require('./save-pr-data');
|
||||
const { saveToFile } = require('./save-to-file');
|
||||
const { openJSONFile } = require('./open-json-file');
|
||||
const { ProcessingLog } = require('./processing-log');
|
||||
|
||||
module.exports = {
|
||||
rateLimiter,
|
||||
savePrData,
|
||||
saveToFile,
|
||||
openJSONFile,
|
||||
ProcessingLog
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const openJSONFile = (fileName) => {
|
||||
const data = JSON.parse(fs.readFileSync(fileName, 'utf8'));
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = { openJSONFile };
|
@ -1,90 +0,0 @@
|
||||
const config = require('../../lib/config');
|
||||
const formatDate = require('date-fns/format');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const { saveToFile } = require('./save-to-file');
|
||||
|
||||
class ProcessingLog {
|
||||
constructor(script) {
|
||||
this._script = script;
|
||||
this._startTime = null;
|
||||
this._finishTime = null;
|
||||
this._elapsedTime = null;
|
||||
this._prs = [];
|
||||
this._prCount = null;
|
||||
this._logfile = path.resolve(
|
||||
__dirname,
|
||||
`../../work-logs/data-for_${this.getRunType()}_${this._script}.json`
|
||||
);
|
||||
}
|
||||
|
||||
getRunType() {
|
||||
return config.oneoff.productionRun ? 'production' : 'test';
|
||||
}
|
||||
|
||||
export() {
|
||||
const log = {
|
||||
startTime: this._startTime,
|
||||
finishTime: this._finishTime,
|
||||
elapsedTime: this._elapsedTime,
|
||||
prCount: this._prs.length,
|
||||
firstPR: this._firstPR,
|
||||
lastPR: this._lastPR,
|
||||
prs: this._prs
|
||||
};
|
||||
saveToFile(this._logfile, JSON.stringify(log, null, 2));
|
||||
}
|
||||
|
||||
add(prNum, props) {
|
||||
this._prs.push(props);
|
||||
}
|
||||
|
||||
getPrRange() {
|
||||
if (this._prs.length) {
|
||||
const first = this._prs[0].number;
|
||||
const last = this._prs[this._prs.length - 1].number;
|
||||
return [first, last];
|
||||
}
|
||||
console.log('Current log file does not contain any PRs');
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
start() {
|
||||
this._startTime = new Date();
|
||||
this.export();
|
||||
}
|
||||
|
||||
finish(logFileName = '') {
|
||||
this._finishTime = new Date();
|
||||
const minutesElapsed = (this._finishTime - this._startTime) / 1000 / 60;
|
||||
this._elapsedTime = minutesElapsed.toFixed(2) + ' mins';
|
||||
let [first, last] = this.getPrRange();
|
||||
this._firstPR = first;
|
||||
this._lastPR = last;
|
||||
this.export();
|
||||
this.changeFilename(logFileName);
|
||||
}
|
||||
|
||||
changeFilename(logFileName) {
|
||||
const now = formatDate(new Date(), 'YYYY-MM-DDTHHmmss');
|
||||
const prRange = `${this._firstPR}-${this._lastPR}`;
|
||||
let finalFilename = `${this.getRunType()}_${
|
||||
this._script
|
||||
}_${prRange}_${now}.json`;
|
||||
let newFilename = path.resolve(
|
||||
__dirname,
|
||||
`../../work-logs/${finalFilename}`
|
||||
);
|
||||
if (logFileName) {
|
||||
newFilename = logFileName;
|
||||
}
|
||||
fs.renameSync(this._logfile, newFilename);
|
||||
if (!fs.existsSync(newFilename)) {
|
||||
throw 'File rename unsuccessful.';
|
||||
}
|
||||
this._logfile = newFilename;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ProcessingLog };
|
@ -1,7 +0,0 @@
|
||||
const rateLimiter = (delay = 1500) => {
|
||||
/* The 1500 delay will guarantee the script will not exceed Github request
|
||||
limit of 1500 per hour. Only increase if you have a higher rate limit */
|
||||
return new Promise((resolve) => setTimeout(() => resolve(true), delay));
|
||||
};
|
||||
|
||||
module.exports = { rateLimiter };
|
@ -1,18 +0,0 @@
|
||||
const formatDate = require('date-fns/format');
|
||||
const path = require('path');
|
||||
|
||||
const { saveToFile } = require('./save-to-file');
|
||||
|
||||
const savePrData = (openPRs, firstPR, lastPR) => {
|
||||
const now = formatDate(new Date(), 'YYYY-MM-DDTHHmmss');
|
||||
const filename = path.resolve(
|
||||
__dirname,
|
||||
`../../work-logs/data-for-openprs_${firstPR}-${lastPR}_${now}.json`
|
||||
);
|
||||
console.log(`# of PRs Retrieved: ${openPRs.length}`);
|
||||
console.log(`PR Range: ${firstPR} - ${lastPR}`);
|
||||
saveToFile(filename, JSON.stringify(openPRs));
|
||||
console.log(`Data saved in file: ${filename}`);
|
||||
};
|
||||
|
||||
module.exports = { savePrData };
|
@ -1,12 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const saveToFile = (fileName, data) => {
|
||||
fs.writeFileSync(fileName, data, (err) => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { saveToFile };
|
@ -1,3 +0,0 @@
|
||||
const { validLabels } = require('./valid-labels');
|
||||
|
||||
module.exports = { validLabels };
|
@ -1,12 +0,0 @@
|
||||
const validLabels = {
|
||||
arabic: 'language: Arabic',
|
||||
chinese: 'language: Chinese',
|
||||
english: 'language: English',
|
||||
portuguese: 'language: Portuguese',
|
||||
russian: 'language: Russian',
|
||||
spanish: 'language: Spanish',
|
||||
curriculum: 'scope: curriculum',
|
||||
docs: 'scope: docs'
|
||||
};
|
||||
|
||||
module.exports = { validLabels };
|
@ -1,157 +0,0 @@
|
||||
/*
|
||||
This is a one-off script to run on all open PRs to add
|
||||
a comment and "status: needs update" label to any PR with guide articles which
|
||||
have frontmatter issues.
|
||||
*/
|
||||
|
||||
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');
|
||||
const {
|
||||
frontmatterCheck
|
||||
} = require('../lib/validation/guide-folder-checks/frontmatter-check');
|
||||
const {
|
||||
createErrorMsg
|
||||
} = require('../lib/validation/guide-folder-checks/create-error-msg');
|
||||
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../config');
|
||||
|
||||
const allowedLangDirNames = [
|
||||
'arabic',
|
||||
'chinese',
|
||||
'english',
|
||||
'portuguese',
|
||||
'russian',
|
||||
'spanish'
|
||||
];
|
||||
|
||||
const log = new ProcessingLog('all-frontmatter-checks');
|
||||
|
||||
const labeler = async (
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels,
|
||||
guideFolderErrorsComment
|
||||
) => {
|
||||
// holds potential labels to add based on file path
|
||||
const labelsToAdd = {};
|
||||
if (guideFolderErrorsComment) {
|
||||
labelsToAdd['status: needs update'] = 1;
|
||||
}
|
||||
const existingLabels = currentLabels.map(({ name }) => name);
|
||||
|
||||
/* only adds needed labels which are NOT currently on the PR. */
|
||||
const newLabels = Object.keys(labelsToAdd).filter((label) => {
|
||||
return !existingLabels.includes(label);
|
||||
});
|
||||
if (newLabels.length) {
|
||||
if (productionRun) {
|
||||
addLabels(number, newLabels);
|
||||
await rateLimiter();
|
||||
}
|
||||
}
|
||||
return newLabels;
|
||||
};
|
||||
|
||||
const checkPath = (fullPath, fileContent) => {
|
||||
let errorMsgs = [];
|
||||
const remaining = fullPath.split('/');
|
||||
const isTranslation =
|
||||
allowedLangDirNames.includes(remaining[1]) && remaining[1] !== 'english';
|
||||
const frontMatterErrMsgs = frontmatterCheck(
|
||||
fullPath,
|
||||
isTranslation,
|
||||
fileContent
|
||||
);
|
||||
return errorMsgs.concat(frontMatterErrMsgs);
|
||||
};
|
||||
|
||||
const guideFolderChecks = async (number, prFiles, user) => {
|
||||
let prErrors = [];
|
||||
for (let { filename: fullPath, raw_url: fileUrl } of prFiles) {
|
||||
let newErrors;
|
||||
if (/^guide\//.test(fullPath)) {
|
||||
const response = await fetch(fileUrl);
|
||||
const fileContent = await response.text();
|
||||
newErrors = checkPath(fullPath, fileContent);
|
||||
}
|
||||
if (newErrors) {
|
||||
prErrors = prErrors.concat(newErrors);
|
||||
}
|
||||
}
|
||||
|
||||
if (prErrors.length) {
|
||||
const comment = createErrorMsg(prErrors, user);
|
||||
if (productionRun) {
|
||||
await addComment(number, comment);
|
||||
await rateLimiter();
|
||||
}
|
||||
return comment;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
log.start();
|
||||
console.log('Starting frontmatter checks process...');
|
||||
let count = 0;
|
||||
for (let i = 0; i < openPRs.length; i++) {
|
||||
if (openPRs.length) {
|
||||
let {
|
||||
number,
|
||||
labels: currentLabels,
|
||||
user: { login: username }
|
||||
} = openPRs[count];
|
||||
|
||||
const prFiles = await getFiles(freeCodeCampRepo, number);
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
const guideFolderErrorsComment = await guideFolderChecks(
|
||||
number,
|
||||
prFiles,
|
||||
username
|
||||
);
|
||||
const commentLogVal = guideFolderErrorsComment
|
||||
? guideFolderErrorsComment
|
||||
: 'none';
|
||||
|
||||
const labelsAdded = await labeler(
|
||||
number,
|
||||
prFiles,
|
||||
currentLabels,
|
||||
guideFolderErrorsComment
|
||||
);
|
||||
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
|
||||
|
||||
log.add(number, { number, comment: commentLogVal, labels: labelLogVal });
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('Successfully completed frontmatter checks');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
This script was created to iterate over all open PRs to label.
|
||||
|
||||
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');
|
||||
|
||||
const log = new ProcessingLog('add-language-labels');
|
||||
|
||||
log.start();
|
||||
console.log('Curriculum File language labeler started...');
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
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(freeCodeCampRepo, number);
|
||||
count++;
|
||||
|
||||
const labelsAdded = await labeler(number, prFiles, currentLabels);
|
||||
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
|
||||
|
||||
log.add(number, { number, labels: labelLogVal });
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('Labeler complete');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
This is a one-off script to run on all open PRs to add the
|
||||
"status: need to test locally" label to any PR with an existing
|
||||
"scope: curriculum" label on it.
|
||||
*/
|
||||
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../lib/config');
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { addLabels } = require('../lib/pr-tasks');
|
||||
const { rateLimiter, ProcessingLog } = require('../lib/utils');
|
||||
|
||||
const log = new ProcessingLog('all-locally-tested-labels');
|
||||
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels'];
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
if (openPRs.length) {
|
||||
log.start();
|
||||
console.log('Starting labeling process...');
|
||||
for (let count = 0; count < openPRs.length; count++) {
|
||||
let { number, labels } = openPRs[count];
|
||||
// holds potential labels to add based on file path
|
||||
const labelsToAdd = {};
|
||||
const existingLabels = labels.map(({ name }) => name);
|
||||
if (existingLabels.includes('scope: curriculum')) {
|
||||
labelsToAdd['status: need to test locally'] = 1;
|
||||
}
|
||||
|
||||
// only adds needed labels which are NOT currently on the PR
|
||||
const newLabels = Object.keys(labelsToAdd).filter((label) => {
|
||||
return !existingLabels.includes(label);
|
||||
});
|
||||
|
||||
if (newLabels.length) {
|
||||
log.add(number, { number, labels: newLabels });
|
||||
if (productionRun) {
|
||||
addLabels(number, newLabels, log);
|
||||
await rateLimiter();
|
||||
}
|
||||
} else {
|
||||
log.add(number, { number, labels: 'none added' });
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('Successfully completed labeling');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
const {
|
||||
github: { freeCodeCampRepo, defaultBase },
|
||||
oneoff: { productionRun }
|
||||
} = require('../lib/config');
|
||||
|
||||
const { closeOpen } = require('../lib/pr-tasks');
|
||||
const { openJSONFile, ProcessingLog, rateLimiter } = require('../lib/utils');
|
||||
|
||||
const log = new ProcessingLog('prs-closed-reopened');
|
||||
|
||||
log.start();
|
||||
const getUserInput = async () => {
|
||||
let filename = process.argv[2];
|
||||
|
||||
if (!filename) {
|
||||
throw 'Specify a file with PRs which needed to be closed and reopened.';
|
||||
}
|
||||
|
||||
let fileObj = openJSONFile(filename);
|
||||
let { prs } = fileObj;
|
||||
if (!prs.length) {
|
||||
throw 'Either no PRs found in file or there or an error occurred.';
|
||||
}
|
||||
return { prs };
|
||||
};
|
||||
|
||||
(async () => {
|
||||
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 (productionRun) {
|
||||
await closeOpen(number);
|
||||
await rateLimiter(90000);
|
||||
}
|
||||
} else {
|
||||
log.add(number, { number, closedOpened: false, errorDesc });
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('closing/reopening of PRs complete');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
/*
|
||||
This is a one-off script that was was used to summarize the results of a
|
||||
test_sweeper json log file after sweeper.js was run on a particular set of data.
|
||||
It generates a text file referencing only PRs with any comments/labels
|
||||
which would have beeen added (test) based on data stored in the
|
||||
specific JSON log file. You must run sweeper with environment variable
|
||||
PRODUCTION_RUN set to false, to get the test version. Technically, you
|
||||
could also run this on a production_sweeper json log file, if you wanted to see
|
||||
if the sweeper commented or labeled any PRs during its run.
|
||||
*/
|
||||
|
||||
const { saveToFile, openJSONFile } = require('../lib/utils');
|
||||
const path = require('path');
|
||||
const dedent = require('dedent');
|
||||
|
||||
const specificLogFile = path.resolve(
|
||||
__dirname,
|
||||
'../work-logs/test_add-language-labels_26001-29000_2019-01-14T215420.json'
|
||||
);
|
||||
|
||||
(() => {
|
||||
let fileObj = openJSONFile(specificLogFile);
|
||||
let { prs } = fileObj;
|
||||
|
||||
let count = 0;
|
||||
let prsWithComments = prs.reduce((text, { number, comment, labels }) => {
|
||||
if ((comment && comment !== 'none') || labels !== 'none added') {
|
||||
text += dedent`
|
||||
|
||||
PR #${number}
|
||||
Comment: ${comment}
|
||||
|
||||
Labels: ${JSON.stringify(labels)}
|
||||
|
||||
*************************\n
|
||||
|
||||
`;
|
||||
count++;
|
||||
}
|
||||
return text;
|
||||
}, '');
|
||||
|
||||
prsWithComments = dedent`
|
||||
# of PRs with comments or labels added: ${count}
|
||||
|
||||
*************************
|
||||
${prsWithComments}
|
||||
`;
|
||||
|
||||
saveToFile(
|
||||
path.resolve(__dirname, '../work-logs/guideErrorComments.txt'),
|
||||
prsWithComments
|
||||
);
|
||||
console.log('guideErrorComments.txt created');
|
||||
})();
|
@ -1,102 +0,0 @@
|
||||
/*
|
||||
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 {
|
||||
github: { owner, secret, freeCodeCampRepo, defaultBase }
|
||||
} = require('../lib/config');
|
||||
|
||||
const octokit = new Octokit({ auth: secret });
|
||||
|
||||
const { getPRs, getUserInput } = require('../lib/get-prs');
|
||||
const { savePrData, ProcessingLog } = require('../lib/utils');
|
||||
|
||||
const log = new ProcessingLog('find-failures-script');
|
||||
|
||||
const errorsToFind = [
|
||||
{
|
||||
error: '',
|
||||
regex: ''
|
||||
}
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'head'];
|
||||
const { openPRs } = await getPRs(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
totalPRs,
|
||||
firstPR,
|
||||
lastPR,
|
||||
prPropsToGet
|
||||
);
|
||||
|
||||
if (openPRs.length) {
|
||||
savePrData(openPRs, firstPR, lastPR);
|
||||
log.start();
|
||||
console.log('Starting error finding process...');
|
||||
for (let count = 0; count < openPRs.length; count++) {
|
||||
let {
|
||||
number,
|
||||
labels,
|
||||
head: { sha: ref }
|
||||
} = openPRs[count];
|
||||
const existingLabels = labels.map(({ name }) => name);
|
||||
|
||||
if (
|
||||
!existingLabels.includes('status: merge conflict') &&
|
||||
!existingLabels.includes('status: needs update') &&
|
||||
!existingLabels.includes('status: discussing')
|
||||
) {
|
||||
const { data: statuses } = await octokit.repos.listStatusesForRef({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
ref
|
||||
});
|
||||
if (statuses.length) {
|
||||
// first element contain most recent status
|
||||
const { state, target_url: targetUrl } = statuses[0];
|
||||
const hasProblem = state === 'failure' || state === 'error';
|
||||
if (hasProblem) {
|
||||
let buildNum = Number(targetUrl.match(/\/builds\/(\d+)\?/i)[1]);
|
||||
/*
|
||||
const logNumber = 'need to use Travis api to
|
||||
access the full log for the buildNum above'
|
||||
*/
|
||||
const logNumber = ++buildNum;
|
||||
const travisBaseUrl = 'https://api.travis-ci.org/v3/job/';
|
||||
const travisLogUrl = `${travisBaseUrl + logNumber}/log.txt`;
|
||||
const response = await fetch(travisLogUrl);
|
||||
const logText = await response.text();
|
||||
let error;
|
||||
for (let { error: errorDesc, regex } of errorsToFind) {
|
||||
regex = RegExp(regex);
|
||||
if (regex.test(logText)) {
|
||||
error = errorDesc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const errorDesc = error ? error : 'unknown error';
|
||||
log.add(number, { number, errorDesc, buildLog: travisLogUrl });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('Successfully finished finding all specified errors.');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,108 +0,0 @@
|
||||
/*
|
||||
This script was created to find all open PRs with unknown repos which
|
||||
potentially have merge conflicts.
|
||||
|
||||
To run the script for a specific language, call the script with the language
|
||||
name as the first argument.
|
||||
|
||||
Note: If any PR displayed in the console shows "unknown", you will need to rerun
|
||||
the script again.
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
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('unknown-repo-prs-with-merge-conflicts');
|
||||
log.start();
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase,
|
||||
'all'
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user', 'head'];
|
||||
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];
|
||||
|
||||
const hasLanguage =
|
||||
languageLabel && labels.some(({ name }) => languageLabel === name);
|
||||
|
||||
if (headRepo === null && (!languageLabel || hasLanguage)) {
|
||||
let data = await octokit.pulls.get({
|
||||
owner,
|
||||
repo: freeCodeCampRepo,
|
||||
number
|
||||
});
|
||||
let mergeableState = data.data.mergeable_state;
|
||||
if (mergeableState === 'unknown') {
|
||||
// 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++;
|
||||
|
||||
if (mergeableState === 'dirty' || mergeableState === 'unknown') {
|
||||
log.add(number, { number, mergeableState });
|
||||
console.log(`${number} (${mergeableState})`);
|
||||
mergeConflictCount++;
|
||||
}
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
}
|
||||
}
|
||||
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';
|
||||
}
|
||||
})()
|
||||
.then(async () => {
|
||||
log.finish();
|
||||
console.log('Finished finding unknown repo PRs with merge conflicts');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@freecodecamp/contributor-tools-one-off-scripts",
|
||||
"version": "0.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "18.5.2",
|
||||
"cli-progress": "3.6.1",
|
||||
"dedent": "0.7.0",
|
||||
"path": "0.12.7"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
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);
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
This script was originally created to iterate over all open PRs to label and
|
||||
comment on specific PR errors (i.e. guide related filenmame syntax and
|
||||
frontmatter).
|
||||
|
||||
Since the first run which covered over 10,000+ PRs, it is curently ran every
|
||||
couple of days for just the most recent PRs.
|
||||
|
||||
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');
|
||||
|
||||
const log = new ProcessingLog('sweeper');
|
||||
|
||||
log.start();
|
||||
console.log('Sweeper started...');
|
||||
(async () => {
|
||||
const { totalPRs, firstPR, lastPR } = await getUserInput(
|
||||
freeCodeCampRepo,
|
||||
defaultBase
|
||||
);
|
||||
const prPropsToGet = ['number', 'labels', 'user'];
|
||||
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(freeCodeCampRepo, number);
|
||||
count++;
|
||||
|
||||
const labelsAdded = await labeler(number, prFiles, currentLabels);
|
||||
const labelLogVal = labelsAdded.length ? labelsAdded : 'none added';
|
||||
|
||||
log.add(number, { number, labels: labelLogVal });
|
||||
if (count > 4000) {
|
||||
await rateLimiter(2350);
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
log.finish();
|
||||
console.log('Sweeper complete');
|
||||
})
|
||||
.catch((err) => {
|
||||
log.finish();
|
||||
console.log(err);
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "@freecodecamp/contributor-tools",
|
||||
"version": "0.0.1",
|
||||
"description": "The freeCodeCamp.org open-source codebase and curriculum",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap --ci",
|
||||
"build": "lerna run build",
|
||||
"develop": "run-p develop:*",
|
||||
"develop:client": "cd ./dashboard-app/client && npm run develop",
|
||||
"develop:server": "cd ./dashboard-app/server && npm run develop",
|
||||
"format": "prettier --write es5 ./**/*.{js,json} && npm run lint",
|
||||
"lint": "eslint ./**/*.js --fix",
|
||||
"postinstall": "npm run bootstrap",
|
||||
"start": "cd dashboard-app/server && npm start",
|
||||
"test": "run-p test:*",
|
||||
"test:client": "cd ./dashboard-app/client && npm run test"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "npm start",
|
||||
"watch": [
|
||||
".env",
|
||||
"."
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "6.12.6",
|
||||
"ajv-keywords": "3.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "8.2.0",
|
||||
"eslint": "5.16.0",
|
||||
"joi": "14.3.1",
|
||||
"lerna": "3.22.1",
|
||||
"mocha": "5.2.0",
|
||||
"nodemon": "1.19.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "1.19.1"
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
GITHUB_USERNAME='camperbot'
|
||||
GITHUB_ACCESS_TOKEN=
|
||||
REPOSITORY_OWNER='freeCodeCamp'
|
||||
REPOSITORY='freeCodeCamp'
|
||||
DEFAULT_BASE='main'
|
||||
PORT=3001
|
||||
MONGO_HOST=mongodb://localhost/contributor-tools
|
||||
MONGO_PORT=27017
|
||||
PRODUCTION_RUN=false
|
Reference in New Issue
Block a user