From 57fa0319209600b4214f0c97223542e004721cd6 Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Sun, 27 Aug 2023 18:10:57 +0100 Subject: [PATCH 1/6] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 394e386..386cb0b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # GitHub +1 Chrome Extension -Enriches GitHub open pull requests list page with number of files changed, lines added, and lines deleted. \ No newline at end of file +Enriches GitHub open pull requests list page with number of files changed, lines added, and lines deleted. From 6199931e36bebc126fc8e38ad878a12d2e180f3a Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Sun, 27 Aug 2023 20:56:17 +0100 Subject: [PATCH 2/6] feat: send notification upon failure --- background.js | 15 +++++++++++++++ manifest.json | 2 +- script.js | 52 ++++++++++++++++++++++++++++++--------------------- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/background.js b/background.js index 0d93613..50174cc 100644 --- a/background.js +++ b/background.js @@ -1,5 +1,20 @@ const REGEX = /^(https:\/\/)?github.com\/[^\/]+\/[^\/]+\/pulls/ +const NOTIFICATION_ID = 'failure' +const NOTIFICATION = { + type: 'basic', + iconUrl: 'logo.png', + title: `GitHub +1 Failure`, + message: + 'Please verify that the given access token has sufficient access (scope "repo" is required).', +} + +chrome.runtime.onMessage.addListener(({ success }) => + success + ? chrome.notifications.clear(NOTIFICATION_ID) + : chrome.notifications.create(NOTIFICATION_ID, NOTIFICATION) +) + chrome.tabs.onUpdated.addListener( (id, changes) => REGEX.test(changes.title) && chrome.tabs.sendMessage(id, {}) ) diff --git a/manifest.json b/manifest.json index 92024a0..f6800cb 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "GitHub +1", "description": "Enrich GitHub open pull requests list page with number of files changed, lines added, and lines deleted.", "version": "1.0", - "permissions": ["storage", "tabs"], + "permissions": ["storage", "tabs", "notifications"], "host_permissions": ["https://api.github.com/graphql"], "action": { "default_icon": "logo.png" diff --git a/script.js b/script.js index b97bbf7..630b811 100644 --- a/script.js +++ b/script.js @@ -3,10 +3,15 @@ function get(token, query) { const request = new XMLHttpRequest() request.onreadystatechange = () => { if (request.readyState == 4) { - if (request.status == 200) - resolve(JSON.parse(request.responseText).data) - else - reject(request.status) + if (request.status != 200) { + reject() + } else { + const body = JSON.parse(request.responseText) + if (body.errors?.length > 0) + reject() + else + resolve(body.data) + } } } request.open('POST', 'https://api.github.com/graphql', true) @@ -71,23 +76,28 @@ function injectHtml(div, pr) { const REGEX = /^\/([^\/]+)\/([^\/]+)\/pulls$/ async function run() { - const { token } = await chrome.storage.sync.get('token') - const match = document.location.pathname.match(REGEX) - if (!token || !match) - return - - const [, org, repo] = match - - const total = await getOpenPullRequestCount(token, org, repo) - const prs = await getOpenPullRequests(token, org, repo, total) - - const divs = document.body.querySelector('div[aria-label="Issues"]').children.item(0).children - - for (const div of divs) { - const [, id] = div.id.match(/issue_(\d+)/) - const pr = prs[id] - if (pr) - injectHtml(div, pr) + try { + const { token } = await chrome.storage.sync.get('token') + const match = document.location.pathname.match(REGEX) + if (!token || !match) + return + + const [, org, repo] = match + + const total = await getOpenPullRequestCount(token, org, repo) + const prs = await getOpenPullRequests(token, org, repo, total) + + const divs = document.body.querySelector('div[aria-label="Issues"]').children.item(0).children + + for (const div of divs) { + const [, id] = div.id.match(/issue_(\d+)/) + const pr = prs[id] + if (pr) + injectHtml(div, pr) + } + chrome.runtime.sendMessage({ success: true }) + } catch (error) { + chrome.runtime.sendMessage({ success: false }) } } From f24bd2a2d4dc6061518d5c7e52ad4db69b5df4b7 Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Mon, 28 Aug 2023 15:25:26 +0100 Subject: [PATCH 3/6] fix: add pagination to graphql requests with more than 100 prs --- script.js => content.js | 60 +++++++++++++++++++++-------------------- manifest.json | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) rename script.js => content.js (70%) diff --git a/script.js b/content.js similarity index 70% rename from script.js rename to content.js index 630b811..cd57c0f 100644 --- a/script.js +++ b/content.js @@ -1,3 +1,6 @@ +const PATH_REGEX = /^\/([^\/]+)\/([^\/]+)\/pulls$/ +const PAGE_SIZE = 100 + function get(token, query) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest() @@ -20,30 +23,30 @@ function get(token, query) { }) } -async function getOpenPullRequestCount(token, org, repo) { - const query = `repository(owner: "${org}", name: "${repo}") { - pullRequests(states: OPEN) { - totalCount - } - }` - return (await get(token, query)).repository.pullRequests.totalCount -} - -async function getOpenPullRequests(token, org, repo, total) { - const query = `repository(owner: "${org}", name: "${repo}") { - pullRequests(last: ${total}, states: OPEN) { - nodes { - number - changedFiles - additions - deletions +async function getOpenPullRequests(token, org, repo) { + const prs = {} + let cursor + do { + const query = `repository(owner: "${org}", name: "${repo}") { + pullRequests(first: ${PAGE_SIZE}${cursor ? `, after: "${cursor}"` : ''}, states: OPEN) { + pageInfo { + endCursor + hasNextPage + } + nodes { + number + changedFiles + additions + deletions + } } - } - }` - return (await get(token, query)).repository.pullRequests.nodes.reduce( - (prs, pr) => ({ ...prs, [pr.number]: pr }), - {} - ) + }` + const { pageInfo, nodes } = (await get(token, query)).repository.pullRequests + cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null + for (const { number, ...pr } of nodes) + prs[number] = pr + } while (cursor != null) + return prs } function appendSpan(div, style, id, value) { @@ -73,20 +76,18 @@ function injectHtml(div, pr) { } } -const REGEX = /^\/([^\/]+)\/([^\/]+)\/pulls$/ - async function run() { try { const { token } = await chrome.storage.sync.get('token') - const match = document.location.pathname.match(REGEX) + const match = document.location.pathname.match(PATH_REGEX) + if (!token || !match) return const [, org, repo] = match - const total = await getOpenPullRequestCount(token, org, repo) - const prs = await getOpenPullRequests(token, org, repo, total) - + const prs = await getOpenPullRequests(token, org, repo) + const divs = document.body.querySelector('div[aria-label="Issues"]').children.item(0).children for (const div of divs) { @@ -95,6 +96,7 @@ async function run() { if (pr) injectHtml(div, pr) } + chrome.runtime.sendMessage({ success: true }) } catch (error) { chrome.runtime.sendMessage({ success: false }) diff --git a/manifest.json b/manifest.json index f6800cb..92c8071 100644 --- a/manifest.json +++ b/manifest.json @@ -18,7 +18,7 @@ "content_scripts": [ { "matches": ["https://*.github.com/*"], - "js": ["script.js"] + "js": ["content.js"] } ] } From 9df2924e0cdbeb4195c753b9e2e4c87cf1544b3a Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Mon, 28 Aug 2023 23:26:43 +0100 Subject: [PATCH 4/6] feat: support generic /pulls endpoint --- background.js | 3 +- content.js | 81 ++++++++++++++++++++------------------------------- options.js | 22 +++++++------- 3 files changed, 43 insertions(+), 63 deletions(-) diff --git a/background.js b/background.js index 50174cc..bbf1bfa 100644 --- a/background.js +++ b/background.js @@ -1,6 +1,5 @@ -const REGEX = /^(https:\/\/)?github.com\/[^\/]+\/[^\/]+\/pulls/ +const REGEX = /(\/[^\/]+\/[^\/]+)?\/pulls/ -const NOTIFICATION_ID = 'failure' const NOTIFICATION = { type: 'basic', iconUrl: 'logo.png', diff --git a/content.js b/content.js index cd57c0f..ea2cbfb 100644 --- a/content.js +++ b/content.js @@ -1,5 +1,4 @@ -const PATH_REGEX = /^\/([^\/]+)\/([^\/]+)\/pulls$/ -const PAGE_SIZE = 100 +const REGEX = /^(?:\/([^\/]+)\/([^\/]+))?\/pulls$/ function get(token, query) { return new Promise((resolve, reject) => { @@ -10,10 +9,11 @@ function get(token, query) { reject() } else { const body = JSON.parse(request.responseText) - if (body.errors?.length > 0) + if (body.errors?.length > 0) { reject() - else + } else { resolve(body.data) + } } } } @@ -23,30 +23,15 @@ function get(token, query) { }) } -async function getOpenPullRequests(token, org, repo) { - const prs = {} - let cursor - do { - const query = `repository(owner: "${org}", name: "${repo}") { - pullRequests(first: ${PAGE_SIZE}${cursor ? `, after: "${cursor}"` : ''}, states: OPEN) { - pageInfo { - endCursor - hasNextPage - } - nodes { - number - changedFiles - additions - deletions - } - } - }` - const { pageInfo, nodes } = (await get(token, query)).repository.pullRequests - cursor = pageInfo.hasNextPage ? pageInfo.endCursor : null - for (const { number, ...pr } of nodes) - prs[number] = pr - } while (cursor != null) - return prs +async function getPullRequestDiffStat(token, org, repo, id) { + const query = `repository(owner: "${org}", name: "${repo}") { + pullRequest(number: ${id}) { + changedFiles + additions + deletions + } + }` + return (await get(token, query)).repository.pullRequest } function appendSpan(div, style, id, value) { @@ -61,42 +46,38 @@ function updateSpan(div, id, value) { div.querySelector(`[id="${id}"]`).textContent = value } -function injectHtml(div, pr) { +function injectHtml(div, diff) { if (!div.querySelector('[id="stats"]')) { const element = document.createElement('div') element.id = 'stats' - appendSpan(element, 'Counter', 'changedFiles', pr.changedFiles) - appendSpan(element, 'color-fg-success', 'additions', '+' + pr.additions) - appendSpan(element, 'color-fg-danger', 'deletions', '-' + pr.deletions) + appendSpan(element, 'Counter', 'changedFiles', diff.changedFiles) + appendSpan(element, 'color-fg-success', 'additions', '+' + diff.additions) + appendSpan(element, 'color-fg-danger', 'deletions', '-' + diff.deletions) div.querySelector('[class="opened-by"]').parentNode.append(element) } else { - updateSpan(div, 'changedFiles', pr.changedFiles) - updateSpan(div, 'additions', '+' + pr.additions) - updateSpan(div, 'deletions', '-' + pr.deletions) + updateSpan(div, 'changedFiles', diff.changedFiles) + updateSpan(div, 'additions', '+' + diff.additions) + updateSpan(div, 'deletions', '-' + diff.deletions) } } async function run() { try { const { token } = await chrome.storage.sync.get('token') - const match = document.location.pathname.match(PATH_REGEX) - - if (!token || !match) + const match = document.location.pathname.match(REGEX) + if (!token || !match) { return - - const [, org, repo] = match - - const prs = await getOpenPullRequests(token, org, repo) - - const divs = document.body.querySelector('div[aria-label="Issues"]').children.item(0).children - + } + const divs = document.body.querySelectorAll('div[id^=issue_]') + const promises = [] for (const div of divs) { - const [, id] = div.id.match(/issue_(\d+)/) - const pr = prs[id] - if (pr) - injectHtml(div, pr) + const [, id] = div.id.match(/^issue_(\d+)/) + const [, org, repo] = + match[0] === '/pulls' ? div.id.match(/^issue_\d+_([^_]+)_([^_]+)$/) : match + const promise = getPullRequestDiffStat(token, org, repo, id) + promises.concat(promise.then((diff) => injectHtml(div, diff))) } - + await Promise.all(promises) chrome.runtime.sendMessage({ success: true }) } catch (error) { chrome.runtime.sendMessage({ success: false }) diff --git a/options.js b/options.js index 532ce6b..a429ed1 100644 --- a/options.js +++ b/options.js @@ -1,16 +1,16 @@ -const restoreOptions = () => - chrome.storage.sync.get('token', ({ token }) => { - document.getElementById('token').value = token - }) +const MESSAGE_TIMEOUT = 2 * 1000 -const saveOptions = () => { - const token = document.getElementById('token').value +const restoreOptions = async () => { + const { token } = await chrome.storage.sync.get('token') + document.getElementById('token').value = token +} - chrome.storage.sync.set({ token }, () => { - const status = document.getElementById('status') - status.textContent = 'Token saved successfully' - setTimeout(() => (status.textContent = ''), 2 * 1000) - }) +const saveOptions = async () => { + const token = document.getElementById('token').value + await chrome.storage.sync.set({ token }) + const status = document.getElementById('status') + status.textContent = 'Token saved successfully' + setTimeout(() => (status.textContent = ''), MESSAGE_TIMEOUT) } document.addEventListener('DOMContentLoaded', restoreOptions) From 24cdfd7cc0f4fb08e068e309a33f739814b518f3 Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Sun, 3 Sep 2023 17:43:52 +0100 Subject: [PATCH 5/6] feat: add support for gitlab --- background.js | 11 +++--- content.js | 89 ++++------------------------------------------- manifest.json | 14 +++++--- options.html | 8 +++-- options.js | 22 ++++++++---- sources/github.js | 80 ++++++++++++++++++++++++++++++++++++++++++ sources/gitlab.js | 79 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 103 deletions(-) create mode 100644 sources/github.js create mode 100644 sources/gitlab.js diff --git a/background.js b/background.js index bbf1bfa..f794d63 100644 --- a/background.js +++ b/background.js @@ -3,15 +3,12 @@ const REGEX = /(\/[^\/]+\/[^\/]+)?\/pulls/ const NOTIFICATION = { type: 'basic', iconUrl: 'logo.png', - title: `GitHub +1 Failure`, - message: - 'Please verify that the given access token has sufficient access (scope "repo" is required).', + title: `Git +1 Failure`, + message: 'Please verify that the given credentials are correct and have sufficient access.', } -chrome.runtime.onMessage.addListener(({ success }) => - success - ? chrome.notifications.clear(NOTIFICATION_ID) - : chrome.notifications.create(NOTIFICATION_ID, NOTIFICATION) +chrome.runtime.onMessage.addListener(({ success, source }) => + success ? chrome.notifications.clear(source) : chrome.notifications.create(source, NOTIFICATION) ) chrome.tabs.onUpdated.addListener( diff --git a/content.js b/content.js index ea2cbfb..79b1149 100644 --- a/content.js +++ b/content.js @@ -1,87 +1,12 @@ -const REGEX = /^(?:\/([^\/]+)\/([^\/]+))?\/pulls$/ - -function get(token, query) { - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest() - request.onreadystatechange = () => { - if (request.readyState == 4) { - if (request.status != 200) { - reject() - } else { - const body = JSON.parse(request.responseText) - if (body.errors?.length > 0) { - reject() - } else { - resolve(body.data) - } - } - } - } - request.open('POST', 'https://api.github.com/graphql', true) - request.setRequestHeader('Authorization', `Bearer ${token}`) - request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) - }) -} - -async function getPullRequestDiffStat(token, org, repo, id) { - const query = `repository(owner: "${org}", name: "${repo}") { - pullRequest(number: ${id}) { - changedFiles - additions - deletions - } - }` - return (await get(token, query)).repository.pullRequest -} - -function appendSpan(div, style, id, value) { - const span = document.createElement('span') - span.className = `${style} ml-1` - span.id = id - span.textContent = value - div.append(span) -} - -function updateSpan(div, id, value) { - div.querySelector(`[id="${id}"]`).textContent = value -} - -function injectHtml(div, diff) { - if (!div.querySelector('[id="stats"]')) { - const element = document.createElement('div') - element.id = 'stats' - appendSpan(element, 'Counter', 'changedFiles', diff.changedFiles) - appendSpan(element, 'color-fg-success', 'additions', '+' + diff.additions) - appendSpan(element, 'color-fg-danger', 'deletions', '-' + diff.deletions) - div.querySelector('[class="opened-by"]').parentNode.append(element) - } else { - updateSpan(div, 'changedFiles', diff.changedFiles) - updateSpan(div, 'additions', '+' + diff.additions) - updateSpan(div, 'deletions', '-' + diff.deletions) - } -} +const DOMAIN_REGEX = /([^\.]+)\.\w+$/ async function run() { - try { - const { token } = await chrome.storage.sync.get('token') - const match = document.location.pathname.match(REGEX) - if (!token || !match) { - return - } - const divs = document.body.querySelectorAll('div[id^=issue_]') - const promises = [] - for (const div of divs) { - const [, id] = div.id.match(/^issue_(\d+)/) - const [, org, repo] = - match[0] === '/pulls' ? div.id.match(/^issue_\d+_([^_]+)_([^_]+)$/) : match - const promise = getPullRequestDiffStat(token, org, repo, id) - promises.concat(promise.then((diff) => injectHtml(div, diff))) - } - await Promise.all(promises) - chrome.runtime.sendMessage({ success: true }) - } catch (error) { - chrome.runtime.sendMessage({ success: false }) - } + const [, source] = document.location.hostname.match(DOMAIN_REGEX) + const { inject } = await import(chrome.runtime.getURL(`sources/${source}.js`)) + const { options } = await chrome.storage.sync.get('options') + await inject(options ?? {}, document.location.pathname) + .then(() => chrome.runtime.sendMessage({ success: true, source })) + .catch(() => chrome.runtime.sendMessage({ success: false, source })) } chrome.runtime.onMessage.addListener(run) diff --git a/manifest.json b/manifest.json index 92c8071..db286bc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 3, - "name": "GitHub +1", - "description": "Enrich GitHub open pull requests list page with number of files changed, lines added, and lines deleted.", + "name": "Git +1", + "description": "Enrich Git pull/merge requests lists with number of files changed, lines added, and lines deleted.", "version": "1.0", "permissions": ["storage", "tabs", "notifications"], - "host_permissions": ["https://api.github.com/graphql"], + "host_permissions": ["https://api.github.com/graphql", "https://gitlab.com/api/graphql"], "action": { "default_icon": "logo.png" }, @@ -17,8 +17,14 @@ }, "content_scripts": [ { - "matches": ["https://*.github.com/*"], + "matches": ["https://*.github.com/*", "https://*.gitlab.com/*"], "js": ["content.js"] } + ], + "web_accessible_resources": [ + { + "matches": ["https://*/*"], + "resources": ["sources/*.js"] + } ] } diff --git a/options.html b/options.html index 8fde4b8..2cd9549 100644 --- a/options.html +++ b/options.html @@ -1,11 +1,13 @@ - GitHub +1 Extension Options + Git +1 Extension Options - - + +

