Last active
April 11, 2025 06:55
-
-
Save Kolobok12309/b6ba74e98a7981159361d6dfb3dbc3a4 to your computer and use it in GitHub Desktop.
Github review has very slow gui for big pr's, this helpers fix it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class GithubReviewHelperUtils { | |
static sleep = (ms) => new Promise((res) => setTimeout(res, ms)); | |
static retry = async (fn, timesLimit = 100) => { | |
let counter = 0; | |
let res = null; | |
let success = false; | |
while (!success && counter < timesLimit) { | |
try { | |
res = await fn(); | |
success = true; | |
} catch (err) { | |
counter++; | |
if (counter >= timesLimit) { | |
console.error('limit'); | |
throw err; | |
} | |
} | |
} | |
return res; | |
}; | |
} | |
class GithubReviewHelper { | |
static #pageRegex = /github\.com\/(?<repo>.+\/.+)\/pull\/(?<prId>\d+)\/files/; | |
constructor() { | |
const match = location.href.match(GithubReviewHelper.#pageRegex); | |
if (!match) { | |
throw new Error( | |
`GithubReviewHelper launched not on github.com/{{ user }}/{{ repo }}/pull/{{ prId }}/files page`, | |
); | |
} | |
this.repo = match.groups.repo; | |
this.prId = match.groups.prId; | |
console.log(`Detect repo "${this.repo}" and pr ${this.prId}`); | |
} | |
async #viewFile(fileEl) { | |
const formData = new FormData(); | |
const viewForm = fileEl.querySelector('.js-toggle-user-reviewed-file-form'); | |
const tokenInput = viewForm.querySelector( | |
'input[type="hidden"][name="authenticity_token"]', | |
); | |
const pathInput = viewForm.querySelector( | |
'input[type="hidden"][name="path"]', | |
); | |
formData.append('authenticity_token', tokenInput.value); | |
formData.append('path', pathInput.value); | |
formData.append('viewed', 'viewed'); | |
console.log(`view: ${pathInput.value}`); | |
await fetch( | |
`https://github.com/${this.repo}/pull/${this.prId}/file_review`, | |
{ | |
method: 'POST', | |
body: formData, | |
headers: { | |
'x-requested-with': 'XMLHttpRequest', | |
}, | |
}, | |
); | |
} | |
#isNeedLoading(fileEl) { | |
return fileEl.classList.contains('hide-file-notes-toggle'); | |
} | |
#getFileName(fileEl) { | |
if (!fileEl) return null; | |
return fileEl.dataset.tagsearchPath; | |
} | |
async #startLoading(fileEl) { | |
console.log('startLoading', this.#getFileName(fileEl)); | |
const count = 500; | |
const step = 100; | |
let isNeedLoad = this.#isNeedLoading(fileEl); | |
let counter = 0; | |
if (!isNeedLoad) return; | |
const btn = fileEl.querySelector('button.load-diff-button'); | |
btn.click(); | |
while (isNeedLoad && counter < count) { | |
await GithubReviewHelperUtils.sleep(step); | |
isNeedLoad = this.#isNeedLoading(fileEl); | |
} | |
} | |
#getFileElByName(fileName) { | |
return document.querySelector( | |
`div.file[data-details-container-group="file"][data-tagsearch-path="${fileName}"]`, | |
); | |
} | |
#allSidebarLi = null; | |
#isWrapper(fileEl, iconTitle) { | |
const fileName = this.#getFileName(fileEl); | |
const allSidebarLi = this.#allSidebarLi || [ | |
...document.querySelectorAll( | |
'li.ActionList-item.ActionList-item--subItem', | |
), | |
]; | |
const found = allSidebarLi.find((liEl) => { | |
const span = liEl.querySelector('span[data-filterable-item-text]'); | |
if (!span) return false; | |
const text = span.textContent.trim(); | |
return text === fileName; | |
}); | |
if (!found) return false; | |
return !!found.querySelector( | |
`svg > use[href="#octicon_diff-${iconTitle}_16"]`, | |
); | |
} | |
#isNewFile = (fileEl) => this.#isWrapper(fileEl, 'added'); | |
#isDeletedFile = (fileEl) => this.#isWrapper(fileEl, 'removed'); | |
#isChangedFile = (fileEl) => this.#isWrapper(fileEl, 'modified'); | |
#isRenamedFile = (fileEl) => this.#isWrapper(fileEl, 'renamed'); | |
#getFileStat = (fileEl) => { | |
const span = fileEl.querySelector('span.diffstat'); | |
if (!span) return null; | |
return span.textContent.trim(); | |
}; | |
#checkTableRow = (tableRowEl, replacers = []) => { | |
if (!tableRowEl) return false; | |
const leftDeletion = tableRowEl.querySelector( | |
'td[data-split-side="left"].blob-code-deletion', | |
); | |
const leftEmpty = tableRowEl.querySelector( | |
'td[data-split-side="left"].empty-cell', | |
); | |
const rightAddition = tableRowEl.querySelector( | |
'td[data-split-side="right"].blob-code-addition', | |
); | |
const rightEmpty = tableRowEl.querySelector( | |
'td[data-split-side="right"].empty-cell', | |
); | |
if (leftEmpty || rightEmpty) return false; | |
if (!leftDeletion || !rightAddition) return true; | |
const leftChangeSpan = leftDeletion.querySelector('span.x'); | |
const rightChangeSpan = rightAddition.querySelector('span.x'); | |
if (!leftChangeSpan && !rightChangeSpan) return true; | |
if (!leftChangeSpan || !rightChangeSpan) return false; | |
const leftChange = leftChangeSpan.textContent.trim(); | |
const rightChange = rightChangeSpan.textContent.trim(); | |
return replacers.reduce((acc, replacer) => { | |
if (acc) return acc; | |
if (typeof replacer === 'function') { | |
return replacer(leftChange, rightChange); | |
} | |
return leftChange === replacer.left && rightChange === replacer.right; | |
}, false); | |
}; | |
/** | |
* | |
* @param replacers Список допустимых replacer | |
* | |
* @example | |
* [ | |
* { | |
* left: '@foo', | |
* right: '@bar', | |
* }, | |
* (left, right) => false, | |
* ] | |
*/ | |
checkModifiedFile(fileEl, replacers = []) { | |
if (!fileEl) return false; | |
const diffTable = fileEl.querySelector('table.diff-table'); | |
if (!diffTable) return false; | |
const tableRows = [...diffTable.querySelectorAll('tr[data-hunk]')]; | |
let result = true; | |
for (let tableRow of tableRows) { | |
result = this.#checkTableRow(tableRow, replacers); | |
if (!result) return false; | |
} | |
return result; | |
} | |
async startReview( | |
reviewCb = (fileEl, fileName, options) => {}, | |
{ forceLoad = false } = {}, | |
) { | |
const files = [ | |
...document.querySelectorAll( | |
'div.file[data-details-container-group="file"]:not([hidden])', | |
), | |
]; | |
await files.reduce(async (acc, fileEl, index) => { | |
await acc; | |
const fileName = this.#getFileName(fileEl); | |
try { | |
const isNew = this.#isNewFile(fileEl); | |
const isModified = this.#isChangedFile(fileEl); | |
const isDeleted = this.#isDeletedFile(fileEl); | |
const isRenamed = this.#isRenamedFile(fileEl); | |
if (forceLoad && this.#isNeedLoading(fileEl)) { | |
await this.#startLoading(fileEl); | |
} | |
console.log( | |
`check file(${index + 1}/${ | |
files.length | |
}): "${fileName}", n:${isNew}, m:${isModified}, d:${isDeleted}, mv:${isRenamed}`, | |
); | |
const viewFile = async (fileEl) => | |
GithubReviewHelperUtils.retry(async () => { | |
console.log('approve', this.#getFileName(fileEl)); | |
await GithubReviewHelperUtils.sleep(300); | |
await this.#viewFile(fileEl); | |
}); | |
const loadFile = async (fileEl) => { | |
if (forceLoad) return; | |
if (this.#isNeedLoading(fileEl)) { | |
await this.#startLoading(fileEl); | |
} | |
}; | |
await reviewCb(fileEl, fileName, { | |
viewFile, | |
loadFile, | |
isNew, | |
isModified, | |
isDeleted, | |
isRenamed, | |
}); | |
} catch (err) { | |
console.error(`Error while checking: "${fileName}"`); | |
throw err; | |
} | |
}, Promise.resolve()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class GithubReviewOptimizer { | |
#observer = null; | |
#fragment = new DocumentFragment(); | |
#elMap = new WeakMap(); | |
init() { | |
this.#observer = new IntersectionObserver( | |
(entries, observer) => { | |
requestAnimationFrame(() => | |
entries.forEach(({ target, isIntersecting }) => { | |
const { children, placeholder } = this.#elMap.get(target); | |
if (isIntersecting) { | |
this.#fragment.appendChild(placeholder); | |
target.appendChild(children); | |
} else { | |
placeholder.style.height = getComputedStyle(children).height; | |
this.#fragment.appendChild(children); | |
target.appendChild(placeholder); | |
} | |
}), | |
); | |
}, | |
{ | |
rootMargin: '600px', | |
threshold: 0, | |
}, | |
); | |
const files = [...document.querySelectorAll('copilot-diff-entry')]; | |
files.forEach((fileEl) => { | |
const placeholder = document.createElement('div'); | |
placeholder.classList.add('file'); | |
const children = fileEl.children[0]; | |
if (!children) return; | |
this.#fragment.appendChild(placeholder); | |
this.#elMap.set(fileEl, { | |
children, | |
placeholder, | |
}); | |
this.#observer.observe(fileEl); | |
}); | |
} | |
destroy() { | |
this.#observer.disconnect(); | |
const files = [...document.querySelectorAll('copilot-diff-entry')]; | |
requestAnimationFrame(() => { | |
files.forEach((fileEl) => { | |
const { children, placeholder } = this.#elMap.get(fileEl); | |
fileEl.appendChild(children); | |
placeholder.remove(); | |
}); | |
}); | |
this.#elMap = new WeakSet(); | |
} | |
#hiddenMap = new WeakMap(); | |
#hiddenFragment = new DocumentFragment(); | |
cleanHidden() { | |
const files = [ | |
...document.querySelectorAll( | |
'div.file[data-details-container-group="file"][hidden]', | |
), | |
]; | |
requestAnimationFrame(() => { | |
files.forEach((fileEl) => { | |
this.#hiddenMap.set(fileEl, fileEl.parentElement); | |
this.#hiddenFragment.appendChild(fileEl); | |
}); | |
}); | |
} | |
showHidden() { | |
requestAnimationFrame(() => { | |
[...this.#hiddenFragment.childNodes].forEach((fileEl) => { | |
const parent = this.#hiddenMap.get(fileEl); | |
parent.appendChild(fileEl); | |
}); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment