Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions .github/workflows/reviewstack.dev-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
name: Publish https://reviewstack.dev
name: Deploy ReviewStack to Cloudflare Pages

on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 1-5'
push:
branches:
- main
paths:
- 'eden/contrib/reviewstack/**'
- 'eden/contrib/reviewstack.dev/**'
- 'eden/contrib/shared/**'
- '.github/workflows/reviewstack.dev-deploy.yml'

jobs:
deploy:
runs-on: ubuntu-22.04
# Our build container already has Node, Yarn, and Python installed.
container:
image: ${{ format('ghcr.io/{0}/build_ubuntu_22_04:latest', github.repository) }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Grant Access
run: git config --global --add safe.directory "$PWD"
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
cache-dependency-path: eden/contrib/yarn.lock
- name: Install dependencies
working-directory: ./eden/contrib/
run: yarn install --prefer-offline
run: yarn install

# Build codegen and then do some sanity checks so we don't push the site
# when the tests are broken.
Expand All @@ -37,11 +44,10 @@ jobs:
working-directory: ./eden/contrib/reviewstack.dev
run: yarn release

# Push to the release branch.
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
if: ${{ github.ref == 'refs/heads/main' }}
# Deploy to Cloudflare Pages
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: reviewstack.dev-prod
publish_dir: ./eden/contrib/reviewstack.dev/build
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ./eden/contrib/reviewstack.dev/build --project-name=reviewstack
37 changes: 37 additions & 0 deletions eden/contrib/reviewstack.dev/functions/_oauth/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interface GitHubTokenResponse {
access_token?: string;
error?: string;
error_description?: string;
}

export const onRequestGet: PagesFunction<{ CLIENT_ID: string; CLIENT_SECRET: string }> = async (context) => {
const url = new URL(context.request.url);
const code = url.searchParams.get('code');

if (!code) {
return new Response('Missing code parameter', { status: 400 });
}

const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
client_id: context.env.CLIENT_ID,
client_secret: context.env.CLIENT_SECRET,
code,
}),
});

const data: GitHubTokenResponse = await tokenResponse.json();

if (data.error || !data.access_token) {
const errorMsg = encodeURIComponent(data.error_description || data.error || 'Unknown error');
return Response.redirect(`${url.origin}/?error=${errorMsg}`, 302);
}