+ +

diff --git a/options.js b/options.js index a429ed1..26c52fe 100644 --- a/options.js +++ b/options.js @@ -1,15 +1,23 @@ const MESSAGE_TIMEOUT = 2 * 1000 -const restoreOptions = async () => { - const { token } = await chrome.storage.sync.get('token') - document.getElementById('token').value = token +async function restoreOptions() { + const { options } = await chrome.storage.sync.get('options') + document.getElementById('github_token').value = options?.github?.token || '' + document.getElementById('gitlab_token').value = options?.gitlab?.token || '' } -const saveOptions = async () => { - const token = document.getElementById('token').value - await chrome.storage.sync.set({ token }) +async function saveOptions() { + const options = { + github: { + token: document.getElementById('github_token').value, + }, + gitlab: { + token: document.getElementById('gitlab_token').value, + }, + } + await chrome.storage.sync.set({ options }) const status = document.getElementById('status') - status.textContent = 'Token saved successfully' + status.textContent = 'Options saved successfully' setTimeout(() => (status.textContent = ''), MESSAGE_TIMEOUT) } diff --git a/sources/github.js b/sources/github.js new file mode 100644 index 0000000..960d475 --- /dev/null +++ b/sources/github.js @@ -0,0 +1,80 @@ +const REGEX = /^(?:\/([^\/]+)\/([^\/]+))?\/pulls$/ + +function get(token, query) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.onreadystatechange = () => { + if (request.readyState == 4) { + if (request.status != 200) { + reject() + } else { + const body = JSON.parse(request.responseText) + if (body.errors?.length > 0) { + reject() + } else { + resolve(body.data) + } + } + } + } + request.open('POST', 'https://api.github.com/graphql', true) + request.setRequestHeader('Authorization', `Bearer ${token}`) + request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) + }) +} + +async function getDiffStats(token, org, repo, id) { + const query = `repository(owner: "${org}", name: "${repo}") { + pullRequest(number: ${id}) { + changedFiles + additions + deletions + } + }` + return (await get(token, query)).repository.pullRequest +} + +function appendSpan(item, style, id, value) { + const span = document.createElement('span') + span.className = `${style} ml-1` + span.id = id + span.textContent = value + item.append(span) +} + +function updateSpan(item, id, value) { + item.querySelector(`[id="${id}"]`).textContent = value +} + +function injectHtml(item, stats) { + if (!item.querySelector('[id="stats"]')) { + const element = document.createElement('span') + element.id = 'stats' + appendSpan(element, 'Counter', 'files', stats.changedFiles) + appendSpan(element, 'color-fg-success', 'additions', '+' + stats.additions) + appendSpan(element, 'color-fg-danger', 'deletions', '-' + stats.deletions) + item.querySelector('[class="opened-by"]').parentNode.append(element) + } else { + updateSpan(item, 'files', stats.changedFiles) + updateSpan(item, 'additions', '+' + stats.additions) + updateSpan(item, 'deletions', '-' + stats.deletions) + } +} + +export async function inject(options, path) { + const { token } = options.github ?? {} + const match = path.match(REGEX) + if (!token || !match) { + return + } + const items = document.body.querySelectorAll('div[id^=issue_]') + const promises = [] + for (const item of items) { + const [, id] = item.id.match(/^issue_(\d+)/) + const [, org, repo] = + match[0] === '/pulls' ? item.id.match(/^issue_\d+_([^_]+)_([^_]+)$/) : match + const promise = getDiffStats(token, org, repo, id) + promises.concat(promise.then((diff) => injectHtml(item, diff))) + } + await Promise.all(promises) +} diff --git a/sources/gitlab.js b/sources/gitlab.js new file mode 100644 index 0000000..366d681 --- /dev/null +++ b/sources/gitlab.js @@ -0,0 +1,79 @@ +const REGEX = /^(\/[^\/]+\/[^\/]+\/-\/merge_requests)|(\/dashboard\/merge_requests)$/ + +function get(token, query) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.onreadystatechange = () => { + if (request.readyState == 4) { + if (request.status != 200) { + reject() + } else { + const body = JSON.parse(request.responseText) + if (body.errors?.length > 0) { + reject() + } else { + resolve(body.data) + } + } + } + } + request.open('POST', 'https://gitlab.com/api/graphql', true) + request.setRequestHeader('Authorization', `Bearer ${token}`) + request.setRequestHeader('Content-Type', 'application/json') + request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) + }) +} + +async function getDiffStats(token, id) { + const query = `mergeRequest(id: "gid://gitlab/MergeRequest/${id}") { + diffStatsSummary { + fileCount + additions + deletions + } + }` + return (await get(token, query)).mergeRequest.diffStatsSummary +} + +function appendSpan(item, style, id, value) { + const span = document.createElement('span') + span.className = `${style} ml-1` + span.id = id + span.textContent = value + item.append(span) +} + +function updateSpan(item, id, value) { + item.querySelector(`[id="${id}"]`).textContent = value +} + +function injectHtml(item, stats) { + if (!item.querySelector('[id="stats"]')) { + const element = document.createElement('span') + element.id = 'stats' + appendSpan(element, 'gl-badge badge badge-pill badge-muted sm', 'files', stats.fileCount) + appendSpan(element, 'gl-text-green-600 bold', 'additions', '+' + stats.additions) + appendSpan(element, 'gl-text-red-500 bold', 'deletions', '-' + stats.deletions) + item.querySelector('[class*="issuable-authored"]').append(element) + } else { + updateSpan(item, 'files', stats.fileCount) + updateSpan(item, 'additions', '+' + stats.additions) + updateSpan(item, 'deletions', '-' + stats.deletions) + } +} + +export async function inject(options, path) { + const { token } = options?.gitlab ?? {} + const match = REGEX.test(path) + if (!token || !match) { + return + } + const items = document.body.querySelectorAll('li[id^=merge_request_]') + const promises = [] + for (const item of items) { + const id = item.getAttribute('data-id') + const promise = getDiffStats(token, id) + promises.concat(promise.then((stats) => injectHtml(item, stats))) + } + await Promise.all(promises) +} From a42d92b11f612789985e5ba570741e99d496e53a Mon Sep 17 00:00:00 2001 From: Luana Silva Date: Sun, 3 Sep 2023 17:43:52 +0100 Subject: [PATCH 6/6] feat: add support for gitlab --- background.js | 11 +++--- content.js | 89 ++++------------------------------------------- manifest.json | 14 +++++--- options.html | 8 +++-- options.js | 22 ++++++++---- sources/github.js | 80 ++++++++++++++++++++++++++++++++++++++++++ sources/gitlab.js | 79 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 103 deletions(-) create mode 100644 sources/github.js create mode 100644 sources/gitlab.js diff --git a/background.js b/background.js index bbf1bfa..f794d63 100644 --- a/background.js +++ b/background.js @@ -3,15 +3,12 @@ const REGEX = /(\/[^\/]+\/[^\/]+)?\/pulls/ const NOTIFICATION = { type: 'basic', iconUrl: 'logo.png', - title: `GitHub +1 Failure`, - message: - 'Please verify that the given access token has sufficient access (scope "repo" is required).', + title: `Git +1 Failure`, + message: 'Please verify that the given credentials are correct and have sufficient access.', } -chrome.runtime.onMessage.addListener(({ success }) => - success - ? chrome.notifications.clear(NOTIFICATION_ID) - : chrome.notifications.create(NOTIFICATION_ID, NOTIFICATION) +chrome.runtime.onMessage.addListener(({ success, source }) => + success ? chrome.notifications.clear(source) : chrome.notifications.create(source, NOTIFICATION) ) chrome.tabs.onUpdated.addListener( diff --git a/content.js b/content.js index ea2cbfb..79b1149 100644 --- a/content.js +++ b/content.js @@ -1,87 +1,12 @@ -const REGEX = /^(?:\/([^\/]+)\/([^\/]+))?\/pulls$/ - -function get(token, query) { - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest() - request.onreadystatechange = () => { - if (request.readyState == 4) { - if (request.status != 200) { - reject() - } else { - const body = JSON.parse(request.responseText) - if (body.errors?.length > 0) { - reject() - } else { - resolve(body.data) - } - } - } - } - request.open('POST', 'https://api.github.com/graphql', true) - request.setRequestHeader('Authorization', `Bearer ${token}`) - request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) - }) -} - -async function getPullRequestDiffStat(token, org, repo, id) { - const query = `repository(owner: "${org}", name: "${repo}") { - pullRequest(number: ${id}) { - changedFiles - additions - deletions - } - }` - return (await get(token, query)).repository.pullRequest -} - -function appendSpan(div, style, id, value) { - const span = document.createElement('span') - span.className = `${style} ml-1` - span.id = id - span.textContent = value - div.append(span) -} - -function updateSpan(div, id, value) { - div.querySelector(`[id="${id}"]`).textContent = value -} - -function injectHtml(div, diff) { - if (!div.querySelector('[id="stats"]')) { - const element = document.createElement('div') - element.id = 'stats' - appendSpan(element, 'Counter', 'changedFiles', diff.changedFiles) - appendSpan(element, 'color-fg-success', 'additions', '+' + diff.additions) - appendSpan(element, 'color-fg-danger', 'deletions', '-' + diff.deletions) - div.querySelector('[class="opened-by"]').parentNode.append(element) - } else { - updateSpan(div, 'changedFiles', diff.changedFiles) - updateSpan(div, 'additions', '+' + diff.additions) - updateSpan(div, 'deletions', '-' + diff.deletions) - } -} +const DOMAIN_REGEX = /([^\.]+)\.\w+$/ async function run() { - try { - const { token } = await chrome.storage.sync.get('token') - const match = document.location.pathname.match(REGEX) - if (!token || !match) { - return - } - const divs = document.body.querySelectorAll('div[id^=issue_]') - const promises = [] - for (const div of divs) { - const [, id] = div.id.match(/^issue_(\d+)/) - const [, org, repo] = - match[0] === '/pulls' ? div.id.match(/^issue_\d+_([^_]+)_([^_]+)$/) : match - const promise = getPullRequestDiffStat(token, org, repo, id) - promises.concat(promise.then((diff) => injectHtml(div, diff))) - } - await Promise.all(promises) - chrome.runtime.sendMessage({ success: true }) - } catch (error) { - chrome.runtime.sendMessage({ success: false }) - } + const [, source] = document.location.hostname.match(DOMAIN_REGEX) + const { inject } = await import(chrome.runtime.getURL(`sources/${source}.js`)) + const { options } = await chrome.storage.sync.get('options') + await inject(options ?? {}, document.location.pathname) + .then(() => chrome.runtime.sendMessage({ success: true, source })) + .catch(() => chrome.runtime.sendMessage({ success: false, source })) } chrome.runtime.onMessage.addListener(run) diff --git a/manifest.json b/manifest.json index 92c8071..db286bc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 3, - "name": "GitHub +1", - "description": "Enrich GitHub open pull requests list page with number of files changed, lines added, and lines deleted.", + "name": "Git +1", + "description": "Enrich Git pull/merge requests lists with number of files changed, lines added, and lines deleted.", "version": "1.0", "permissions": ["storage", "tabs", "notifications"], - "host_permissions": ["https://api.github.com/graphql"], + "host_permissions": ["https://api.github.com/graphql", "https://gitlab.com/api/graphql"], "action": { "default_icon": "logo.png" }, @@ -17,8 +17,14 @@ }, "content_scripts": [ { - "matches": ["https://*.github.com/*"], + "matches": ["https://*.github.com/*", "https://*.gitlab.com/*"], "js": ["content.js"] } + ], + "web_accessible_resources": [ + { + "matches": ["https://*/*"], + "resources": ["sources/*.js"] + } ] } diff --git a/options.html b/options.html index 8fde4b8..2cd9549 100644 --- a/options.html +++ b/options.html @@ -1,11 +1,13 @@ - GitHub +1 Extension Options + Git +1 Extension Options - - + +

