diff --git a/readme.md b/readme.md index 84c5e0da14a2..70eb1647da08 100644 --- a/readme.md +++ b/readme.md @@ -99,7 +99,8 @@ GitHub Enterprise is also supported. More info in the options. - [Use the pull request title as the commit title when merging with `Squash and merge`](https://github.com/sindresorhus/refined-github/issues/276). - [View linked gists inline in comments.](https://user-images.githubusercontent.com/6978877/33911900-c62ee968-df8b-11e7-8685-506ffafc60b4.PNG) - [Avoid opening duplicate issues thanks to the list of possibly-related issues.](https://user-images.githubusercontent.com/29176678/37566899-85953e6e-2abf-11e8-9f0e-52d18c87bbe3.gif) -- [Use the pull request description as the commit message when merging with `Squash and merge`](https://github.com/sindresorhus/refined-github/issues/1322). +- [Use the pull request description as the commit message when merging with `Squash and merge`.](https://github.com/sindresorhus/refined-github/issues/1322). +- [Access related pages on 404 pages.](https://user-images.githubusercontent.com/1402241/46402857-7bdada80-c733-11e8-91a1-856573078ff5.png) ### More actions diff --git a/source/content.js b/source/content.js index 2aa4ba5716e1..65b8eae559a0 100644 --- a/source/content.js +++ b/source/content.js @@ -81,6 +81,7 @@ import hideCommentsFaster from './features/hide-comments-faster'; import linkifyCommitSha from './features/linkify-commit-sha'; import hideIssueListAutocomplete from './features/hide-issue-list-autocomplete'; import userProfileFollowerBadge from './features/user-profile-follower-badge'; +import usefulNotFoundPage from './features/useful-not-found-page'; import setDefaultRepositoriesTypeToSources from './features/set-default-repositories-type-to-sources'; import markPrivateOrgs from './features/mark-private-orgs'; import navigatePagesWithArrowKeys from './features/navigate-pages-with-arrow-keys'; @@ -95,10 +96,14 @@ window.select = select; async function init() { await safeElementReady('body'); - if (pageDetect.is404() || pageDetect.is500()) { + if (pageDetect.is500()) { return; } + if (pageDetect.is404()) { + enableFeature(usefulNotFoundPage); + return; + } if (document.body.classList.contains('logged-out')) { console.warn('%cRefined GitHub%c only works when you’re logged in to GitHub.', 'font-weight: bold', ''); return; diff --git a/source/features/add-branch-buttons.js b/source/features/add-branch-buttons.js index 2384e09e521f..31e412c51f23 100644 --- a/source/features/add-branch-buttons.js +++ b/source/features/add-branch-buttons.js @@ -2,17 +2,10 @@ import {h} from 'dom-chef'; import select from 'select-dom'; import compareVersions from 'tiny-version-compare'; import * as icons from '../libs/icons'; -import * as cache from '../libs/cache'; import {appendBefore} from '../libs/utils'; import {groupSiblings} from '../libs/group-buttons'; -import {getRepoURL, isRepoRoot, getOwnerAndRepo} from '../libs/page-detect'; - -// This regex should match all of these combinations: -// "This branch is even with master." -// "This branch is 1 commit behind master." -// "This branch is 1 commit ahead of master." -// "This branch is 1 commit ahead, 27 commits behind master." -const branchInfoRegex = /([^ ]+)\.$/; +import getDefaultBranch from '../libs/get-default-branch'; +import {getRepoURL, isRepoRoot} from '../libs/page-detect'; function getTagLink() { const tags = select @@ -45,38 +38,12 @@ function getTagLink() { return link; } -async function getDefaultBranchNameIfDifferent() { - const {ownerName, repoName} = getOwnerAndRepo(); - const cacheKey = `default-branch:${ownerName}/${repoName}`; - - // Return the cached name if it differs from the current one - const cachedName = await cache.get(cacheKey); - if (cachedName) { - const currentBranch = select('[data-hotkey="w"] span').textContent; - return cachedName === currentBranch ? false : cachedName; - } - - // We can find the name in the infobar, available in folder views - const branchInfo = select('.branch-infobar'); - if (!branchInfo) { - return; - } - - // Parse the infobar - const [, branchName] = branchInfo.textContent.trim().match(branchInfoRegex) || []; - if (branchName) { - cache.set(cacheKey, branchName, 1); - return branchName; - } -} - async function getDefaultBranchLink() { - if (select.exists('.repohead h1 .octicon-repo-forked')) { - return; // It's a fork, no "default branch" info available #1132 - } + const defaultBranch = await getDefaultBranch(); + const currentBranch = select('[data-hotkey="w"] span').textContent; - const branchName = await getDefaultBranchNameIfDifferent(); - if (!branchName) { + // Don't show the button if we’re already on the default branch + if (defaultBranch === currentBranch) { return; } @@ -84,7 +51,7 @@ async function getDefaultBranchLink() { if (isRepoRoot()) { url = `/${getRepoURL()}`; } else { - const branchLink = select(`.select-menu-item[data-name='${branchName}']`); + const branchLink = select(`.select-menu-item[data-name='${defaultBranch}']`); if (!branchLink) { return; } @@ -98,7 +65,7 @@ async function getDefaultBranchLink() { aria-label="Visit the default branch"> {icons.branch()} {' '} - {branchName} + {defaultBranch} ); } diff --git a/source/features/useful-not-found-page.js b/source/features/useful-not-found-page.js new file mode 100644 index 000000000000..6d32eccc6147 --- /dev/null +++ b/source/features/useful-not-found-page.js @@ -0,0 +1,104 @@ +/* +This feature adds more useful 404 (not found) page. +- Display the full URL clickable piece by piece +- Strikethrough all anchor that return a 404 status code +*/ + +import {h} from 'dom-chef'; +import select from 'select-dom'; +import {getCleanPathname} from '../libs/page-detect'; +import getDefaultBranch from '../libs/get-default-branch'; + +async function is404(url) { + const {status} = await fetch(url, {method: 'head'}); + return status === 404; +} + +function getStrikeThrough(text) { + return {text}; +} + +async function checkAnchor(anchor) { + if (await is404(anchor.href)) { + anchor.replaceWith(getStrikeThrough(anchor.textContent)); + } +} + +function parseCurrentURL() { + const parts = getCleanPathname().split('/'); + if (parts[2] === 'blob') { // Blob URLs are never useful + parts[2] = 'tree'; + } + return parts; +} + +// If the resource was deleted, link to the commit history +async function addCommitHistoryLink(bar) { + const parts = parseCurrentURL(); + if (parts[2] !== 'tree') { + return; + } + parts[2] = 'commits'; + const url = '/' + parts.join('/'); + if (await is404(url)) { + return; + } + bar.after( +

+ See also the file’s {commit history} +

+ ); +} + +// If the resource exists in the default branch, link to it +async function addDefaultBranchLink(bar) { + const parts = getCleanPathname().split('/'); + const branch = parts[3]; + if (!branch) { + return; + } + const defaultBranch = await getDefaultBranch(); + if (!defaultBranch || branch === defaultBranch) { + return; + } + parts[3] = defaultBranch; // Change branch + const url = '/' + parts.join('/'); + if (await is404(url)) { + return; + } + bar.after( +