// Redirect back to app with token in hash (not exposed to server logs)
return Response.redirect(`${url.origin}/#token=${data.access_token}`, 302);
};
9 changes: 9 additions & 0 deletions eden/contrib/reviewstack.dev/functions/_oauth/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const onRequestGet: PagesFunction<{ CLIENT_ID: string }> = async (context) => {
const redirectUri = new URL('/_oauth/callback', context.request.url).toString();
const githubAuthUrl = new URL('https://github.com/login/oauth/authorize');
githubAuthUrl.searchParams.set('client_id', context.env.CLIENT_ID);
githubAuthUrl.searchParams.set('redirect_uri', redirectUri);
githubAuthUrl.searchParams.set('scope', 'user repo');

return Response.redirect(githubAuthUrl.toString(), 302);
};
4 changes: 3 additions & 1 deletion eden/contrib/reviewstack.dev/src/LazyLoginDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export default function LazyLoginDialog({
}) {
const {hostname} = window.location;
const LoginComponent =
hostname === 'reviewstack.netlify.app' || hostname === 'reviewstack.dev'
hostname === 'reviewstack.netlify.app' ||
hostname === 'reviewstack.dev' ||
hostname === 'reviews.qlax.dev'
? NetlifyLoginDialog
: DefaultLoginDialog;

Expand Down
61 changes: 25 additions & 36 deletions eden/contrib/reviewstack.dev/src/NetlifyLoginDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,10 @@ import type {CustomLoginDialogProps} from 'reviewstack/src/LoginDialog';
import Footer from './Footer';
import InlineCode from './InlineCode';
import {Box, Button, Heading, Text, TextInput} from '@primer/react';
import Authenticator from 'netlify-auth-providers';
import React, {useCallback, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import AppHeader from 'reviewstack/src/AppHeader';
import Link from 'reviewstack/src/Link';

/**
* See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
*/
const GITHUB_OAUTH_SCOPE = ['user', 'repo'].join(' ');

export default function NetlifyLoginDialog(props: CustomLoginDialogProps): React.ReactElement {
return (
<Box display="flex" flexDirection="column" height="100vh">
Expand Down Expand Up @@ -58,17 +52,32 @@ function EndUserInstructions(props: CustomLoginDialogProps): React.ReactElement
const {setTokenAndHostname} = props;
const [isButtonDisabled, setButtonDisabled] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const onClick = useCallback(async () => {
setButtonDisabled(true);
try {
const token = await fetchGitHubToken();

// Check for OAuth token in URL hash on mount (from OAuth callback)
useEffect(() => {
const hash = window.location.hash;
const tokenMatch = hash.match(/token=([^&]+)/);
if (tokenMatch) {
const token = tokenMatch[1];
// Clear the hash from URL
window.history.replaceState(null, '', window.location.pathname + window.location.search);
setTokenAndHostname(token, 'github.com');
} catch (e) {
const message = e instanceof Error ? e.message : 'error fetching OAuth token';
setErrorMessage(message);
}
setButtonDisabled(false);
}, [setButtonDisabled, setErrorMessage, setTokenAndHostname]);
// Check for error in URL params (from failed OAuth)
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
if (error) {
setErrorMessage(error);
// Clear error from URL
window.history.replaceState(null, '', window.location.pathname);
}
}, [setTokenAndHostname]);

const onClick = useCallback(() => {
setButtonDisabled(true);
// Redirect to OAuth login endpoint
window.location.href = '/_oauth/login';
}, []);

return (
<Box>
Expand Down Expand Up @@ -222,23 +231,3 @@ function H3({children}: {children: React.ReactNode}): React.ReactElement {
);
}

function fetchGitHubToken(): Promise<string> {
return new Promise((resolve, reject) => {
const authenticator = new Authenticator({});
authenticator.authenticate(
{provider: 'github', scope: GITHUB_OAUTH_SCOPE},
(error: Error | null, data: {token: string} | null) => {
if (error) {
reject(error);
} else {
const token = data?.token;
if (typeof token === 'string') {
resolve(token);
} else {
reject(new Error('token missing in OAuth response'));
}
}
},
);
});
}
4 changes: 3 additions & 1 deletion eden/contrib/reviewstack/src/PullRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ function PullRequestDetails() {

const stack = pullRequestStack.contents;
const { bodyHTML } = pullRequest;
let pullRequestBodyHTML;
let pullRequestBodyHTML: string;
switch (stack.type) {
case 'no-stack':
case 'graphite':
// Graphite stack info is in a comment, not the PR body, so no stripping needed
pullRequestBodyHTML = bodyHTML;
break;
case 'sapling':
Expand Down
31 changes: 27 additions & 4 deletions eden/contrib/reviewstack/src/PullRequestChangeCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,46 @@
* LICENSE file in the root directory of this source tree.
*/

import {gitHubPullRequest} from './recoil';
import {CounterLabel} from '@primer/react';
import {gitHubPullRequest, pullRequestSLOC} from './recoil';
import {CounterLabel, Tooltip} from '@primer/react';
import {useRecoilValue} from 'recoil';

export default function PullRequestChangeCount(): React.ReactElement | null {
const pullRequest = useRecoilValue(gitHubPullRequest);
const slocInfo = useRecoilValue(pullRequestSLOC);

if (pullRequest == null) {
return null;
}

const {additions, deletions} = pullRequest;
const {significantLines, generatedFileCount} = slocInfo;

const tooltipText =
generatedFileCount > 0
? `${significantLines} significant lines (excludes ${generatedFileCount} generated file${generatedFileCount === 1 ? '' : 's'})`
: `${significantLines} significant lines`;

return (
<>
<CounterLabel sx={{ backgroundColor: "success.muted" }}>+{additions}</CounterLabel>
<CounterLabel scheme="primary" sx={{ backgroundColor: "danger.muted", color: "black" }}>-{deletions}</CounterLabel>
<CounterLabel sx={{backgroundColor: 'success.muted'}}>+{additions}</CounterLabel>
<CounterLabel
scheme="primary"
sx={{backgroundColor: 'danger.muted', color: 'black'}}>
-{deletions}
</CounterLabel>
{significantLines > 0 && (
<Tooltip aria-label={tooltipText} direction="s">
<CounterLabel
sx={{
backgroundColor: 'accent.subtle',
color: 'fg.default',
marginLeft: 1,
}}>
{significantLines} sig
</CounterLabel>
</Tooltip>
)}
</>
);
}
17 changes: 16 additions & 1 deletion eden/contrib/reviewstack/src/SplitDiffFileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
*/

import {ChevronDownIcon, ChevronRightIcon} from '@primer/octicons-react';
import {Box, Text, Tooltip} from '@primer/react';
import {Box, Label, Text, Tooltip} from '@primer/react';

export function FileHeader({
path,
open,
onChangeOpen,
isGenerated,
}: {
path: string;
open?: boolean;
onChangeOpen?: (open: boolean) => void;
isGenerated?: boolean;
}) {
// Even though the enclosing <SplitDiffView> will have border-radius set, we
// have to define it again here or things don't look right.
Expand Down Expand Up @@ -79,6 +81,19 @@ export function FileHeader({
</Box>
)}
<Box sx={{display: 'flex', flexGrow: 1}}>{filePathParts}</Box>
{isGenerated && (
<Tooltip aria-label="This file is generated and excluded from significant lines count" direction="sw">
<Label
variant="secondary"
sx={{
marginLeft: 2,
fontSize: 0,
fontWeight: 'normal',
}}>
Generated
</Label>
</Tooltip>
)}
</Box>
);
}
29 changes: 24 additions & 5 deletions eden/contrib/reviewstack/src/SplitDiffView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import SplitDiffRow from './SplitDiffRow';
import {diffAndTokenize, lineRange} from './diffServiceClient';
import {DiffSide} from './generated/graphql';
import {grammars, languages} from './generated/textmate/TextMateGrammarManifest';
import {GeneratedStatus} from './github/types';
import {
fileGeneratedStatus,
gitHubPullRequestLineToPositionForFile,
gitHubPullRequestSelectedVersionIndex,
gitHubPullRequestVersions,
Expand All @@ -38,7 +40,7 @@ import {UnfoldIcon} from '@primer/octicons-react';
import {Box, Spinner, Text} from '@primer/react';
import {diffChars} from 'diff';
import React, {useCallback, useEffect, useState} from 'react';
import {useRecoilValue, useRecoilValueLoadable, waitForAll} from 'recoil';
import {constSelector, useRecoilValue, useRecoilValueLoadable, waitForAll} from 'recoil';
import organizeLinesIntoGroups from 'shared/SplitDiffView/organizeLinesIntoGroups';
import {
applyTokenizationToLine,
Expand Down Expand Up @@ -81,7 +83,6 @@ export default function SplitDiffView({
after,
isPullRequest,
}: Props): React.ReactElement {
const [open, setOpen] = useState(true);
const scopeName = getFilepathClassifier().findScopeNameForPath(path);
const colorMode = useRecoilValue(primerColorMode);
const loadable = useRecoilValueLoadable(
Expand All @@ -104,14 +105,32 @@ export default function SplitDiffView({
isPullRequest ? gitHubPullRequestVersions : nullAtom,
isPullRequest ? gitHubPullRequestSelectedVersionIndex : nullAtom,
isPullRequest ? gitHubPullRequestLineToPositionForFile(path) : nullAtom,
// Check if file is generated to default collapse state for generated files
isPullRequest ? fileGeneratedStatus(path) : constSelector(GeneratedStatus.Manual),
]),
);

const [{patch, tokenization}, allThreads, newCommentInputCallbacks, commitIDs] =
loadable.getValue();
const [
{patch, tokenization},
allThreads,
newCommentInputCallbacks,
commitIDs,
_versions,
_selectedVersionIndex,
_lineToPosition,
generatedStatus,
] = loadable.getValue();
const isGenerated = generatedStatus === GeneratedStatus.Generated;
// Default to collapsed for generated files
const [open, setOpen] = useState(!isGenerated);
return (
<Box borderWidth="1px" borderStyle="solid" borderColor="border.default" borderRadius={2}>
<FileHeader path={path} open={open} onChangeOpen={open => setOpen(open)} />
<FileHeader
path={path}
open={open}
onChangeOpen={open => setOpen(open)}
isGenerated={isGenerated}
/>
{open && (
<SplitDiffViewTable
path={path}
Expand Down
Loading