+ +

diff --git a/options.js b/options.js index a429ed1..26c52fe 100644 --- a/options.js +++ b/options.js @@ -1,15 +1,23 @@ const MESSAGE_TIMEOUT = 2 * 1000 -const restoreOptions = async () => { - const { token } = await chrome.storage.sync.get('token') - document.getElementById('token').value = token +async function restoreOptions() { + const { options } = await chrome.storage.sync.get('options') + document.getElementById('github_token').value = options?.github?.token || '' + document.getElementById('gitlab_token').value = options?.gitlab?.token || '' } -const saveOptions = async () => { - const token = document.getElementById('token').value - await chrome.storage.sync.set({ token }) +async function saveOptions() { + const options = { + github: { + token: document.getElementById('github_token').value, + }, + gitlab: { + token: document.getElementById('gitlab_token').value, + }, + } + await chrome.storage.sync.set({ options }) const status = document.getElementById('status') - status.textContent = 'Token saved successfully' + status.textContent = 'Options saved successfully' setTimeout(() => (status.textContent = ''), MESSAGE_TIMEOUT) } diff --git a/sources/github.js b/sources/github.js new file mode 100644 index 0000000..960d475 --- /dev/null +++ b/sources/github.js @@ -0,0 +1,80 @@ +const REGEX = /^(?:\/([^\/]+)\/([^\/]+))?\/pulls$/ + +function get(token, query) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.onreadystatechange = () => { + if (request.readyState == 4) { + if (request.status != 200) { + reject() + } else { + const body = JSON.parse(request.responseText) + if (body.errors?.length > 0) { + reject() + } else { + resolve(body.data) + } + } + } + } + request.open('POST', 'https://api.github.com/graphql', true) + request.setRequestHeader('Authorization', `Bearer ${token}`) + request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) + }) +} + +async function getDiffStats(token, org, repo, id) { + const query = `repository(owner: "${org}", name: "${repo}") { + pullRequest(number: ${id}) { + changedFiles + additions + deletions + } + }` + return (await get(token, query)).repository.pullRequest +} + +function appendSpan(item, style, id, value) { + const span = document.createElement('span') + span.className = `${style} ml-1` + span.id = id + span.textContent = value + item.append(span) +} + +function updateSpan(item, id, value) { + item.querySelector(`[id="${id}"]`).textContent = value +} + +function injectHtml(item, stats) { + if (!item.querySelector('[id="stats"]')) { + const element = document.createElement('span') + element.id = 'stats' + appendSpan(element, 'Counter', 'files', stats.changedFiles) + appendSpan(element, 'color-fg-success', 'additions', '+' + stats.additions) + appendSpan(element, 'color-fg-danger', 'deletions', '-' + stats.deletions) + item.querySelector('[class="opened-by"]').parentNode.append(element) + } else { + updateSpan(item, 'files', stats.changedFiles) + updateSpan(item, 'additions', '+' + stats.additions) + updateSpan(item, 'deletions', '-' + stats.deletions) + } +} + +export async function inject(options, path) { + const { token } = options.github ?? {} + const match = path.match(REGEX) + if (!token || !match) { + return + } + const items = document.body.querySelectorAll('div[id^=issue_]') + const promises = [] + for (const item of items) { + const [, id] = item.id.match(/^issue_(\d+)/) + const [, org, repo] = + match[0] === '/pulls' ? item.id.match(/^issue_\d+_([^_]+)_([^_]+)$/) : match + const promise = getDiffStats(token, org, repo, id) + promises.concat(promise.then((diff) => injectHtml(item, diff))) + } + await Promise.all(promises) +} diff --git a/sources/gitlab.js b/sources/gitlab.js new file mode 100644 index 0000000..366d681 --- /dev/null +++ b/sources/gitlab.js @@ -0,0 +1,79 @@ +const REGEX = /^(\/[^\/]+\/[^\/]+\/-\/merge_requests)|(\/dashboard\/merge_requests)$/ + +function get(token, query) { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.onreadystatechange = () => { + if (request.readyState == 4) { + if (request.status != 200) { + reject() + } else { + const body = JSON.parse(request.responseText) + if (body.errors?.length > 0) { + reject() + } else { + resolve(body.data) + } + } + } + } + request.open('POST', 'https://gitlab.com/api/graphql', true) + request.setRequestHeader('Authorization', `Bearer ${token}`) + request.setRequestHeader('Content-Type', 'application/json') + request.send(`{ "query": "{ ${query.replace(/\s+/g, ' ').replace(/"/g, '\\"')} }" }`) + }) +} + +async function getDiffStats(token, id) { + const query = `mergeRequest(id: "gid://gitlab/MergeRequest/${id}") { + diffStatsSummary { + fileCount + additions + deletions + } + }` + return (await get(token, query)).mergeRequest.diffStatsSummary +} + +function appendSpan(item, style, id, value) { + const span = document.createElement('span') + span.className = `${style} ml-1` + span.id = id + span.textContent = value + item.append(span) +} + +function updateSpan(item, id, value) { + item.querySelector(`[id="${id}"]`).textContent = value +} + +function injectHtml(item, stats) { + if (!item.querySelector('[id="stats"]')) { + const element = document.createElement('span') + element.id = 'stats' + appendSpan(element, 'gl-badge badge badge-pill badge-muted sm', 'files', stats.fileCount) + appendSpan(element, 'gl-text-green-600 bold', 'additions', '+' + stats.additions) + appendSpan(element, 'gl-text-red-500 bold', 'deletions', '-' + stats.deletions) + item.querySelector('[class*="issuable-authored"]').append(element) + } else { + updateSpan(item, 'files', stats.fileCount) + updateSpan(item, 'additions', '+' + stats.additions) + updateSpan(item, 'deletions', '-' + stats.deletions) + } +} + +export async function inject(options, path) { + const { token } = options?.gitlab ?? {} + const match = REGEX.test(path) + if (!token || !match) { + return + } + const items = document.body.querySelectorAll('li[id^=merge_request_]') + const promises = [] + for (const item of items) { + const id = item.getAttribute('data-id') + const promise = getDiffStats(token, id) + promises.concat(promise.then((stats) => injectHtml(item, stats))) + } + await Promise.all(promises) +}