12 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/options/options.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | $(() => {
4 | $('.message a').click(e => {
5 | $('.error').hide();
6 | $('.login-container').animate({
7 | height: 'hide',
8 | opacity: 'hide'
9 | }, 'slow');
10 | $(`.${e.target.name}-login-container`).animate({
11 | height: 'show',
12 | opacity: 'show'
13 | }, 'slow');
14 | });
15 | $('#login').click(e => {
16 | addCred(getGithubParam());
17 | });
18 | $('#ghe-login').click(e => {
19 | addCred(getGHEParam());
20 | });
21 | $('#bitbucket-login').click(e => {
22 | addCred(getBitbucketParam());
23 | });
24 | $('#gitlab-login').click(e => {
25 | addCred(getGitLabParam());
26 | });
27 | $('#logout').click(e => {
28 | logout();
29 | });
30 |
31 | checkToken()
32 | .then(item => {
33 | $('.login-container').hide();
34 | $('.logout-container').show();
35 | let user = item.user,
36 | domain, userLink, tokenLink;
37 |
38 | if (item.scm === 'bitbucket') {
39 | domain = '@Bitbucket.org';
40 | userLink = `https://bitbucket.org/${user}`;
41 | tokenLink = `https://bitbucket.org/account/user/${user}/api`;
42 | } else if (item.scm === 'gitlab') {
43 | if (item.baseUrl !== 'https://gitlab.com/api/v4') {
44 | let match = item.baseUrl.match(/:\/\/(.*)\/api\/v4/);
45 | if (!match || !match[1]) {
46 | domain = '';
47 | userLink = '';
48 | tokenLink = '';
49 | } else {
50 | domain = `@${match[1].match(/\w+\.\w+(?=\/|$)/)}`;
51 | userLink = `https://${match[1]}/${user}`;
52 | tokenLink = `https://${match[1]}/profile/personal_access_tokens`;
53 | }
54 | } else {
55 | domain = '@gitlab.com';
56 | userLink = `https://gitlab.com/${user}`;
57 | tokenLink = `https://gitlab.com/profile/personal_access_tokens`;
58 | }
59 | } else {
60 | domain = '@Github.com';
61 | userLink = `https://github.com/${item.user}`;
62 | tokenLink = 'https://github.com/settings/tokens';
63 | if (item.baseUrl !== 'https://api.github.com') {
64 | let match = item.baseUrl.match(/:\/\/(.*)\/api\/v3/);
65 | if (!match || !match[1]) {
66 | domain = '';
67 | userLink = '';
68 | tokenLink = '';
69 | } else {
70 | domain = `@${match[1].match(/\w+\.\w+(?=\/|$)/)}`;
71 | userLink = `https://${match[1]}/${item.user}`;
72 | tokenLink = `https://${match[1]}/settings/tokens`;
73 | }
74 | }
75 | }
76 |
77 | $('#login-user').text(`${user}${domain}`).attr('href', userLink);
78 | $('#token').attr('href', tokenLink);
79 | })
80 | .then(() => {
81 | auth();
82 | })
83 | .catch(err => {
84 | //not logged in
85 | })
86 | })
87 |
88 | function getGithubParam() {
89 | const scm = 'github';
90 | const username = $('#username').val();
91 | const token = $('#accesstoken').val();
92 | // const apiKey = $('#api-key').val();
93 | const apiKey = null;
94 | const baseUrl = `https://api.github.com`;
95 | const otp = $('#otp').val();
96 | return {
97 | scm,
98 | username,
99 | token,
100 | apiKey,
101 | baseUrl,
102 | otp
103 | };
104 | }
105 |
106 | function getGHEParam() {
107 | const scm = 'github';
108 | const username = $('#ghe-username').val();
109 | const password = $('#ghe-password').val();
110 | const token = $('#ghe-accesstoken').val();
111 | // const apiKey = $('#ghe-api-key').val();
112 | const apiKey = null;
113 | const baseUrl = $('#ghe-url').val() + '/api/v3';
114 | const otp = $('#ghe-otp').val();
115 | return {
116 | scm,
117 | username,
118 | password,
119 | token,
120 | apiKey,
121 | baseUrl,
122 | otp
123 | };
124 | }
125 |
126 | function getBitbucketParam() {
127 | const scm = 'bitbucket';
128 | const username = $('#bitbucket-email').val();
129 | const password = $('#bitbucket-password').val();
130 | // const apiKey = $('#bitbucket-api-key').val();
131 | const apiKey = null;
132 | const baseUrl = `https://api.bitbucket.org/2.0`;
133 | return {
134 | scm,
135 | username,
136 | password,
137 | apiKey,
138 | baseUrl
139 | }
140 | }
141 |
142 | function getGitLabParam() {
143 | const scm = 'gitlab';
144 | const username = $('#gitlab-email').val();
145 | const password = $('#gitlab-password').val();
146 | const token = $('#gitlab-accesstoken').val();
147 | const tokenType = (token && token.length > 0) ? 'personalToken' : 'oAuth';
148 | // const apiKey = $('#gitlab-api-key').val();
149 | const apiKey = null;
150 | const baseUrl = ($('#gitlab-url').val() || 'https://gitlab.com') + '/api/v4';
151 | return {
152 | scm,
153 | username,
154 | password,
155 | tokenType,
156 | token,
157 | apiKey,
158 | baseUrl
159 | }
160 | }
161 |
162 | function addCred(param) {
163 | if (param.username === '') {
164 | return;
165 | }
166 | if (param.password === '' && param.token === '') {
167 | return;
168 | }
169 |
170 | if (param.apiKey && param.apiKey !== '') {
171 | const payload = {
172 | code: param.apiKey,
173 | client_id: "971735641612-am059p55sofdp30p2t4djecn72l6kmpf.apps.googleusercontent.com",
174 | client_secret: __SECRET__,
175 | redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
176 | grant_type: "authorization_code",
177 | access_type: "offline"
178 | }
179 | $.ajax({
180 | url: "https://www.googleapis.com/oauth2/v4/token",
181 | method: 'POST',
182 | dataType: 'json',
183 | contentType: 'application/json',
184 | data: JSON.stringify(payload)
185 | })
186 | .done(response => {
187 | chrome.storage.sync.set({
188 | gapiRefreshToken: response.refresh_token,
189 | gapiToken: response.access_token
190 | }, () => {
191 | login(param);
192 | });
193 | });
194 | } else {
195 | login(param);
196 | }
197 |
198 | }
199 |
200 | function login(param) {
201 | if (param.scm === 'bitbucket') return loginBitbucket(param);
202 | if (param.scm === 'gitlab') {
203 | if (param.token !== '') return loginGitLabToken(param);
204 | return loginGitLabOauth(param);
205 | }
206 | if (param.scm === 'github') {
207 | addStar(param.token)
208 | .then(() => {
209 | chrome.storage.sync.set({
210 | scm: param.scm,
211 | user: param.username,
212 | token: param.token,
213 | baseUrl: param.baseUrl
214 | }, () => {
215 | location.reload();
216 | });
217 | chrome.storage.local.get('tab', (item) => {
218 | if (item.tab) {
219 | chrome.tabs.reload(item.tab);
220 | }
221 | });
222 | })
223 | }
224 | }
225 |
226 | function loginGithub(param) {
227 | const username = param.username;
228 | const password = param.password;
229 | const baseUrl = param.baseUrl;
230 | const otp = param.otp
231 | const payload = {
232 | scopes: [
233 | 'repo',
234 | 'gist'
235 | ],
236 | note: 'gas-github_' + Date.now()
237 | }
238 | let headers = {
239 | Authorization: 'Basic ' + btoa(`${username}:${password}`)
240 | };
241 | if (otp && otp !== '') {
242 | headers['X-GitHub-OTP'] = otp;
243 | }
244 | $.ajax({
245 | url: `${baseUrl}/authorizations`,
246 | headers: headers,
247 | method: 'POST',
248 | dataType: 'json',
249 | contentType: 'application/json',
250 | data: JSON.stringify(payload)
251 | })
252 | .done(response => {
253 | addStar(response.token)
254 | .then(() => {
255 | return $.getJSON(
256 | `${baseUrl}/user`, {
257 | access_token: response.token
258 | }
259 | )
260 | })
261 | .then(userinfo => {
262 | chrome.storage.sync.set({
263 | scm: param.scm,
264 | user: userinfo.login,
265 | token: response.token,
266 | baseUrl: baseUrl
267 | }, () => {
268 | location.reload();
269 | });
270 | chrome.storage.local.get('tab', (item) => {
271 | if (item.tab) {
272 | chrome.tabs.reload(item.tab);
273 | }
274 | });
275 | })
276 | })
277 | .fail(err => {
278 | if (err.status == 401 &&
279 | err.getResponseHeader('X-GitHub-OTP') !== null &&
280 | $('.login-item-otp').filter(':visible').length == 0) {
281 | $('.login-item').animate({
282 | height: 'toggle',
283 | opacity: 'toggle'
284 | }, 'slow');
285 | } else {
286 | $('.error').show();
287 | }
288 | })
289 | }
290 |
291 | function loginBitbucket(param) {
292 | const username = param.username;
293 | const password = param.password;
294 | const baseUrl = param.baseUrl;
295 | const headers = {
296 | Authorization: `Basic RmZIVE02ZnN5NDJQQlJDRjRQOmVDZDN0TTh5TUpUeTJSMld4bTJWUzZoYWVKdnpuNzdw`
297 | }
298 | $.ajax({
299 | url: 'https://bitbucket.org/site/oauth2/access_token',
300 | headers: headers,
301 | method: 'POST',
302 | dataType: 'json',
303 | contentType: 'application/x-www-form-urlencoded',
304 | data: {
305 | grant_type: 'password',
306 | username: username,
307 | password: password
308 | }
309 | })
310 | .done(response => {
311 | return $.getJSON(
312 | `${baseUrl}/user`, {
313 | access_token: response.access_token
314 | }
315 | )
316 | .done(user => {
317 | chrome.storage.sync.set({
318 | scm: param.scm,
319 | user: user.username,
320 | token: response.refresh_token,
321 | baseUrl: baseUrl
322 | }, () => {
323 | location.reload();
324 | });
325 | chrome.storage.local.get('tab', (item) => {
326 | if (item.tab) {
327 | chrome.tabs.reload(item.tab);
328 | }
329 | });
330 | });
331 | })
332 | .fail(() => {
333 | $('.error').show();
334 | })
335 | }
336 |
337 | function loginGitLabOauth(param) {
338 | const username = param.username;
339 | const password = param.password;
340 | const baseUrl = param.baseUrl;
341 | const headers = {}
342 | const domain = baseUrl.replace('/api/v4', '');
343 | $.ajax({
344 | url: `${domain}/oauth/token`,
345 | headers: headers,
346 | method: 'POST',
347 | dataType: 'json',
348 | crossDomain: true,
349 | contentType: 'application/x-www-form-urlencoded',
350 | data: {
351 | grant_type: 'password',
352 | username: username,
353 | password: password
354 | }
355 | })
356 | .done(response => {
357 | return $.getJSON(
358 | `${baseUrl}/user`, {
359 | access_token: response.access_token
360 | }
361 | )
362 | .done(user => {
363 | chrome.storage.sync.set({
364 | scm: param.scm,
365 | user: user.username,
366 | token: {
367 | type: 'oAuth',
368 | token: response.access_token
369 | },
370 | baseUrl: baseUrl
371 | }, () => {
372 | location.reload();
373 | });
374 | chrome.storage.local.get('tab', (item) => {
375 | if (item.tab) {
376 | chrome.tabs.reload(item.tab);
377 | }
378 | });
379 | });
380 | })
381 | .fail(err => {
382 | $('.error').show();
383 | })
384 | }
385 |
386 | function loginGitLabToken(param) {
387 | const personalToken = param.token;
388 | const baseUrl = param.baseUrl;
389 | $.getJSON(
390 | `${baseUrl}/user`, {
391 | private_token: personalToken
392 | }
393 | )
394 | .done(user => {
395 | chrome.storage.sync.set({
396 | scm: param.scm,
397 | user: user.username,
398 | token: {
399 | type: 'personalToken',
400 | token: personalToken
401 | },
402 | baseUrl: baseUrl
403 | }, () => {
404 | location.reload();
405 | });
406 | chrome.storage.local.get('tab', (item) => {
407 | if (item.tab) {
408 | chrome.tabs.reload(item.tab);
409 | }
410 | });
411 | })
412 | .fail(err => {
413 | $('.error').show();
414 | })
415 | }
416 |
417 | function logout() {
418 | chrome.storage.sync.remove(['scm', 'token', 'user', 'baseUrl', 'gapiToken', 'gapiRefreshToken'], () => {
419 | location.reload();
420 | });
421 | chrome.storage.local.get('tab', (item) => {
422 | if (item.tab) {
423 | chrome.tabs.reload(item.tab);
424 | }
425 | });
426 | }
427 |
428 | function checkToken() {
429 | return new Promise((resolve, reject) => {
430 | chrome.storage.sync.get(['scm', 'token', 'user', 'baseUrl', 'googleApiKey'], (item) => {
431 | if (item.token && item.token !== '') {
432 | resolve(item);
433 | } else reject(new Error('can not get access token'));
434 | });
435 | })
436 | }
437 |
438 | function addStar(token) {
439 | if (!$('#star').is(':checked') || $('#star').is(':hidden')) {
440 | return Promise.resolve(null);
441 | }
442 | return new Promise(resolve => {
443 | $.ajax({
444 | url: `https://api.github.com/user/starred/leonhartX/gas-github`,
445 | headers: {
446 | 'Content-Length': 0,
447 | 'Authorization': `token ${token}`
448 | },
449 | method: 'PUT',
450 | })
451 | .always(resolve);
452 | })
453 | }
454 |
455 | function auth() {
456 | return new Promise((resolve, reject) => {
457 | chrome.runtime.sendMessage({
458 | cmd: 'login',
459 | interactive: true
460 | }, token => {
461 | if (token == null) {
462 | reject("can not get oauth token, currently only support Chrome");
463 | } else {
464 | chrome.storage.sync.set({
465 | "gapiToken": token
466 | });
467 | resolve(token);
468 | }
469 | });
470 | });
471 | }
472 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gas-github",
3 | "version": "2.0.0",
4 | "description": "Chrome-extension to manage Google Apps Script(GAS) code with github/github enterprise.",
5 | "main": "src/gas-hub.js",
6 | "dependencies": {
7 | "eslint": "^4.0.0"
8 | },
9 | "devDependencies": {
10 | "eslint": "^4.0.0"
11 | },
12 | "scripts": {
13 | "test": "./node_modules/.bin/eslint ."
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/leonhartX/gas-github.git"
18 | },
19 | "author": "leonhartX",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/leonhartX/gas-github/issues"
23 | },
24 | "homepage": "https://github.com/leonhartX/gas-github#readme"
25 | }
26 |
--------------------------------------------------------------------------------
/src/autocomplete.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Autocomplete functionality for repository and branch dropdowns
3 | */
4 |
5 | // Initialize autocomplete functionality
6 | function initAutocomplete() {
7 | // Setup repository autocomplete
8 | setupRepoAutocomplete();
9 |
10 | // Setup branch autocomplete
11 | setupBranchAutocomplete();
12 | }
13 |
14 | // Setup repository autocomplete
15 | function setupRepoAutocomplete() {
16 | // Add input field for repository search
17 | const repoSearchHtml = `
18 |
19 |
21 |
22 | `;
23 | $('.repo-menu').prepend(repoSearchHtml);
24 |
25 | // Add event listener for repository search
26 | $('#repo-search').on('input', function() {
27 | const searchTerm = $(this).val().toLowerCase();
28 |
29 | // Show all items initially
30 | $('.repo-menu .scm-item').show();
31 |
32 | // Hide items that don't match the search term
33 | if (searchTerm) {
34 | $('.repo-menu .scm-item').each(function() {
35 | const repoName = $(this).find('.vRMGwf').text().toLowerCase();
36 | if (repoName.indexOf(searchTerm) === -1) {
37 | $(this).hide();
38 | }
39 | });
40 | }
41 | });
42 |
43 | // Prevent dropdown from closing when clicking on the search input
44 | $('#repo-search').on('click', function(e) {
45 | e.stopPropagation();
46 | });
47 |
48 | // Handle keyboard navigation
49 | $('#repo-search').on('keydown', function(e) {
50 | const visibleItems = $('.repo-menu .scm-item:visible');
51 | let currentIndex = -1;
52 |
53 | // Find currently focused item
54 | visibleItems.each(function(index) {
55 | if ($(this).hasClass('KKjvXb')) {
56 | currentIndex = index;
57 | return false;
58 | }
59 | });
60 |
61 | switch (e.keyCode) {
62 | case 40: // Down arrow
63 | e.preventDefault();
64 | if (currentIndex < visibleItems.length - 1) {
65 | if (currentIndex >= 0) {
66 | $(visibleItems[currentIndex]).removeClass('KKjvXb');
67 | }
68 | $(visibleItems[currentIndex + 1]).addClass('KKjvXb');
69 | }
70 | break;
71 | case 38: // Up arrow
72 | e.preventDefault();
73 | if (currentIndex > 0) {
74 | $(visibleItems[currentIndex]).removeClass('KKjvXb');
75 | $(visibleItems[currentIndex - 1]).addClass('KKjvXb');
76 | }
77 | break;
78 | case 13: // Enter
79 | e.preventDefault();
80 | if (currentIndex >= 0) {
81 | $(visibleItems[currentIndex]).click();
82 | } else if (visibleItems.length > 0) {
83 | $(visibleItems[0]).click();
84 | }
85 | break;
86 | case 27: // Escape
87 | e.preventDefault();
88 | $('.repo-menu').hide();
89 | $('#repoSelect').removeClass('iWO5td');
90 | break;
91 | }
92 | });
93 | }
94 |
95 | // Setup branch autocomplete
96 | function setupBranchAutocomplete() {
97 | // Add input field for branch search
98 | const branchSearchHtml = `
99 |
100 |
102 |
103 | `;
104 | $('.branch-menu').prepend(branchSearchHtml);
105 |
106 | // Add event listener for branch search
107 | $('#branch-search').on('input', function() {
108 | const searchTerm = $(this).val().toLowerCase();
109 |
110 | // Show all items initially
111 | $('.branch-menu .scm-item').show();
112 |
113 | // Hide items that don't match the search term
114 | if (searchTerm) {
115 | $('.branch-menu .scm-item').each(function() {
116 | const branchName = $(this).find('.vRMGwf').text().toLowerCase();
117 | if (branchName.indexOf(searchTerm) === -1) {
118 | $(this).hide();
119 | }
120 | });
121 | }
122 | });
123 |
124 | // Prevent dropdown from closing when clicking on the search input
125 | $('#branch-search').on('click', function(e) {
126 | e.stopPropagation();
127 | });
128 |
129 | // Handle keyboard navigation
130 | $('#branch-search').on('keydown', function(e) {
131 | const visibleItems = $('.branch-menu .scm-item:visible');
132 | let currentIndex = -1;
133 |
134 | // Find currently focused item
135 | visibleItems.each(function(index) {
136 | if ($(this).hasClass('KKjvXb')) {
137 | currentIndex = index;
138 | return false;
139 | }
140 | });
141 |
142 | switch (e.keyCode) {
143 | case 40: // Down arrow
144 | e.preventDefault();
145 | if (currentIndex < visibleItems.length - 1) {
146 | if (currentIndex >= 0) {
147 | $(visibleItems[currentIndex]).removeClass('KKjvXb');
148 | }
149 | $(visibleItems[currentIndex + 1]).addClass('KKjvXb');
150 | }
151 | break;
152 | case 38: // Up arrow
153 | e.preventDefault();
154 | if (currentIndex > 0) {
155 | $(visibleItems[currentIndex]).removeClass('KKjvXb');
156 | $(visibleItems[currentIndex - 1]).addClass('KKjvXb');
157 | }
158 | break;
159 | case 13: // Enter
160 | e.preventDefault();
161 | if (currentIndex >= 0) {
162 | $(visibleItems[currentIndex]).click();
163 | } else if (visibleItems.length > 0) {
164 | $(visibleItems[0]).click();
165 | }
166 | break;
167 | case 27: // Escape
168 | e.preventDefault();
169 | $('.branch-menu').hide();
170 | $('#branchSelect').removeClass('iWO5td');
171 | break;
172 | }
173 | });
174 | }
175 |
176 | // Add event handlers to prevent dropdown from closing when clicking on search inputs
177 | function addSearchInputEventHandlers() {
178 | // Add a custom event handler to document mouseup
179 | $(document).on('mouseup', function(e) {
180 | // If the target is a search input, stop propagation
181 | if ($(e.target).hasClass('repo-search') || $(e.target).hasClass('branch-search')) {
182 | e.stopPropagation();
183 | }
184 | });
185 | }
186 |
187 | // Initialize autocomplete when document is ready
188 | $(document).ready(function() {
189 | // Wait for the dropdowns to be populated
190 | const observer = new MutationObserver(function(mutations) {
191 | mutations.forEach(function(mutation) {
192 | if (mutation.addedNodes.length) {
193 | // Check if repo menu has items
194 | if ($('.repo-menu .scm-item').length > 0 && !$('#repo-search').length) {
195 | setupRepoAutocomplete();
196 | }
197 |
198 | // Check if branch menu has items
199 | if ($('.branch-menu .scm-item').length > 0 && !$('#branch-search').length) {
200 | setupBranchAutocomplete();
201 | }
202 | }
203 | });
204 | });
205 |
206 | // Observe changes to the repo and branch menus - safely check if elements exist first
207 | const repoMenu = document.querySelector('.repo-menu');
208 | const branchMenu = document.querySelector('.branch-menu');
209 |
210 | if (repoMenu) {
211 | observer.observe(repoMenu, { childList: true });
212 | }
213 |
214 | if (branchMenu) {
215 | observer.observe(branchMenu, { childList: true });
216 | }
217 |
218 | // If elements don't exist yet, set up a mutation observer for the body to detect when they are added
219 | if (!repoMenu || !branchMenu) {
220 | const bodyObserver = new MutationObserver(function(mutations) {
221 | const repoMenuNew = document.querySelector('.repo-menu');
222 | const branchMenuNew = document.querySelector('.branch-menu');
223 |
224 | if (repoMenuNew && !repoMenu) {
225 | observer.observe(repoMenuNew, { childList: true });
226 | if ($('.repo-menu .scm-item').length > 0 && !$('#repo-search').length) {
227 | setupRepoAutocomplete();
228 | }
229 | }
230 |
231 | if (branchMenuNew && !branchMenu) {
232 | observer.observe(branchMenuNew, { childList: true });
233 | if ($('.branch-menu .scm-item').length > 0 && !$('#branch-search').length) {
234 | setupBranchAutocomplete();
235 | }
236 | }
237 |
238 | // If both elements are found, disconnect the body observer
239 | if (repoMenuNew && branchMenuNew) {
240 | bodyObserver.disconnect();
241 | }
242 | });
243 |
244 | // Start observing the body
245 | bodyObserver.observe(document.body, { childList: true, subtree: true });
246 | }
247 |
248 | // Add event handlers for search inputs
249 | addSearchInputEventHandlers();
250 |
251 | // Focus search input when dropdown is opened
252 | $(document).on('click', '#repoSelect', function() {
253 | setTimeout(function() {
254 | if ($('.repo-menu').is(':visible')) {
255 | $('#repo-search').focus();
256 | }
257 | }, 100);
258 | });
259 |
260 | $(document).on('click', '#branchSelect', function() {
261 | setTimeout(function() {
262 | if ($('.branch-menu').is(':visible')) {
263 | $('#branch-search').focus();
264 | }
265 | }, 100);
266 | });
267 | });
--------------------------------------------------------------------------------
/src/gas-hub.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let inited = false;
4 | let currentUrl;
5 | let gas;
6 | let scm;
7 | let context = {};
8 | const LEVEL_ERROR = 'Error';
9 | const LEVEL_WARN = 'Warn';
10 | const LEVEL_INFO = 'Notice';
11 | const observer = new MutationObserver((e) => {
12 | let url = window.location.href;
13 | if (url !== currentUrl) {
14 | if (url.endsWith('/edit') && currentUrl) {
15 | let contents = $('[jsrenderer=mUUYlf]');
16 | if (contents.length > 1) {
17 | contents[0].remove();
18 | }
19 | load();
20 | }
21 | }
22 | currentUrl = url;
23 | });
24 |
25 | observer.observe(document.getElementsByTagName('title')[0], {
26 | childList: true
27 | })
28 |
29 | $(() => {
30 | load();
31 | })
32 |
33 | function load() {
34 | initPageContent()
35 | .then(initContext)
36 | .then(updateRepo)
37 | .then(updateBranch)
38 | .then(updateGist)
39 | .then(initPageEvent)
40 | .catch((err) => {
41 | switch (err.message) {
42 | case 'need login':
43 | initLoginContent();
44 | break;
45 | case 'need auth':
46 | auth()
47 | .then(initContext)
48 | .then(updateRepo)
49 | .then(updateBranch)
50 | .then(updateGist)
51 | .then(initPageEvent)
52 | .catch((err) => {
53 | showLog(err, LEVEL_ERROR);
54 | })
55 | break;
56 | case 'not match':
57 | break;
58 | case 'nothing':
59 | break;
60 | default:
61 | showLog(err, LEVEL_ERROR);
62 | break;
63 | }
64 | });
65 | }
66 |
67 | function unload() {
68 | $('.Hu42fb').remove();
69 | }
70 |
71 |
72 |
73 | function initContext() {
74 | context = {};
75 | const id = getId();
76 |
77 | return new Promise((resolve, reject) => {
78 | chrome.storage.sync.get([
79 | 'scm',
80 | 'token',
81 | 'user',
82 | 'baseUrl',
83 | 'bindRepo',
84 | 'bindBranch',
85 | 'bindType',
86 | 'bindPattern',
87 | 'bindConfig',
88 | 'gapiRefreshToken',
89 | 'gapiToken'
90 | ], (item) => {
91 | if (!item.token) {
92 | reject(new Error('need login'));
93 | } else if (!item.gapiToken) {
94 | reject(new Error('need auth'));
95 | } else {
96 | showLog('Updating Repository');
97 | }
98 | gas = new ScriptApi();
99 | context.gapiRefreshToken = item.gapiRefreshToken;
100 | context.gapiToken = item.gapiToken;
101 | scm = createSCM(item);
102 | context.bindRepo = item.bindRepo || {};
103 | context.bindBranch = item.bindBranch || {};
104 | context.bindType = item.bindType || {};
105 | context.bindPattern = item.bindPattern || {};
106 | context.bindConfig = item.bindConfig || {};
107 | resolve(scm);
108 | });
109 | })
110 | .then(scm => {
111 | return scm.getNamespaces()
112 | .then((owners) => {
113 | let first = true;
114 | owners.forEach((owner) => {
115 | let content = `
117 |
118 | ${owner}`
119 | $('#repo-owner-list').append(content);
120 | if (first) {
121 | $('#selected-repo-owner').text(owner);
122 | first = false;
123 | }
124 | });
125 | return scm;
126 | })
127 | })
128 | .then(scm => {
129 | return scm.getRepos();
130 | })
131 | }
132 |
133 | function initPageContent() {
134 | return Promise.all([
135 | $.get(chrome.runtime.getURL('content/button.html')),
136 | $.get(chrome.runtime.getURL('content/modal.html'))
137 | ])
138 | .then((content) => {
139 | $('.INSTk').last().before(content[0]);
140 | $('body').children().last().after(content[1]);
141 | })
142 | .then(() => {
143 | $(document).on('click', '.scm-alert-dismiss', () => {
144 | $('.scm-alert').remove();
145 | });
146 | chrome.runtime.sendMessage({
147 | cmd: 'tab'
148 | });
149 | });
150 | }
151 |
152 | function initLoginContent() {
153 | $.get(chrome.runtime.getURL('content/login.html'))
154 | .then((content) => {
155 | $('.INSTk').before(content);
156 | $('#login').click(() => {
157 | if (chrome.runtime.openOptionsPage) {
158 | chrome.runtime.openOptionsPage();
159 | } else {
160 | window.open(chrome.runtime.getURL('options/options.html'));
161 | }
162 | });
163 | chrome.runtime.sendMessage({
164 | cmd: 'tab'
165 | });
166 | });
167 | }
168 |
169 | function initPageEvent() {
170 | if (inited) {
171 | return;
172 | }
173 | //bind global ui event handler
174 | $(document).mouseup((event) => {
175 | ['repo', 'branch'].forEach((type) => {
176 | const container = $(`.${type}-menu`);
177 | const button = $(`#${type}Select`);
178 | if (!container.is(event.target) &&
179 | !button.is(event.target) &&
180 | container.has(event.target).length === 0 &&
181 | button.has(event.target).length == 0) {
182 | container.hide();
183 | $(`#${type}Select`).removeClass('iWO5td');
184 | }
185 | });
186 | });
187 |
188 | $(document).on('mouseover', '.scm-item', (event) => {
189 | let target = $(event.target);
190 | if (!target.hasClass('scm-item')) {
191 | target = target.parent('.scm-item');
192 | }
193 | target.addClass('KKjvXb');
194 | });
195 |
196 | $(document).on('mouseleave', '.scm-item', (event) => {
197 | let target = $(event.target);
198 | if (!target.hasClass('scm-item')) {
199 | target = target.parent('.scm-item');
200 | }
201 | target.removeClass('KKjvXb');
202 | });
203 |
204 | ['repo', 'branch'].forEach((type) => {
205 | $(document).on('click', `#${type}Select`, () => {
206 | $(`#${type}Select`).toggleClass('iWO5td')
207 | const offset = $("[jsname=enzUi]").width() + 60;
208 | $(`.${type}-menu`).css('left', $(`#${type}Select`).position().left + offset).toggle();
209 | });
210 | });
211 |
212 | ['repo', 'branch', 'gist'].forEach((type) => {
213 | $(document).on('click', `.scm-new-${type}`, () => {
214 | $(`.${type}-menu`).hide();
215 | changeModalState(type, true);
216 | });
217 |
218 | $(document).on('click', `#scm-create-${type}`, () => {
219 | changeModalState(type, false);
220 | scm[`create${type.capitalize()}`]()
221 | .then(window[`handle${type.capitalize()}Created`])
222 | .catch(err => {
223 | showLog(err.message, LEVEL_ERROR);
224 | })
225 | });
226 |
227 | $(document).on('input propertychange', `#new-${type}-name`, (event) => {
228 | changeButtonState(type, event.target.value);
229 | });
230 | });
231 |
232 | ['repo', 'branch', 'gist', 'diff', 'config'].forEach((type) => {
233 | $(document).on('click', `.scm-${type}-modal-close`, () => {
234 | changeModalState(type, false);
235 | });
236 | });
237 |
238 | ['pull', 'push'].forEach(type => {
239 | $(document).on('click', `#${type}-button`, () => {
240 | prepareCode()
241 | .then((data) => {
242 | showDiff(data, type);
243 | }) //get more performance over callback
244 | .catch((err) => {
245 | showLog(err.message, LEVEL_ERROR);
246 | });
247 | });
248 | })
249 | $(document).on('click', '#config-button', () => {
250 | let config = getConfig();
251 | $('#selected-suffix').text(config.filetype);
252 | $('#manage-manifest').prop("checked", config.manifestEnabled);
253 | $('#ignore-pattern').val(config.ignorePattern.join(';'));
254 | changeModalState('config', true);
255 | });
256 |
257 | ['suffix', 'repo-type', 'repo-owner', 'gist-type'].forEach(type => {
258 | $(document).on('click', `#${type}-select`, () => {
259 | $(`#${type}-select`).toggleClass('nnGvjf');
260 | $(`#${type}-list`).toggleClass('VfPpkd-xl07Ob-XxIAqe-OWXEXe-FNFY6c').toggleClass('VfPpkd-xl07Ob-XxIAqe-OWXEXe-uxVfW-FNFY6c-uFfGwd');
261 | });
262 |
263 | $(document).on('click', `.${type}-option`, (event) => {
264 | $(`#selected-${type}`).text(event.target.textContent.trim());
265 | $(`#${type}-select`).toggleClass('nnGvjf');
266 | $(`#${type}-list`).toggleClass('VfPpkd-xl07Ob-XxIAqe-OWXEXe-FNFY6c').toggleClass('VfPpkd-xl07Ob-XxIAqe-OWXEXe-uxVfW-FNFY6c-uFfGwd');
267 | });
268 | })
269 |
270 | $(document).on('click', '#save-config', () => {
271 | const config = {
272 | filetype: $('#selected-suffix').text(),
273 | manifestEnabled: $('#manage-manifest').prop("checked"),
274 | ignorePattern: $('#ignore-pattern').val().split(';').filter(p => p !== '')
275 | }
276 | context.bindConfig[getId()] = config;
277 | try {
278 | chrome.storage.sync.set({
279 | bindConfig: context.bindConfig
280 | });
281 | changeModalState('config', false);
282 | } catch (err) {
283 | showLog(err.message, LEVEL_ERROR);
284 | }
285 | })
286 |
287 | $(document).on('click', '.scm-item', (event) => {
288 | let target = $(event.target);
289 | if (!target.attr('scm-content')) {
290 | target = target.parent();
291 | }
292 | const type = target.attr('scm-content');
293 | let content;
294 | let label;
295 | switch (type) {
296 | case 'repo':
297 | if (getRepo() && target.attr('data') === getRepo().fullName) return;
298 | const fullName = target.attr('data');
299 | content = {
300 | fullName: fullName,
301 | gist: fullName === 'gist'
302 | }
303 | label = fullName;
304 | break;
305 | case 'branch':
306 | if (getBranch() && target.text() === getBranch()) return;
307 | content = target.attr('data');
308 | label = target.attr('data');
309 | break;
310 | default:
311 | return;
312 | }
313 | const bindName = `bind${type.capitalize()}`;
314 | Object.assign(context[bindName], {
315 | [getId()]: content
316 | });
317 | chrome.storage.sync.set({
318 | [bindName]: context[bindName]
319 | }, () => {
320 | $(`.${type}-menu`).hide();
321 | if (type === 'repo') {
322 | $('#scm-bind-repo').text(label);
323 | if (content.gist) {
324 | updateGist();
325 | } else {
326 | updateBranch();
327 | }
328 | } else {
329 | $('#scm-bind-branch').text(label);
330 | }
331 | });
332 | });
333 |
334 | inited = true;
335 | }
336 |
337 | function auth() {
338 | return new Promise((resolve, reject) => {
339 | chrome.runtime.sendMessage({
340 | cmd: 'login',
341 | interactive: true
342 | }, token => {
343 | if (token == null) {
344 | reject("can not get oauth token, currently only support Chrome");
345 | } else {
346 | context.gapiToken = token;
347 | chrome.storage.sync.set({
348 | "gapiToken": token
349 | });
350 | resolve(token);
351 | }
352 | });
353 | });
354 | }
355 |
356 | function prepareCode() {
357 | if (!getRepo()) {
358 | return new Promise((resolve, reject) => {
359 | reject(new Error("Repository is not set"));
360 | })
361 | }
362 | return Promise.all([gas.getGasCode(), scm.getCode()])
363 | .then((data) => {
364 | return {
365 | gas: data[0],
366 | scm: data[1]
367 | };
368 | })
369 | }
370 |
371 | function showDiff(code, type) {
372 | if (Object.keys(code.scm).length === 0 && type === 'pull') {
373 | showLog('There is nothing to pull', LEVEL_INFO);
374 | return;
375 | }
376 | //setting the diff model
377 | const oldCode = type === 'push' ? code.scm : code.gas;
378 | const newCode = type === 'push' ? code.gas : code.scm;
379 | const gasFiles = Object.keys(code.gas);
380 | const scmFiles = Object.keys(code.scm);
381 | let diff = scmFiles.filter((e) => {
382 | return gasFiles.indexOf(e) < 0;
383 | })
384 | .concat(gasFiles)
385 | .filter(file => {
386 | const config = getConfig();
387 | if (config.manifestEnabled && file === 'appsscript.json') {
388 | return true;
389 | }
390 | for (let i = 0; i < config.ignorePattern.length; i++) {
391 | let p = new RegExp(config.ignorePattern[i]);
392 | if (p.test(file)) return false;
393 | }
394 | const regex = new RegExp(`(.*?)(${getConfig().filetype}|\.html)$`)
395 | const match = file.match(regex);
396 | return match && match[1] && match[2];
397 | })
398 | .reduce((diff, file) => {
399 | let mode = null;
400 | if (!oldCode[file]) {
401 | mode = 'new file mode 100644';
402 | } else if (!newCode[file]) {
403 | if (file === 'appsscript.json') {
404 | return diff; //can not delete manifest file
405 | }
406 | mode = 'deleted file mode 100644';
407 | }
408 | let fileDiff = JsDiff.createPatch(file, oldCode[file] || '', newCode[file] || '');
409 | if (fileDiff.indexOf('@@') < 0) return diff; //no diff
410 | let diffArr = fileDiff.split('\n');
411 | diffArr.splice(0, 2, `diff --git a/${file} b/${file}`);
412 | if (mode) {
413 | diffArr.splice(1, 0, mode);
414 | }
415 | fileDiff = diffArr.join('\n');
416 | return diff + fileDiff;
417 | }, '');
418 |
419 | if (diff === '') {
420 | showLog('Everything already up-to-date', LEVEL_INFO);
421 | return;
422 | }
423 |
424 | const diffHtml = new Diff2HtmlUI({
425 | diff: diff
426 | });
427 | diffHtml.draw('.scm-diff', {
428 | inputFormat: 'json',
429 | showFiles: false
430 | });
431 | diffHtml.highlightCode('.scm-diff');
432 | $('.d2h-file-name-wrapper').each((i, e) => {
433 | const filename = $(e).children('.d2h-file-name').text();
434 | $(e).prepend(`
`);
435 | });
436 | $('#commit-comment').off().val('');
437 | $('#gist-desc').val('');
438 | $('#scm-diff-handler').prop('disabled', false);
439 | if (oldCode === newCode) {
440 | $('#scm-diff-handler').prop('disabled', true);
441 | $('.scm-comment').hide();
442 | } else {
443 | if (type === 'push' && !isGist()) { //push to repo must have commit comment
444 | $('.scm-comment').show();
445 | $('.gist-desc').hide();
446 | $('#scm-diff-handler').prop('disabled', true);
447 | $('#commit-comment').on('input propertychange', (event) => {
448 | $(`#scm-diff-handler`).prop('disabled', event.target.value === '');
449 | });
450 | } else if (type === 'push') { //push to gist can change desc
451 | $('.gist-desc').show();
452 | $('.scm-comment').hide();
453 | } else {
454 | $('.scm-comment').hide();
455 | $('.gist-desc').hide();
456 | }
457 | }
458 | $('#scm-diff-label').text(type.capitalize());
459 | $('#scm-diff-handler').off().click(() => {
460 | changeModalState('diff', false);
461 | if (type === 'push') {
462 | scm.push(code);
463 | } else {
464 | gas.pull(code);
465 | }
466 | });
467 | changeModalState('diff', true);
468 | }
469 |
470 | function updateRepo(repos) {
471 | $('.repo-menu').empty().append('
');
472 | if (scm.canUseGist) {
473 | $('.repo-menu').append('
');
474 | }
475 |
476 | repos.forEach((repo) => {
477 | let content = `
`;
478 | $('.repo-menu').append(content);
479 | });
480 | showLog("Repository updated");
481 | const repo = getRepo();
482 | if (repo) {
483 | $('#scm-bind-repo').text(repo.fullName);
484 | return repo.fullName;
485 | }
486 | return null;
487 | }
488 |
489 | function updateGist() {
490 | if (!isGist()) {
491 | return null;
492 | }
493 | return scm.getAllGists()
494 | .then((gists) => {
495 | $('.branch-menu').empty().append('
');
496 | gists.forEach((gist) => {
497 | let tooltip = gist.description === '' ? 'no description' : gist.description;
498 | let content = `
`;
499 | $('.branch-menu').append(content);
500 | });
501 | let gist = context.bindBranch[getId()];
502 | if ($.inArray(gist, gists.map(gist => gist.id)) < 0) {
503 | gist = '';
504 | }
505 | $('#scm-bind-branch').text(gist);
506 | //update context and storage
507 | Object.assign(context.bindBranch, {
508 | [getId()]: gist
509 | });
510 | chrome.storage.sync.set({
511 | bindBranch: context.bindBranch
512 | });
513 | return gist;
514 | });
515 | }
516 |
517 | function updateBranch() {
518 | if (!getRepo() || isGist()) {
519 | return null;
520 | }
521 | return scm.getAllBranches()
522 | .then((branches) => {
523 | $('.branch-menu').empty().append('
');
524 | branches.forEach((branch) => {
525 | let content = `
`;
526 | $('.branch-menu').append(content);
527 | });
528 | let branch = context.bindBranch[getId()];
529 | if (branches.length === 0) {
530 | branch = '';
531 | if (scm.name === 'github') {
532 | showLog('This repository is empty, try to create a new branch such as [main] in Github', LEVEL_WARN);
533 | } else {
534 | showLog('This repository is empty, first create a new branch', LEVEL_WARN);
535 | }
536 | } else if ($.inArray(branch, branches.map(branch => branch.name)) < 0) {
537 | if ($.inArray("master", branches.map(branch => branch.name)) >= 0) {
538 | branch = 'master';
539 | } else if ($.inArray("main", branches.map(branch => branch.name)) >= 0) {
540 | branch = 'main';
541 | } else {
542 | branch = branches[0].name;
543 | }
544 | }
545 | $('#scm-bind-branch').text(branch);
546 | //update context and storage
547 | Object.assign(context.bindBranch, {
548 | [getId()]: branch
549 | });
550 | chrome.storage.sync.set({
551 | bindBranch: context.bindBranch
552 | });
553 | return branch;
554 | });
555 | }
556 |
557 | function handleRepoCreated(repo) {
558 | return scm.getRepos()
559 | .then(updateRepo)
560 | .then(updateBranch)
561 | .then(() => {
562 | $('#new-repo-name').val('');
563 | $('#new-repo-desc').val('');
564 | $('#new-repo-type').val('public');
565 | showLog(`Successfully create new repository ${repo}`);
566 | })
567 | .catch(() => {
568 | throw new Error('Repository created, but failed to show the new repository.');
569 | });
570 | }
571 |
572 | function handleBranchCreated(branch) {
573 | return updateBranch()
574 | .then(() => {
575 | $('#new-branch-name').val('');
576 | showLog(`Successfully create new branch: ${branch}`);
577 | })
578 | .catch(() => {
579 | throw new Error('Branch created, but failed to show the new branch.');
580 | });
581 | }
582 |
583 | function handleGistCreated() {
584 | return updateGist()
585 | .then(() => {
586 | $('#new-gist-name').val('');
587 | $('#new-gist-public').val('public');
588 | showLog(`Successfully create new gist.`);
589 | })
590 | .catch(err => {
591 | throw new Error('Gist created, but failed to show the new gist.');
592 | });
593 | }
594 |
595 | function getBaseUrl() {
596 | return context.gasUrl.substring(0, context.gasUrl.indexOf('/gwt/')) + '/gwt/';
597 | }
598 |
599 | function changeModalState(type, toShow) {
600 | if (toShow) {
601 | const margin = 600;
602 | const width = $('body').width();
603 | const height = $('body').height();
604 | const left = (width - margin) / 2;
605 | $(`#${type}-modal`).show();
606 | } else {
607 | $(`#${type}-modal`).hide();
608 | $(`#new-${type}-name`).css('border-color', '');
609 | }
610 | }
611 |
612 | function changeButtonState(type, value) {
613 | if (!value || value === '') {
614 | $(`#scm-create-${type}`).prop('disabled', true);
615 | $(`#new-${type}-name`).css('border-color', '#e0331e');
616 | } else {
617 | $(`#scm-create-${type}`).prop('disabled', false);
618 | $(`#new-${type}-name`).css('border-color', '');
619 | }
620 | }
621 |
622 | /* show alert using gas ui
623 | * level: info, warning, error
624 | * but the class is promo. info, warning
625 | */
626 | function showLog(message, level = LEVEL_INFO) {
627 |
628 | $.get(chrome.runtime.getURL('content/alert.html'))
629 | .then((content) => {
630 | const colorClass = level == LEVEL_ERROR ? "nN3Fac" : "ptNZqd";
631 | $("[jsname=cFQkCb]").removeClass("LcqFFc");
632 | const currentOffset = $("[jsname=cFQkCb] > [jsname=NR4lfb]").css("flex-basis");
633 | const offset = (currentOffset === "0px" || currentOffset === "auto") ? "150px" : currentOffset;
634 | $("[jsname=cFQkCb] > [jsname=NR4lfb]").css("flex-basis", offset);
635 | $('.Vod31b').html(content
636 | .replace(/_COLOR_CLASS_/, colorClass)
637 | .replace(/_TIMESTAMP_/g, new Date().toLocaleTimeString())
638 | .replace(/_LEVEL_/g, level)
639 | .replace(/_MESSAGE_/, message));
640 | })
641 | }
642 |
643 | String.prototype.capitalize = function () {
644 | return this.charAt(0).toUpperCase() + this.slice(1);
645 | }
646 |
--------------------------------------------------------------------------------
/src/gas/script-api.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const API_URL = "https://script.googleapis.com/v1/projects";
4 |
5 | class ScriptApi {
6 | pull(code) {
7 | const config = getConfig();
8 | const changed = $('.diff-file:checked').toArray().map(elem => elem.value);
9 | const updatedFiles = changed.filter(f => code.scm[f]).map(f => {
10 | const suffix = f.substr(f.lastIndexOf("."));
11 | const file = f.substr(0, f.lastIndexOf("."));
12 | let type;
13 | switch (suffix) {
14 | case ".html":
15 | type = "HTML";
16 | break;
17 | case ".json":
18 | type = "JSON";
19 | break;
20 | case config.filetype:
21 | type = "SERVER_JS";
22 | break;
23 | }
24 | return {
25 | name: file,
26 | source: code.scm[f],
27 | type: type
28 | }
29 | });
30 | const deleteFiles = changed.filter(f => !code.scm[f]);
31 | const remainedFiles = Object.keys(code.gas)
32 | .filter(f => !deleteFiles.includes(f) && !changed.includes(f))
33 | .map(f => {
34 | const suffix = f.substr(f.lastIndexOf("."));
35 | const file = f.substr(0, f.lastIndexOf("."));
36 | let type;
37 | switch (suffix) {
38 | case ".html":
39 | type = "HTML";
40 | break;
41 | case ".json":
42 | type = "JSON";
43 | break;
44 | case config.filetype:
45 | type = "SERVER_JS";
46 | break;
47 | }
48 | return {
49 | name: file,
50 | source: code.gas[f],
51 | type: type
52 | }
53 | });
54 | const files = updatedFiles.concat(remainedFiles);
55 | console.log(files)
56 |
57 | this.updateToken()
58 | .then(() => {
59 | return new Promise((resolve, reject) => {
60 | $.ajax({
61 | url: `${API_URL}/${getId()}/content?access_token=${context.gapiToken}`,
62 | method: 'PUT',
63 | contentType: 'application/json',
64 | data: JSON.stringify({
65 | files: files
66 | })
67 | })
68 | .then(resolve)
69 | .fail(reject)
70 | })
71 | })
72 | .then(() => {
73 | showLog('Successfully pulled from scm');
74 | location.reload();
75 | })
76 | .catch((err) => {
77 | showLog(err.message, LEVEL_ERROR);
78 | });
79 | }
80 |
81 | getGasCode() {
82 | return this.updateToken()
83 | .then(() => {
84 | return new Promise((resolve, reject) => {
85 | $.getJSON(
86 | `${API_URL}/${getId()}/content`, {
87 | access_token: context.gapiToken
88 | }
89 | )
90 | .then(resolve)
91 | .fail(reject)
92 | })
93 | })
94 | .then(content => {
95 | const code = content.files.reduce((hash, elem) => {
96 | if (elem) {
97 | let type;
98 | switch (elem.type) {
99 | case "HTML":
100 | type = ".html"
101 | break;
102 | case "JSON":
103 | type = ".json"
104 | break;
105 | case "SERVER_JS":
106 | type = getConfig().filetype
107 | break;
108 | }
109 | hash[elem.name + type] = elem.source;
110 | }
111 | return hash;
112 | }, {});
113 | return code;
114 | });
115 | }
116 |
117 | updateGas() {
118 | return new Promise((resolve, reject) => {
119 | $.ajax({
120 | url: `${API_URL}/${getId()}/content?access_token=${context.gapiToken}`,
121 | method: 'PUT',
122 | contentType: 'application/json',
123 | data: JSON.stringify({
124 | files: files
125 | })
126 | })
127 | .then(resolve)
128 | .fail(reject)
129 | })
130 | .then(() => {
131 | showLog('Successfully pulled from scm');
132 | location.reload();
133 | })
134 | .catch((err) => {
135 | showLog(err.message, LEVEL_ERROR);
136 | });
137 | }
138 |
139 | updateToken() {
140 | if (context.gapiRefreshToken) {
141 | return new Promise((resolve, reject) => {
142 | const payload = {
143 | refresh_token: context.gapiRefreshToken,
144 | client_id: "971735641612-am059p55sofdp30p2t4djecn72l6kmpf.apps.googleusercontent.com",
145 | client_secret: __SECRET__,
146 | redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
147 | grant_type: "refresh_token",
148 | }
149 | $.ajax({
150 | url: "https://www.googleapis.com/oauth2/v4/token",
151 | method: 'POST',
152 | dataType: 'json',
153 | contentType: 'application/json',
154 | data: JSON.stringify(payload)
155 | })
156 | .done(response => {
157 | context.gapiToken = response.access_token;
158 | chrome.storage.sync.set({
159 | gapiToken: response.access_token
160 | }, () => {
161 | resolve();
162 | });
163 | });
164 | })
165 | } else {
166 | return new Promise((resolve, reject) => {
167 | chrome.runtime.sendMessage({
168 | cmd: 'login',
169 | interactive: false
170 | }, token => {
171 | context.gapiToken = token;
172 | resolve();
173 | })
174 | });
175 | }
176 | }
177 | }
--------------------------------------------------------------------------------
/src/scm/bitbucket.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Bitbucket {
4 | constructor(baseUrl, user, token) {
5 | this.baseUrl = baseUrl;
6 | this.user = user;
7 | this.token = token;
8 | this.accessToken = null;
9 | this.namespaces = [user];
10 | }
11 |
12 | get name() {
13 | return 'bitbucket';
14 | }
15 |
16 | get canUseGist() {
17 | return false;
18 | }
19 |
20 | getAccessToken() {
21 | return new Promise((resolve, reject) => {
22 | $.ajax({
23 | url: 'https://bitbucket.org/site/oauth2/access_token',
24 | headers: {
25 | Authorization: `Basic RmZIVE02ZnN5NDJQQlJDRjRQOmVDZDN0TTh5TUpUeTJSMld4bTJWUzZoYWVKdnpuNzdw`
26 | },
27 | method: 'POST',
28 | dataType: 'json',
29 | contentType: 'application/x-www-form-urlencoded',
30 | data: {
31 | grant_type: 'refresh_token',
32 | refresh_token: this.token
33 | }
34 | })
35 | .then(resolve)
36 | .fail(reject)
37 | })
38 | .then(response => {
39 | chrome.storage.sync.set({
40 | token: response.refresh_token
41 | });
42 | this.accessToken = response.access_token;
43 | return response.access_token;
44 | })
45 | .catch(err => {
46 | showLog(`Failed to refresh access token: ${err}`, LEVEL_ERROR);
47 | })
48 | }
49 |
50 | commitFiles(repo, branch, parent, files, deleteFiles, comment) {
51 | return new Promise((resolve, reject) => {
52 | let data = files.reduce((hash, f) => {
53 | hash[f.name] = f.content;
54 | return hash;
55 | }, {});
56 | data.message = comment;
57 | if (deleteFiles && deleteFiles.length > 0) {
58 | data.files = deleteFiles;
59 | }
60 | if (branch) {
61 | data.branch = branch;
62 | }
63 | if (parent) {
64 | data.parents = parent;
65 | }
66 | $.ajax({
67 | url: `${this.baseUrl}/repositories/${repo}/src`,
68 | headers: {
69 | 'Authorization': `Bearer ${this.accessToken}`
70 | },
71 | contentType: 'application/x-www-form-urlencoded',
72 | method: 'POST',
73 | crossDomain: true,
74 | traditional: true,
75 | data: data,
76 | })
77 | .then(resolve)
78 | .fail(reject);
79 | });
80 | }
81 |
82 | push(code) {
83 | const changed = $('.diff-file:checked').toArray().map(elem => elem.value);
84 | const files = changed.filter(f => code.gas[f]).map(f => {
85 | return {
86 | name: f,
87 | content: code.gas[f]
88 | }
89 | });
90 | const deleteFiles = changed.filter(f => !code.gas[f]);
91 | const comment = $('#commit-comment').val();
92 | const repo = getRepo();
93 | this.commitFiles(repo.fullName, getBranch(), null, files, deleteFiles, comment)
94 | .then(() => {
95 | showLog(`Successfully push to ${getBranch()} of ${repo.fullName}`);
96 | })
97 | .catch((err) => {
98 | showLog('Failed to push', LEVEL_ERROR);
99 | });
100 | }
101 |
102 | getAllBranches() {
103 | return this.getAccessToken()
104 | .then(accessToken => {
105 | return getAllItems(Promise.resolve({
106 | token: accessToken,
107 | items: [],
108 | url: `${this.baseUrl}/repositories/${getRepo().fullName}/refs/branches?access_token=${accessToken}`
109 | }),
110 | this.followPaginate,
111 | 'bitbucket'
112 | );
113 | });
114 | }
115 |
116 | getCode() {
117 | return this.getAccessToken()
118 | .then(accessToken => {
119 | return $.getJSON(
120 | `${this.baseUrl}/repositories/${getRepo().fullName}/refs/branches/${getBranch()}`, {
121 | access_token: accessToken
122 | }
123 | )
124 | })
125 | .then(response => {
126 | return getAllItems(Promise.resolve({
127 | token: this.accessToken,
128 | items: [],
129 | urls: [],
130 | url: `${this.baseUrl}/repositories/${getRepo().fullName}/src/${response.target.hash}/?access_token=${this.accessToken}`
131 | }),
132 | this.followDirectory,
133 | 'bitbucket'
134 | )
135 | .then(response => {
136 | const promises = response.map(src => {
137 | return new Promise((resolve, reject) => {
138 | $.get(src.links.self.href, {
139 | access_token: this.accessToken
140 | })
141 | .then(content => {
142 | resolve({
143 | file: src.path,
144 | content: content
145 | });
146 | })
147 | .fail(reject)
148 | });
149 | });
150 | return Promise.all(promises);
151 | });
152 | })
153 | .then(code => {
154 | return code.reduce((hash, elem) => {
155 | if (elem) {
156 | hash[elem.file] = elem.content;
157 | }
158 | return hash;
159 | }, {})
160 | });
161 | }
162 |
163 | getNamespaces() {
164 | return this.getAccessToken()
165 | .then(accessToken => {
166 | return getAllItems(Promise.resolve({
167 | token: accessToken,
168 | items: [],
169 | url: `${this.baseUrl}/teams?access_token=${accessToken}&role=contributor`
170 | }),
171 | this.followPaginate,
172 | 'bitbucket'
173 | );
174 | })
175 | .then(teams => {
176 | this.namespaces = [this.user].concat(teams.map(team => team.username));
177 | return this.namespaces;
178 | })
179 | .catch((err) => {
180 | showLog('Failed to get user info.', LEVEL_ERROR);
181 | });
182 | }
183 |
184 | getRepos() {
185 | return this.getAccessToken()
186 | .then(accessToken => {
187 | return getAllItems(Promise.resolve({
188 | token: accessToken,
189 | items: [],
190 | url: `${this.baseUrl}/repositories/?access_token=${accessToken}&q=scm="git"&role=contributor`
191 | }),
192 | this.followPaginate,
193 | 'bitbucket'
194 | );
195 | })
196 | .then(response => {
197 | return response.map(repo => repo.full_name);
198 | });
199 | }
200 |
201 | createRepo() {
202 | const owner = $('#selected-repo-owner').text();
203 | const name = $('#new-repo-name').val();
204 | const desc = $('#new-repo-desc').val();
205 | const isPrivate = $('#selected-repo-type').val() !== 'Public';
206 | const payload = {
207 | scm: 'git',
208 | description: desc,
209 | is_private: isPrivate
210 | }
211 | if (!name || name === '') return;
212 | return this.getAccessToken()
213 | .then(() => {
214 | return $.ajax({
215 | url: `${this.baseUrl}/repositories/${owner}/${name}`,
216 | headers: {
217 | 'Authorization': `Bearer ${this.accessToken}`
218 | },
219 | method: 'POST',
220 | crossDomain: true,
221 | dataType: 'json',
222 | contentType: 'application/json',
223 | data: JSON.stringify(payload)
224 | })
225 | })
226 | .then(response => {
227 | const repo = {
228 | fullName: response.full_name
229 | };
230 | Object.assign(context.bindRepo, {
231 | [getId()]: repo
232 | });
233 | if (context.bindBranch[getId()]) {
234 | delete context.bindBranch[getId()];
235 | }
236 | chrome.storage.sync.set({
237 | bindRepo: context.bindRepo
238 | });
239 | return response.full_name;
240 | })
241 | .then(repo => {
242 | return this.commitFiles(repo, 'master', null, [{
243 | name: "README.md",
244 | content: "initialed by gas-github"
245 | }], null, 'initial commit')
246 | .then(() => {
247 | return repo;
248 | });
249 | })
250 | .catch((err) => {
251 | throw new Error('Failed to create new repository.');
252 | });
253 | }
254 |
255 | createBranch() {
256 | const branch = $('#new-branch-name').val();
257 | if (!branch || branch === '') return;
258 | return this.getAccessToken()
259 | .then(() => {
260 | return $.getJSON(
261 | `${this.baseUrl}/repositories/${getRepo().fullName}/refs/branches/${getBranch()}`, {
262 | access_token: this.accessToken
263 | }
264 | );
265 | })
266 | .then(res => {
267 | const parent = res.target ? res.target.hash : null;
268 | return this.commitFiles(getRepo().fullName, branch, parent, [], null, `create new branch ${branch}`);
269 | })
270 | .then(() => {
271 | Object.assign(context.bindBranch, {
272 | [getId()]: branch
273 | });
274 | chrome.storage.sync.set({
275 | bindBranch: context.bindBranch
276 | });
277 | return branch;
278 | })
279 | .catch(err => {
280 | throw new Error('Failed to create new branch.');
281 | });
282 | }
283 |
284 | followPaginate(data) {
285 | return new Promise((resolve, reject) => {
286 | $.getJSON(data.url)
287 | .then(response => {
288 | data.items = data.items.concat(response.values);
289 | const link = response.next;
290 | let url = null;
291 | if (link) {
292 | url = link;
293 | }
294 | resolve({
295 | items: data.items,
296 | url: url
297 | });
298 | })
299 | .fail(reject);
300 | })
301 | }
302 |
303 | followDirectory(data) {
304 | return new Promise((resolve, reject) => {
305 | $.getJSON(data.url)
306 | .then(response => {
307 | const dirs = response.values.filter(src => {
308 | return src.type === 'commit_directory';
309 | }).map(dir => {
310 | return `${dir.links.self.href}?access_token=${data.token}`;
311 | })
312 | const config = getConfig();
313 | const re = new RegExp(`(\\${config.filetype}|\\.html${config.manifestEnabled ? '|^appsscript.json' : ''})$`);
314 | const files = response.values.filter(src => {
315 | return src.type === 'commit_file' && re.test(src.path);
316 | });
317 | data.items = data.items.concat(files);
318 | data.urls = data.urls.concat(dirs);
319 | let link = response.next;
320 | if (link) {
321 | data.urls.push(`${link}&access_token=${data.token}`);
322 | }
323 | data.url = data.urls.shift();
324 | resolve(data);
325 | })
326 | .fail(reject);
327 | })
328 | }
329 | }
--------------------------------------------------------------------------------
/src/scm/github.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Github {
4 | constructor(baseUrl, user, accessToken) {
5 | this.baseUrl = baseUrl;
6 | this.user = user;
7 | this.accessToken = accessToken;
8 | this.namespaces = [user];
9 | }
10 |
11 | get name() {
12 | return 'github';
13 | }
14 |
15 | get canUseGist() {
16 | return true;
17 | }
18 |
19 |
20 | push(code) {
21 | if (isGist()) return this.pushToGist(code);
22 | return this.pushToRepo(code);
23 | }
24 |
25 | pushToRepo(code) {
26 | const changed = $('.diff-file:checked').toArray().map(elem => elem.value);
27 | const promises = changed.filter(f => code.gas[f]).map((file) => {
28 | const payload = {
29 | content: code.gas[file],
30 | encoding: 'utf-8'
31 | };
32 | return $.ajax({
33 | url: `${this.baseUrl}/repos/${getRepo().fullName}/git/blobs`,
34 | headers: {
35 | 'Authorization': `token ${this.accessToken}`
36 | },
37 | method: 'POST',
38 | crossDomain: true,
39 | dataType: 'json',
40 | contentType: 'application/json',
41 | data: JSON.stringify(payload)
42 | })
43 | .then(response => {
44 | return {
45 | file: file,
46 | blob: response
47 | };
48 | })
49 | });
50 | if (changed.length === 0) {
51 | showLog('Nothing to do', LEVEL_INFO);
52 | return;
53 | }
54 |
55 | Promise.all([
56 | Promise.all(promises),
57 | getGitHubJSON(
58 | `${this.baseUrl}/repos/${getRepo().fullName}/branches/${getBranch()}`,
59 | this.accessToken)
60 | ])
61 | .then(responses => {
62 | return getGitHubJSON(
63 | responses[1].commit.commit.tree.url,
64 | this.accessToken, {
65 | recursive: 1
66 | }
67 | )
68 | .then(baseTree => {
69 | const tree = responses[0].map((data) => {
70 | return {
71 | path: data.file,
72 | mode: '100644',
73 | type: 'blob',
74 | sha: data.blob.sha
75 | }
76 | })
77 | .concat(baseTree.tree.filter((t) => {
78 | return (t.type != 'tree') && (changed.indexOf(t.path) < 0);
79 | }));
80 | return {
81 | tree: tree
82 | };
83 | })
84 | .then(payload => {
85 | return $.ajax({
86 | url: `${this.baseUrl}/repos/${getRepo().fullName}/git/trees`,
87 | headers: {
88 | 'Authorization': `token ${this.accessToken}`
89 | },
90 | method: 'POST',
91 | crossDomain: true,
92 | dataType: 'json',
93 | contentType: 'application/json',
94 | data: JSON.stringify(payload)
95 | });
96 | })
97 | .then(response => {
98 | return Object.assign(response, {
99 | parent: responses[1].commit.sha
100 | })
101 | })
102 | .fail(err => {
103 | throw err;
104 | });
105 | })
106 | .then(response => {
107 | const payload = {
108 | message: $('#commit-comment').val(),
109 | tree: response.sha,
110 | parents: [
111 | response.parent
112 | ]
113 | };
114 | return $.ajax({
115 | url: `${this.baseUrl}/repos/${getRepo().fullName}/git/commits`,
116 | headers: {
117 | 'Authorization': `token ${this.accessToken}`
118 | },
119 | method: 'POST',
120 | crossDomain: true,
121 | dataType: 'json',
122 | contentType: 'application/json',
123 | data: JSON.stringify(payload)
124 | });
125 | })
126 | .then(response => {
127 | const payload = {
128 | force: true,
129 | sha: response.sha
130 | };
131 | return $.ajax({
132 | url: `${this.baseUrl}/repos/${getRepo().fullName}/git/refs/heads/${getBranch()}`,
133 | headers: {
134 | 'Authorization': `token ${this.accessToken}`
135 | },
136 | method: 'PATCH',
137 | crossDomain: true,
138 | dataType: 'json',
139 | contentType: 'application/json',
140 | data: JSON.stringify(payload)
141 | });
142 | })
143 | .then(() => {
144 | showLog(`Successfully push to ${getBranch()} of ${getRepo().fullName}`);
145 | })
146 | .catch(err => {
147 | showLog(`Failed to push: ${err}`, LEVEL_ERROR);
148 | });
149 | }
150 |
151 | pushToGist(code) {
152 | const files = $('.diff-file:checked').toArray().map((elem) => elem.value);
153 | if (files.length === 0) {
154 | showLog('Nothing to do', LEVEL_INFO);
155 | return;
156 | }
157 | const payload = {
158 | files: {}
159 | };
160 | files.forEach(file => {
161 | payload.files[file] = {
162 | content: code.gas[file]
163 | };
164 | });
165 | if (code.scm['init_by_gas_hub.html']) {
166 | payload.files['init_by_gas_hub.html'] = null;
167 | }
168 | if ($('#gist-desc').val() !== '') {
169 | payload.description = $('#gist-desc').val();
170 | }
171 | return $.ajax({
172 | url: `${this.baseUrl}/gists/${getBranch()}`,
173 | headers: {
174 | 'Authorization': `token ${this.accessToken}`
175 | },
176 | method: 'PATCH',
177 | crossDomain: true,
178 | dataType: 'json',
179 | contentType: 'application/json',
180 | data: JSON.stringify(payload)
181 | })
182 | .then(() => {
183 | showLog(`Successfully update gist: ${getBranch()}`);
184 | })
185 | .fail(err => {
186 | showLog(`Failed to update: ${err}`, LEVEL_ERROR);
187 | });
188 | }
189 |
190 | getAllGists() {
191 | return getAllItems(
192 | Promise.resolve({
193 | accessToken: this.accessToken,
194 | items: [],
195 | url: `${this.baseUrl}/users/${this.user}/gists`
196 | }),
197 | this.followPaginate,
198 | 'github'
199 | );
200 | }
201 |
202 | getAllBranches() {
203 | return getAllItems(
204 | Promise.resolve({
205 | accessToken: this.accessToken,
206 | items: [],
207 | url: `${this.baseUrl}/repos/${getRepo().fullName}/branches`
208 | }),
209 | this.followPaginate,
210 | 'github'
211 | );
212 | }
213 |
214 | getCode() {
215 | let code;
216 | if (isGist()) {
217 | code = this.getGistCode();
218 | } else {
219 | code = this.getRepoCode();
220 | }
221 | return code.then(code => {
222 | return code.reduce((hash, elem) => {
223 | if (elem) {
224 | hash[elem.file] = elem.content;
225 | }
226 | return hash;
227 | }, {})
228 | });
229 | }
230 |
231 | getRepoCode() {
232 | return new Promise((resolve, reject) => {
233 | getGitHubJSON(
234 | `${this.baseUrl}/repos/${getRepo().fullName}/branches/${getBranch()}`,
235 | this.accessToken)
236 | .then(resolve)
237 | .fail(reject);
238 | })
239 | .then(response => {
240 | return getGitHubJSON(
241 | `${this.baseUrl}/repos/${getRepo().fullName}/git/trees/${response.commit.commit.tree.sha}`,
242 | this.accessToken, {
243 | recursive: 1
244 | }
245 | );
246 | })
247 | .then(response => {
248 | const config = getConfig();
249 | const re = new RegExp(`(\\${config.filetype}|\\.html${config.manifestEnabled ? '|^appsscript.json' : ''})$`);
250 | const promises = response.tree.filter((tree) => {
251 | return tree.type === 'blob' && re.test(tree.path);
252 | })
253 | .map(tree => {
254 | return new Promise((resolve, reject) => {
255 | getGitHubJSON(`${tree.url}?ts=${new Date().getTime()}`, this.accessToken)
256 | .then((content) => {
257 | resolve({
258 | file: tree.path,
259 | content: decodeURIComponent(escape(atob(content.content)))
260 | });
261 | })
262 | .fail(reject)
263 | });
264 | });
265 | return Promise.all(promises);
266 | });
267 | }
268 |
269 | getGistCode() {
270 | return new Promise((resolve, reject) => {
271 | getGitHubJSON(`${this.baseUrl}/gists/${getBranch()}`, this.accessToken)
272 | .then(resolve)
273 | .fail(reject);
274 | })
275 | .then((response) => {
276 | const promises = Object.keys(response.files).map((filename) => {
277 | let file = response.files[filename];
278 | return new Promise((resolve, reject) => {
279 | if (file.truncated) {
280 | getGitHubJSON(file.raw_url, this.accessToken)
281 | .then((content) => {
282 | resolve({
283 | file: filename,
284 | content: content
285 | });
286 | })
287 | .fail(reject)
288 | } else {
289 | resolve({
290 | file: filename,
291 | content: file.content
292 | });
293 | }
294 | });
295 | });
296 | return Promise.all(promises);
297 | });
298 | }
299 |
300 | getRepos() {
301 | return getAllItems(
302 | Promise.resolve({
303 | accessToken: this.accessToken,
304 | items: [],
305 | url: `${this.baseUrl}/user/repos`
306 | }),
307 | this.followPaginate,
308 | 'github'
309 | )
310 | .then(response => {
311 | const repos = response.map(repo => repo.full_name);
312 | const repo = getRepo();
313 | if (repo && !repo.gist && !($.inArray(repo.fullName, repos) >= 0)) {
314 | delete context.bindRepo[getId()];
315 | chrome.storage.sync.set({
316 | bindRepo: context.bindRepo
317 | });
318 | }
319 | return repos;
320 | });
321 | }
322 |
323 | getNamespaces() {
324 | return getAllItems(
325 | Promise.resolve({
326 | accessToken: this.accessToken,
327 | items: [],
328 | url: `${this.baseUrl}/user/orgs`
329 | }),
330 | this.followPaginate,
331 | 'github'
332 | )
333 | .then(orgs => {
334 | this.namespaces = [this.user].concat(orgs.map(org => org.login));
335 | return this.namespaces;
336 | })
337 | .catch((err) => {
338 | showLog(`Failed to get user info: ${err}`, LEVEL_ERROR);
339 | });
340 | }
341 |
342 | createRepo() {
343 | const owner = $('#selected-repo-owner').text();
344 | const name = $('#new-repo-name').val();
345 | const desc = $('#new-repo-desc').val();
346 | const isPrivate = $('#selected-repo-type').val() !== 'Public';
347 | const payload = {
348 | name: name,
349 | description: desc,
350 | auto_init: true,
351 | private: isPrivate
352 | }
353 | const path = owner === this.user ? '/user/repos' : `/orgs/${owner}/repos`;
354 | if (!name || name === '') return;
355 | return new Promise((resolve, reject) => {
356 | $.ajax({
357 | url: `${this.baseUrl}${path}`,
358 | headers: {
359 | 'Authorization': `token ${this.accessToken}`
360 | },
361 | method: 'POST',
362 | crossDomain: true,
363 | dataType: 'json',
364 | contentType: 'application/json',
365 | data: JSON.stringify(payload)
366 | })
367 | .then(resolve)
368 | .fail(reject);
369 | })
370 | .then(response => {
371 | const repo = {
372 | fullName: response.full_name
373 | };
374 | Object.assign(context.bindRepo, {
375 | [getId()]: repo
376 | });
377 | if (context.bindBranch[getId()]) {
378 | delete context.bindBranch[getId()];
379 | }
380 | chrome.storage.sync.set({
381 | bindRepo: context.bindRepo
382 | });
383 | return response.full_name;
384 | })
385 | .catch(err => {
386 | throw new Error('Failed to create new repository.');
387 | });
388 | }
389 |
390 | createGist() {
391 | const desc = $('#new-gist-name').val();
392 | const isPublic = $('#new-gist-public').val() !== 'secret';
393 | const payload = {
394 | 'description': desc,
395 | 'public': isPublic,
396 | 'files': {
397 | 'init_by_gas_hub.html': {
398 | 'content': 'init by gas-hub, just delete this file.'
399 | }
400 | }
401 | };
402 |
403 | return new Promise((resolve, reject) => {
404 | $.ajax({
405 | url: `${this.baseUrl}/gists`,
406 | headers: {
407 | 'Authorization': `token ${this.accessToken}`
408 | },
409 | method: 'POST',
410 | crossDomain: true,
411 | dataType: 'json',
412 | contentType: 'application/json',
413 | data: JSON.stringify(payload)
414 | })
415 | .then(resolve)
416 | .fail(reject);
417 | })
418 | .then(response => {
419 | const gist = response.id;
420 | Object.assign(context.bindBranch, {
421 | [getId()]: gist
422 | });
423 | chrome.storage.sync.set({
424 | bindBranch: context.bindBranch
425 | });
426 | return response;
427 | })
428 | .catch(err => {
429 | throw new Error('Failed to create new gist.');
430 | });
431 | }
432 |
433 | createBranch() {
434 | const branch = $('#new-branch-name').val();
435 | if (!branch || branch === '') return;
436 | return new Promise((resolve, reject) => {
437 | getGitHubJSON(
438 | `${this.baseUrl}/repos/${getRepo().fullName}/git/refs/heads/${getBranch()}`,
439 | this.accessToken)
440 | .then(resolve)
441 | .fail(reject)
442 | })
443 | .then(response => {
444 | if (response.object) {
445 | return response.object.sha;
446 | } else {
447 | return getGitHubJSON(
448 | `${this.baseUrl}/repos/${getRepo().fullName}/git/refs/heads`,
449 | this.accessToken)
450 | .then(response => {
451 | return response[0].object.sha;
452 | })
453 | }
454 | })
455 | .then(sha => {
456 | const payload = {
457 | ref: `refs/heads/${branch}`,
458 | sha: sha
459 | };
460 | return $.ajax({
461 | url: `${this.baseUrl}/repos/${getRepo().fullName}/git/refs`,
462 | headers: {
463 | 'Authorization': `token ${this.accessToken}`
464 | },
465 | method: 'POST',
466 | crossDomain: true,
467 | dataType: 'json',
468 | contentType: 'application/json',
469 | data: JSON.stringify(payload)
470 | });
471 | })
472 | .then(response => {
473 | Object.assign(context.bindBranch, {
474 | [getId()]: branch
475 | });
476 | chrome.storage.sync.set({
477 | bindBranch: context.bindBranch
478 | });
479 | return branch;
480 | })
481 | .catch(err => {
482 | if (err.status === 409) {
483 | throw new Error('Cannot create branch in empty repository with API, try to create branch in Github.');
484 | } else {
485 | throw new Error('Failed to create new branch.');
486 | }
487 | });
488 | }
489 |
490 | followPaginate(data) {
491 | return new Promise((resolve, reject) => {
492 | getGitHubJSON(data.url, data.accessToken)
493 | .then((response, status, xhr) => {
494 | data.items = data.items.concat(response);
495 | const link = xhr.getResponseHeader('Link');
496 | let url = null;
497 | if (link) {
498 | const match = link.match(/.*<(.*?)>; rel="next"/);
499 | url = match && match[1] ? match[1] : null;
500 | }
501 | resolve({
502 | items: data.items,
503 | url: url,
504 | accessToken: data.accessToken
505 | });
506 | })
507 | .fail(reject);
508 | });
509 | }
510 | }
--------------------------------------------------------------------------------
/src/scm/gitlab.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | class Gitlab {
4 | constructor(baseUrl, user, token) {
5 | this.baseUrl = baseUrl;
6 | this.user = user;
7 | this.namesToIds = {
8 | repos: {},
9 | groups: {}
10 | };
11 | this.accessToken = token.token;
12 | if (token.type === 'oAuth') {
13 | this.tokenParam = `access_token=${this.accessToken}`;
14 | this.tokenHeader = {
15 | 'Authorization': `Bearer ${this.accessToken}`
16 | };
17 | } else {
18 | this.tokenParam = `private_token=${this.accessToken}`;
19 | this.tokenHeader = {
20 | 'Private-Token': this.accessToken
21 | };
22 | }
23 | this.namespaces = [user];
24 | }
25 |
26 | get name() {
27 | return 'gitlab';
28 | }
29 |
30 | get canUseGist() {
31 | return false;
32 | }
33 |
34 | commitFiles(repo, branch, parent, newFiles, changedFiles, deleteFiles, comment) {
35 | return new Promise((resolve, reject) => {
36 | const data = {
37 | branch: branch,
38 | commit_message: comment,
39 | actions: []
40 | };
41 | if (newFiles && newFiles.length > 0) {
42 | data.actions = data.actions.concat(newFiles.map((file) => {
43 | return {
44 | action: 'create',
45 | file_path: file.name,
46 | content: file.content
47 | }
48 | }));
49 | }
50 | if (changedFiles && changedFiles.length > 0) {
51 | data.actions = data.actions.concat(changedFiles.map((file) => {
52 | return {
53 | action: 'update',
54 | file_path: file.name,
55 | content: file.content
56 | }
57 | }));
58 | }
59 | if (deleteFiles && deleteFiles.length > 0) {
60 | data.actions = data.actions.concat(deleteFiles.map((file) => {
61 | return {
62 | action: 'delete',
63 | file_path: file
64 | }
65 | }));
66 | }
67 | let repoId = getRepo().id || this.namesToIds.repos[getRepo().fullName];
68 | $.ajax({
69 | url: `${this.baseUrl}/projects/${repoId}/repository/commits`,
70 | headers: this.tokenHeader,
71 | contentType: 'application/json',
72 | method: 'POST',
73 | crossDomain: true,
74 | traditional: true,
75 | data: JSON.stringify(data)
76 | })
77 | .then(resolve)
78 | .fail(reject);
79 | });
80 | }
81 |
82 | push(code) {
83 | const changed = $('.diff-file:checked').toArray().map(elem => elem.value);
84 | const changedFiles = changed.filter(f => code.gas[f]).map(f => {
85 | return {
86 | name: f,
87 | content: code.gas[f]
88 | }
89 | });
90 | const deleteFiles = changed.filter(f => !code.gas[f]);
91 | const newFileNames = changed.filter(f => !code.scm[f]);
92 | const updatedFileNames = changed.filter(f => !newFileNames.includes(f));
93 |
94 | const newFiles = changedFiles.filter(f => newFileNames.includes(f.name));
95 | const updatedFiles = changedFiles.filter(f => updatedFileNames.includes(f.name));
96 |
97 | const comment = $('#commit-comment').val();
98 |
99 | this.commitFiles(getRepo().fullName, getBranch(), null, newFiles, updatedFiles, deleteFiles, comment)
100 | .then(() => {
101 | showLog(`Successfully push to ${getBranch()} of ${getRepo().fullName}`);
102 | })
103 | .catch((err) => {
104 | showLog(`Failed to push: ${err}`, LEVEL_ERROR);
105 | });
106 | }
107 |
108 | getAllBranches() {
109 | let repoId = getRepo().id || this.namesToIds.repos[getRepo().fullName];
110 | return getAllItems(Promise.resolve({
111 | tokenParam: this.tokenParam,
112 | items: [],
113 | url: `${this.baseUrl}/projects/${repoId}/repository/branches?per_page=25`
114 | }),
115 | this.followPaginate,
116 | 'gitlab'
117 | );
118 | }
119 |
120 | getCode() {
121 | let repoId = getRepo().id || this.namesToIds.repos[getRepo().fullName];
122 | return new Promise((resolve, reject) => {
123 | return $.getJSON(
124 | `${this.baseUrl}/projects/${repoId}/repository/tree?ref=${getBranch()}&recursive=true&${this.tokenParam}`, {}
125 | )
126 | .then(resolve)
127 | .fail(reject)
128 | })
129 | .then(response => {
130 | const config = getConfig();
131 | const re = new RegExp(`(\\${config.filetype}|\\.html${config.manifestEnabled ? '|^appsscript.json' : ''})$`);
132 | const promises = response.filter((tree) => {
133 | return tree.type === 'blob' && re.test(tree.path);
134 | })
135 | .map(tree => {
136 | return new Promise((resolve, reject) => {
137 | $.getJSON(`${this.baseUrl}/projects/${repoId}/repository/files/${encodeURIComponent(tree.path)}?ref=${getBranch()}&${this.tokenParam}`, {})
138 | .then((content) => {
139 | resolve({
140 | file: tree.path,
141 | content: decodeURIComponent(escape(atob(content.content)))
142 | });
143 | })
144 | .fail(reject)
145 | });
146 | });
147 | return Promise.all(promises);
148 | })
149 | .then(code => {
150 | return code.reduce((hash, elem) => {
151 | if (elem) {
152 | hash[elem.file] = elem.content;
153 | }
154 | return hash;
155 | }, {})
156 | })
157 | }
158 |
159 | getNamespaces() {
160 | return getAllItems(Promise.resolve({
161 | tokenParam: this.tokenParam,
162 | items: [],
163 | url: `${this.baseUrl}/groups?per_page=25`
164 | }),
165 | this.followPaginate,
166 | 'gitlab'
167 | )
168 | .then(groups => {
169 | this.namespaces = [this.user].concat(groups.map(group => group.name));
170 | this.namesToIds.groups = groups.reduce((obj, item) => (obj[item.name] = item.id, obj), {});
171 | return this.namespaces;
172 | })
173 | .catch((err) => {
174 | showLog('Failed to get user info.', LEVEL_ERROR);
175 | });
176 | }
177 |
178 | getRepos() { // Named Projects in gitlab
179 | return getAllItems(Promise.resolve({
180 | tokenParam: this.tokenParam,
181 | items: [],
182 | url: `${this.baseUrl}/projects?per_page=25&membership=true`
183 | }),
184 | this.followPaginate,
185 | 'gitlab'
186 | )
187 | .then(response => {
188 | this.namesToIds.repos = response.reduce((obj, item) => (obj[item.path_with_namespace] = item.id, obj), {});
189 | return Object.keys(this.namesToIds.repos);
190 | });
191 | }
192 |
193 | createRepo() {
194 | const owner = $('#selected-repo-owner').text();
195 | const name = $('#new-repo-name').val();
196 | const desc = $('#new-repo-desc').val();
197 | const visibility = ($('#selected-repo-type').val() !== 'Public') ? 'private' : 'public';
198 | const payload = {
199 | path: name,
200 | description: desc,
201 | visibility: visibility
202 | };
203 | if (this.namesToIds.groups[owner]) {
204 | payload.namespace_id = this.namesToIds.groups[owner];
205 | }
206 | if (!name || name === '') return;
207 | return new Promise((resolve, reject) => {
208 | return $.ajax({
209 | url: `${this.baseUrl}/projects`,
210 | headers: this.tokenHeader,
211 | method: 'POST',
212 | crossDomain: true,
213 | dataType: 'json',
214 | contentType: 'application/json',
215 | data: JSON.stringify(payload)
216 | })
217 | .then(resolve)
218 | .fail(reject);
219 | })
220 | .then(response => {
221 | const repo = {
222 | fullName: response.path_with_namespace,
223 | id: response.id
224 | };
225 | Object.assign(context.bindRepo, {
226 | [getId()]: repo
227 | });
228 | if (context.bindBranch[getId()]) {
229 | delete context.bindBranch[getId()];
230 | }
231 | chrome.storage.sync.set({
232 | bindRepo: context.bindRepo
233 | });
234 | return response.path_with_namespace;
235 | })
236 | .then(repo => {
237 | return this.commitFiles(repo, 'master', null, [{
238 | name: "README.md",
239 | content: "initialed by gas-github"
240 | }], null, null, 'initial commit')
241 | .then(() => {
242 | return repo;
243 | });
244 | })
245 | .catch((err) => {
246 | throw new Error(`Failed to create new repository: ${err}`);
247 | });
248 | }
249 |
250 | createBranch() {
251 | const branch = $('#new-branch-name').val();
252 | const payload = {
253 | branch: branch,
254 | ref: getBranch()
255 | };
256 | if (!branch || branch === '') return;
257 | let repoId = getRepo().id || this.namesToIds.repos[getRepo().fullName];
258 | return new Promise((resolve, reject) => {
259 | return $.ajax({
260 | url: `${this.baseUrl}/projects/${repoId}/repository/branches`,
261 | headers: this.tokenHeader,
262 | method: 'POST',
263 | crossDomain: true,
264 | dataType: 'json',
265 | contentType: 'application/json',
266 | data: JSON.stringify(payload)
267 | })
268 | .then(resolve)
269 | .fail(reject);
270 | })
271 | .then(response => {
272 | Object.assign(context.bindBranch, {
273 | [getId()]: branch
274 | });
275 | chrome.storage.sync.set({
276 | bindBranch: context.bindBranch
277 | });
278 | return branch;
279 | })
280 | .catch(err => {
281 | throw new Error('Failed to create new branch.');
282 | });
283 | }
284 |
285 | followPaginate(data) {
286 | return new Promise((resolve, reject) => {
287 | $.getJSON(`${data.url}&${data.tokenParam}`)
288 | .then((response, status, xhr) => {
289 | data.items = data.items.concat(response);
290 | const link = xhr.getResponseHeader('Link');
291 | let url = null;
292 | if (link) {
293 | const match = link.match(/<([^ ]*?)>; rel="next"/);
294 | url = match && match[1] ? match[1] : null;
295 | }
296 | resolve({
297 | items: data.items,
298 | url: url
299 | });
300 | })
301 | .fail(reject);
302 | })
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | function getId() {
3 | const match = window.location.href.match(/https:\/\/script\.google\.com(.*?)\/home\/projects\/([^/]*)\//);
4 | if (!match) return null;
5 | return match[2];
6 | }
7 |
8 | function getRepo() {
9 | return context.bindRepo[getId()];
10 | }
11 |
12 | function getBranch() {
13 | return context.bindBranch[getId()];
14 | }
15 |
16 | function getConfig() {
17 | let config = context.bindConfig[getId()];
18 | if (!config) {
19 | config = {
20 | filetype: '.gs',
21 | ignorePattern: [],
22 | manifestEnabled: true,
23 | }
24 | }
25 | return config;
26 | }
27 |
28 | function isGist() {
29 | const repo = getRepo();
30 | if(repo) {
31 | return repo.gist;
32 | }
33 | return false;
34 | }
35 |
36 | function getAllItems(promise, followMethod, type) {
37 | return promise.then(followMethod)
38 | .then((data) => {
39 | return data.url ? getAllItems(Promise.resolve(data), followMethod, type) : data.items;
40 | });
41 | }
42 |
43 | function createSCM(item) {
44 | switch (item.scm) {
45 | case 'github':
46 | return new Github(item.baseUrl, item.user, item.token);
47 | case 'bitbucket':
48 | return new Bitbucket(item.baseUrl, item.user, item.token);
49 | case 'gitlab':
50 | return new Gitlab(item.baseUrl, item.user, item.token);
51 | default:
52 | return new Github(item.baseUrl, item.user, item.token);
53 | }
54 | }
55 |
56 | function getGitHubJSON(url, accessToken, data) {
57 | return $.ajax({
58 | url: url,
59 | headers: {
60 | 'Authorization': `token ${accessToken}`
61 | },
62 | method: 'GET',
63 | crossDomain: true,
64 | dataType: 'json',
65 | data: data,
66 | contentType: 'application/json'
67 | })
68 | }
--------------------------------------------------------------------------------