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