From 8cfedac6a42f427388f94a29722cf93be4784ea5 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 10:14:11 +0200 Subject: [PATCH 01/18] gets gitlab auth functioning --- manifest.json | 4 +- options/options.css | 2 +- options/options.html | 16 +++++-- options/options.js | 106 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/manifest.json b/manifest.json index 9099eeb..507e8dd 100644 --- a/manifest.json +++ b/manifest.json @@ -57,6 +57,8 @@ "https://script.google.com/*", "storage", "declarativeContent", - "webRequest" + "webRequest", + "https://gitlab.com/*" + ] } diff --git a/options/options.css b/options/options.css index 68c7ed5..5dfc0a7 100644 --- a/options/options.css +++ b/options/options.css @@ -61,7 +61,7 @@ .form .logout-container { display: none; } -.form .ghe-login-container, .form .bitbucket-login-container { +.form .ghe-login-container, .form .bitbucket-login-container, .form .gitlab-login-container { display: none; } .form .error { diff --git a/options/options.html b/options/options.html index a6ea46c..e62c551 100644 --- a/options/options.html +++ b/options/options.html @@ -26,7 +26,7 @@

Logged in as: XXX

Star this extension on Github for support

-

Switch to Github Enterprise or Bitbucket

+

Switch to Github Enterprise, Bitbucket or GitLab

@@ -43,7 +43,7 @@

Logged in as: XXX

Please star this extension on Github for support

-

Switch to Github or Bitbucket

+

Switch to Github, Bitbucket or GitLab

@@ -54,9 +54,19 @@

Logged in as: XXX

Please star this extension on Github for support

-

Switch to Github or Github Enterprise

+

Switch to Github, Github Enterprise or GitLab

+
+ + +

Please star this extension on Github for support

+

Switch to Github, Github Enterprise or Bitbucket

+ +
diff --git a/options/options.js b/options/options.js index 49373e1..54f6642 100644 --- a/options/options.js +++ b/options/options.js @@ -15,6 +15,9 @@ $(() => { $('#bitbucket-login').click(e => { addCred(getBitbucketParam()); }); + $('#gitlab-login').click(e => { + addCred(getGitLabParam()); + }); $('#logout').click(e => { logout(); }); @@ -24,26 +27,33 @@ $(() => { $('.login-container').hide(); $('.logout-container').show(); let user = item.user, domain, userLink, tokenLink; - if(item.scm !== 'bitbucket') { - domain = '@Github.com'; - userLink = `https://github.com/${item.user}`; - tokenLink = 'https://github.com/settings/tokens'; - if (item.baseUrl !== 'https://api.github.com') { - let match = item.baseUrl.match(/:\/\/(.*)\/api\/v3/); - if (!match || !match[1]) { - domain = ''; - userLink = ''; - tokenLink = ''; - } else { - domain = `@${match[1]}`; - userLink = `https://${match[1]}/${item.user}`; - tokenLink = `https://${match[1]}/settings/tokens`; - } - } + + + + if(item.scm === 'bitbucket') { + domain = '@Bitbucket.org'; + userLink = `https://bitbucket.org/${user}`; + tokenLink = `https://bitbucket.org/account/user/${user}/api`; + } else if(item.scm === 'gitlab') { + domain = '@gitlab.com'; + userLink = `https://gitlab.com/${user}`; + tokenLink = `https://gitlab.com/account/user/${user}/api`; } else { - domain = '@Bitbucket.org'; - userLink = `https://bitbucket.org/${user}`; - tokenLink = `https://bitbucket.org/account/user/${user}/api`; + domain = '@Github.com'; + userLink = `https://github.com/${item.user}`; + tokenLink = 'https://github.com/settings/tokens'; + if (item.baseUrl !== 'https://api.github.com') { + let match = item.baseUrl.match(/:\/\/(.*)\/api\/v3/); + if (!match || !match[1]) { + domain = ''; + userLink = ''; + tokenLink = ''; + } else { + domain = `@${match[1]}`; + userLink = `https://${match[1]}/${item.user}`; + tokenLink = `https://${match[1]}/settings/tokens`; + } + } } $('#login-user').text(`${user}${domain}`).attr('href', userLink); @@ -101,6 +111,20 @@ function getBitbucketParam() { } } +function getGitLabParam() { + const scm = 'gitlab'; + const username = $('#gitlab-email').val(); + const password = $('#gitlab-password').val(); + const baseUrl = 'https://gitlab.com/api/v4'; + return { + scm, + username, + password, + baseUrl + } +} + + function addCred(param) { if (param.username === '') { return; @@ -110,6 +134,7 @@ function addCred(param) { } if (param.scm === 'bitbucket') return loginBitbucket(param); + if (param.scm === 'gitlab') return loginGitLab(param); if (param.password !== '' && param.scm === 'github') return loginGithub(param); addStar(param.token) @@ -220,6 +245,49 @@ function loginBitbucket(param) { $('.error').show(); }) } +function loginGitLab(param) { + const username = param.username; + const password = param.password; + const baseUrl = param.baseUrl; + const headers = {} + $.ajax({ + url: `https://gitlab.com/oauth/token`, + headers: headers, + method: 'POST', + dataType: 'json', + contentType: 'application/x-www-form-urlencoded', + data: { + grant_type: 'password', + username: username, + password: password + } + }) + .done(response => { + return $.getJSON( + `${baseUrl}/user`, + { access_token: response.access_token } + ) + .done(user => { + chrome.storage.sync.set({scm: param.scm, user: user.username, token: response.refresh_token, baseUrl: baseUrl}, () => { + location.reload(); + }); + chrome.storage.local.get('tab', (item) => { + if(item.tab) { + chrome.tabs.reload(item.tab); + } + }); + }); + }) + .fail(err => { + if (err.status == 401 && + err.getResponseHeader('X-GitLab-OTP') !== null && + $('.login-item-otp').filter(':visible').length == 0) { + $('.login-item').animate({height: 'toggle', opacity: 'toggle'}, 'slow'); + } else { + $('.error').show(); + } + }) +} function logout() { chrome.storage.sync.remove(['scm', 'token', 'user', 'baseUrl'], () => { From 88e6871a6126a2c05a47fb1a2a2ad7315e8591e8 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 12:45:02 +0200 Subject: [PATCH 02/18] gets getnamespaces and getrepos working --- manifest.json | 1 + options/options.js | 2 +- src/gas-hub.js | 1 + src/scm/gitlab.js | 274 +++++++++++++++++++++++++++++++++++++++++++++ src/util.js | 2 + 5 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 src/scm/gitlab.js diff --git a/manifest.json b/manifest.json index 507e8dd..182adb3 100644 --- a/manifest.json +++ b/manifest.json @@ -32,6 +32,7 @@ "src/util.js", "src/scm/github.js", "src/scm/bitbucket.js", + "src/scm/gitlab.js", "src/gas-api.js", "src/gas-hub.js" ], diff --git a/options/options.js b/options/options.js index 54f6642..549530b 100644 --- a/options/options.js +++ b/options/options.js @@ -268,7 +268,7 @@ function loginGitLab(param) { { access_token: response.access_token } ) .done(user => { - chrome.storage.sync.set({scm: param.scm, user: user.username, token: response.refresh_token, baseUrl: baseUrl}, () => { + chrome.storage.sync.set({scm: param.scm, user: user.username, token: response.access_token, baseUrl: baseUrl}, () => { location.reload(); }); chrome.storage.local.get('tab', (item) => { diff --git a/src/gas-hub.js b/src/gas-hub.js index d39642a..d9cc757 100644 --- a/src/gas-hub.js +++ b/src/gas-hub.js @@ -19,6 +19,7 @@ $(() => { .then(updateGist) .then(initPageEvent) .catch((err) => { + debugger; switch (err.message) { case 'need login' : initLoginContent(); diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js new file mode 100644 index 0000000..63cde67 --- /dev/null +++ b/src/scm/gitlab.js @@ -0,0 +1,274 @@ +"use strict"; + +class Gitlab { + constructor(baseUrl, user, token) { + this.baseUrl = baseUrl; + this.user = user; + this.accessToken = token; + this.namespaces = [user]; + debugger; + } + + get name() { + return 'gitlab'; + } + + get canUseGist() { + return false; + } + + + commitFiles(repo, branch, parent, files, deleteFiles, comment) { + return new Promise((resolve, reject) => { + let data = files.reduce((hash, f) => { + hash[f.name] = f.content; + return hash; + }, {}); + data.message = comment; + if (deleteFiles && deleteFiles.length > 0) { + data.files = deleteFiles; + } + if (branch) { + data.branch = branch; + } + if (parent) { + data.parents = parent; + } + $.ajax({ + url: `${this.baseUrl}/repositories/${repo}/src`, + headers: { + 'Authorization': `Bearer ${this.accessToken}` + }, + contentType: 'application/x-www-form-urlencoded', + method: 'POST', + crossDomain: true, + traditional: true, + data: data, + }) + .then(resolve) + .fail(reject); + }); + } + + push(code){ + const changed = $('.diff-file:checked').toArray().map(elem => elem.value); + const files = changed.filter(f => code.gas[f]).map(f => { + return { name: f.replace(/\.gs$/, context.config.filetype), content: code.gas[f] } + }); + const deleteFiles = changed.filter(f => !code.gas[f]); + const comment = $('#commit-comment').val(); + + this.commitFiles(context.repo.fullName, context.branch, null, files, deleteFiles, comment) + .then(() => { + showAlert(`Successfully push to ${context.branch} of ${context.repo.fullName}`); + }) + .catch((err) => { + showAlert('Failed to push', LEVEL_ERROR); + }); + } + + getAllBranches() { + return this.getAccessToken() + .then(accessToken => { + return getAllItems(Promise.resolve( + { + token: accessToken, + items: [], + url: `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches?access_token=${accessToken}` + }), + this.followPaginate, + 'gitlab' + ); + }); + } + + getCode() { + return this.getAccessToken() + .then(accessToken => { + return $.getJSON( + `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, + { access_token: accessToken } + ) + }) + .then(response => { + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + urls: [], + url: `${this.baseUrl}/repositories/${context.repo.fullName}/src/${response.target.hash}/?access_token=${this.accessToken}` + }), + this.followDirectory, + 'gitlab' + ) + .then(response => { + const promises = response.map(src => { + return new Promise((resolve, reject) => { + $.get(src.links.self.href, { access_token: this.accessToken }) + .then(content => { + resolve({ file: src.path, content: content}); + }) + .fail(reject) + }); + }); + return Promise.all(promises); + }); + }); + } + + getNamespaces() { + let testUrl = `${this.baseUrl}/users/${this.user}/projects?access_token=${this.accessToken}`; + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + url: testUrl + }), + this.followPaginate, + 'gitlab' + ) + .then(projects => { + this.namespaces = [this.user].concat(projects.map(project => project.username)); + return this.namespaces; + }) + .catch((err) => { + showAlert('Failed to get user info.', LEVEL_ERROR); + }); + } + + getRepos() { + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + url: `${this.baseUrl}/users/${this.user}/projects?access_token=${this.accessToken}` + }), + this.followPaginate, + 'gitlab' + ) + .then(response => { + const repos = response.map(repo => repo.name); + //if current bind still existed, use it + const repo = context.bindRepo[context.id]; + if (repo && $.inArray(repo.fullName, repos) >= 0) { + context.repo = repo; + } + return repos; + }); + } + + createRepo() { + const owner = $('#new-repo-owner').val(); + const name = $('#new-repo-name').val(); + const desc = $('#new-repo-desc').val(); + const isPrivate = $('#new-repo-type').val() !== 'public'; + const payload = { + scm: 'git', + description : desc, + is_private: isPrivate + } + if (!name || name === '') return; + return this.getAccessToken() + .then(() => { + return $.ajax({ + url: `${this.baseUrl}/repositories/${owner}/${name}`, + headers: { + 'Authorization': `Bearer ${this.accessToken}` + }, + method: 'POST', + crossDomain: true, + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(payload) + }) + }) + .then(response => { + const repo = { + fullName : response.full_name + }; + context.repo = repo; + Object.assign(context.bindRepo, { [context.id] : repo }); + if (context.bindBranch[context.id]) { + delete context.bindBranch[context.id]; + } + chrome.storage.sync.set({ bindRepo: context.bindRepo }); + return response.full_name; + }) + .then(repo => { + return this.commitFiles(repo, 'master', null, [{name: "README.md", content: "initialed by gas-github"}], null, 'initial commit') + .then(() => { + return repo; + }); + }) + .catch((err) => { + throw new Error('Failed to create new repository.'); + }); + } + + createBranch() { + const branch = $('#new-branch-name').val(); + if (!branch || branch === '') return; + return this.getAccessToken() + .then(() => { + return $.getJSON( + `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, + { access_token: this.accessToken } + ); + }) + .then(res => { + const parent = res.target? res.target.hash : null; + return this.commitFiles(context.repo.fullName, branch, parent, [], null, `create new branch ${branch}`); + }) + .then(() => { + context.branch = branch; + Object.assign(context.bindBranch, { [context.id] : branch }); + chrome.storage.sync.set({ bindBranch: context.bindBranch }); + return branch; + }) + .catch(err => { + throw new Error('Failed to create new branch.'); + }); + } + + followPaginate(data) { + return new Promise((resolve, reject) => { + $.getJSON(data.url) + .then(response => { + data.items = data.items.concat(response); + const link = response.next; + let url = null; + if (link) { + url = link; + } + resolve({ items: data.items, url: url }); + }) + .fail(reject); + }) + } + + followDirectory(data) { + return new Promise((resolve, reject) => { + $.getJSON(data.url) + .then(response => { + const dirs = response.values.filter(src => { + return src.type === 'commit_directory'; + }).map(dir => { + return `${dir.links.self.href}?access_token=${data.token}`; + }) + const re = new RegExp(`(\\${context.config.filetype}|\\.html${context.config.manifestEnabled ? '|^appsscript.json' : ''})$`); + const files = response.values.filter(src => { + return src.type === 'commit_file' && re.test(src.path); + }); + data.items = data.items.concat(files); + data.urls = data.urls.concat(dirs); + let link = response.next; + if (link) { + data.urls.push(`${link}&access_token=${data.token}`); + } + data.url = data.urls.shift(); + resolve(data); + }) + .fail(reject); + }) + } +} diff --git a/src/util.js b/src/util.js index 14b5ee8..99d6f82 100644 --- a/src/util.js +++ b/src/util.js @@ -13,6 +13,8 @@ function createSCM(item) { return new Github(item.baseUrl, item.user, item.token); case 'bitbucket': return new Bitbucket(item.baseUrl, item.user, item.token); + case 'gitlab': + return new Gitlab(item.baseUrl, item.user, item.token); default: return new Github(item.baseUrl, item.user, item.token); } From 50745afbe9746bc0da8039edeaf3b7d309c0c8cf Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 12:57:50 +0200 Subject: [PATCH 03/18] changes getnamespaces to access groups rather than projects --- src/scm/gitlab.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index 63cde67..8900869 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -117,7 +117,7 @@ class Gitlab { } getNamespaces() { - let testUrl = `${this.baseUrl}/users/${this.user}/projects?access_token=${this.accessToken}`; + let testUrl = `${this.baseUrl}/groups?access_token=${this.accessToken}`; return getAllItems(Promise.resolve( { token: this.accessToken, @@ -127,8 +127,8 @@ class Gitlab { this.followPaginate, 'gitlab' ) - .then(projects => { - this.namespaces = [this.user].concat(projects.map(project => project.username)); + .then(groups => { + this.namespaces = [this.user].concat(groups.map(group => group.name)); return this.namespaces; }) .catch((err) => { From 2c9454712b7550775a4a7ceff9edaaf1fc141b10 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 13:31:20 +0200 Subject: [PATCH 04/18] gets create repo working --- src/scm/gitlab.js | 269 ++++++++++++++++++++++++---------------------- 1 file changed, 138 insertions(+), 131 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index 8900869..dfd21e9 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -37,7 +37,7 @@ class Gitlab { $.ajax({ url: `${this.baseUrl}/repositories/${repo}/src`, headers: { - 'Authorization': `Bearer ${this.accessToken}` + 'Authorization': `Bearer ${this.accessToken}` }, contentType: 'application/x-www-form-urlencoded', method: 'POST', @@ -45,133 +45,132 @@ class Gitlab { traditional: true, data: data, }) - .then(resolve) - .fail(reject); + .then(resolve) + .fail(reject); }); } - push(code){ + push(code) { const changed = $('.diff-file:checked').toArray().map(elem => elem.value); const files = changed.filter(f => code.gas[f]).map(f => { - return { name: f.replace(/\.gs$/, context.config.filetype), content: code.gas[f] } + return {name: f.replace(/\.gs$/, context.config.filetype), content: code.gas[f]} }); const deleteFiles = changed.filter(f => !code.gas[f]); const comment = $('#commit-comment').val(); this.commitFiles(context.repo.fullName, context.branch, null, files, deleteFiles, comment) - .then(() => { - showAlert(`Successfully push to ${context.branch} of ${context.repo.fullName}`); - }) - .catch((err) => { - showAlert('Failed to push', LEVEL_ERROR); - }); + .then(() => { + showAlert(`Successfully push to ${context.branch} of ${context.repo.fullName}`); + }) + .catch((err) => { + showAlert('Failed to push', LEVEL_ERROR); + }); } getAllBranches() { return this.getAccessToken() - .then(accessToken => { - return getAllItems(Promise.resolve( - { - token: accessToken, - items: [], - url: `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches?access_token=${accessToken}` - }), - this.followPaginate, - 'gitlab' - ); - }); + .then(accessToken => { + return getAllItems(Promise.resolve( + { + token: accessToken, + items: [], + url: `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches?access_token=${accessToken}` + }), + this.followPaginate, + 'gitlab' + ); + }); } getCode() { return this.getAccessToken() - .then(accessToken => { - return $.getJSON( - `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, - { access_token: accessToken } - ) - }) - .then(response => { - return getAllItems(Promise.resolve( - { - token: this.accessToken, - items: [], - urls: [], - url: `${this.baseUrl}/repositories/${context.repo.fullName}/src/${response.target.hash}/?access_token=${this.accessToken}` - }), - this.followDirectory, - 'gitlab' - ) + .then(accessToken => { + return $.getJSON( + `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, + {access_token: accessToken} + ) + }) .then(response => { - const promises = response.map(src => { - return new Promise((resolve, reject) => { - $.get(src.links.self.href, { access_token: this.accessToken }) - .then(content => { - resolve({ file: src.path, content: content}); - }) - .fail(reject) + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + urls: [], + url: `${this.baseUrl}/repositories/${context.repo.fullName}/src/${response.target.hash}/?access_token=${this.accessToken}` + }), + this.followDirectory, + 'gitlab' + ) + .then(response => { + const promises = response.map(src => { + return new Promise((resolve, reject) => { + $.get(src.links.self.href, {access_token: this.accessToken}) + .then(content => { + resolve({file: src.path, content: content}); + }) + .fail(reject) + }); + }); + return Promise.all(promises); }); - }); - return Promise.all(promises); }); - }); } getNamespaces() { let testUrl = `${this.baseUrl}/groups?access_token=${this.accessToken}`; - return getAllItems(Promise.resolve( - { - token: this.accessToken, - items: [], - url: testUrl - }), - this.followPaginate, - 'gitlab' - ) - .then(groups => { - this.namespaces = [this.user].concat(groups.map(group => group.name)); - return this.namespaces; - }) - .catch((err) => { - showAlert('Failed to get user info.', LEVEL_ERROR); - }); + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + url: testUrl + }), + this.followPaginate, + 'gitlab' + ) + .then(groups => { + this.namespaces = [this.user].concat(groups.map(group => group.name)); + return this.namespaces; + }) + .catch((err) => { + showAlert('Failed to get user info.', LEVEL_ERROR); + }); } getRepos() { - return getAllItems(Promise.resolve( - { - token: this.accessToken, - items: [], - url: `${this.baseUrl}/users/${this.user}/projects?access_token=${this.accessToken}` - }), - this.followPaginate, - 'gitlab' - ) - .then(response => { - const repos = response.map(repo => repo.name); - //if current bind still existed, use it - const repo = context.bindRepo[context.id]; - if (repo && $.inArray(repo.fullName, repos) >= 0) { - context.repo = repo; - } - return repos; - }); + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + url: `${this.baseUrl}/users/${this.user}/projects?access_token=${this.accessToken}` + }), + this.followPaginate, + 'gitlab' + ) + .then(response => { + const repos = response.map(repo => repo.name); + //if current bind still existed, use it + const repo = context.bindRepo[context.id]; + if (repo && $.inArray(repo.fullName, repos) >= 0) { + context.repo = repo; + } + return repos; + }); } createRepo() { const owner = $('#new-repo-owner').val(); const name = $('#new-repo-name').val(); const desc = $('#new-repo-desc').val(); - const isPrivate = $('#new-repo-type').val() !== 'public'; + const visibility = ($('#new-repo-type').val() !== 'public') ? 'private' : 'public'; const payload = { - scm: 'git', - description : desc, - is_private: isPrivate - } + name : name, + description: desc, + visibility: visibility + }; if (!name || name === '') return; - return this.getAccessToken() - .then(() => { + return new Promise((resolve, reject) => { return $.ajax({ - url: `${this.baseUrl}/repositories/${owner}/${name}`, + url: `${this.baseUrl}/projects`, headers: { 'Authorization': `Bearer ${this.accessToken}` }, @@ -181,58 +180,65 @@ class Gitlab { contentType: 'application/json', data: JSON.stringify(payload) }) + .then(resolve) + .fail(reject); }) - .then(response => { - const repo = { - fullName : response.full_name - }; - context.repo = repo; - Object.assign(context.bindRepo, { [context.id] : repo }); - if (context.bindBranch[context.id]) { - delete context.bindBranch[context.id]; - } - chrome.storage.sync.set({ bindRepo: context.bindRepo }); - return response.full_name; - }) - .then(repo => { - return this.commitFiles(repo, 'master', null, [{name: "README.md", content: "initialed by gas-github"}], null, 'initial commit') - .then(() => { - return repo; + .then(response => { + const repo = { + fullName: response.name + }; + context.repo = repo; + Object.assign(context.bindRepo, {[context.id]: repo}); + if (context.bindBranch[context.id]) { + delete context.bindBranch[context.id]; + } + chrome.storage.sync.set({bindRepo: context.bindRepo}); + return response.name; + }) + .then(repo => { + return this.commitFiles(repo, 'master', null, [{ + name: "README.md", + content: "initialed by gas-github" + }], null, 'initial commit') + .then(() => { + return repo; + }); + }) + .catch((err) => { + throw new Error('Failed to create new repository.'); }); - }) - .catch((err) => { - throw new Error('Failed to create new repository.'); - }); } - createBranch() { - const branch = $('#new-branch-name').val(); - if (!branch || branch === '') return; - return this.getAccessToken() +createBranch() +{ + const branch = $('#new-branch-name').val(); + if (!branch || branch === '') return; + return this.getAccessToken() .then(() => { return $.getJSON( `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, - { access_token: this.accessToken } + {access_token: this.accessToken} ); }) .then(res => { - const parent = res.target? res.target.hash : null; + const parent = res.target ? res.target.hash : null; return this.commitFiles(context.repo.fullName, branch, parent, [], null, `create new branch ${branch}`); }) .then(() => { context.branch = branch; - Object.assign(context.bindBranch, { [context.id] : branch }); - chrome.storage.sync.set({ bindBranch: context.bindBranch }); + Object.assign(context.bindBranch, {[context.id]: branch}); + chrome.storage.sync.set({bindBranch: context.bindBranch}); return branch; }) .catch(err => { throw new Error('Failed to create new branch.'); }); - } +} - followPaginate(data) { - return new Promise((resolve, reject) => { - $.getJSON(data.url) +followPaginate(data) +{ + return new Promise((resolve, reject) => { + $.getJSON(data.url) .then(response => { data.items = data.items.concat(response); const link = response.next; @@ -240,15 +246,16 @@ class Gitlab { if (link) { url = link; } - resolve({ items: data.items, url: url }); + resolve({items: data.items, url: url}); }) .fail(reject); - }) - } + }) +} - followDirectory(data) { - return new Promise((resolve, reject) => { - $.getJSON(data.url) +followDirectory(data) +{ + return new Promise((resolve, reject) => { + $.getJSON(data.url) .then(response => { const dirs = response.values.filter(src => { return src.type === 'commit_directory'; @@ -269,6 +276,6 @@ class Gitlab { resolve(data); }) .fail(reject); - }) - } + }) +} } From 7b6e7442591cea9a79f90997ec06ac7cb389daa2 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 15:46:20 +0200 Subject: [PATCH 05/18] create repo fixed to not need to make an initial commit --- src/scm/gitlab.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index dfd21e9..f56a775 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -195,15 +195,6 @@ class Gitlab { chrome.storage.sync.set({bindRepo: context.bindRepo}); return response.name; }) - .then(repo => { - return this.commitFiles(repo, 'master', null, [{ - name: "README.md", - content: "initialed by gas-github" - }], null, 'initial commit') - .then(() => { - return repo; - }); - }) .catch((err) => { throw new Error('Failed to create new repository.'); }); From a063fdd34b98044e0336fbd934e2e026c26be1ba Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 16:55:45 +0200 Subject: [PATCH 06/18] gets namesToIds in place and getAllBranches working --- src/scm/gitlab.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index f56a775..a9b7bd3 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -4,6 +4,10 @@ class Gitlab { constructor(baseUrl, user, token) { this.baseUrl = baseUrl; this.user = user; + this.namesToIds = { + repos : {}, + groups : {} + }; this.accessToken = token; this.namespaces = [user]; debugger; @@ -68,18 +72,15 @@ class Gitlab { } getAllBranches() { - return this.getAccessToken() - .then(accessToken => { return getAllItems(Promise.resolve( { - token: accessToken, + token: this.accessToken, items: [], - url: `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches?access_token=${accessToken}` + url: `${this.baseUrl}/projects/${context.repo.id}/repository/branches?access_token=${this.accessToken}` }), this.followPaginate, 'gitlab' ); - }); } getCode() { @@ -136,7 +137,7 @@ class Gitlab { }); } - getRepos() { + getRepos() { // Named Projects in gitlab return getAllItems(Promise.resolve( { token: this.accessToken, @@ -147,7 +148,8 @@ class Gitlab { 'gitlab' ) .then(response => { - const repos = response.map(repo => repo.name); + this.namesToIds.repos = response.reduce((obj, item) => (obj[item.name] = item.id, obj) ,{}); + const repos = Object.keys(this.namesToIds.repos); //if current bind still existed, use it const repo = context.bindRepo[context.id]; if (repo && $.inArray(repo.fullName, repos) >= 0) { @@ -185,7 +187,8 @@ class Gitlab { }) .then(response => { const repo = { - fullName: response.name + fullName: response.name, + id: response.id }; context.repo = repo; Object.assign(context.bindRepo, {[context.id]: repo}); From b3ce2a5bf20d216407ec8862a3558e083218914c Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 19:41:05 +0200 Subject: [PATCH 07/18] get getCode working --- src/scm/gitlab.js | 164 ++++++++++++++++++---------------------------- 1 file changed, 64 insertions(+), 100 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index a9b7bd3..06906df 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -5,8 +5,8 @@ class Gitlab { this.baseUrl = baseUrl; this.user = user; this.namesToIds = { - repos : {}, - groups : {} + repos: {}, + groups: {} }; this.accessToken = token; this.namespaces = [user]; @@ -72,49 +72,43 @@ class Gitlab { } getAllBranches() { - return getAllItems(Promise.resolve( - { - token: this.accessToken, - items: [], - url: `${this.baseUrl}/projects/${context.repo.id}/repository/branches?access_token=${this.accessToken}` - }), - this.followPaginate, - 'gitlab' - ); + context.repo.id = context.repo.id || this.namesToIds.repos[context.repo.fullName]; + return getAllItems(Promise.resolve( + { + token: this.accessToken, + items: [], + url: `${this.baseUrl}/projects/${context.repo.id}/repository/branches?access_token=${this.accessToken}` + }), + this.followPaginate, + 'gitlab' + ); } getCode() { - return this.getAccessToken() - .then(accessToken => { - return $.getJSON( - `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, - {access_token: accessToken} - ) - }) - .then(response => { - return getAllItems(Promise.resolve( - { - token: this.accessToken, - items: [], - urls: [], - url: `${this.baseUrl}/repositories/${context.repo.fullName}/src/${response.target.hash}/?access_token=${this.accessToken}` - }), - this.followDirectory, - 'gitlab' - ) - .then(response => { - const promises = response.map(src => { + return new Promise((resolve, reject) => { + return $.getJSON( + `${this.baseUrl}/projects/${context.repo.id}/repository/tree?ref=${context.branch}&access_token=${this.accessToken}`, {} + ) + .then(resolve) + .fail(reject) + }) + .then(response => { + const re = new RegExp(`(\\${context.config.filetype}|\\.html${context.config.manifestEnabled ? '|^appsscript.json' : ''})$`); + const promises = response.filter((tree) => { + return tree.type === 'blob' && re.test(tree.path); + }) + .map(tree => { + var xx = `${this.baseUrl}/projects/${context.repo.id}/repository/files/${tree.path}?ref=${context.branch}&access_token=${this.accessToken}`; return new Promise((resolve, reject) => { - $.get(src.links.self.href, {access_token: this.accessToken}) - .then(content => { - resolve({file: src.path, content: content}); + $.getJSON(xx, {}) + .then((content) => { + resolve({file: tree.path, content: decodeURIComponent(escape(atob(content.content)))}); }) .fail(reject) }); }); - return Promise.all(promises); - }); - }); + return Promise.all(promises); + }); } getNamespaces() { @@ -148,7 +142,7 @@ class Gitlab { 'gitlab' ) .then(response => { - this.namesToIds.repos = response.reduce((obj, item) => (obj[item.name] = item.id, obj) ,{}); + this.namesToIds.repos = response.reduce((obj, item) => (obj[item.name] = item.id, obj), {}); const repos = Object.keys(this.namesToIds.repos); //if current bind still existed, use it const repo = context.bindRepo[context.id]; @@ -165,7 +159,7 @@ class Gitlab { const desc = $('#new-repo-desc').val(); const visibility = ($('#new-repo-type').val() !== 'public') ? 'private' : 'public'; const payload = { - name : name, + name: name, description: desc, visibility: visibility }; @@ -203,73 +197,43 @@ class Gitlab { }); } -createBranch() -{ - const branch = $('#new-branch-name').val(); - if (!branch || branch === '') return; - return this.getAccessToken() - .then(() => { + createBranch() { + const branch = $('#new-branch-name').val(); + if (!branch || branch === '') return; + return new Promise((resolve, reject) => { return $.getJSON( `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, {access_token: this.accessToken} ); }) - .then(res => { - const parent = res.target ? res.target.hash : null; - return this.commitFiles(context.repo.fullName, branch, parent, [], null, `create new branch ${branch}`); - }) - .then(() => { - context.branch = branch; - Object.assign(context.bindBranch, {[context.id]: branch}); - chrome.storage.sync.set({bindBranch: context.bindBranch}); - return branch; - }) - .catch(err => { - throw new Error('Failed to create new branch.'); - }); -} - -followPaginate(data) -{ - return new Promise((resolve, reject) => { - $.getJSON(data.url) - .then(response => { - data.items = data.items.concat(response); - const link = response.next; - let url = null; - if (link) { - url = link; - } - resolve({items: data.items, url: url}); + .then(res => { + const parent = res.target ? res.target.hash : null; + return this.commitFiles(context.repo.fullName, branch, parent, [], null, `create new branch ${branch}`); }) - .fail(reject); - }) -} + .then(() => { + context.branch = branch; + Object.assign(context.bindBranch, {[context.id]: branch}); + chrome.storage.sync.set({bindBranch: context.bindBranch}); + return branch; + }) + .catch(err => { + throw new Error('Failed to create new branch.'); + }); + } -followDirectory(data) -{ - return new Promise((resolve, reject) => { - $.getJSON(data.url) - .then(response => { - const dirs = response.values.filter(src => { - return src.type === 'commit_directory'; - }).map(dir => { - return `${dir.links.self.href}?access_token=${data.token}`; + followPaginate(data) { + return new Promise((resolve, reject) => { + $.getJSON(data.url) + .then(response => { + data.items = data.items.concat(response); + const link = response.next; + let url = null; + if (link) { + url = link; + } + resolve({items: data.items, url: url}); }) - const re = new RegExp(`(\\${context.config.filetype}|\\.html${context.config.manifestEnabled ? '|^appsscript.json' : ''})$`); - const files = response.values.filter(src => { - return src.type === 'commit_file' && re.test(src.path); - }); - data.items = data.items.concat(files); - data.urls = data.urls.concat(dirs); - let link = response.next; - if (link) { - data.urls.push(`${link}&access_token=${data.token}`); - } - data.url = data.urls.shift(); - resolve(data); - }) - .fail(reject); - }) -} + .fail(reject); + }) + } } From 7e23a5b5323628e22359fa7a8583c632713950a2 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Fri, 30 Mar 2018 23:39:05 +0200 Subject: [PATCH 08/18] gets commiting code working - yay!! --- src/scm/gitlab.js | 57 +++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index 06906df..4760da3 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -22,32 +22,45 @@ class Gitlab { } - commitFiles(repo, branch, parent, files, deleteFiles, comment) { + commitFiles(repo, branch, parent, newFiles, changedFiles, deleteFiles, comment) { return new Promise((resolve, reject) => { - let data = files.reduce((hash, f) => { - hash[f.name] = f.content; - return hash; - }, {}); - data.message = comment; - if (deleteFiles && deleteFiles.length > 0) { - data.files = deleteFiles; + let data = {branch: branch, + commit_message: comment, + actions: []}; + if (newFiles && newFiles.length > 0) { + data.actions = data.actions.concat(newFiles.map((file) => { + return { + action: 'create', + file_path: file.name, + content: file.content + } + })); } - if (branch) { - data.branch = branch; + if (changedFiles && changedFiles.length > 0) { + data.actions = data.actions.concat(changedFiles.map((file) => { + return { + action: 'update', + file_path: file.name, + content: file.content + } + })); } - if (parent) { - data.parents = parent; + if (deleteFiles && deleteFiles.length > 0) { + data.actions = data.actions.concat(deleteFiles.map((file) => {return { + action : 'delete', + file_path : file + }})); } $.ajax({ - url: `${this.baseUrl}/repositories/${repo}/src`, + url: `${this.baseUrl}/projects/${context.repo.id}/repository/commits`, headers: { 'Authorization': `Bearer ${this.accessToken}` }, - contentType: 'application/x-www-form-urlencoded', + contentType: 'application/json', method: 'POST', crossDomain: true, traditional: true, - data: data, + data: JSON.stringify(data) }) .then(resolve) .fail(reject); @@ -56,13 +69,19 @@ class Gitlab { push(code) { const changed = $('.diff-file:checked').toArray().map(elem => elem.value); - const files = changed.filter(f => code.gas[f]).map(f => { + const changedFiles = changed.filter(f => code.gas[f]).map(f => { return {name: f.replace(/\.gs$/, context.config.filetype), content: code.gas[f]} }); const deleteFiles = changed.filter(f => !code.gas[f]); + const newFileNames = changed.filter(f => !code.scm[f]); + const updatedFileNames = changed.filter(f => !newFileNames.includes(f)); + + const newFiles = changedFiles.filter(f => newFileNames.includes(f.name)); + const updatedFiles = changedFiles.filter(f => updatedFileNames.includes(f.name)); + const comment = $('#commit-comment').val(); - this.commitFiles(context.repo.fullName, context.branch, null, files, deleteFiles, comment) + this.commitFiles(context.repo.fullName, context.branch, null, newFiles, updatedFiles, deleteFiles, comment) .then(() => { showAlert(`Successfully push to ${context.branch} of ${context.repo.fullName}`); }) @@ -87,7 +106,7 @@ class Gitlab { getCode() { return new Promise((resolve, reject) => { return $.getJSON( - `${this.baseUrl}/projects/${context.repo.id}/repository/tree?ref=${context.branch}&access_token=${this.accessToken}`, {} + `${this.baseUrl}/projects/${context.repo.id}/repository/tree?ref=${context.branch}&recursive=true&access_token=${this.accessToken}`, {} ) .then(resolve) .fail(reject) @@ -98,7 +117,7 @@ class Gitlab { return tree.type === 'blob' && re.test(tree.path); }) .map(tree => { - var xx = `${this.baseUrl}/projects/${context.repo.id}/repository/files/${tree.path}?ref=${context.branch}&access_token=${this.accessToken}`; + var xx = `${this.baseUrl}/projects/${context.repo.id}/repository/files/${encodeURIComponent(tree.path)}?ref=${context.branch}&access_token=${this.accessToken}`; return new Promise((resolve, reject) => { $.getJSON(xx, {}) .then((content) => { From 2d9e62e8f91a994cf9ab5a687678c6bb4ed769de Mon Sep 17 00:00:00 2001 From: Gulliver Date: Sat, 31 Mar 2018 00:07:02 +0200 Subject: [PATCH 09/18] gets createBranch working --- src/scm/gitlab.js | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/scm/gitlab.js b/src/scm/gitlab.js index 4760da3..9596131 100644 --- a/src/scm/gitlab.js +++ b/src/scm/gitlab.js @@ -218,23 +218,32 @@ class Gitlab { createBranch() { const branch = $('#new-branch-name').val(); + const payload = { + branch: branch, + ref: context.branch + }; if (!branch || branch === '') return; return new Promise((resolve, reject) => { - return $.getJSON( - `${this.baseUrl}/repositories/${context.repo.fullName}/refs/branches/${context.branch}`, - {access_token: this.accessToken} - ); - }) - .then(res => { - const parent = res.target ? res.target.hash : null; - return this.commitFiles(context.repo.fullName, branch, parent, [], null, `create new branch ${branch}`); - }) - .then(() => { - context.branch = branch; - Object.assign(context.bindBranch, {[context.id]: branch}); - chrome.storage.sync.set({bindBranch: context.bindBranch}); - return branch; + return $.ajax({ + url: `${this.baseUrl}/projects/${context.repo.id}/repository/branches`, + headers: { + 'Authorization': `Bearer ${this.accessToken}` + }, + method: 'POST', + crossDomain: true, + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify(payload) + }) + .then(resolve) + .fail(reject); }) + .then(response => { + context.branch = branch; + Object.assign(context.bindBranch, { [context.id] : branch }); + chrome.storage.sync.set({ bindBranch: context.bindBranch }); + return branch; + }) .catch(err => { throw new Error('Failed to create new branch.'); }); From 2e5b9baf4a0fd8ec2c9d828e592cea82d1f11edd Mon Sep 17 00:00:00 2001 From: Gulliver Date: Sat, 31 Mar 2018 19:56:39 +0200 Subject: [PATCH 10/18] updates messanging to support Gitlab --- README.md | 20 ++++++++++---------- _locales/en/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/ja/messages.json | 2 +- _locales/ru_RU/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6ace6cf..f33b2f4 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ [![Chrome Web Store](https://img.shields.io/chrome-web-store/rating-count/lfjcgcmkmjjlieihflfhjopckgpelofo.svg)](https://chrome.google.com/webstore/detail/google-apps-script-github/lfjcgcmkmjjlieihflfhjopckgpelofo) [![CircleCI](https://img.shields.io/circleci/project/github/leonhartX/gas-github.svg)](https://circleci.com/gh/leonhartX/gas-github) -Chrome-extension to manage Google Apps Script(GAS) code with your favorite SCM service(github/github enterprise/bitbucket). +Chrome-extension to manage Google Apps Script(GAS) code with your favorite SCM service(github/github enterprise/bitbucket/gitlab). With this extension, you can manage your code in GAS editor, push code to a new created branch, pull from a repository/branch. -The extension does not use Google Drive API, so you don't need any google authentication. Moreover, this extension support **Bound scripts**. +The extension does not use the Google Drive API, so you don't need any google authentication. Moreover, this extension supports **Bound scripts**. # **NOTICE** -This extension is a hack of the GAS IDE's internal RPC, so there's no guarantee of anything. This extension can broken **ANYTIME** if Google changed their api. +This extension is a hack of the GAS IDE's internal RPC, so there's no guarantee of anything. This extension can break at **ANYTIME** if Google changes their api. # 1.Install Install this extension from [chrome web store](https://chrome.google.com/webstore/detail/lfjcgcmkmjjlieihflfhjopckgpelofo). @@ -21,7 +21,7 @@ Install this extension from [chrome web store](https://chrome.google.com/webstor After install, when you open GAS editor, a new button will appear to allow you to login to Github/Github Enterprise/Bitbucket. ## 2.1.Login -Login to your Github/Github Enterprise/Bitbucket account, with Two-factor authentication support for Github/Github Enterprise. +Login to your Github/Github Enterprise/Bitbucket/Gitlab account, with Two-factor authentication support for Github/Github Enterprise. Actually, this is not a login action, but to create the `access token` which will be used for the extension >Note: the access token will be stored in `chrome.storage.sync`(password will not be stored), if you take this as a security hole, pleast **DO NOT** use this extension. @@ -30,12 +30,12 @@ Actually, this is not a login action, but to create the `access token` which wil After login, you can bind your GAS Project with repo and branch, or create a new one. ## 2.3.Manage -Manage your code with the similar `Push` and `Pull`.But there are something need to know before use it. +Manage your code with the similar `Push` and `Pull`.But there are somethings you need to know before you use it. ### 2.3.1.Create Repository/Branch In `Repo` and `Branch` dropdown list, there is an option to Create new Repo and Branch. -New Repo will be created with init, mean's a default README.md. +New Repo will be created with an init, with a default README.md. ### 2.3.2.Pull and Push The **PULL/PUSH** is not actually the same as Github/Bitbucket's **PULL/PUSH**, because GAS project does not have any git info, so what we can do is limited. @@ -64,8 +64,8 @@ but you will need to delete the token or revoke Bitbucket's oauth yourself from # 3.Features - - Manage code with Github, Github Enterprise and Bitbucket - - Support embedded script + - Manage code with Github, Github Enterprise, Bitbucket and Gitlab + - Support embedded scripts - Push/Pull code between SCM and GAS - Sync code to public/secret Gist - Create repo, branch from GAS IDE @@ -75,9 +75,9 @@ but you will need to delete the token or revoke Bitbucket's oauth yourself from - Add Commit comment when push - Support two-factor authentication(Github, Github Enterprise only) - Work with directory(with slash in filename) - - Support Github Organization and Bitbucket Team. + - Support Github Organizations, Bitbucket Teams and Gitlab Groups. - Google Apps Script native ui - - Option to change filetype from `.gs` to `.js` when upload to SCM + - Option to change filetype from `.gs` to `.js` when uploading to SCM - Option to add ignore file pattern. # 4.Support diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9026481..672b880 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -3,6 +3,6 @@ "message": "Google Apps Script Github Assistant" }, "appDesc": { - "message": "Manage your gas code with github/github enterprise/bitbucket" + "message": "Manage your gas code with github/github enterprise/bitbucket/gitlab" } } \ No newline at end of file diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 9ac0fc2..3f39fca 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -3,6 +3,6 @@ "message": "Google Apps Script Github Assistant" }, "appDesc": { - "message": "Administrez votre code gas avec github/github enterprise/bitbucket" + "message": "Administrez votre code gas avec github/github enterprise/bitbucket/gitlab" } } diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index c6666b5..6d437b7 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -3,6 +3,6 @@ "message": "Google Apps Script Github アシスタント" }, "appDesc": { - "message": "Github/Github Enterprise/BitbucketでGASのインラインコードを管理" + "message": "Github/Github Enterprise/Bitbucket/gitlabでGASのインラインコードを管理" } } \ No newline at end of file diff --git a/_locales/ru_RU/messages.json b/_locales/ru_RU/messages.json index 4b2ab5c..04914f4 100644 --- a/_locales/ru_RU/messages.json +++ b/_locales/ru_RU/messages.json @@ -3,6 +3,6 @@ "message": "Google Apps Script Github помощник" }, "appDesc": { - "message": "Управляйте своим кодом gas чререз github/или github для предприятий/bitbucket" + "message": "Управляйте своим кодом gas чререз github/или github для предприятий/bitbucket/gitlab" } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index eeaa498..3300b7e 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -3,6 +3,6 @@ "message": "Google Apps Script Github 助手" }, "appDesc": { - "message": "使用Github/Github Enterprise/Bitbucket管理GAS的inline代码" + "message": "使用Github/Github Enterprise/Bitbucket/gitlab管理GAS的inline代码" } } \ No newline at end of file From f0ac976cb44848129fc267118ff7336bd47b3554 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Sat, 31 Mar 2018 20:17:42 +0200 Subject: [PATCH 11/18] gets login via token or password working --- options/options.html | 4 ++ options/options.js | 98 ++++++++++++++++++++++++++++++-------------- src/scm/gitlab.js | 36 ++++++++-------- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/options/options.html b/options/options.html index e62c551..a2c3bb2 100644 --- a/options/options.html +++ b/options/options.html @@ -59,8 +59,12 @@

Logged in as: XXX