Skip to content

Commit 770e1e1

Browse files
tvytlxbuunguyen
authored andcommitted
Support BitBucket
1 parent c5cfdb4 commit 770e1e1

File tree

13 files changed

+515
-76
lines changed

13 files changed

+515
-76
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ Octotree uses [GitHub API](https://developer.github.com/v3/) to retrieve reposit
4848

4949
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).
5050

51+
#### Bitbucket
52+
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.
53+
54+
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`.
55+
5156
### Enterprise URLs
5257
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:
5358

gulpfile.babel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ function buildJs(overrides, ctx) {
173173
'./tmp/template.js',
174174
'./src/constants.js',
175175
'./src/adapters/adapter.js',
176+
'./src/adapters/bitbucket.js',
176177
'./src/adapters/github.js',
177178
'./src/view.help.js',
178179
'./src/view.error.js',

src/adapters/adapter.js

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Adapter {
7676
// encodes but retains the slashes, see #274
7777
const encodedPath = path.split('/').map(encodeURIComponent).join('/')
7878
item.a_attr = {
79-
href: `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
79+
href: this._getItemHref(repo, type, path)
8080
}
8181
}
8282
else if (type === 'commit') {
@@ -273,7 +273,7 @@ class Adapter {
273273
*/
274274
downloadFile(path, fileName) {
275275
const link = document.createElement('a')
276-
link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
276+
link.setAttribute('href', path.replace(/\/blob\/|\/src\//, '/raw/'))
277277
link.setAttribute('download', fileName)
278278
link.click()
279279
}
@@ -295,4 +295,95 @@ class Adapter {
295295
_getSubmodules(tree, opts, cb) {
296296
throw new Error('Not implemented')
297297
}
298+
299+
/**
300+
* Returns item's href value.
301+
* @api protected
302+
*/
303+
_getItemHref(repo, type, encodedPath) {
304+
return `/${repo.username}/${repo.reponame}/${type}/${repo.branch}/${encodedPath}`
305+
}
306+
}
307+
308+
309+
class PjaxAdapter extends Adapter {
310+
constructor() {
311+
super(['jquery.pjax.js'])
312+
313+
$.pjax.defaults.timeout = 0 // no timeout
314+
$(document)
315+
.on('pjax:send', () => $(document).trigger(EVENT.REQ_START))
316+
.on('pjax:end', () => $(document).trigger(EVENT.REQ_END))
317+
}
318+
319+
320+
// @override
321+
// @param {Object} opts - {pjaxContainer: the specified pjax container}
322+
// @api public
323+
init($sidebar, opts) {
324+
super.init($sidebar)
325+
326+
opts = opts || {}
327+
const pjaxContainer = opts.pjaxContainer
328+
329+
if (!window.MutationObserver) return
330+
331+
// Some host switch pages using pjax. This observer detects if the pjax container
332+
// has been updated with new contents and trigger layout.
333+
const pageChangeObserver = new window.MutationObserver(() => {
334+
// Trigger location change, can't just relayout as Octotree might need to
335+
// hide/show depending on whether the current page is a code page or not.
336+
return $(document).trigger(EVENT.LOC_CHANGE)
337+
})
338+
339+
if (pjaxContainer) {
340+
pageChangeObserver.observe(pjaxContainer, {
341+
childList: true,
342+
})
343+
}
344+
else { // Fall back if DOM has been changed
345+
let firstLoad = true, href, hash
346+
347+
function detectLocChange() {
348+
if (location.href !== href || location.hash !== hash) {
349+
href = location.href
350+
hash = location.hash
351+
352+
// If this is the first time this is called, no need to notify change as
353+
// Octotree does its own initialization after loading options.
354+
if (firstLoad) {
355+
firstLoad = false
356+
}
357+
else {
358+
setTimeout(() => {
359+
$(document).trigger(EVENT.LOC_CHANGE)
360+
}, 300) // Wait a bit for pjax DOM change
361+
}
362+
}
363+
setTimeout(detectLocChange, 200)
364+
}
365+
366+
detectLocChange()
367+
}
368+
}
369+
370+
371+
// @override
372+
// @param {Object} opts - {$pjax_container: jQuery object}
373+
// @api public
374+
selectFile(path, opts) {
375+
opts = opts || {}
376+
const $pjaxContainer = opts.$pjaxContainer
377+
378+
if ($pjaxContainer.length) {
379+
$.pjax({
380+
// needs full path for pjax to work with Firefox as per cross-domain-content setting
381+
url: location.protocol + '//' + location.host + path,
382+
container: $pjaxContainer
383+
})
384+
}
385+
else { // falls back
386+
super.selectFile(path)
387+
}
388+
}
298389
}

src/adapters/bitbucket.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
const BB_RESERVED_USER_NAMES = [
2+
'account', 'dashboard', 'integrations', 'product',
3+
'repo', 'snippets', 'support', 'whats-new'
4+
]
5+
const BB_RESERVED_REPO_NAMES = []
6+
const BB_RESERVED_TYPES = ['raw']
7+
const BB_404_SEL = '#error.404'
8+
const BB_PJAX_CONTAINER_SEL = '#source-container'
9+
10+
class Bitbucket extends PjaxAdapter {
11+
12+
constructor() {
13+
super(['jquery.pjax.js'])
14+
}
15+
16+
// @override
17+
init($sidebar) {
18+
const pjaxContainer = $(BB_PJAX_CONTAINER_SEL)[0]
19+
super.init($sidebar, {'pjaxContainer': pjaxContainer})
20+
}
21+
22+
// @override
23+
getCssClass() {
24+
return 'octotree_bitbucket_sidebar'
25+
}
26+
27+
// @override
28+
getCreateTokenUrl() {
29+
return `${location.protocol}//${location.host}/account/admin/app-passwords/new`
30+
}
31+
32+
// @override
33+
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
34+
$('.octotree_toggle').css('right', sidebarVisible ? '' : -44)
35+
$('.aui-header').css('padding-left', sidebarVisible ? '' : 56)
36+
$('html').css('margin-left', sidebarVisible ? sidebarWidth : '')
37+
}
38+
39+
// @override
40+
getRepoFromPath(showInNonCodePage, currentRepo, token, cb) {
41+
42+
// 404 page, skip
43+
if ($(BB_404_SEL).length) {
44+
return cb()
45+
}
46+
47+
// (username)/(reponame)[/(type)]
48+
const match = window.location.pathname.match(/([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/)
49+
if (!match) {
50+
return cb()
51+
}
52+
53+
const username = match[1]
54+
const reponame = match[2]
55+
const type = match[3]
56+
57+
// Not a repository, skip
58+
if (~BB_RESERVED_USER_NAMES.indexOf(username) ||
59+
~BB_RESERVED_REPO_NAMES.indexOf(reponame) ||
60+
~BB_RESERVED_TYPES.indexOf(type)) {
61+
return cb()
62+
}
63+
64+
// Skip non-code page unless showInNonCodePage is true
65+
// with Bitbucket /username/repo is non-code page
66+
if (!showInNonCodePage &&
67+
(!type || (type && type !== 'src'))) {
68+
return cb()
69+
}
70+
71+
// Get branch by inspecting page, quite fragile so provide multiple fallbacks
72+
const BB_BRANCH_SEL_1 = '.branch-dialog-trigger'
73+
74+
const branch =
75+
// Code page
76+
$(BB_BRANCH_SEL_1).attr('title') ||
77+
// Assume same with previously
78+
(currentRepo.username === username && currentRepo.reponame === reponame && currentRepo.branch) ||
79+
// Default from cache
80+
this._defaultBranch[username + '/' + reponame]
81+
82+
const repo = {username: username, reponame: reponame, branch: branch}
83+
84+
if (repo.branch) {
85+
cb(null, repo)
86+
}
87+
else {
88+
this._get('/main-branch', {repo, token}, (err, data) => {
89+
if (err) return cb(err)
90+
repo.branch = this._defaultBranch[username + '/' + reponame] = data.name || 'master'
91+
cb(null, repo)
92+
})
93+
}
94+
}
95+
96+
// @override
97+
selectFile(path) {
98+
const $pjaxContainer = $(BB_PJAX_CONTAINER_SEL)
99+
super.selectFile(path, {'$pjaxContainer': $pjaxContainer})
100+
}
101+
102+
// @override
103+
loadCodeTree(opts, cb) {
104+
opts.path = opts.node.path
105+
this._loadCodeTree(opts, (item) => {
106+
if (!item.type) {
107+
item.type = 'blob'
108+
}
109+
}, cb)
110+
}
111+
112+
// @override
113+
_getTree(path, opts, cb) {
114+
this._get(`/src/${opts.repo.branch}/${path}`, opts, (err, res) => {
115+
if (err) return cb(err)
116+
const directories = res.directories.map((dir) => ({path: dir, type: 'tree'}))
117+
res.files.forEach((file) => {
118+
if (file.path.startsWith(res.path)) {
119+
file.path = file.path.substring(res.path.length)
120+
}
121+
})
122+
const tree = res.files.concat(directories)
123+
cb(null, tree)
124+
})
125+
}
126+
127+
// @override
128+
_getSubmodules(tree, opts, cb) {
129+
if (opts.repo.submodules) {
130+
return this._getSubmodulesInCurrentPath(tree, opts, cb)
131+
}
132+
133+
const item = tree.filter((item) => /^\.gitmodules$/i.test(item.path))[0]
134+
if (!item) return cb()
135+
136+
this._get(`/src/${opts.encodedBranch}/${item.path}`, opts, (err, res) => {
137+
if (err) return cb(err)
138+
// Memoize submodules so that they will be inserted into the tree later.
139+
opts.repo.submodules = parseGitmodules(res.data)
140+
this._getSubmodulesInCurrentPath(tree, opts, cb)
141+
})
142+
}
143+
144+
// @override
145+
_getSubmodulesInCurrentPath(tree, opts, cb) {
146+
const currentPath = opts.path
147+
const isInCurrentPath = currentPath
148+
? (path) => path.startsWith(`${currentPath}/`)
149+
: (path) => path.indexOf('/') === -1
150+
151+
const submodules = opts.repo.submodules
152+
const submodulesInCurrentPath = {}
153+
Object.keys(submodules).filter(isInCurrentPath).forEach((key) => {
154+
submodulesInCurrentPath[key] = submodules[key]
155+
})
156+
157+
// Insert submodules in current path into the tree because submodules can not
158+
// be retrieved with Bitbucket API but can only by reading .gitmodules.
159+
Object.keys(submodulesInCurrentPath).forEach((path) => {
160+
if (currentPath) {
161+
// `currentPath` is prefixed to `path`, so delete it.
162+
path = path.substring(currentPath.length + 1)
163+
}
164+
tree.push({path: path, type: 'commit'})
165+
})
166+
cb(null, submodulesInCurrentPath)
167+
}
168+
169+
// @override
170+
_get(path, opts, cb) {
171+
const host = location.protocol + '//' + 'api.bitbucket.org/1.0'
172+
const url = `${host}/repositories/${opts.repo.username}/${opts.repo.reponame}${path || ''}`
173+
const cfg = { url, method: 'GET', cache: false }
174+
175+
if (opts.token) {
176+
// Bitbucket App passwords can be used only for Basic Authentication.
177+
// Get username of logged-in user.
178+
let username = null, token = null
179+
180+
// Or get username by spliting token.
181+
if (opts.token.includes(':')) {
182+
const result = opts.token.split(':')
183+
username = result[0], token = result[1]
184+
}
185+
else {
186+
const currentUser = JSON.parse($('body').attr('data-current-user'))
187+
if (!currentUser || !currentUser.username) {
188+
return cb({
189+
error: 'Error: Invalid token',
190+
message: `Cannot retrieve your user name from the current page.
191+
Please update the token setting to prepend your user
192+
name to the token, separated by a colon, i.e. USERNAME:TOKEN`,
193+
needAuth: true
194+
})
195+
}
196+
username = currentUser.username, token = opts.token
197+
}
198+
cfg.headers = { Authorization: 'Basic ' + btoa(username + ':' + token) }
199+
}
200+
201+
$.ajax(cfg)
202+
.done((data) => cb(null, data))
203+
.fail((jqXHR) => {
204+
this._handleError(jqXHR, cb)
205+
})
206+
}
207+
208+
// @override
209+
_getItemHref(repo, type, encodedPath) {
210+
return `/${repo.username}/${repo.reponame}/src/${repo.branch}/${encodedPath}`
211+
}
212+
}

0 commit comments

Comments
 (0)