Skip to content

Instantly share code, notes, and snippets.

@Kolobok12309
Last active April 11, 2025 06:55
Show Gist options
  • Save Kolobok12309/b6ba74e98a7981159361d6dfb3dbc3a4 to your computer and use it in GitHub Desktop.
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.
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());
}
}
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