Support commit diffs (#8)

* cra boilerplate

* Dummy chrome extension

* eslint and working url popup

* content script and dummy messaging

* replace .obj diffs with dummy test

* comment and in-order multiple type support

* get pull api data from url

* README title and desc

* api/elements match with filename check

* github token signin signout

* manifest v3, service request for CORS

* working kittycad api in service worker

* First real background message

* Clean up,  better types

* Fix settings

* multiservice settings

* Tweaks

* WIP: download file

* Working downloads and kittycad conversion

* Inject react, add three dependencies

* Working stl canvas

* primer for github-like style

* Loading before model

* diff colors

* colorMode auto

* Popup clean up

* clean up

* Working loading

* Logos

* Add GitHub CI

* Working test

* yarn test in ci

* Little tweak

* Update README

* component tests

* Better test

* Clean up

* UserCard test

* working caddiff test

* Note

* Rename App to Settings

* storage test

* Clean up

* Clean up content script

* further content cleanup

* Fix test

* Little tweaks to modelview

* More tests and testing

* Regex fix

* LFS file download test

* prettier config from kittycad/website

* Little tweaks

* comment

* log level

* Tweaks

* README update

* more prettier

* comment

* Irrelevant comment

* No .vscode and readme update

* Remove .vscode

* Package.json update after vscode removal

* Working commit diff

* Start cleaning up

* Clean up

* Add artifact upload

* return when matched

* Better test

* Clean up

* prettier

* Clean up html snippet
This commit is contained in:
Pierre Jacquier
2023-03-02 21:16:22 -05:00
committed by GitHub
parent fd44076a18
commit a1a90a7d2a
6 changed files with 228 additions and 74 deletions

View File

@ -23,3 +23,7 @@ jobs:
- run: yarn build - run: yarn build
- run: yarn test - run: yarn test
- uses: actions/upload-artifact@v3
with:
path: build

View File

@ -4,6 +4,7 @@ import {
KittycadUser, KittycadUser,
Message, Message,
MessageGetFileDiff, MessageGetFileDiff,
MessageGetGithubCommitData,
MessageGetGithubPullFilesData, MessageGetGithubPullFilesData,
MessageIds, MessageIds,
MessageResponse, MessageResponse,
@ -86,6 +87,16 @@ chrome.runtime.onMessage.addListener(
return true return true
} }
if (message.id === MessageIds.GetGithubCommit) {
const { owner, repo, sha } =
message.data as MessageGetGithubCommitData
github.rest.repos
.getCommit({ owner, repo, ref: sha })
.then(r => sendResponse(r.data))
.catch(e => sendResponse(e))
return true
}
if (message.id === MessageIds.GetGithubUser) { if (message.id === MessageIds.GetGithubUser) {
github.rest.users github.rest.users
.getAuthenticated() .getAuthenticated()

View File

@ -2,12 +2,11 @@ import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { CadDiff } from '../components/CadDiff' import { CadDiff } from '../components/CadDiff'
import { Loading } from '../components/Loading' import { Loading } from '../components/Loading'
import { isFilenameSupported } from './diff' import { Commit, DiffEntry, FileDiff, Message, MessageIds, Pull } from './types'
import { DiffEntry, FileDiff, Message, MessageIds, Pull } from './types'
import { import {
getGithubUrlParams, getGithubPullUrlParams,
getWebPullElements, mapInjectableDiffElements,
getInjectablePullElements, getGithubCommitUrlParams,
} from './web' } from './web'
// https://github.com/OctoLinker/injection // https://github.com/OctoLinker/injection
@ -15,34 +14,20 @@ import {
// no ts support // no ts support
const gitHubInjection = require('github-injection') const gitHubInjection = require('github-injection')
async function injectPullDiff( async function injectDiff(
owner: string, owner: string,
repo: string, repo: string,
pull: number, sha: string,
parentSha: string,
files: DiffEntry[],
document: Document document: Document
) { ) {
const allApiFiles = await chrome.runtime.sendMessage<Message, DiffEntry[]>({ const map = mapInjectableDiffElements(document, files)
id: MessageIds.GetGithubPullFiles, for (const { element } of map) {
data: { owner, repo, pull },
})
const apiFiles = allApiFiles.filter(f => isFilenameSupported(f.filename))
console.log(`Found ${apiFiles.length} supported files with the API`)
const elements = getWebPullElements(document)
console.log(`Found ${elements.length} elements in the web page`)
const injectableElements = getInjectablePullElements(elements, apiFiles)
for (const { element } of injectableElements) {
createRoot(element).render(React.createElement(Loading)) createRoot(element).render(React.createElement(Loading))
} }
const pullData = await chrome.runtime.sendMessage<Message, Pull>({ for (const { element, file } of map) {
id: MessageIds.GetGithubPull,
data: { owner, repo, pull },
})
const sha = pullData.head.sha
const parentSha = pullData.base.sha
for (const { element, file } of injectableElements) {
const fileDiff = await chrome.runtime.sendMessage<Message, FileDiff>({ const fileDiff = await chrome.runtime.sendMessage<Message, FileDiff>({
id: MessageIds.GetFileDiff, id: MessageIds.GetFileDiff,
data: { owner, repo, sha, parentSha, file }, data: { owner, repo, sha, parentSha, file },
@ -51,17 +36,54 @@ async function injectPullDiff(
} }
} }
async function injectPullDiff(
owner: string,
repo: string,
pull: number,
document: Document
) {
const files = await chrome.runtime.sendMessage<Message, DiffEntry[]>({
id: MessageIds.GetGithubPullFiles,
data: { owner, repo, pull },
})
const pullData = await chrome.runtime.sendMessage<Message, Pull>({
id: MessageIds.GetGithubPull,
data: { owner, repo, pull },
})
const sha = pullData.head.sha
const parentSha = pullData.base.sha
await injectDiff(owner, repo, sha, parentSha, files, document)
}
async function injectCommitDiff(
owner: string,
repo: string,
sha: string,
document: Document
) {
const commit = await chrome.runtime.sendMessage<Message, Commit>({
id: MessageIds.GetGithubCommit,
data: { owner, repo, sha },
})
if (!commit.files) throw Error('Found no file changes in commit')
if (!commit.parents.length) throw Error('Found no commit parent')
const parentSha = commit.parents[0].sha
injectDiff(owner, repo, sha, parentSha, commit.files, document)
}
gitHubInjection(async () => { gitHubInjection(async () => {
const params = getGithubUrlParams(window.location.href) const url = window.location.href
if (!params) { const pullParams = getGithubPullUrlParams(url)
console.log("URL doesn't match pull request pattern.") if (pullParams) {
const { owner, repo, pull } = pullParams
await injectPullDiff(owner, repo, pull, window.document)
return return
} }
const { owner, repo, pull } = params
console.log('Found pull request diff URL', owner, repo, pull) const commitParams = getGithubCommitUrlParams(url)
try { if (commitParams) {
await injectPullDiff(owner, repo, pull, window.document) const { owner, repo, sha } = commitParams
} catch (e) { await injectCommitDiff(owner, repo, sha, window.document)
console.error(e) return
} }
}) })

View File

@ -11,6 +11,7 @@ export type DiffEntry = components['schemas']['diff-entry']
export type ContentFile = components['schemas']['content-file'] export type ContentFile = components['schemas']['content-file']
export type User = components['schemas']['simple-user'] export type User = components['schemas']['simple-user']
export type Pull = components['schemas']['pull-request'] export type Pull = components['schemas']['pull-request']
export type Commit = components['schemas']['commit']
// chrome extension // chrome extension
@ -27,6 +28,7 @@ export enum MessageIds {
GetKittycadUser = 'GetKittyCadUser', GetKittycadUser = 'GetKittyCadUser',
GetFileDiff = 'GetFileDiff', GetFileDiff = 'GetFileDiff',
GetGithubPull = 'GetGithubPull', GetGithubPull = 'GetGithubPull',
GetGithubCommit = 'GetGithubCommit',
} }
export type MessageGetGithubPullFilesData = { export type MessageGetGithubPullFilesData = {
@ -35,6 +37,12 @@ export type MessageGetGithubPullFilesData = {
pull: number pull: number
} }
export type MessageGetGithubCommitData = {
owner: string
repo: string
sha: string
}
export type MessageGetFileDiff = { export type MessageGetFileDiff = {
owner: string owner: string
repo: string repo: string
@ -49,12 +57,17 @@ export type MessageSaveToken = {
export type Message = { export type Message = {
id: MessageIds id: MessageIds
data?: MessageGetGithubPullFilesData | MessageSaveToken | MessageGetFileDiff data?:
| MessageGetGithubPullFilesData
| MessageGetGithubCommitData
| MessageSaveToken
| MessageGetFileDiff
} }
export type MessageResponse = export type MessageResponse =
| DiffEntry[] | DiffEntry[]
| Pull | Pull
| Commit
| User | User
| KittycadUser | KittycadUser
| MessageSaveToken | MessageSaveToken

View File

@ -1,22 +1,50 @@
import { DiffEntry } from './types' import { DiffEntry } from './types'
import { import {
getElementFilename, getElementFilename,
getGithubUrlParams, getGithubCommitUrlParams,
getInjectablePullElements, getGithubPullUrlParams,
getWebPullElements, mapInjectableDiffElements,
getSupportedWebDiffElements,
} from './web' } from './web'
const githubPullHtmlSnippet = ` const githubPullHtmlSnippet = `
<div class="file js-file js-details-container js-targetable-element show-inline-notes Details Details--on open js-tagsearch-file" data-file-type=".obj"> <div class="file" data-file-type=".json">
<div class="file-header d-flex flex-md-row flex-column flex-md-items-center file-header--expandable js-file-header js-skip-tagsearch sticky-file-header js-position-sticky js-position-sticky-stacked"> <div class="file-header">
<div class="file-info flex-auto min-width-0 mb-md-0 mb-2"> <div class="file-info">
<span class="Truncate"> <span class="Truncate">
<a title="samples/file_center_of_mass/output.obj" class="Link--primary Truncate-text" href="#diff-5f8df244900f6383db3354c02b8a984a044b272e6bfe4cacc1ec8d4892ad3e21">samples/file_center_of_mass/output.obj</a> <a title="samples/file_center_of_mass/output.json">samples/file_center_of_mass/output.json</a>
</span>
</div>
<div class="js-file-content">
<div class="data highlight js-blob-wrapper" style="">
// was a code diff
</div>
</div>
</div>
<div class="file" data-file-type=".obj">
<div class="file-header">
<div class="file-info">
<span class="Truncate">
<a title="samples/file_center_of_mass/output.obj">samples/file_center_of_mass/output.obj</a>
</span> </span>
</div> </div>
</div> </div>
<div class="js-file-content">
<div class="data highlight empty">
Git LFS file not shown
</div>
</div>
</div>
<div class="js-file-content Details-content--hidden position-relative"> <div class="file" data-file-type=".obj">
<div class="file-header">
<div class="file-info">
<span class="Truncate">
<a title="seesaw.obj">seesaw.obj</a>
</span>
</div>
<div class="js-file-content">
<div class="data highlight empty"> <div class="data highlight empty">
Git LFS file not shown Git LFS file not shown
</div> </div>
@ -30,6 +58,21 @@ const githubPullHtmlDocument = parser.parseFromString(
) )
const githubPullFilesSample: DiffEntry[] = [ const githubPullFilesSample: DiffEntry[] = [
{
sha: 'c24ca35738a99e6bf834e0ee141db27c62fce499',
filename: 'samples/file_center_of_mass/output.json',
status: 'modified',
additions: 3,
deletions: 3,
changes: 6,
blob_url:
'https://github.com/KittyCAD/litterbox/blob/11510a02d8294cac5943b8ebdc416170f5b738b5/samples%2Ffile_center_of_mass%2Foutput.json',
raw_url:
'https://github.com/KittyCAD/litterbox/raw/11510a02d8294cac5943b8ebdc416170f5b738b5/samples%2Ffile_center_of_mass%2Foutput.json',
contents_url:
'https://api.github.com/repos/KittyCAD/litterbox/contents/samples%2Ffile_center_of_mass%2Foutput.json?ref=11510a02d8294cac5943b8ebdc416170f5b738b5',
patch: '@@ -1,8 +1,8 @@\n {\n "title": "output.json",\n "center_of_mass": [\n- -1.7249649e-08,\n- 2.96097,\n- -0.36378\n+ -0.12732863,\n+ 1.0363415,\n+ -9.5138624e-08\n ]\n }\n\\ No newline at end of file',
},
{ {
sha: '2f35d962a711bea7a8bf57481b8717f7dedbe1c5', sha: '2f35d962a711bea7a8bf57481b8717f7dedbe1c5',
filename: 'samples/file_center_of_mass/output.obj', filename: 'samples/file_center_of_mass/output.obj',
@ -45,12 +88,27 @@ const githubPullFilesSample: DiffEntry[] = [
'https://api.github.com/repos/KittyCAD/litterbox/contents/samples%2Ffile_center_of_mass%2Foutput.obj?ref=11510a02d8294cac5943b8ebdc416170f5b738b5', 'https://api.github.com/repos/KittyCAD/litterbox/contents/samples%2Ffile_center_of_mass%2Foutput.obj?ref=11510a02d8294cac5943b8ebdc416170f5b738b5',
patch: '@@ -1,3 +1,3 @@\n version https://git-lfs.github.com/spec/v1\n-oid sha256:2a07f53add3eee88b80a0bbe0412cf91df3d3bd9d45934ce849e0440eff90ee1\n-size 62122\n+oid sha256:0c0eb961e7e0589d83693335408b90d3b8adae9f4054c3e396c6eedbc5ed16ec\n+size 62545', patch: '@@ -1,3 +1,3 @@\n version https://git-lfs.github.com/spec/v1\n-oid sha256:2a07f53add3eee88b80a0bbe0412cf91df3d3bd9d45934ce849e0440eff90ee1\n-size 62122\n+oid sha256:0c0eb961e7e0589d83693335408b90d3b8adae9f4054c3e396c6eedbc5ed16ec\n+size 62545',
}, },
{
sha: '2f35d962a711bea7a8bf57481b8717f7dedbe1c5',
filename: 'seesaw.obj',
status: 'modified',
additions: 2,
deletions: 2,
changes: 4,
blob_url:
'https://github.com/KittyCAD/litterbox/blob/11510a02d8294cac5943b8ebdc416170f5b738b5/seesaw.obj',
raw_url:
'https://github.com/KittyCAD/litterbox/raw/11510a02d8294cac5943b8ebdc416170f5b738b5/seesaw.obj',
contents_url:
'https://api.github.com/repos/KittyCAD/litterbox/contents/seesaw.obj?ref=11510a02d8294cac5943b8ebdc416170f5b738b5',
patch: '@@ -1,3 +1,3 @@\n version https://git-lfs.github.com/spec/v1\n-oid sha256:2a07f53add3eee88b80a0bbe0412cf91df3d3bd9d45934ce849e0440eff90ee1\n-size 62122\n+oid sha256:0c0eb961e7e0589d83693335408b90d3b8adae9f4054c3e396c6eedbc5ed16ec\n+size 62545',
},
] ]
describe('Function getGithubUrlParams', () => { describe('Function getGithubPullUrlParams', () => {
it('gets params out of a valid github pull request link', () => { it('gets params out of a valid github pull request link', () => {
const pullUrl = 'https://github.com/KittyCAD/kittycad.ts/pull/67/files' const url = 'https://github.com/KittyCAD/kittycad.ts/pull/67/files'
const params = getGithubUrlParams(pullUrl) const params = getGithubPullUrlParams(url)
expect(params).toBeDefined() expect(params).toBeDefined()
const { owner, repo, pull } = params! const { owner, repo, pull } = params!
expect(owner).toEqual('KittyCAD') expect(owner).toEqual('KittyCAD')
@ -59,31 +117,50 @@ describe('Function getGithubUrlParams', () => {
}) })
it("doesn't match other URLs", () => { it("doesn't match other URLs", () => {
expect(getGithubUrlParams('http://google.com')).toBeUndefined() expect(getGithubPullUrlParams('http://google.com')).toBeUndefined()
expect( expect(
getGithubUrlParams('https://github.com/KittyCAD/litterbox') getGithubPullUrlParams('https://github.com/KittyCAD/litterbox')
).toBeUndefined()
})
})
describe('Function getGithubCommitUrlParams', () => {
it('gets params out of a valid github commit link', () => {
const url =
'https://github.com/KittyCAD/litterbox/commit/4ddf899550addf41d6bf1b790ce79e46501411b3'
const params = getGithubCommitUrlParams(url)
expect(params).toBeDefined()
const { owner, repo, sha } = params!
expect(owner).toEqual('KittyCAD')
expect(repo).toEqual('litterbox')
expect(sha).toEqual('4ddf899550addf41d6bf1b790ce79e46501411b3')
})
it("doesn't match other URLs", () => {
expect(getGithubPullUrlParams('http://google.com')).toBeUndefined()
expect(
getGithubPullUrlParams('https://github.com/KittyCAD/litterbox')
).toBeUndefined() ).toBeUndefined()
}) })
}) })
it('finds web elements for supported files', () => { it('finds web elements for supported files', () => {
const elements = getWebPullElements(githubPullHtmlDocument) const elements = getSupportedWebDiffElements(githubPullHtmlDocument)
expect(elements).toHaveLength(1) expect(elements).toHaveLength(2)
}) })
it('finds the filename of a supported file element', () => { it('finds the filename of a supported file element', () => {
const elements = getWebPullElements(githubPullHtmlDocument) const elements = getSupportedWebDiffElements(githubPullHtmlDocument)
const filename = getElementFilename(elements[0]) const filename = getElementFilename(elements[0])
expect(filename).toEqual('samples/file_center_of_mass/output.obj') expect(filename).toEqual('samples/file_center_of_mass/output.obj')
}) })
it('finds injectable elements from html and api results', () => { it('finds injectable elements from html and api results', () => {
const elements = getWebPullElements(githubPullHtmlDocument) const injectableElements = mapInjectableDiffElements(
const injectableElements = getInjectablePullElements( githubPullHtmlDocument,
elements,
githubPullFilesSample githubPullFilesSample
) )
expect(injectableElements).toHaveLength(1) expect(injectableElements).toHaveLength(2)
const { element, file } = injectableElements[0] const { element, file } = injectableElements[0]
expect(element).toBeDefined() expect(element).toBeDefined()
expect(file).toBeDefined() expect(file).toBeDefined()

View File

@ -1,16 +1,15 @@
import { supportedSrcFormats } from './diff' import { isFilenameSupported, supportedSrcFormats } from './diff'
import { DiffEntry } from './types' import { DiffEntry } from './types'
export type GithubUrlParams = export type GithubPullUrlParams = {
| {
owner: string owner: string
repo: string repo: string
pull: number pull: number
} }
| undefined
export function getGithubUrlParams(url: string): GithubUrlParams { export function getGithubPullUrlParams(
// TODO: support commit diff url: string
): GithubPullUrlParams | undefined {
const pullRe = const pullRe =
/https:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\/pull\/(\d+)\/files/ /https:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\/pull\/(\d+)\/files/
const result = pullRe.exec(url) const result = pullRe.exec(url)
@ -19,10 +18,32 @@ export function getGithubUrlParams(url: string): GithubUrlParams {
} }
const [, owner, repo, pull] = result const [, owner, repo, pull] = result
console.log('Found a supported Github Pull Request URL:', owner, repo, pull)
return { owner, repo, pull: parseInt(pull) } return { owner, repo, pull: parseInt(pull) }
} }
export function getWebPullElements(document: Document): HTMLElement[] { export type GithubCommitUrlParams = {
owner: string
repo: string
sha: string
}
export function getGithubCommitUrlParams(
url: string
): GithubCommitUrlParams | undefined {
const pullRe =
/https:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)\/commit\/(\w+)/
const result = pullRe.exec(url)
if (!result) {
return undefined
}
const [, owner, repo, sha] = result
console.log('Found a supported Github Commit URL:', owner, repo, sha)
return { owner, repo, sha }
}
export function getSupportedWebDiffElements(document: Document): HTMLElement[] {
const fileTypeSelectors = Array.from(supportedSrcFormats).map( const fileTypeSelectors = Array.from(supportedSrcFormats).map(
t => `.file[data-file-type=".${t}"]` t => `.file[data-file-type=".${t}"]`
) )
@ -37,19 +58,25 @@ export function getElementFilename(element: HTMLElement) {
return titleElement.getAttribute('title') return titleElement.getAttribute('title')
} }
export function getInjectablePullElements( export function mapInjectableDiffElements(
elements: HTMLElement[], document: Document,
files: DiffEntry[] files: DiffEntry[]
) { ) {
if (elements.length !== files.length) { const supportedFiles = files.filter(f => isFilenameSupported(f.filename))
console.log(`Found ${supportedFiles.length} supported files with the API`)
const supportedElements = getSupportedWebDiffElements(document)
console.log(`Found ${supportedElements.length} elements in the web page`)
if (supportedElements.length !== supportedFiles.length) {
throw Error( throw Error(
`elements and files have different length. Got ${elements.length} and ${files.length}` `elements and files have different length. Got ${supportedElements.length} and ${files.length}`
) )
} }
const injectableElements = [] const injectableElements: { element: HTMLElement; file: DiffEntry }[] = []
for (const [index, element] of elements.entries()) { for (const [index, element] of supportedElements.entries()) {
const apiFile = files[index] const apiFile = supportedFiles[index]
const filename = getElementFilename(element) const filename = getElementFilename(element)
if (filename !== apiFile.filename) { if (filename !== apiFile.filename) {
throw Error( throw Error(