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.
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)
+}