Skip to content

Commit

Permalink
Support BitBucket
Browse files Browse the repository at this point in the history
  • Loading branch information
tvytlx authored and Buu Nguyen committed May 16, 2017
1 parent c5cfdb4 commit 770e1e1
Show file tree
Hide file tree
Showing 13 changed files with 515 additions and 76 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Octotree uses [GitHub API](https://developer.github.com/v3/) to retrieve reposit

When that happens, Octotree will ask for your [GitHub personal access token](https://help.github.com/articles/creating-an-access-token-for-command-line-use). If you don't already have one, [create one](https://github.com/settings/tokens/new), then copy and paste it into the textbox. Note that the minimal scopes that should be granted are `public_repo` and `repo` (if you need access to private repositories).

#### Bitbucket
Octotree uses [Bitbucket API](https://confluence.atlassian.com/bitbucket/repositories-endpoint-1-0-296092719.html) to retrieve repository metadata. By defualt, Octotree will ask for your [Bitbucket App password](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html). If you don't already have one, [create one](https://bitbucket.org/account/admin/app-passwords) (the minimal requirement is `Repositories`'s `Read` permission), then copy and paste it into the textbox.

Note that Octotree extract your username from your current page by default for calling Bitbucket API. If fail to extract, Octotree will ask you for a token update, then you just need to prepend your username to the token, separated by a colon, i.e. `USERNAME:TOKEN`.

### Enterprise URLs
By default, Octotree only works on `github.com`. To support enterprise version (Chrome and Opera only), you must grant Octotree sufficient permissions. Follow these steps to do so:

Expand Down
1 change: 1 addition & 0 deletions gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ function buildJs(overrides, ctx) {
'./tmp/template.js',
'./src/constants.js',
'./src/adapters/adapter.js',
'./src/adapters/bitbucket.js',
'./src/adapters/github.js',
'./src/view.help.js',
'./src/view.error.js',
Expand Down
95 changes: 93 additions & 2 deletions src/adapters/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class Adapter {
// encodes but retains the slashes, see #274
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
item.a_attr = {
href: `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
href: this._getItemHref(repo, type, path)
}
}
else if (type === 'commit') {
Expand Down Expand Up @@ -273,7 +273,7 @@ class Adapter {
*/
downloadFile(path, fileName) {
const link = document.createElement('a')
link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
link.setAttribute('href', path.replace(/\/blob\/|\/src\//, '/raw/'))
link.setAttribute('download', fileName)
link.click()
}
Expand All @@ -295,4 +295,95 @@ class Adapter {
_getSubmodules(tree, opts, cb) {
throw new Error('Not implemented')
}

/**
* Returns item's href value.
* @api protected
*/
_getItemHref(repo, type, encodedPath) {
return `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
}
}


class PjaxAdapter 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
// @param {Object} opts - {pjaxContainer: the specified pjax container}
// @api public
init($sidebar, opts) {
super.init($sidebar)

opts = opts || {}
const pjaxContainer = opts.pjaxContainer

if (!window.MutationObserver) return

// Some host 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)
})

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
// @param {Object} opts - {$pjax_container: jQuery object}
// @api public
selectFile(path, opts) {
opts = opts || {}
const $pjaxContainer = opts.$pjaxContainer

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)
}
}
}
212 changes: 212 additions & 0 deletions src/adapters/bitbucket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
const BB_RESERVED_USER_NAMES = [
'account', 'dashboard', 'integrations', 'product',
'repo', 'snippets', 'support', 'whats-new'
]
const BB_RESERVED_REPO_NAMES = []
const BB_RESERVED_TYPES = ['raw']
const BB_404_SEL = '#error.404'
const BB_PJAX_CONTAINER_SEL = '#source-container'

class Bitbucket extends PjaxAdapter {

constructor() {
super(['jquery.pjax.js'])
}

// @override
init($sidebar) {
const pjaxContainer = $(BB_PJAX_CONTAINER_SEL)[0]
super.init($sidebar, {'pjaxContainer': pjaxContainer})
}

// @override
getCssClass() {
return 'octotree_bitbucket_sidebar'
}

// @override
getCreateTokenUrl() {
return `${location.protocol}//${location.host}/account/admin/app-passwords/new`
}

// @override
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
$('.octotree_toggle').css('right', sidebarVisible ? '' : -44)
$('.aui-header').css('padding-left', sidebarVisible ? '' : 56)
$('html').css('margin-left', sidebarVisible ? sidebarWidth : '')
}

