chore: remove contributor tools (#44368)

They're being stored in freeCodeCamp/tools now
This commit is contained in:
Oliver Eyton-Williams
2021-12-03 16:10:03 +01:00
committed by GitHub
parent 8751f55bc7
commit 0cbb499e9c
74 changed files with 0 additions and 3692 deletions

View File

@ -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

View File

@ -1,5 +0,0 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "none"
}

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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);
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,7 +0,0 @@
import styled from 'styled-components';
const FullWidthDiv = styled.div`
width: 100%;
`;
export default FullWidthDiv;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
};

View File

@ -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;
}

View File

@ -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();

View File

@ -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();
});
}
}

View File

@ -1,5 +0,0 @@
const theme = {
primary: '#0a0a23'
};
export default theme;

View File

@ -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;

View File

@ -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 };

View File

@ -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"
}
}

View File

@ -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 };

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
});

View File

@ -1,7 +0,0 @@
# Contributing
Todo
## Usage
Todo

View File

@ -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 PRs 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.

View File

@ -1,10 +0,0 @@
{
"packages": [
"dashboard-app",
"dashboard-app/client",
"dashboard-app/server",
"lib",
"one-off-scripts"
],
"version": "independent"
}

View File

@ -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;

View File

@ -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 };

View File

@ -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 };

View File

@ -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": {}
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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
};

View File

@ -1,8 +0,0 @@
const fs = require('fs');
const openJSONFile = (fileName) => {
const data = JSON.parse(fs.readFileSync(fileName, 'utf8'));
return data;
};
module.exports = { openJSONFile };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -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 };

View File

@ -1,3 +0,0 @@
const { validLabels } = require('./valid-labels');
module.exports = { validLabels };

View File

@ -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 };

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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');
})();

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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": {}
}

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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"
}
}

View File

@ -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