diff --git a/.eslintrc.json b/.eslintrc.json index c3734c864..3178f2252 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,9 @@ { "root": true, - "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended" + ], "parserOptions": { "ecmaVersion": 2020 }, @@ -10,8 +13,19 @@ }, "rules": { "array-callback-return": "error", - "no-unused-vars": ["error", { "args": "none" }], + "no-unused-vars": [ + "error", + { + "args": "none" + } + ], "prefer-const": "warn", - "quotes": ["warn", "single", { "avoidEscape": true }] + "quotes": [ + "warn", + "single", + { + "avoidEscape": true + } + ] } -} +} \ No newline at end of file diff --git a/.mochaclicktest.json b/.mochaclicktest.json index 2fce8b884..c6a93e9e8 100644 --- a/.mochaclicktest.json +++ b/.mochaclicktest.json @@ -1,7 +1,7 @@ { "spec": "clicktests/spec.*.js", - "file": "./source/utils/winston.js", - "timeout": 35000, + "file": "./source/utils/logger.js", + "timeout": 20000, "bail": true, "exit": true -} +} \ No newline at end of file diff --git a/.mochatest.json b/.mochatest.json index 143e7484e..a5e6d283e 100644 --- a/.mochatest.json +++ b/.mochatest.json @@ -1,6 +1,6 @@ { "spec": "test/spec.*.js", - "file": "./source/utils/winston.js", - "timeout": 5000, + "file": "./source/utils/logger.js", + "timeout": 12000, "exit": true -} +} \ No newline at end of file diff --git a/bin/credentials-helper b/bin/credentials-helper index f974535ce..101573d69 100755 --- a/bin/credentials-helper +++ b/bin/credentials-helper @@ -1,22 +1,17 @@ #!/usr/bin/env node -const winston = require('../source/utils/winston'); const http = require('http'); const socketId = process.argv[2]; const portAndRootPath = process.argv[3]; const remote = process.argv[4]; const action = process.argv[5]; -winston.info(`Credentials helper invoked; portAndRootPath:${portAndRootPath} socketId:${socketId}`); - if (action == 'get') { - winston.info('Getting credentials'); http .get( `http://localhost:${portAndRootPath}/api/credentials?socketId=${socketId}&remote=${encodeURIComponent( remote )}`, (res) => { - winston.info('Got credentials'); let rawData = ''; res.on('data', (chunk) => { rawData += chunk; @@ -29,8 +24,8 @@ if (action == 'get') { } ) .on('error', (err) => { - winston.error("Error getting credentials, couldn't query server", err); + console.error("Error getting credentials, couldn't query server", err); }); } else { - winston.info(`Unhandled action: ${action}`); + console.info(`Unhandled action: ${action}`); } diff --git a/bin/ungit b/bin/ungit index 240b4e5b7..0eb3ee98e 100755 --- a/bin/ungit +++ b/bin/ungit @@ -3,7 +3,6 @@ const startLaunchTime = Date.now(); // eslint-disable-next-line no-unused-vars -- Imported for side effects -const winston = require('../source/utils/winston'); const config = require('../source/config'); const open = require('open'); const path = require('path'); diff --git a/clicktests/environment.js b/clicktests/environment.js index ef1255025..2ae988382 100644 --- a/clicktests/environment.js +++ b/clicktests/environment.js @@ -1,13 +1,13 @@ 'use strict'; -const winston = require('../source/utils/winston'); +const logger = require('../source/utils/logger'); const child_process = require('child_process'); const puppeteer = require('puppeteer'); -const net = require('net'); const request = require('superagent'); const mkdirp = require('mkdirp'); const util = require('util'); const rimraf = util.promisify(require('rimraf')); const { encodePath } = require('../source/address-parser'); +const portfinder = require('portfinder'); const portrange = 45032; module.exports = (config) => new Environment(config); @@ -29,8 +29,6 @@ class Environment { this.config.headless = this.config.headless === undefined ? true : this.config.headless; this.config.viewWidth = 1920; this.config.viewHeight = 1080; - this.config.showServerOutput = - this.config.showServerOutput === undefined ? true : this.config.showServerOutput; this.config.serverStartupOptions = this.config.serverStartupOptions || []; this.shuttinDown = false; } @@ -39,26 +37,6 @@ class Environment { return this.rootUrl; } - getPort() { - const tmpPortrange = portrange + Math.floor(Math.random() * 5000); - - return new Promise((resolve, reject) => { - const server = net.createServer(); - - server.listen({ port: tmpPortrange }, () => { - server.once('close', () => { - this.port = tmpPortrange; - this.rootUrl = `http://127.0.0.1:${this.port}${this.config.rootPath}`; - resolve(); - }); - server.close(); - }); - server.on('error', () => { - this.getPort().then(resolve); - }); - }); - } - async init() { try { this.browser = await puppeteer.launch({ @@ -68,17 +46,18 @@ class Environment { height: this.config.viewHeight, }, }); - - await this.getPort(); await this.startServer(); + await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (err) { - winston.error(err); + logger.error(err); throw new Error('Cannot confirm ungit start!!\n' + err); } } - startServer() { - winston.info('Starting ungit server...', this.config.serverStartupOptions); + async startServer() { + this.port = await portfinder.getPortPromise({ port: portrange }); + this.rootUrl = `http://127.0.0.1:${this.port}${this.config.rootPath}`; + logger.info(`Starting ungit server:${this.port} with ${this.config.serverStartupOptions}`); this.hasStarted = false; const options = [ @@ -102,10 +81,10 @@ class Environment { return new Promise((resolve, reject) => { ungitServer.stdout.on('data', (stdout) => { const stdoutStr = stdout.toString(); - if (this.config.showServerOutput) winston.verbose(prependLines('[server] ', stdoutStr)); + console.log(prependLines('[server] ', stdoutStr)); if (stdoutStr.indexOf('Ungit server already running') >= 0) { - winston.info('server-already-running'); + logger.info('server-already-running'); } if (stdoutStr.indexOf('## Ungit started ##') >= 0) { @@ -113,21 +92,21 @@ class Environment { reject(new Error('Ungit started twice, probably crashed.')); } else { this.hasStarted = true; - winston.info('Ungit server started.'); + logger.info('Ungit server started.'); resolve(); } } }); ungitServer.stderr.on('data', (stderr) => { const stderrStr = stderr.toString(); - winston.error(prependLines('[server ERROR] ', stderrStr)); + logger.error(prependLines('[server ERROR] ', stderrStr)); if (stderrStr.indexOf('EADDRINUSE') > -1) { - winston.info('retrying with different port'); + logger.info('retrying with different port'); ungitServer.kill('SIGINT'); reject(new Error('EADDRINUSE')); } }); - ungitServer.on('exit', () => winston.info('UNGIT SERVER EXITED')); + ungitServer.on('exit', () => logger.info('UNGIT SERVER EXITED')); }); } @@ -183,7 +162,7 @@ class Environment { await rimraf(options.path); await mkdirp(options.path); } else { - winston.info('Creating temp folder'); + logger.info('Creating temp folder'); options.path = await this.createTempFolder(); } await this.backgroundAction('POST', '/api/init', options); @@ -204,11 +183,12 @@ class Environment { message: `Init Commit ${x}`, files: [{ name: `testy${x}` }], }); + // `createCommits()` is used at create repo `this.page` may not be inited await this.createCommits(config, limit, x + 1); } - createTestFile(filename, repoPath) { - return this.backgroundAction('POST', '/api/testing/createfile', { + async createTestFile(filename, repoPath) { + await this.backgroundAction('POST', '/api/testing/createfile', { file: filename, path: repoPath, }); @@ -217,19 +197,23 @@ class Environment { // browser helpers async goto(url) { - winston.info('Go to page: ' + url); + logger.info('Go to page: ' + url); if (!this.page) { const pages = await this.browser.pages(); - const page = (this.page = pages[0]); - - page.on('console', (message) => { - const text = `[ui ${message.type()}] ${new Date().toISOString()} - ${message.text()}}`; + this.page = pages[0]; + this.page.on('console', (message) => { + const text = `[ui ${message.type()}] ${message.text()}`; if (message.type() === 'error' && !this.shuttinDown) { - winston.error(text); + const stackTraceString = message + .stackTrace() + .map((trace) => `\t${trace.lineNumber}: ${trace.url}`) + .join('\n'); + logger.error(text, stackTraceString); } else { - winston.info(text); + // text already has timestamp and etc as it is generated by logger as well. + console.log(text); } }); } @@ -240,14 +224,16 @@ class Environment { async openUngit(tempDirPath) { await this.goto(`${this.getRootUrl()}/#/repository?path=${encodePath(tempDirPath)}`); await this.waitForElementVisible('.repository-actions'); - await this.wait(1000); + await this.page.waitForNetworkIdle(); } - waitForElementVisible(selector) { - return this.page.waitForSelector(selector, { visible: true }); + waitForElementVisible(selector, timeout) { + logger.debug(`Waiting for visible: "${selector}"`); + return this.page.waitForSelector(selector, { visible: true, timeout: timeout || 6000 }); } - waitForElementHidden(selector) { - return this.page.waitForSelector(selector, { hidden: true }); + waitForElementHidden(selector, timeout) { + logger.debug(`Waiting for hidden: "${selector}"`); + return this.page.waitForSelector(selector, { hidden: true, timeout: timeout || 6000 }); } wait(duration) { return this.page.waitForTimeout(duration); @@ -267,33 +253,50 @@ class Environment { } async click(selector, clickCount) { - let elementHandle = await this.waitForElementVisible(selector); - try { - await elementHandle.click({ clickCount: clickCount }); - } catch (err1) { - elementHandle = await this.waitForElementVisible(selector); + logger.info(`clicking "${selector}"`); + + for (let i = 0; i < 3; i++) { try { - await elementHandle.click({ clickCount: clickCount }); // try click a second time to reduce test flakiness - } catch (err2) { - winston.error(`Failed to click element: ${selector}`); - throw err2; + const toClick = await this.waitForElementVisible(selector); + await this.wait(200); + await toClick.click({ delay: 100, clickCount: clickCount }); + break; + } catch (err) { + logger.error('error while clicking', err); } } + logger.info(`clicked "${selector}`); + } + + waitForBranch(branchName) { + const currentBranch = 'document.querySelector(".ref.branch.current")'; + return this.page.waitForFunction( + `${currentBranch} && ${currentBranch}.innerText && ${currentBranch}.innerText.trim() === "${branchName}"`, + { polling: 250 } + ); } async commit(commitMessage) { await this.waitForElementVisible('.files .file .btn-default'); await this.insert('.staging input.form-control', commitMessage); + const postCommitProm = this.setApiListener('/commit', 'POST'); await this.click('.commit-btn'); + await postCommitProm; await this.waitForElementHidden('.files .file .btn-default'); - await this.wait(1000); } async _createRef(type, name) { await this.click('.current ~ .new-ref button.showBranchingForm'); await this.insert('.ref-icons.new-ref.editing input', name); + await this.wait(500); + const createRefProm = + type === 'branch' + ? this.setApiListener('/branches', 'POST') + : this.setApiListener('/tags', 'POST'); await this.click(`.new-ref ${type === 'branch' ? '.btn-primary' : '.btn-default'}`); + await createRefProm; await this.waitForElementVisible(`.ref.${type}[data-ta-name="${name}"]`); + await this.ensureRedraw(); } createTag(name) { return this._createRef('tag', name); @@ -307,25 +310,176 @@ class Environment { await this.page.waitForSelector('.modal-dialog .btn-primary', { visible: true, timeout: 2000, - }); - await this.click('.modal-dialog .btn-primary'); + }); // not all ref actions opens dialog, this line may throw exception. + await this.awaitAndClick('.modal-dialog .btn-primary'); } catch (err) { /* ignore */ } await this.waitForElementHidden(`[data-ta-action="${action}"]:not([style*="display: none"])`); + await this.ensureRedraw(); } - async refAction(ref, local, action) { - await this.click(`.branch[data-ta-name="${ref}"][data-ta-local="${local}"]`); + async _refAction(ref, local, action, validateFunc) { + if (!this[`_${action}ResponseWatcher`]) { + this.page.on('response', async (response) => { + const url = response.url(); + const method = response.request().method(); + + if (validateFunc(url, method)) { + this.page.evaluate(`ungit._${action}Response = true`); + } + }); + this[`_${action}ResponseWatcher`] = true; + } + await this.clickOnNode(`.branch[data-ta-name="${ref}"][data-ta-local="${local}"]`); await this.click(`[data-ta-action="${action}"]:not([style*="display: none"]) .dropmask`); await this._verifyRefAction(action); + await this.page.waitForFunction(`ungit._${action}Response`, { polling: 250 }); + await this.page.evaluate(`ungit._${action}Response = undefined`); + } + + async pushRefAction(ref, local) { + await this._refAction(ref, local, 'push', (url, method) => { + if (method !== 'POST') { + return false; + } + if ( + url.indexOf('/push') === -1 && + url.indexOf('/tags') === -1 && + url.indexOf('/branches') === -1 + ) { + return false; + } + return true; + }); + } + + async rebaseRefAction(ref, local) { + await this._refAction(ref, local, 'rebase', (url, method) => { + return method === 'POST' && url.indexOf('/rebase') >= -1; + }); + } + + async mergeRefAction(ref, local) { + await this._refAction(ref, local, 'merge', (url, method) => { + return method === 'POST' && url.indexOf('/merge') >= -1; + }); } async moveRef(ref, targetNodeCommitTitle) { - await this.click(`.branch[data-ta-name="${ref}"]`); + await this.clickOnNode(`.branch[data-ta-name="${ref}"]`); + if (!this._isMoveResponseWatcherSet) { + this.page.on('response', async (response) => { + const url = response.url(); + if (response.request().method() !== 'POST') { + return; + } + if ( + url.indexOf('/reset') === -1 && + url.indexOf('/tags') === -1 && + url.indexOf('/branches') === -1 + ) { + return; + } + this.page.evaluate('ungit._moveEventResponded = true'); + }); + this._isMoveResponseWatcherSet = true; + } await this.click( `[data-ta-node-title="${targetNodeCommitTitle}"] [data-ta-action="move"]:not([style*="display: none"]) .dropmask` ); await this._verifyRefAction('move'); + await this.page.waitForFunction('ungit._moveEventResponded', { polling: 250 }); + await this.page.evaluate('ungit._moveEventResponded = undefined'); + } + + // Explicitly trigger two program events. + // Usually these events are triggered by mouse movements, or api calls + // and etc. This function is to help mimic those movements. + triggerProgramEvents() { + return this.page.evaluate((_) => { + const isActive = ungit.programEvents.active; + if (!isActive) { + ungit.programEvents.active = true; + } + ungit.programEvents.dispatch({ event: 'working-tree-changed' }); + if (!isActive) { + ungit.programEvents.active = false; + } + }); + } + + async ensureRedraw() { + logger.debug('ensureRedraw triggered'); + if (!this._gitlogResposneWatcher) { + this.page.on('response', async (response) => { + if (response.url().indexOf('/gitlog') > 0 && response.request().method() === 'GET') { + this.page.evaluate('ungit._gitlogResponse = true'); + } + }); + this._gitlogResposneWatcher = true; + } + await this.page.evaluate('ungit._gitlogResponse = undefined'); + await this.triggerProgramEvents(); + await this.page.waitForFunction('ungit._gitlogResponse', { polling: 250 }); + await this.page.waitForFunction( + 'ungit.__app.content().repository().graph._isLoadNodesFromApiRunning === false', + { polling: 250 } + ); + logger.debug('ensureRedraw finished'); + } + + async awaitAndClick(selector, time = 1000) { + await this.wait(time); + await this.click(selector); + } + + // After a click on `git-node` or `git-ref`, ensure `currentActionContext` is set + async clickOnNode(nodeSelector) { + await this.awaitAndClick(nodeSelector); + await this.page.waitForFunction( + () => { + const app = ungit.__app; + if (!app) { + return; + } + const path = app.content(); + if (!path || path.constructor.name !== 'PathViewModel') { + return; + } + const repository = path.repository(); + if (!repository) { + return; + } + const graph = repository.graph; + if (!graph) { + return; + } + return graph.currentActionContext(); + }, + { polling: 250 } + ); + logger.debug(`clickOnNode ${nodeSelector} finished`); + } + + // If an api call matches `apiPart` and `method` is called, set the `globalVarName` + // to true. Use for detect if an API call was made and responded. + setApiListener(apiPart, method, bodyMatcher = () => true) { + const randomVariable = `ungit._${Math.floor(Math.random() * 500000)}`; + this.page.on( + 'response', + async (response) => { + if (response.url().indexOf(apiPart) > -1 && response.request().method() === method) { + if (bodyMatcher(await response.json())) { + // reponse body matcher is matched, set the value to true + this.page.evaluate(`${randomVariable} = true`); + } + } + }, + { polling: 250 } + ); + return this.page + .waitForFunction(`${randomVariable} === true`, { polling: 250 }) + .then(() => this.page.evaluate(`${randomVariable} = undefined`)); } } diff --git a/clicktests/spec.authentication.js b/clicktests/spec.authentication.js index f5a9f4937..9b17b9e76 100644 --- a/clicktests/spec.authentication.js +++ b/clicktests/spec.authentication.js @@ -9,19 +9,22 @@ describe('[AUTHENTICATION]', () => { before('Environment init without temp folder', () => environment.init()); after('Environment stop', () => environment.shutdown()); - it('Open home screen should show authentication dialog', async () => { + it('Open home screen should show authentication dialog', async function () { + this.retries(3); await environment.goto(environment.getRootUrl()); await environment.waitForElementVisible('.login'); }); - it('Filling out the authentication with wrong details should result in an error', async () => { + it('Filling out the authentication with wrong details should result in an error', async function () { + this.retries(3); await environment.insert('.login #inputUsername', testuser.username); await environment.insert('.login #inputPassword', 'notthepassword'); await environment.click('.login button'); await environment.waitForElementVisible('.login .loginError'); }); - it('Filling out the authentication should bring you to the home screen', async () => { + it('Filling out the authentication should bring you to the home screen', async function () { + this.retries(3); await environment.insert('.login #inputUsername', testuser.username); await environment.insert('.login #inputPassword', testuser.password); await environment.click('.login button'); diff --git a/clicktests/spec.bare.js b/clicktests/spec.bare.js index 014349da2..9755302b2 100644 --- a/clicktests/spec.bare.js +++ b/clicktests/spec.bare.js @@ -14,7 +14,10 @@ describe('[BARE]', () => { }); it('update branches button without branches', async () => { + const apiResponseProm = environment.setApiListener('/branches?', 'GET'); + const refResponseProm = environment.setApiListener('/refs?', 'GET'); await environment.click('.btn-group.branch .btn-main'); - await environment.waitForElementHidden('#nprogress'); + await apiResponseProm; + await refResponseProm; }); }); diff --git a/clicktests/spec.branches.js b/clicktests/spec.branches.js index 0fba63839..442ad1cf9 100644 --- a/clicktests/spec.branches.js +++ b/clicktests/spec.branches.js @@ -1,6 +1,7 @@ 'use strict'; const environment = require('./environment')(); const testRepoPaths = []; +const _ = require('lodash'); describe('[BRANCHES]', () => { before('Environment init', async () => { @@ -28,25 +29,63 @@ describe('[BRANCHES]', () => { }); it('add tag should make one of the branch disappear', async () => { + const branchesResponse = environment.setApiListener('/tags', 'POST'); await environment.createTag('tag-1'); + await branchesResponse; await environment.waitForElementHidden('[data-ta-name="search-4"]'); }); it('search for the hidden branch', async () => { - await environment.wait(1000); // sleep to avoid `git-directory-changed` event, which refreshes git nodes and closes search box - await environment.click('.showSearchForm'); - - await environment.type('-4'); + await environment.awaitAndClick('.showSearchForm'); await environment.wait(500); + await environment.type('-4'); + await environment.waitForElementVisible('.branch-search'); + await environment.page.waitForFunction( + 'document.querySelectorAll(".ui-menu-item-wrapper").length > 0 && document.querySelectorAll(".ui-menu-item-wrapper")[0].text.trim() === "search-4"', + { polling: 250 } + ); await environment.press('ArrowDown'); await environment.press('Enter'); - await environment.waitForElementVisible('[data-ta-name="search-4"]'); + await environment.waitForElementVisible('[data-ta-name="search-4"]', 10000); }); it('updateBranches button without branches', async () => { + const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => { + return _.isEqual(body, [ + { name: 'master', current: true }, + { name: 'search-1' }, + { name: 'search-2' }, + { name: 'search-3' }, + { name: 'search-4' }, + ]); + }); + const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => { + body.forEach((ref) => delete ref.sha1); + return _.isEqual(body, [ + { + name: 'refs/heads/master', + }, + { + name: 'refs/heads/search-1', + }, + { + name: 'refs/heads/search-2', + }, + { + name: 'refs/heads/search-3', + }, + { + name: 'refs/heads/search-4', + }, + { + name: 'refs/tags/tag-1', + }, + ]); + }); await environment.click('.btn-group.branch .btn-main'); - await environment.waitForElementHidden('#nprogress'); + await branchesResponse; + await refsResponse; }); it('add a branch', () => { @@ -54,8 +93,43 @@ describe('[BRANCHES]', () => { }); it('updateBranches button with one branch', async () => { + const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => { + return _.isEqual(body, [ + { name: 'branch-1' }, + { name: 'master', current: true }, + { name: 'search-1' }, + { name: 'search-2' }, + { name: 'search-3' }, + { name: 'search-4' }, + ]); + }); + const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => { + body.forEach((ref) => delete ref.sha1); + return _.isEqual(body, [ + { name: 'refs/heads/branch-1' }, + { + name: 'refs/heads/master', + }, + { + name: 'refs/heads/search-1', + }, + { + name: 'refs/heads/search-2', + }, + { + name: 'refs/heads/search-3', + }, + { + name: 'refs/heads/search-4', + }, + { + name: 'refs/tags/tag-1', + }, + ]); + }); await environment.click('.btn-group.branch .btn-main'); - await environment.waitForElementHidden('#nprogress'); + await branchesResponse; + await refsResponse; }); it('add second branch', async () => { @@ -73,56 +147,80 @@ describe('[BRANCHES]', () => { }); it('Delete a branch via selection', async () => { + const branchDeleteResponse = environment.setApiListener('/branches?', 'DELETE'); await environment.click('.branch .dropdown-toggle'); await environment.click('[data-ta-clickable="refs/heads/branch-3-remove"]'); - await environment.click('.modal-dialog .btn-primary'); - await environment.waitForElementHidden('#nprogress'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await branchDeleteResponse; }); it('add another commit', async () => { await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]); await environment.commit('commit-3'); + await environment.ensureRedraw(); }); - it('checkout cherypick base', async () => { + it('checkout cherrypick base', async () => { + const checkoutResponse = environment.setApiListener('/checkout', 'POST'); await environment.click('.branch .dropdown-toggle'); await environment.click('[data-ta-clickable="checkoutrefs/heads/branch-1"]'); + await checkoutResponse; + await environment.ensureRedraw(); await environment.waitForElementVisible('[data-ta-name="branch-1"].current'); - await environment.waitForElementHidden('#nprogress'); }); - it('cherrypick fail case', async () => { - await environment.click('[data-ta-clickable="node-clickable-0"]'); - await environment.click( + it('cherrypick abort case', async () => { + await environment.wait(1000); + await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]'); + await environment.awaitAndClick( '[data-ta-action="cherry-pick"]:not([style*="display: none"]) .dropmask' ); - await environment.click('.staging .btn-stg-abort'); - await environment.click('.modal-dialog .btn-primary'); - - await environment.waitForElementVisible('[data-ta-clickable="node-clickable-0"]'); // wait for nodes to come back + await environment.awaitAndClick('.modal-dialog .btn-primary', 2000); + const gitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => { + return _.isEqual( + body.nodes.map((node) => node.message), + ['commit-3', 'commit-2', 'commit-1'] + ); + }); + await environment.ensureRedraw(); + await gitlogResponse; }); it('cherrypick success case', async () => { - await environment.click('[data-ta-clickable="node-clickable-1"]'); + const cherrypickPostResponed = environment.setApiListener('/cherrypick', 'POST'); + await environment.clickOnNode('[data-ta-clickable="node-clickable-1"]'); await environment.click( '[data-ta-action="cherry-pick"]:not([style*="display: none"]) .dropmask' ); + await cherrypickPostResponed; + const cherrypickGitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => { + return _.isEqual( + body.nodes.map((node) => node.message), + ['commit-2', 'commit-3', 'commit-2', 'commit-1'] + ); + }); + await environment.ensureRedraw(); + await cherrypickGitlogResponse; await environment.waitForElementVisible('[data-ta-node-title="commit-2"] .ref.branch.current'); }); it('test backward squash from own lineage', async () => { - await environment.click('.ref.branch.current'); + await environment.wait(1000); + await environment.waitForBranch('branch-1'); + await environment.clickOnNode('.ref.branch.current'); await environment.click('[data-ta-node-title="commit-1"] .squash .dropmask'); await environment.waitForElementVisible('.staging .files .file'); await environment.click('.files button.discard'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary', 2000); + await environment.ensureRedraw(); await environment.waitForElementHidden('.staging .files .file'); }); it('test forward squash from different lineage', async () => { - await environment.click('.ref.branch.current'); + await environment.clickOnNode('.ref.branch.current'); await environment.click('[data-ta-node-title="commit-3"] .squash .dropmask'); + await environment.ensureRedraw(); await environment.waitForElementVisible('.staging .files .file'); }); diff --git a/clicktests/spec.commands.js b/clicktests/spec.commands.js index c5b6ed7d7..33eab7710 100644 --- a/clicktests/spec.commands.js +++ b/clicktests/spec.commands.js @@ -1,6 +1,7 @@ 'use strict'; const environment = require('./environment')(); const testRepoPaths = []; +const _ = require('lodash'); const gitCommand = (options) => { return environment.backgroundAction('POST', '/api/testing/git', options); @@ -17,7 +18,7 @@ const testForBranchMove = async (branch, command) => { const newLoc = document.querySelector(branch).getBoundingClientRect(); return newLoc.top !== oldLoc.top || newLoc.left !== oldLoc.left; }, - {}, + { timeout: 6000, polling: 250 }, branch, JSON.parse(branchTagLoc) ); @@ -61,18 +62,45 @@ describe('[COMMANDS]', () => { }); it('test branch delete from command line', async () => { + const brachesResponseProm = environment.setApiListener('/branches?', 'GET', (body) => { + return _.isEqual(body, [ + { name: 'branch-1' }, + { name: 'branch-2' }, + { name: 'master', current: true }, + ]); + }); await gitCommand({ command: ['branch', '-D', 'gitCommandBranch'], path: testRepoPaths[0] }); - await environment.waitForElementHidden('[data-ta-name="gitCommandBranch"]'); + await brachesResponseProm; + await environment.waitForElementHidden('[data-ta-name="gitCommandBranch"]', 10000); }); it('test tag create from command line', async () => { + const refsResponseProm = environment.setApiListener('/refs?', 'GET', (body) => { + body.forEach((ref) => delete ref.sha1); + return _.isEqual(body, [ + { name: 'refs/heads/branch-1' }, + { name: 'refs/heads/branch-2' }, + { name: 'refs/heads/master' }, + { name: 'refs/tags/tag1' }, + ]); + }); await gitCommand({ command: ['tag', 'tag1'], path: testRepoPaths[0] }); - await environment.waitForElementVisible('[data-ta-name="tag1"]'); + await refsResponseProm; + await environment.waitForElementVisible('[data-ta-name="tag1"]', 10000); }); it('test tag delete from command line', async () => { + const refDeleteResponseProm = environment.setApiListener('/refs?', 'GET', (body) => { + body.forEach((ref) => delete ref.sha1); + return _.isEqual(body, [ + { name: 'refs/heads/branch-1' }, + { name: 'refs/heads/branch-2' }, + { name: 'refs/heads/master' }, + ]); + }); await gitCommand({ command: ['tag', '-d', 'tag1'], path: testRepoPaths[0] }); - await environment.waitForElementHidden('[data-ta-name="tag1"]'); + await refDeleteResponseProm; + await environment.waitForElementHidden('[data-ta-name="tag1"]', 10000); }); it('test reset from command line', () => { diff --git a/clicktests/spec.discard.js b/clicktests/spec.discard.js index 7880eeefa..fdc3615ce 100644 --- a/clicktests/spec.discard.js +++ b/clicktests/spec.discard.js @@ -1,21 +1,23 @@ 'use strict'; -const muteGraceTimeDuration = 3000; + +const muteGraceTimeDuration = 5000; const createAndDiscard = async (env, testRepoPath, dialogButtonToClick) => { await env.createTestFile(testRepoPath + '/testfile2.txt', testRepoPath); + await env.ensureRedraw(); await env.waitForElementVisible('.files .file .btn-default'); - await env.click('.files button.discard'); + await env.click('.files button.discard'); if (dialogButtonToClick === 'yes') { - await env.click('.modal-dialog [data-ta-action="yes"]'); + await env.awaitAndClick('.modal-dialog [data-ta-action="yes"]'); } else if (dialogButtonToClick === 'mute') { - await env.click('.modal-dialog [data-ta-action="mute"]'); + await env.awaitAndClick('.modal-dialog [data-ta-action="mute"]'); } else if (dialogButtonToClick === 'no') { - await env.click('.modal-dialog [data-ta-action="no"]'); + await env.awaitAndClick('.modal-dialog [data-ta-action="no"]'); } else { await env.waitForElementHidden('.modal-dialog [data-ta-action="yes"]'); } - if (dialogButtonToClick !== 'no') { + await env.ensureRedraw(); await env.waitForElementHidden('.files .file .btn-default'); } else { await env.waitForElementVisible('.files .file .btn-default'); @@ -72,8 +74,14 @@ describe('[DISCARD - withWarn]', () => { it('Should be possible to discard a created file and disable warn for awhile', async () => { await createAndDiscard(environment, testRepoPaths[0], 'mute'); + const start = new Date().getTime(); // this is when the "mute" timestamp is stamped await createAndDiscard(environment, testRepoPaths[0]); - await environment.wait(2000); + // ensure, at least 2 seconds has passed since mute timestamp is stamped + const end = new Date().getTime(); + const diff = muteGraceTimeDuration + 500 - (end - start); + if (diff > 0) { + await environment.wait(diff); + } await createAndDiscard(environment, testRepoPaths[0], 'yes'); }); }); diff --git a/clicktests/spec.generic.js b/clicktests/spec.generic.js index 1291cdce0..06dc19f9b 100644 --- a/clicktests/spec.generic.js +++ b/clicktests/spec.generic.js @@ -13,6 +13,7 @@ const changeTestFile = async (filename, repoPath) => { file: filename, path: repoPath, }); + await environment.ensureRedraw(); }; const amendCommit = async () => { try { @@ -21,7 +22,9 @@ const amendCommit = async () => { } catch (err) { await environment.click('.amend-link'); } + await environment.ensureRedraw(); await environment.click('.commit-btn'); + await environment.ensureRedraw(); await environment.waitForElementHidden('.files .file .btn-default'); }; @@ -67,14 +70,21 @@ describe('[GENERIC]', () => { it('Should be able to add a new file to .gitignore', async () => { await environment.createTestFile(`${testRepoPaths[0]}/addMeToIgnore.txt`, testRepoPaths[0]); await environment.waitForElementVisible('.files .file .btn-default'); + await environment.page.waitForFunction( + 'document.querySelectorAll(".files .file .btn-default").length === 1', + { polling: 250 } + ); await environment.click('.files button.ignore'); - await environment.wait(2000); + await environment.page.waitForFunction( + 'document.querySelector(".name.btn.btn-default").innerText.trim() === ".gitignore"' + ), + { polling: 250 }; await environment.click('.files button.ignore'); await environment.waitForElementHidden('.files .file .btn-default'); }); it('Test showing commit diff between two commits', async () => { - await environment.click('[data-ta-clickable="node-clickable-0"]'); + await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]'); await environment.waitForElementVisible('.diff-wrapper'); await environment.click('.commit-diff-filename'); await environment.waitForElementVisible('.commit-line-diffs'); @@ -92,7 +102,6 @@ describe('[GENERIC]', () => { it('Test whitespace', async () => { await environment.click('.commit-whitespace'); - await environment.waitForElementVisible('.commit-line-diffs'); await environment.click('[data-ta-clickable="node-clickable-0"]'); }); @@ -102,27 +111,29 @@ describe('[GENERIC]', () => { await environment.click('.files button'); await environment.waitForElementHidden('[data-ta-container="patch-file"]'); await environment.click('.files button.discard'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); await environment.waitForElementHidden('.files .file .btn-default'); }); - it('Should be possible to create a branch', () => { - return environment.createBranch('testbranch'); + it('Should be possible to create a branch', async () => { + await environment.createBranch('testbranch'); }); it('Should be possible to create and destroy a branch', async () => { await environment.createBranch('willbedeleted'); - await environment.click('.branch[data-ta-name="willbedeleted"]'); + await environment.clickOnNode('.branch[data-ta-name="willbedeleted"]'); await environment.click('[data-ta-action="delete"]:not([style*="display: none"]) .dropmask'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await environment.ensureRedraw(); await environment.waitForElementHidden('.branch[data-ta-name="willbedeleted"]'); }); it('Should be possible to create and destroy a tag', async () => { await environment.createTag('tagwillbedeleted'); - await environment.click('.graph .ref.tag[data-ta-name="tagwillbedeleted"]'); + await environment.clickOnNode('.graph .ref.tag[data-ta-name="tagwillbedeleted"]'); await environment.click('[data-ta-action="delete"]:not([style*="display: none"]) .dropmask'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await environment.ensureRedraw(); await environment.waitForElementHidden('.graph .ref.tag[data-ta-name="tagwillbedeleted"]'); }); @@ -131,6 +142,7 @@ describe('[GENERIC]', () => { await environment.waitForElementVisible('.files .file .btn-default'); await environment.insert('.staging input.form-control', 'My commit message'); await environment.click('.commit-btn'); + await environment.ensureRedraw(); await environment.waitForElementHidden('.files .file .btn-default'); }); @@ -138,9 +150,9 @@ describe('[GENERIC]', () => { await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]); await environment.waitForElementVisible('.files .file .additions'); await environment.waitForElementVisible('.files .file .deletions'); - await environment.click('.files button.discard'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await environment.ensureRedraw(); await environment.waitForElementHidden('.files .file .btn-default'); }); @@ -152,18 +164,20 @@ describe('[GENERIC]', () => { }); it('Checkout testbranch with action', async () => { - await environment.click('.branch[data-ta-name="testbranch"]'); + await environment.clickOnNode('.branch[data-ta-name="testbranch"]'); await environment.click('[data-ta-action="checkout"]:not([style*="display: none"]) .dropmask'); + await environment.ensureRedraw(); await environment.waitForElementVisible('.ref.branch[data-ta-name="testbranch"].current'); }); it('Create another commit', async () => { await environment.createTestFile(`${testRepoPaths[0]}/testy2.txt`, testRepoPaths[0]); await environment.commit('Branch commit'); + await environment.ensureRedraw(); }); - it('Rebase', () => { - return environment.refAction('testbranch', true, 'rebase'); + it('Rebase', async () => { + await environment.rebaseRefAction('testbranch', true); }); it('Checkout master with double click', async () => { @@ -174,16 +188,17 @@ describe('[GENERIC]', () => { it('Create yet another commit', async () => { await environment.createTestFile(`${testRepoPaths[0]}/testy3.txt`, testRepoPaths[0]); await environment.commit('Branch commit'); + await environment.ensureRedraw(); }); - it('Merge', () => { - return environment.refAction('testbranch', true, 'merge'); + it('Merge', async () => { + await environment.mergeRefAction('testbranch', true); }); it('Revert merge', async () => { - await environment.click('[data-ta-clickable="node-clickable-0"]'); - await environment.waitForElementVisible('[data-ta-action="revert"]'); + await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]'); await environment.click('[data-ta-action="revert"]'); + await environment.ensureRedraw(); await environment.waitForElementVisible( '[data-ta-node-title^="Revert \\"Merge branch \'testbranch\'"]' ); diff --git a/clicktests/spec.remotes.js b/clicktests/spec.remotes.js index d2584e265..9085f8cda 100644 --- a/clicktests/spec.remotes.js +++ b/clicktests/spec.remotes.js @@ -23,12 +23,14 @@ describe('[REMOTES]', () => { it('Should not be possible to push without remote', async () => { await environment.click('.branch[data-ta-name="master"][data-ta-local="true"]'); + await environment.ensureRedraw(); await environment.waitForElementHidden('[data-ta-action="push"]:not([style*="display: none"])'); }); it('Should not be possible to commit & push without remote', async () => { await environment.click('.amend-link'); await environment.click('.commit-grp .dropdown-toggle'); + await environment.ensureRedraw(); await environment.waitForElementVisible('.commitnpush.disabled'); }); @@ -38,8 +40,8 @@ describe('[REMOTES]', () => { await environment.insert('.modal #Name', 'myremote'); await environment.insert('.modal #Url', testRepoPaths[0]); - await environment.click('.modal .modal-footer .btn-primary'); - + await environment.awaitAndClick('.modal .modal-footer .btn-primary'); + await environment.ensureRedraw(); await environment.click('.fetchButton .dropdown-toggle'); await environment.waitForElementVisible( '.fetchButton .dropdown-menu [data-ta-clickable="myremote"]' @@ -47,15 +49,16 @@ describe('[REMOTES]', () => { }); it('Fetch from newly added remote', async () => { + const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET'); await environment.click('.fetchButton .btn-main'); - await environment.waitForElementHidden('#nprogress'); + await remoteGetResponseProm; }); it('Remote delete check', async () => { await environment.click('.fetchButton .dropdown-toggle'); await environment.click('[data-ta-clickable="myremote-remove"]'); - await environment.click('.modal-dialog .btn-primary'); - + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await environment.ensureRedraw(); await environment.click('.fetchButton .dropdown-toggle'); await environment.waitForElementHidden('[data-ta-clickable="myremote"]'); }); @@ -73,41 +76,42 @@ describe('[REMOTES]', () => { await environment.insert('#cloneToInput', testRepoPaths[2]); await environment.click('.uninited button[type="submit"]'); await environment.waitForElementVisible('.repository-view'); + await environment.wait(1000); // ensure click bindings are initialized }); it('Should be possible to fetch', async () => { + const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET'); await environment.click('.fetchButton .btn-main'); - await environment.waitForElementHidden('#nprogress'); + await remoteGetResponseProm; }); it('Should be possible to create and push a branch', async () => { await environment.createBranch('branchinclone'); - await environment.refAction('branchinclone', true, 'push'); + await environment.pushRefAction('branchinclone', true); await environment.waitForElementVisible('[data-ta-name="origin/branchinclone"]'); }); it('Should be possible to force push a branch', async () => { await environment.moveRef('branchinclone', 'Init Commit 0'); - await environment.refAction('branchinclone', true, 'push'); + await environment.pushRefAction('branchinclone', true); await environment.waitForElementHidden('[data-ta-action="push"]:not([style*="display: none"])'); }); it('Check for fetching remote branches for the branch list', async () => { await environment.click('.branch .dropdown-toggle'); await environment.click('.options input'); - await environment.wait(1000); - try { - await environment.page.waitForSelector('li .octicon-globe', { visible: true, timeout: 2000 }); - } catch (err) { - await environment.click('.options input'); - await environment.waitForElementVisible('li .octicon-globe'); - } + + await environment.ensureRedraw(); + + await environment.click('.options input'); + await environment.waitForElementVisible('li .octicon-globe'); }); it('checkout remote branches with matching local branch at wrong place', async () => { await environment.moveRef('branchinclone', 'Init Commit 1'); await environment.click('.branch .dropdown-toggle'); await environment.click('[data-ta-clickable="checkoutrefs/remotes/origin/branchinclone"]'); + await environment.ensureRedraw(); await environment.waitForElementVisible('[data-ta-name="branchinclone"][data-ta-local="true"]'); }); @@ -115,6 +119,7 @@ describe('[REMOTES]', () => { await environment.createTestFile(`${testRepoPaths[2]}/commitnpush.txt`, testRepoPaths[2]); await environment.waitForElementVisible('.files .file .btn-default'); await environment.insert('.staging input.form-control', 'Commit & Push'); + await environment.wait(250); await environment.click('.commit-grp .dropdown-toggle'); await environment.click('.commitnpush'); await environment.waitForElementVisible('[data-ta-node-title="Commit & Push"]'); @@ -125,7 +130,7 @@ describe('[REMOTES]', () => { await environment.insert('.staging input.form-control', 'Commit & Push with ff'); await environment.click('.commit-grp .dropdown-toggle'); await environment.click('.commitnpush'); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); await environment.waitForElementVisible('[data-ta-node-title="Commit & Push with ff"]'); }); }); diff --git a/clicktests/spec.screens.js b/clicktests/spec.screens.js index 89dc89a81..198064b0b 100644 --- a/clicktests/spec.screens.js +++ b/clicktests/spec.screens.js @@ -33,6 +33,7 @@ describe('[SCREENS]', () => { it('Clicking logo should bring you to home screen', async () => { await environment.click('.navbar .backlink'); await environment.waitForElementVisible('.home'); + await environment.wait(1000); }); it('Entering an invalid path and create directory in that location', async () => { @@ -44,18 +45,21 @@ describe('[SCREENS]', () => { await environment.waitForElementVisible('.invalid-path'); await environment.click('.invalid-path button'); await environment.waitForElementVisible('.uninited button.btn-primary'); + await environment.wait(1000); }); it('Entering an invalid path should bring you to an error screen', async () => { await environment.insert('.navbar .path-input-form input', '/a/path/that/doesnt/exist'); await environment.press('Enter'); await environment.waitForElementVisible('.invalid-path'); + await environment.wait(1000); }); it('Entering a path to a repo should bring you to that repo', async () => { await environment.insert('.navbar .path-input-form input', testRepoPaths[0]); await environment.press('Enter'); await environment.waitForElementVisible('.repository-view'); + await environment.wait(1000); }); // getting odd cross-domain-error. diff --git a/clicktests/spec.submodules.js b/clicktests/spec.submodules.js index b0254595c..1a99ff520 100644 --- a/clicktests/spec.submodules.js +++ b/clicktests/spec.submodules.js @@ -22,23 +22,25 @@ describe('[SUMBODULES]', () => { await environment.insert('.modal #Path', 'subrepo'); await environment.insert('.modal #Url', testRepoPaths[0]); - await environment.click('.modal-dialog .btn-primary'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await environment.ensureRedraw(); + }); + it('Submodule update', async () => { await environment.click('.submodule .dropdown-toggle'); await environment.waitForElementVisible( '.fetchButton .dropdown-menu [data-ta-clickable="subrepo"]' ); - }); - - it('Submodule update', async () => { - await environment.click('.fetchButton .update-submodule'); - await environment.waitForElementHidden('#nprogress'); + const submoduleResponseProm = environment.setApiListener('/submodules/update', 'POST'); + await environment.awaitAndClick('.fetchButton .update-submodule'); + await submoduleResponseProm; }); it('Submodule delete check', async () => { + const submoduleDeleteResponseProm = environment.setApiListener('/submodules?', 'DELETE'); await environment.click('.submodule .dropdown-toggle'); await environment.click('[data-ta-clickable="subrepo-remove"]'); - await environment.click('.modal-dialog .btn-primary'); - await environment.waitForElementHidden('#nprogress'); + await environment.awaitAndClick('.modal-dialog .btn-primary'); + await submoduleDeleteResponseProm; }); }); diff --git a/components/ComponentRoot.ts b/components/ComponentRoot.ts new file mode 100644 index 000000000..46b1ec95e --- /dev/null +++ b/components/ComponentRoot.ts @@ -0,0 +1,29 @@ +declare var ungit: any; + +export class ComponentRoot { + _apiCache: string; + defaultDebounceOption = { + maxWait: 1500, + leading: false, + trailing: true + } + + constructor() { } + + isSamePayload(value: any) { + const jsonString = JSON.stringify(value); + + if (this._apiCache === jsonString) { + ungit.logger.debug(`ignoring redraw for same ${this.constructor.name} payload.`); + return true; + } + ungit.logger.debug(`redrawing ${this.constructor.name} payload. \n${jsonString}`); + + this._apiCache = jsonString + return false; + } + + clearApiCache() { + this._apiCache = undefined + } +} diff --git a/components/app/app.html b/components/app/app.html index 4cc86279c..9a0052492 100644 --- a/components/app/app.html +++ b/components/app/app.html @@ -1,4 +1,5 @@ - + +
@@ -35,6 +36,7 @@
- - + + + \ No newline at end of file diff --git a/components/app/app.js b/components/app/app.js index 861f2b92e..33181e2aa 100644 --- a/components/app/app.js +++ b/components/app/app.js @@ -1,7 +1,7 @@ const ko = require('knockout'); const components = require('ungit-components'); -const programEvents = require('ungit-program-events'); const storage = require('ungit-storage'); +const $ = require('jquery'); components.register('app', (args) => { return new AppViewModel(args.appContainer, args.server); @@ -15,7 +15,7 @@ class AppViewModel { if (window.location.search.indexOf('noheader=true') < 0) { this.header = components.create('header', { app: this }); } - this.dialog = ko.observable(null); + this.modal = ko.observable(null); this.repoList = ko.observableArray(this.getRepoList()); // visitedRepositories is legacy, remove in the next version this.repoList.subscribe((newValue) => { storage.setItem('repositories', JSON.stringify(newValue)); @@ -94,12 +94,23 @@ class AppViewModel { this.content().updateAnimationFrame(deltaT); } onProgramEvent(event) { - if (event.event == 'request-credentials') this._handleCredentialsRequested(event); - else if (event.event == 'request-show-dialog') this.showDialog(event.dialog); - else if (event.event == 'request-remember-repo') this._handleRequestRememberRepo(event); + if (event.event === 'request-credentials') { + this._handleCredentialsRequested(event); + } else if (event.event === 'request-remember-repo') { + this._handleRequestRememberRepo(event); + } else if (event.event === 'modal-show-dialog') { + this.showModal(event.modal); + } else if (event.event === 'modal-close-dialog') { + $('.modal.fade').modal('hide'); + this.modal(undefined); + } - if (this.content() && this.content().onProgramEvent) this.content().onProgramEvent(event); - if (this.header && this.header.onProgramEvent) this.header.onProgramEvent(event); + if (this.content() && this.content().onProgramEvent) { + this.content().onProgramEvent(event); + } + if (this.header && this.header.onProgramEvent) { + this.header.onProgramEvent(event); + } } _handleRequestRememberRepo(event) { const repoPath = event.repoPath; @@ -111,26 +122,23 @@ class AppViewModel { // This happens for instance when we fetch nodes and remote tags at the same time if (!this._isShowingCredentialsDialog) { this._isShowingCredentialsDialog = true; - components - .create('credentialsdialog', { remote: event.remote }) - .show() - .closeThen((diag) => { - this._isShowingCredentialsDialog = false; - programEvents.dispatch({ - event: 'request-credentials-response', - username: diag.username(), - password: diag.password(), - }); - }); + components.showModal('credentialsmodal', { remote: event.remote }); } } - showDialog(dialog) { - this.dialog( - dialog.closeThen(() => { - this.dialog(null); - return dialog; - }) - ); + showModal(modal) { + this.modal(modal); + + // when dom is ready, open the modal + const checkExists = setInterval(() => { + const modalDom = $('.modal.fade'); + if (modalDom.length) { + clearInterval(checkExists); + modalDom.modal(); + modalDom.on('hidden.bs.modal', function () { + modal.close(); + }); + } + }, 200); } gitSetUserConfig(bugTracking) { this.server.getPromise('/userconfig').then((userConfig) => { diff --git a/components/branches/branches.js b/components/branches/branches.js index 9e1c20235..bccc9dd23 100644 --- a/components/branches/branches.js +++ b/components/branches/branches.js @@ -1,21 +1,23 @@ const ko = require('knockout'); -const _ = require('lodash'); const octicons = require('octicons'); const components = require('ungit-components'); -const programEvents = require('ungit-program-events'); const storage = require('ungit-storage'); const showRemote = 'showRemote'; const showBranch = 'showBranch'; const showTag = 'showTag'; +const { ComponentRoot } = require('../ComponentRoot'); +const _ = require('lodash'); components.register('branches', (args) => { return new BranchesViewModel(args.server, args.graph, args.repoPath); }); -class BranchesViewModel { +class BranchesViewModel extends ComponentRoot { constructor(server, graph, repoPath) { + super(); this.repoPath = repoPath; this.server = server; + this.updateRefs = _.debounce(this._updateRefs, 250, this.defaultDebounceOption); this.branchesAndLocalTags = ko.observableArray(); this.current = ko.observable(); this.isShowRemote = ko.observable(storage.getItem(showRemote) != 'false'); @@ -28,13 +30,21 @@ class BranchesViewModel { return value; }; this.shouldAutoFetch = ungit.config.autoFetch; - this.isShowRemote.subscribe(setLocalStorageAndUpdate.bind(null, showRemote)); - this.isShowBranch.subscribe(setLocalStorageAndUpdate.bind(null, showBranch)); - this.isShowTag.subscribe(setLocalStorageAndUpdate.bind(null, showTag)); + this.isShowRemote.subscribe(() => { + this.clearApiCache(); + setLocalStorageAndUpdate(showRemote); + }); + this.isShowBranch.subscribe(() => { + this.clearApiCache(); + setLocalStorageAndUpdate(showBranch); + }); + this.isShowTag.subscribe(() => { + this.clearApiCache(); + setLocalStorageAndUpdate(showTag); + }); this.refsLabel = ko.computed(() => this.current() || 'master (no commits yet)'); this.branchIcon = octicons['git-branch'].toSVG({ height: 18 }); this.closeIcon = octicons.x.toSVG({ height: 18 }); - this.updateRefsDebounced = _.debounce(this.updateRefs, 500); } checkoutBranch(branch) { @@ -50,73 +60,81 @@ class BranchesViewModel { if ( event.event === 'request-app-content-refresh' || event.event === 'branch-updated' || - event.event === 'git-directory-changed' + event.event === 'git-directory-changed' || + event.event === 'current-remote-changed' ) { - this.updateRefsDebounced(); + this.updateRefs(); } } - updateRefs(forceRemoteFetch) { + async _updateRefs(forceRemoteFetch) { forceRemoteFetch = forceRemoteFetch || this.shouldAutoFetch || ''; - const currentBranchProm = this.server - .getPromise('/branches', { path: this.repoPath() }) - .then((branches) => - branches.forEach((b) => { - if (b.current) { - this.current(b.name); - } - }) - ) - .catch((err) => { - this.current('~error'); + const branchesProm = this.server.getPromise('/branches', { path: this.repoPath() }); + const refsProm = this.server.getPromise('/refs', { + path: this.repoPath(), + remoteFetch: forceRemoteFetch, + }); + + try { + // set current branch + (await branchesProm).forEach((b) => { + if (b.current) { + this.current(b.name); + } }); + } catch (e) { + this.current('~error'); + ungit.logger.warn('error while setting current branch', e); + } + + try { + // update branches and tags references. + const refs = await refsProm; + if (this.isSamePayload(refs)) { + return; + } - // refreshes tags branches and remote branches - const refsProm = this.server - .getPromise('/refs', { path: this.repoPath(), remoteFetch: forceRemoteFetch }) - .then((refs) => { - const version = Date.now(); - const sorted = refs - .map((r) => { - const ref = this.graph.getRef(r.name.replace('refs/tags', 'tag: refs/tags')); - ref.node(this.graph.getNode(r.sha1)); - ref.version = version; - return ref; - }) - .sort((a, b) => { - if (a.current() || b.current()) { - return a.current() ? -1 : 1; - } else if (a.isRemoteBranch === b.isRemoteBranch) { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - } else { - return a.isRemoteBranch ? 1 : -1; + const version = Date.now(); + const sorted = refs + .map((r) => { + const ref = this.graph.getRef(r.name.replace('refs/tags', 'tag: refs/tags')); + ref.node(this.graph.getNode(r.sha1)); + ref.version = version; + return ref; + }) + .sort((a, b) => { + if (a.current() || b.current()) { + return a.current() ? -1 : 1; + } else if (a.isRemoteBranch === b.isRemoteBranch) { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; } - }) - .filter((ref) => { - if (ref.localRefName == 'refs/stash') return false; - if (ref.localRefName.endsWith('/HEAD')) return false; - if (!this.isShowRemote() && ref.isRemote) return false; - if (!this.isShowBranch() && ref.isBranch) return false; - if (!this.isShowTag() && ref.isTag) return false; - return true; - }); - this.branchesAndLocalTags(sorted); - this.graph.refs().forEach((ref) => { - // ref was removed from another source - if (!ref.isRemoteTag && ref.value !== 'HEAD' && (!ref.version || ref.version < version)) { - ref.remove(true); + return 0; + } else { + return a.isRemoteBranch ? 1 : -1; } + }) + .filter((ref) => { + if (ref.localRefName == 'refs/stash') return false; + if (ref.localRefName.endsWith('/HEAD')) return false; + if (!this.isShowRemote() && ref.isRemote) return false; + if (!this.isShowBranch() && ref.isBranch) return false; + if (!this.isShowTag() && ref.isTag) return false; + return true; }); - }) - .catch((e) => this.server.unhandledRejection(e)); - - return Promise.all([currentBranchProm, refsProm]); + this.branchesAndLocalTags(sorted); + this.graph.refs().forEach((ref) => { + // ref was removed from another source + if (!ref.isRemoteTag && ref.value !== 'HEAD' && (!ref.version || ref.version < version)) { + ref.remove(true); + } + }); + } catch (e) { + ungit.logger.error('error during branch update: ', e); + } } branchRemove(branch) { @@ -124,23 +142,13 @@ class BranchesViewModel { if (branch.isRemoteBranch) { details = `REMOTE ${details}`; } - components - .create('yesnodialog', { - title: 'Are you sure?', - details: 'Deleting ' + details + ' branch cannot be undone with ungit.', - }) - .show() - .closeThen((diag) => { - if (!diag.result()) return; - const url = `${branch.isRemote ? '/remote' : ''}/branches`; - return this.server - .delPromise(url, { - path: this.graph.repoPath(), - remote: branch.isRemote ? branch.remote : null, - name: branch.refName, - }) - .then(() => programEvents.dispatch({ event: 'working-tree-changed' })) - .catch((e) => this.server.unhandledRejection(e)); - }); + components.showModal('yesnomodal', { + title: 'Are you sure?', + details: 'Deleting ' + details + ' branch cannot be undone with ungit.', + closeFunc: (isYes) => { + if (!isYes) return; + return branch.remove(); + }, + }); } } diff --git a/components/dialogs/dialogs.js b/components/dialogs/dialogs.js deleted file mode 100644 index 6a499dcca..000000000 --- a/components/dialogs/dialogs.js +++ /dev/null @@ -1,253 +0,0 @@ -const ko = require('knockout'); -const components = require('ungit-components'); -const programEvents = require('ungit-program-events'); - -components.register('formdialog', (args) => new FormDialogViewModel(args.title)); -components.register( - 'credentialsdialog', - (args) => new CredentialsDialogViewModel({ remote: args.remote }) -); -components.register('addremotedialog', (args) => new AddRemoteDialogViewModel()); -components.register('addsubmoduledialog', (args) => new AddSubmoduleDialogViewModel()); -components.register('promptdialog', (args) => new PromptDialogViewModel(args.title, args.details)); -components.register('yesnodialog', (args) => new YesNoDialogViewModel(args.title, args.details)); -components.register( - 'yesnomutedialog', - (args) => new YesNoMuteDialogViewModel(args.title, args.details) -); -components.register( - 'toomanyfilesdialogviewmodel', - (args) => new TooManyFilesDialogViewModel(args.title, args.details) -); -components.register('texteditdialog', (args) => new TextEditDialog(args.title, args.content)); - -class DialogViewModel { - constructor(title) { - this.onclose = null; - this.title = ko.observable(title); - this.taDialogName = ko.observable(''); - this.closePromise = new Promise((resolve) => { - this.onclose = resolve; - }); - } - - closeThen(thenFunc) { - this.closePromise = this.closePromise.then(thenFunc); - return this; - } - - setCloser(closer) { - this.closer = closer; - } - - close() { - this.closer(); - } - - show() { - programEvents.dispatch({ event: 'request-show-dialog', dialog: this }); - return this; - } -} - -class FormDialogViewModel extends DialogViewModel { - constructor(title) { - super(title); - this.items = ko.observable([]); - this.isSubmitted = ko.observable(false); - this.showCancel = ko.observable(true); - } - - get template() { - return 'formDialog'; - } - - submit() { - this.isSubmitted(true); - this.close(); - } -} - -class CredentialsDialogViewModel extends FormDialogViewModel { - constructor(args) { - super(`Remote ${args.remote} requires authentication`); - this.taDialogName('credentials-dialog'); - this.showCancel(false); - this.username = ko.observable(); - this.password = ko.observable(); - const self = this; - this.items([ - { name: 'Username', value: self.username, type: 'text', autofocus: true }, - { name: 'Password', value: self.password, type: 'password', autofocus: false }, - ]); - } -} - -class AddRemoteDialogViewModel extends FormDialogViewModel { - constructor() { - super('Add new remote'); - this.taDialogName('add-remote'); - this.name = ko.observable(); - this.url = ko.observable(); - const self = this; - this.items([ - { name: 'Name', value: self.name, type: 'text', autofocus: true }, - { name: 'Url', value: self.url, type: 'text', autofocus: false }, - ]); - } -} - -class AddSubmoduleDialogViewModel extends FormDialogViewModel { - constructor() { - super('Add new submodule'); - this.taDialogName('add-submodule'); - this.path = ko.observable(); - this.url = ko.observable(); - const self = this; - this.items([ - { name: 'Path', value: self.path, type: 'text', autofocus: true }, - { name: 'Url', value: self.url, type: 'text', autofocus: false }, - ]); - } -} - -class PromptDialogViewModel extends DialogViewModel { - constructor(title, details) { - super(title); - this.alternatives = ko.observable(); - this.details = ko.observable(details); - } - - get template() { - return 'prompt'; - } -} - -class YesNoDialogViewModel extends PromptDialogViewModel { - constructor(title, details) { - super(title, details); - this.taDialogName('yes-no-dialog'); - this.result = ko.observable(false); - const self = this; - this.alternatives([ - { - label: 'Yes', - primary: true, - taId: 'yes', - click() { - self.result(true); - self.close(); - }, - }, - { - label: 'No', - primary: false, - taId: 'no', - click() { - self.result(false); - self.close(); - }, - }, - ]); - } -} - -class YesNoMuteDialogViewModel extends PromptDialogViewModel { - constructor(title, details) { - super(title, details); - this.taDialogName('yes-no-mute-dialog'); - this.result = ko.observable(false); - const self = this; - this.alternatives([ - { - label: 'Yes', - primary: true, - taId: 'yes', - click() { - self.result(true); - self.close(); - }, - }, - { - label: 'Yes and mute for awhile', - primary: false, - taId: 'mute', - click() { - self.result('mute'); - self.close(); - }, - }, - { - label: 'No', - primary: false, - taId: 'no', - click() { - self.result(false); - self.close(); - }, - }, - ]); - } -} - -class TooManyFilesDialogViewModel extends PromptDialogViewModel { - constructor(title, details) { - super(title, details); - this.taDialogName('yes-no-dialog'); - this.result = ko.observable(false); - const self = this; - this.alternatives([ - { - label: "Don't load", - primary: true, - taId: 'noLoad', - click() { - self.result(false); - self.close(); - }, - }, - { - label: 'Load anyway', - primary: false, - taId: 'loadAnyway', - click() { - self.result(true); - self.close(); - }, - }, - ]); - } -} - -class TextEditDialog extends PromptDialogViewModel { - constructor(title, content) { - super( - title, - `` - ); - this.taDialogName('text-edit-dialog'); - this.result = ko.observable(false); - const self = this; - this.alternatives([ - { - label: 'Save', - primary: true, - taId: 'save', - click() { - self.textAreaContent = document.querySelector('.modal-body .text-area-content').value; - self.result(true); - self.close(); - }, - }, - { - label: 'Cancel', - primary: false, - taId: 'cancel', - click() { - self.result(false); - self.close(); - }, - }, - ]); - } -} diff --git a/components/dialogs/ungit-plugin.json b/components/dialogs/ungit-plugin.json deleted file mode 100644 index 76ecc5822..000000000 --- a/components/dialogs/ungit-plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "exports": { - "knockoutTemplates": { - "formDialog": "formDialog.html", - "prompt": "prompt.html" - }, - "javascript": "dialogs.bundle.js" - } -} diff --git a/components/graph/git-graph-actions.js b/components/graph/git-graph-actions.js index f08193823..ff8319492 100644 --- a/components/graph/git-graph-actions.js +++ b/components/graph/git-graph-actions.js @@ -111,20 +111,26 @@ class Reset extends ActionBase { perform() { const context = this.graph.currentActionContext(); const remoteRef = context.getRemoteRef(this.graph.currentRemote()); - return components - .create('yesnodialog', { + return new Promise((resolve, reject) => { + components.showModal('yesnomodal', { title: 'Are you sure?', details: 'Resetting to ref: ' + remoteRef.name + ' cannot be undone with ungit.', - }) - .show() - .closeThen((diag) => { - if (!diag.result()) return; - return this.server - .postPromise('/reset', { path: this.graph.repoPath(), to: remoteRef.name, mode: 'hard' }) - .then(() => { + closeFunc: async (isYes) => { + if (isYes) { + await this.server + .postPromise('/reset', { + path: this.graph.repoPath(), + to: remoteRef.name, + mode: 'hard', + }) + .then(resolve) + .catch(reject); context.node(remoteRef.node()); - }); - }).closePromise; + } + this.isRunning(false); + }, + }); + }); } } @@ -155,7 +161,11 @@ class Rebase extends ActionBase { return this.server .postPromise('/rebase', { path: this.graph.repoPath(), onto: this.node.sha1 }) .catch((err) => { - if (err.errorCode != 'merge-failed') this.server.unhandledRejection(err); + if (err.errorCode != 'merge-failed') { + this.server.unhandledRejection(err); + } else { + ungit.logger.warn('rebase failed', err); + } }); } } @@ -189,7 +199,11 @@ class Merge extends ActionBase { with: this.graph.currentActionContext().localRefName, }) .catch((err) => { - if (err.errorCode != 'merge-failed') this.server.unhandledRejection(err); + if (err.errorCode != 'merge-failed') { + this.server.unhandledRejection(err); + } else { + ungit.logger.warn('merge failed', err); + } }); } } @@ -277,12 +291,18 @@ class Delete extends ActionBase { } details = `Deleting ${details} branch or tag cannot be undone with ungit.`; - return components - .create('yesnodialog', { title: 'Are you sure?', details: details }) - .show() - .closeThen((diag) => { - if (diag.result()) return context.remove(); - }).closePromise; + return new Promise((resolve, reject) => { + components.showModal('yesnomodal', { + title: 'Are you sure?', + details: details, + closeFunc: async (isYes) => { + if (isYes) { + await context.remove().then(resolve).catch(reject); + } + this.isRunning(false); + }, + }); + }); } } @@ -301,7 +321,11 @@ class CherryPick extends ActionBase { return this.server .postPromise('/cherrypick', { path: this.graph.repoPath(), name: this.node.sha1 }) .catch((err) => { - if (err.errorCode != 'merge-failed') this.server.unhandledRejection(err); + if (err.errorCode != 'merge-failed') { + this.server.unhandledRejection(err); + } else { + ungit.logger.warn('cherrypick failed', err); + } }); } } diff --git a/components/graph/git-node.js b/components/graph/git-node.js index 247282549..b7c00eb25 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -202,7 +202,7 @@ class GitNodeViewModel extends Animateable { return false; }, }); - $textBox.focus((event) => { + $textBox.on('focus', (event) => { $(event.target).autocomplete('search', event.target.value); }); $textBox.autocomplete('search', ''); diff --git a/components/graph/git-ref.js b/components/graph/git-ref.js index 954873e22..22dd46e4f 100644 --- a/components/graph/git-ref.js +++ b/components/graph/git-ref.js @@ -121,17 +121,17 @@ class RefViewModel extends Selectable { } if (!rewindWarnOverride && this.node().date > toNode.date) { - promise = components - .create('yesnodialog', { + promise = new Promise((resolve, reject) => { + components.showModal('yesnomodal', { title: 'Are you sure?', details: 'This operation potentially going back in history.', - }) - .show() - .closeThen((diag) => { - if (diag.result()) { - return this.server.postPromise(operation, args); - } - }).closePromise; + closeFunc: (isYes) => { + if (isYes) { + return this.server.postPromise(operation, args).then(resolve).catch(reject); + } + }, + }); + }); } else { promise = this.server.postPromise(operation, args); } @@ -144,17 +144,17 @@ class RefViewModel extends Selectable { }; promise = this.server.postPromise('/push', pushReq).catch((err) => { if (err.errorCode === 'non-fast-forward') { - return components - .create('yesnodialog', { + return new Promise((resolve, reject) => { + components.showModal('yesnomodal', { title: 'Force push?', details: "The remote branch can't be fast-forwarded.", - }) - .show() - .closeThen((diag) => { - if (!diag.result()) return false; - pushReq.force = true; - return this.server.postPromise('/push', pushReq); - }).closePromise; + closeFunc: (isYes) => { + if (!isYes) return resolve(false); + pushReq.force = true; + this.server.postPromise('/push', pushReq).then(resolve).catch(reject); + }, + }); + }); } else { this.server.unhandledRejection(err); } @@ -273,7 +273,11 @@ class RefViewModel extends Selectable { this.graph.HEADref().node(this.node()); }) .catch((err) => { - if (err.errorCode != 'merge-failed') this.server.unhandledRejection(err); + if (err.errorCode != 'merge-failed') { + this.server.unhandledRejection(err); + } else { + ungit.logger.warn('checkout failed', err); + } }); } } diff --git a/components/graph/graph.html b/components/graph/graph.html index f8416fad2..f95484a7b 100644 --- a/components/graph/graph.html +++ b/components/graph/graph.html @@ -109,12 +109,12 @@ data-placement="bottom" title="Search for a branch or tag" > -
+ diff --git a/components/graph/graph.js b/components/graph/graph.js index 9d1fe086a..c3b02377e 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -6,12 +6,17 @@ const components = require('ungit-components'); const GitNodeViewModel = require('./git-node'); const GitRefViewModel = require('./git-ref'); const EdgeViewModel = require('./edge'); +const { ComponentRoot } = require('../ComponentRoot'); const numberOfNodesPerLoad = ungit.config.numberOfNodesPerLoad; components.register('graph', (args) => new GraphViewModel(args.server, args.repoPath)); -class GraphViewModel { +class GraphViewModel extends ComponentRoot { constructor(server, repoPath) { + super(); + this._isLoadNodesFromApiRunning = false; + this.updateBranches = _.debounce(this._updateBranches, 250, this.defaultDebounceOption); + this.loadNodesFromApi = _.debounce(this._loadNodesFromApi, 250, this.defaultDebounceOption); this._markIdeologicalStamp = 0; this.repoPath = repoPath; this.limit = ko.observable(numberOfNodesPerLoad); @@ -74,8 +79,6 @@ class GraphViewModel { } }); - this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000); - this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000); this.loadNodesFromApi(); this.updateBranches(); this.graphWidth = ko.observable(); @@ -108,48 +111,50 @@ class GraphViewModel { return refViewModel; } - loadNodesFromApi() { + async _loadNodesFromApi() { + this._isLoadNodesFromApiRunning = true; + ungit.logger.debug('graph.loadNodesFromApi() triggered'); const nodeSize = this.nodes().length; + const edges = []; - return this.server - .getPromise('/gitlog', { path: this.repoPath(), limit: this.limit(), skip: this.skip() }) - .then((log) => { - // set new limit and skip - this.limit(parseInt(log.limit)); - this.skip(parseInt(log.skip)); - return log.nodes || []; - }) - .then((nodes) => - // create and/or calculate nodes - this.computeNode( - nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry); // convert to node object - }) - ) - ) - .then((nodes) => { - // create edges - const edges = []; - nodes.forEach((node) => { - node.parents().forEach((parentSha1) => { - edges.push(this.getEdge(node.sha1, parentSha1)); - }); - node.render(); + try { + const log = await this.server.getPromise('/gitlog', { + path: this.repoPath(), + limit: this.limit(), + skip: this.skip(), + }); + if (this.isSamePayload(log)) { + return; + } + const nodes = this.computeNode( + (log.nodes || []).map((logEntry) => { + return this.getNode(logEntry.sha1, logEntry); // convert to node object + }) + ); + + // create edges + nodes.forEach((node) => { + node.parents().forEach((parentSha1) => { + edges.push(this.getEdge(node.sha1, parentSha1)); }); - - this.edges(edges); - this.nodes(nodes); - if (nodes.length > 0) { - this.graphHeight(nodes[nodes.length - 1].cy() + 80); - } - this.graphWidth(1000 + this.heighstBranchOrder * 90); - }) - .catch((e) => this.server.unhandledRejection(e)) - .finally(() => { - if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { - this.scrolledToEnd(); - } + node.render(); }); + + this.edges(edges); + this.nodes(nodes); + if (nodes.length > 0) { + this.graphHeight(nodes[nodes.length - 1].cy() + 80); + } + this.graphWidth(1000 + this.heighstBranchOrder * 90); + } catch (e) { + this.server.unhandledRejection(e); + } finally { + if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { + this.scrolledToEnd(); + } + this._isLoadNodesFromApiRunning = false; + ungit.logger.debug('graph.loadNodesFromApi() finished'); + } } traverseNodeLeftParents(node, callback) { @@ -163,7 +168,7 @@ class GraphViewModel { computeNode(nodes) { nodes = nodes || this.nodes(); - this.markNodesIdeologicalBranches(this.refs(), nodes, this.nodesById); + this.markNodesIdeologicalBranches(this.refs()); const updateTimeStamp = moment().valueOf(); if (this.HEAD()) { @@ -218,7 +223,7 @@ class GraphViewModel { return edge; } - markNodesIdeologicalBranches(refs, nodes, nodesById) { + markNodesIdeologicalBranches(refs) { refs = refs.filter((r) => !!r.node()); refs = refs.sort((a, b) => { if (a.isLocal && !b.isLocal) return -1; @@ -272,11 +277,11 @@ class GraphViewModel { } onProgramEvent(event) { - if (event.event == 'git-directory-changed') { - this.loadNodesFromApiThrottled(); - this.updateBranchesThrottled(); + if (event.event == 'git-directory-changed' || event.event === 'working-tree-changed') { + this.loadNodesFromApi(); + this.updateBranches(); } else if (event.event == 'request-app-content-refresh') { - this.loadNodesFromApiThrottled(); + this.loadNodesFromApi(); } else if (event.event == 'remote-tags-update') { this.setRemoteTags(event.tags); } else if (event.event == 'current-remote-changed') { @@ -294,15 +299,19 @@ class GraphViewModel { }); } - updateBranches() { - this.server - .getPromise('/checkout', { path: this.repoPath() }) - .then((res) => { - this.checkedOutBranch(res); - }) - .catch((err) => { - if (err.errorCode != 'not-a-repository') this.server.unhandledRejection(err); - }); + async _updateBranches() { + const checkout = await this.server.getPromise('/checkout', { path: this.repoPath() }); + + try { + ungit.logger.debug('setting checkedOutBranch', checkout); + this.checkedOutBranch(checkout); + } catch (err) { + if (err.errorCode != 'not-a-repository') { + this.server.unhandledRejection(err); + } else { + ungit.logger.warn('updateBranches failed', err); + } + } } setRemoteTags(remoteTags) { diff --git a/components/dialogs/formDialog.html b/components/modals/formModal.html similarity index 54% rename from components/dialogs/formDialog.html rename to components/modals/formModal.html index aab5e62b0..03de18009 100644 --- a/components/dialogs/formDialog.html +++ b/components/modals/formModal.html @@ -1,9 +1,4 @@ -