// @override
getRepoFromPath(showInNonCodePage, currentRepo, token, cb) {

// 404 page, skip
if ($(BB_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]
const type = match[3]

// Not a repository, skip
if (~BB_RESERVED_USER_NAMES.indexOf(username) ||
~BB_RESERVED_REPO_NAMES.indexOf(reponame) ||
~BB_RESERVED_TYPES.indexOf(type)) {
return cb()
}

// Skip non-code page unless showInNonCodePage is true
// with Bitbucket /username/repo is non-code page
if (!showInNonCodePage &&
(!type || (type && type !== 'src'))) {
return cb()
}

// Get branch by inspecting page, quite fragile so provide multiple fallbacks
const BB_BRANCH_SEL_1 = '.branch-dialog-trigger'

const branch =
// Code page
$(BB_BRANCH_SEL_1).attr('title') ||
// Assume same with previously
(currentRepo.username === username && currentRepo.reponame === reponame && currentRepo.branch) ||
// Default from cache
this._defaultBranch[username + '/' + reponame]

const repo = {username: username, reponame: reponame, branch: branch}

if (repo.branch) {
cb(null, repo)
}
else {
this._get('/main-branch', {repo, token}, (err, data) => {
if (err) return cb(err)
repo.branch = this._defaultBranch[username + '/' + reponame] = data.name || 'master'
cb(null, repo)
})
}
}

// @override
selectFile(path) {
const $pjaxContainer = $(BB_PJAX_CONTAINER_SEL)
super.selectFile(path, {'$pjaxContainer': $pjaxContainer})
}

// @override
loadCodeTree(opts, cb) {
opts.path = opts.node.path
this._loadCodeTree(opts, (item) => {
if (!item.type) {
item.type = 'blob'
}
}, cb)
}

// @override
_getTree(path, opts, cb) {
this._get(`/src/${opts.repo.branch}/${path}`, opts, (err, res) => {
if (err) return cb(err)
const directories = res.directories.map((dir) => ({path: dir, type: 'tree'}))
res.files.forEach((file) => {
if (file.path.startsWith(res.path)) {
file.path = file.path.substring(res.path.length)
}
})
const tree = res.files.concat(directories)
cb(null, tree)
})
}

// @override
_getSubmodules(tree, opts, cb) {
if (opts.repo.submodules) {
return this._getSubmodulesInCurrentPath(tree, opts, cb)
}

const item = tree.filter((item) => /^\.gitmodules$/i.test(item.path))[0]
if (!item) return cb()

this._get(`/src/${opts.encodedBranch}/${item.path}`, opts, (err, res) => {
if (err) return cb(err)
// Memoize submodules so that they will be inserted into the tree later.
opts.repo.submodules = parseGitmodules(res.data)
this._getSubmodulesInCurrentPath(tree, opts, cb)
})
}

// @override
_getSubmodulesInCurrentPath(tree, opts, cb) {
const currentPath = opts.path
const isInCurrentPath = currentPath
? (path) => path.startsWith(`${currentPath}/`)
: (path) => path.indexOf('/') === -1

const submodules = opts.repo.submodules
const submodulesInCurrentPath = {}
Object.keys(submodules).filter(isInCurrentPath).forEach((key) => {
submodulesInCurrentPath[key] = submodules[key]
})

// Insert submodules in current path into the tree because submodules can not
// be retrieved with Bitbucket API but can only by reading .gitmodules.
Object.keys(submodulesInCurrentPath).forEach((path) => {
if (currentPath) {
// `currentPath` is prefixed to `path`, so delete it.
path = path.substring(currentPath.length + 1)
}
tree.push({path: path, type: 'commit'})
})
cb(null, submodulesInCurrentPath)
}

// @override
_get(path, opts, cb) {
const host = location.protocol + '//' + 'api.bitbucket.org/1.0'
const url = `${host}/repositories/${opts.repo.username}/${opts.repo.reponame}${path || ''}`
const cfg = { url, method: 'GET', cache: false }

if (opts.token) {
// Bitbucket App passwords can be used only for Basic Authentication.
// Get username of logged-in user.
let username = null, token = null

// Or get username by spliting token.
if (opts.token.includes(':')) {
const result = opts.token.split(':')
username = result[0], token = result[1]
}
else {
const currentUser = JSON.parse($('body').attr('data-current-user'))
if (!currentUser || !currentUser.username) {
return cb({
error: 'Error: Invalid token',
message: `Cannot retrieve your user name from the current page.
Please update the token setting to prepend your user
name to the token, separated by a colon, i.e. USERNAME:TOKEN`,
needAuth: true
})
}
username = currentUser.username, token = opts.token
}
cfg.headers = { Authorization: 'Basic ' + btoa(username + ':' + token) }
}

$.ajax(cfg)
.done((data) => cb(null, data))
.fail((jqXHR) => {
this._handleError(jqXHR, cb)
})
}

// @override
_getItemHref(repo, type, encodedPath) {
return `/${repo.username}/${repo.reponame}/src/${repo.branch}/${encodedPath}`
}
}
Loading

0 comments on commit 770e1e1

Please sign in to comment.