GitCodeTree/src/adapters/github.js

244 lines
7.7 KiB
JavaScript

const GH_RESERVED_USER_NAMES = [
'settings', 'orgs', 'organizations',
'site', 'blog', 'about', 'explore',
'styleguide', 'showcases', 'trending',
'stars', 'dashboard', 'notifications',
'search', 'developer', 'account',
'pulls', 'issues', 'features', 'contact',
'security', 'join', 'login', 'watching',
'new', 'integrations'
]
const GH_RESERVED_REPO_NAMES = ['followers', 'following', 'repositories']
const GH_404_SEL = '#parallax_wrapper'
const GH_PJAX_CONTAINER_SEL = '#js-repo-pjax-container, .context-loader-container, [data-pjax-container]'
const GH_CONTAINERS = '.container'
class GitHub extends Adapter {
constructor() {
super(['jquery.pjax.js'])
$.pjax.defaults.timeout = 0 // no timeout
$(document)
.on('pjax:send', () => $(document).trigger(EVENT.REQ_START))
.on('pjax:end', () => $(document).trigger(EVENT.REQ_END))
}
// @override
init($sidebar) {
super.init($sidebar)
if (!window.MutationObserver) return
// Fix #151 by detecting when page layout is updated.
// In this case, split-diff page has a wider layout, so need to recompute margin.
// Note that couldn't do this in response to URL change, since new DOM via pjax might not be ready.
const diffModeObserver = new window.MutationObserver((mutations) => {
for (const mutation of mutations) {
if (~mutation.oldValue.indexOf('split-diff') ||
~mutation.target.className.indexOf('split-diff')) {
return $(document).trigger(EVENT.LAYOUT_CHANGE)
}
}
})
diffModeObserver.observe(document.body, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true
})
// GitHub switch pages using pjax. This observer detects if the pjax container
// has been updated with new contents and trigger layout.
const pageChangeObserver = new window.MutationObserver(() => {
// Trigger location change, can't just relayout as Octotree might need to
// hide/show depending on whether the current page is a code page or not.
return $(document).trigger(EVENT.LOC_CHANGE)
})
const pjaxContainer = $(GH_PJAX_CONTAINER_SEL)[0]
if (pjaxContainer) {
pageChangeObserver.observe(pjaxContainer, {
childList: true,
})
}
else { // Fall back if DOM has been changed
let firstLoad = true, href, hash
function detectLocChange() {
if (location.href !== href || location.hash !== hash) {
href = location.href
hash = location.hash
// If this is the first time this is called, no need to notify change as
// Octotree does its own initialization after loading options.
if (firstLoad) {
firstLoad = false
}
else {
setTimeout(() => {
$(document).trigger(EVENT.LOC_CHANGE)
}, 300) // Wait a bit for pjax DOM change
}
}
setTimeout(detectLocChange, 200)
}
detectLocChange()
}
}
// @override
getCssClass() {
return 'octotree_github_sidebar'
}
// @override
canLoadEntireTree() {
return true
}
// @override
getCreateTokenUrl() {
return `${location.protocol}//${location.host}/settings/tokens/new`
}
// @override
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
const SPACING = 10
const $containers = $(GH_CONTAINERS)
const autoMarginLeft = ($(document).width() - $containers.width()) / 2
const shouldPushLeft = sidebarVisible && (autoMarginLeft <= sidebarWidth + SPACING)
$('html').css('margin-left', shouldPushLeft ? sidebarWidth : '')
$containers.css('margin-left', shouldPushLeft ? SPACING : '')
}
// @override
getRepoFromPath(showInNonCodePage, currentRepo, token, cb) {
// 404 page, skip
if ($(GH_404_SEL).length) {
return cb()
}
// (username)/(reponame)[/(type)]
const match = window.location.pathname.match(/([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/)
if (!match) {
return cb()
}
const username = match[1]
const reponame = match[2]
// Not a repository, skip
if (~GH_RESERVED_USER_NAMES.indexOf(username) ||
~GH_RESERVED_REPO_NAMES.indexOf(reponame)) {
return cb()
}
// Skip non-code page unless showInNonCodePage is true
if (!showInNonCodePage && match[3] && !~['tree', 'blob'].indexOf(match[3])) {
return cb()
}
// Get branch by inspecting page, quite fragile so provide multiple fallbacks
const GH_BRANCH_SEL_1 = '[aria-label="Switch branches or tags"]'
const GH_BRANCH_SEL_2 = '.repo-root a[data-branch]'
const GH_BRANCH_SEL_3 = '.repository-sidebar a[aria-label="Code"]'
const GH_BRANCH_SEL_4 = '.current-branch'
const GH_BRANCH_SEL_5 = 'link[title*="Recent Commits to"]'
const branch =
// Detect branch in code page (don't care about non-code pages, let them use the next fallback)
$(GH_BRANCH_SEL_1).attr('title') || $(GH_BRANCH_SEL_2).data('branch') ||
// Non-code page
($(GH_BRANCH_SEL_3).attr('href') || ' ').match(/([^\/]+)/g)[3] ||
// Non-code page (new design)
// Specific handle /commit page
($(GH_BRANCH_SEL_4).attr('title') || ' ').match(/([^\:]+)/g)[1] ||
// Ignore if Github expands one more <link> - use last selected one instead
($(GH_BRANCH_SEL_5).length === 1
&& ($(GH_BRANCH_SEL_5).attr('title') || ' ').match(/([^\:]+)/g)[1]) ||
// Reuse last selected branch if exist
(currentRepo.username === username && currentRepo.reponame === reponame && currentRepo.branch)
// Get default branch from cache
this._defaultBranch[username + '/' + reponame]
// Still no luck, get default branch for real
const repo = {username: username, reponame: reponame, branch: branch}
if (repo.branch) {
cb(null, repo)
}
else {
this._get(null, {repo, token}, (err, data) => {
if (err) return cb(err)
repo.branch = this._defaultBranch[username + '/' + reponame] = data.default_branch || 'master'
cb(null, repo)
})
}
}
// @override
selectFile(path) {
const $pjaxContainer = $(GH_PJAX_CONTAINER_SEL)
if ($pjaxContainer.length) {
$.pjax({
// needs full path for pjax to work with Firefox as per cross-domain-content setting
url: location.protocol + '//' + location.host + path,
container: $pjaxContainer
})
}
else { // falls back
super.selectFile(path)
}
}
// @override
loadCodeTree(opts, cb) {
opts.encodedBranch = encodeURIComponent(decodeURIComponent(opts.repo.branch))
opts.path = (opts.node && (opts.node.sha || opts.encodedBranch)) ||
(opts.encodedBranch + '?recursive=1')
this._loadCodeTree(opts, null, cb)
}
// @override
_getTree(path, opts, cb) {
this._get(`/git/trees/${path}`, opts, (err, res) => {
if (err) return cb(err)
cb(null, res.tree)
})
}
// @override
_getSubmodules(tree, opts, cb) {
const item = tree.filter((item) => /^\.gitmodules$/i.test(item.path))[0]
if (!item) return cb()
this._get(`/git/blobs/${item.sha}`, opts, (err, res) => {
if (err) return cb(err)
const data = atob(res.content.replace(/\n/g,''))
cb(null, parseGitmodules(data))
})
}
_get(path, opts, cb) {
const host = location.protocol + '//' +
(location.host === 'github.com' ? 'api.github.com' : (location.host + '/api/v3'))
const url = `${host}/repos/${opts.repo.username}/${opts.repo.reponame}${path || ''}`
const cfg = { url, method: 'GET', cache: false }
if (opts.token) {
cfg.headers = { Authorization: 'token ' + opts.token }
}
$.ajax(cfg)
.done((data) => cb(null, data))
.fail((jqXHR) => this._handleError(jqXHR, cb))
}
}