+ See also the file on the {default branch} +

+ ); +} + +export default function () { + const parts = parseCurrentURL(); + const bar =

; + + for (const [i, part] of parts.entries()) { + if (i === 2 && part === 'tree') { + // `/tree/` is not a real part of the URL + continue; + } + if (i === parts.length - 1) { + // The last part of the URL is a known 404 + bar.append(' / ', getStrikeThrough(part)); + } else { + const pathname = '/' + parts.slice(0, i + 1).join('/'); + bar.append(i ? ' / ' : '', {part}); + } + } + + // NOTE: We need to append it after the parallax_wrapper because other elements might not be available yet. + select('#parallax_wrapper').after(bar); + + // Check parts from right to left; skip the last part + for (let i = bar.children.length - 2; i >= 0; i--) { + checkAnchor(bar.children[i]); + } + + addCommitHistoryLink(bar); + addDefaultBranchLink(bar); +} diff --git a/source/libs/cache.js b/source/libs/cache.js index f209037dbca8..92d190dbd1da 100644 --- a/source/libs/cache.js +++ b/source/libs/cache.js @@ -1,11 +1,12 @@ -export default async function getSet(key, getter, expiration) { +export async function getSet(key, getter, expiration) { const cache = await get(key); - if (cache === undefined) { - const value = getter(); - if (value !== undefined) { - await set(key, value, expiration); - return value; - } + if (cache !== undefined) { + return cache; + } + const value = await getter(); + if (value !== undefined) { + await set(key, value, expiration); + return value; } } @@ -16,7 +17,7 @@ export async function get(key) { }); // If it's not in the cache, it's best to return "undefined" - if (value === null) { + if (value === null || value === undefined) { return undefined; } return value; @@ -41,14 +42,17 @@ if (!browser.runtime.getBackground) { if (code === 'get-cache') { const [cached] = document.cookie.split('; ') .filter(item => item.startsWith(key + '=')); - if (cached) { const [, value] = cached.split('='); sendResponse(JSON.parse(value)); + console.log('CACHE: found', key, value); } else { sendResponse(); + console.log('CACHE: not found', key); } } else if (code === 'set-cache') { + console.log('CACHE: setting', key, value); + // Store as JSON to preserve data type // otherwise Booleans and Numbers become strings document.cookie = `${key}=${JSON.stringify(value)}; max-age=${expiration ? expiration * 3600 * 24 : ''}`; diff --git a/source/libs/get-default-branch.js b/source/libs/get-default-branch.js new file mode 100644 index 000000000000..034d828c25e9 --- /dev/null +++ b/source/libs/get-default-branch.js @@ -0,0 +1,41 @@ +import select from 'select-dom'; +import * as cache from './cache'; +import * as api from './api'; +import {getOwnerAndRepo} from './page-detect'; + +// This regex should match all of these combinations: +// "This branch is even with master." +// "This branch is 1 commit behind master." +// "This branch is 1 commit ahead of master." +// "This branch is 1 commit ahead, 27 commits behind master." +const branchInfoRegex = /([^ ]+)\.$/; + +function parseBranchFromDom() { + if (select.exists('.repohead h1 .octicon-repo-forked')) { + return; // It's a fork, no "default branch" info available #1132 + } + + // We can find the name in the infobar, available in folder views + const branchInfo = select('.branch-infobar'); + if (!branchInfo) { + return; + } + + // Parse the infobar + const [, branchName] = branchInfo.textContent.trim().match(branchInfoRegex) || []; + return branchName; // `string` or undefined +} + +async function fetchFromApi(user, repo) { + const response = await api.v3(`repos/${user}/${repo}`); + if (response && response.default_branch) { + return response.default_branch; + } +} + +export default function () { + const {ownerName, repoName} = getOwnerAndRepo(); + return cache.getSet(`default-branch:${ownerName}/${repoName}`, + () => parseBranchFromDom() || fetchFromApi(ownerName, repoName) + ); +}