diff --git a/img/unlock.svg b/img/unlock.svg
new file mode 100644
index 0000000000..9e7927c140
--- /dev/null
+++ b/img/unlock.svg
@@ -0,0 +1,61 @@
+
+
diff --git a/lib/atom/octicon.js b/lib/atom/octicon.js
index 1f306dda38..c4a3d14676 100644
--- a/lib/atom/octicon.js
+++ b/lib/atom/octicon.js
@@ -2,8 +2,32 @@ import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
+/* eslint-disable max-len */
+const SVG = {
+ unlock: {
+ viewBox: '0 0 24 16',
+ element: (
+
+ ),
+ },
+};
+/* eslint-enable max-len */
+
export default function Octicon({icon, ...others}) {
const classes = cx('icon', `icon-${icon}`, others.className);
+
+ const svgContent = SVG[icon];
+ if (svgContent) {
+ return (
+
+ );
+ }
+
return ;
}
diff --git a/lib/containers/github-tab-header-container.js b/lib/containers/github-tab-header-container.js
index c421ef68b0..8bab9d4e8f 100644
--- a/lib/containers/github-tab-header-container.js
+++ b/lib/containers/github-tab-header-container.js
@@ -17,10 +17,12 @@ export default class GithubTabHeaderContainer extends React.Component {
// Workspace
currentWorkDir: PropTypes.string,
+ contextLocked: PropTypes.bool.isRequired,
+ changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
getCurrentWorkDirs: PropTypes.func.isRequired,
// Event Handlers
- handleWorkDirSelect: PropTypes.func,
onDidChangeWorkDirs: PropTypes.func,
}
@@ -59,12 +61,12 @@ export default class GithubTabHeaderContainer extends React.Component {
environment={environment}
variables={{}}
query={query}
- render={result => this.renderWithResult(result, token)}
+ render={result => this.renderWithResult(result)}
/>
);
}
- renderWithResult({error, props, retry}, token) {
+ renderWithResult({error, props}) {
if (error || props === null) {
return this.renderNoResult();
}
@@ -78,10 +80,12 @@ export default class GithubTabHeaderContainer extends React.Component {
// Workspace
currentWorkDir={this.props.currentWorkDir}
+ contextLocked={this.props.contextLocked}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
+ changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
// Event Handlers
- handleWorkDirSelect={this.props.handleWorkDirSelect}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
/>
);
@@ -94,10 +98,12 @@ export default class GithubTabHeaderContainer extends React.Component {
// Workspace
currentWorkDir={this.props.currentWorkDir}
+ contextLocked={this.props.contextLocked}
+ changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
// Event Handlers
- handleWorkDirSelect={this.props.handleWorkDirSelect}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
/>
);
diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index 3b3a93e553..070abd08e5 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -51,7 +51,9 @@ export default class GitTabController extends React.Component {
openFiles: PropTypes.func.isRequired,
openInitializeDialog: PropTypes.func.isRequired,
controllerRef: RefHolderPropType,
+ contextLocked: PropTypes.bool.isRequired,
changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
onDidChangeWorkDirs: PropTypes.func.isRequired,
getCurrentWorkDirs: PropTypes.func.isRequired,
};
@@ -122,7 +124,9 @@ export default class GitTabController extends React.Component {
openFiles={this.props.openFiles}
discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths}
undoLastDiscard={this.props.undoLastDiscard}
+ contextLocked={this.props.contextLocked}
changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
diff --git a/lib/controllers/git-tab-header-controller.js b/lib/controllers/git-tab-header-controller.js
index b0a6831531..78d4b8f5b7 100644
--- a/lib/controllers/git-tab-header-controller.js
+++ b/lib/controllers/git-tab-header-controller.js
@@ -11,9 +11,11 @@ export default class GitTabHeaderController extends React.Component {
// Workspace
currentWorkDir: PropTypes.string,
getCurrentWorkDirs: PropTypes.func.isRequired,
+ changeWorkingDirectory: PropTypes.func.isRequired,
+ contextLocked: PropTypes.bool.isRequired,
+ setContextLock: PropTypes.func.isRequired,
// Event Handlers
- handleWorkDirSelect: PropTypes.func.isRequired,
onDidChangeWorkDirs: PropTypes.func.isRequired,
onDidUpdateRepo: PropTypes.func.isRequired,
}
@@ -21,11 +23,16 @@ export default class GitTabHeaderController extends React.Component {
constructor(props) {
super(props);
this._isMounted = false;
- this.state = {currentWorkDirs: [], committer: nullAuthor};
+ this.state = {
+ currentWorkDirs: [],
+ committer: nullAuthor,
+ changingLock: null,
+ changingWorkDir: null,
+ };
this.disposable = new CompositeDisposable();
}
- static getDerivedStateFromProps(props, state) {
+ static getDerivedStateFromProps(props) {
return {
currentWorkDirs: props.getCurrentWorkDirs(),
};
@@ -59,17 +66,49 @@ export default class GitTabHeaderController extends React.Component {
committer={this.state.committer}
// Workspace
- workdir={this.props.currentWorkDir}
+ workdir={this.getWorkDir()}
workdirs={this.state.currentWorkDirs}
+ contextLocked={this.getLocked()}
+ changingWorkDir={this.state.changingWorkDir !== null}
+ changingLock={this.state.changingLock !== null}
// Event Handlers
- handleWorkDirSelect={this.props.handleWorkDirSelect}
+ handleWorkDirSelect={this.handleWorkDirSelect}
+ handleLockToggle={this.handleLockToggle}
/>
);
}
+ handleLockToggle = async () => {
+ if (this.state.changingLock !== null) {
+ return;
+ }
+
+ const nextLock = !this.props.contextLocked;
+ try {
+ this.setState({changingLock: nextLock});
+ await this.props.setContextLock(this.getWorkDir(), nextLock);
+ } finally {
+ await new Promise(resolve => this.setState({changingLock: null}, resolve));
+ }
+ }
+
+ handleWorkDirSelect = async e => {
+ if (this.state.changingWorkDir !== null) {
+ return;
+ }
+
+ const nextWorkDir = e.target.value;
+ try {
+ this.setState({changingWorkDir: nextWorkDir});
+ await this.props.changeWorkingDirectory(nextWorkDir);
+ } finally {
+ await new Promise(resolve => this.setState({changingWorkDir: null}, resolve));
+ }
+ }
+
resetWorkDirs = () => {
- this.setState((state, props) => ({
+ this.setState(() => ({
currentWorkDirs: [],
}));
}
@@ -81,6 +120,14 @@ export default class GitTabHeaderController extends React.Component {
}
}
+ getWorkDir() {
+ return this.state.changingWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir;
+ }
+
+ getLocked() {
+ return this.state.changingLock !== null ? this.state.changingLock : this.props.contextLocked;
+ }
+
componentWillUnmount() {
this._isMounted = false;
this.disposable.dispose();
diff --git a/lib/controllers/github-tab-controller.js b/lib/controllers/github-tab-controller.js
index 320d48b328..ea63951897 100644
--- a/lib/controllers/github-tab-controller.js
+++ b/lib/controllers/github-tab-controller.js
@@ -24,6 +24,8 @@ export default class GitHubTabController extends React.Component {
currentWorkDir: PropTypes.string,
changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
+ contextLocked: PropTypes.bool.isRequired,
onDidChangeWorkDirs: PropTypes.func.isRequired,
getCurrentWorkDirs: PropTypes.func.isRequired,
openCreateDialog: PropTypes.func.isRequired,
@@ -54,6 +56,7 @@ export default class GitHubTabController extends React.Component {
rootHolder={this.props.rootHolder}
workingDirectory={this.props.workingDirectory || this.props.currentWorkDir}
+ contextLocked={this.props.contextLocked}
repository={this.props.repository}
branches={this.props.branches}
currentBranch={currentBranch}
@@ -67,6 +70,7 @@ export default class GitHubTabController extends React.Component {
handlePushBranch={this.handlePushBranch}
handleRemoteSelect={this.handleRemoteSelect}
changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
openCreateDialog={this.props.openCreateDialog}
diff --git a/lib/controllers/github-tab-header-controller.js b/lib/controllers/github-tab-header-controller.js
index 78f4fba875..566a54cf3b 100644
--- a/lib/controllers/github-tab-header-controller.js
+++ b/lib/controllers/github-tab-header-controller.js
@@ -9,19 +9,26 @@ export default class GithubTabHeaderController extends React.Component {
// Workspace
currentWorkDir: PropTypes.string,
+ contextLocked: PropTypes.bool.isRequired,
+ changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
getCurrentWorkDirs: PropTypes.func.isRequired,
// Event Handlers
- handleWorkDirSelect: PropTypes.func.isRequired,
onDidChangeWorkDirs: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
- this.state = {currentWorkDirs: []};
+
+ this.state = {
+ currentWorkDirs: [],
+ changingLock: null,
+ changingWorkDir: null,
+ };
}
- static getDerivedStateFromProps(props, state) {
+ static getDerivedStateFromProps(props) {
return {
currentWorkDirs: props.getCurrentWorkDirs(),
};
@@ -46,21 +53,60 @@ export default class GithubTabHeaderController extends React.Component {
user={this.props.user}
// Workspace
- workdir={this.props.currentWorkDir}
+ workdir={this.getWorkDir()}
workdirs={this.state.currentWorkDirs}
+ contextLocked={this.getContextLocked()}
+ changingWorkDir={this.state.changingWorkDir !== null}
+ changingLock={this.state.changingLock !== null}
- // Event Handlers
- handleWorkDirSelect={this.props.handleWorkDirSelect}
+ handleWorkDirChange={this.handleWorkDirChange}
+ handleLockToggle={this.handleLockToggle}
/>
);
}
resetWorkDirs = () => {
- this.setState((state, props) => ({
+ this.setState(() => ({
currentWorkDirs: [],
}));
}
+ handleLockToggle = async () => {
+ if (this.state.changingLock !== null) {
+ return;
+ }
+
+ const nextLock = !this.props.contextLocked;
+ try {
+ this.setState({changingLock: nextLock});
+ await this.props.setContextLock(this.state.changingWorkDir || this.props.currentWorkDir, nextLock);
+ } finally {
+ await new Promise(resolve => this.setState({changingLock: null}, resolve));
+ }
+ }
+
+ handleWorkDirChange = async e => {
+ if (this.state.changingWorkDir !== null) {
+ return;
+ }
+
+ const nextWorkDir = e.target.value;
+ try {
+ this.setState({changingWorkDir: nextWorkDir});
+ await this.props.changeWorkingDirectory(nextWorkDir);
+ } finally {
+ await new Promise(resolve => this.setState({changingWorkDir: null}, resolve));
+ }
+ }
+
+ getWorkDir() {
+ return this.state.changingWorkDir !== null ? this.state.changingWorkDir : this.props.currentWorkDir;
+ }
+
+ getContextLocked() {
+ return this.state.changingLock !== null ? this.state.changingLock : this.props.contextLocked;
+ }
+
componentWillUnmount() {
this.disposable.dispose();
}
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index 708fdc3561..7ba6779ae3 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -67,7 +67,9 @@ export default class RootController extends React.Component {
clone: PropTypes.func.isRequired,
// Control
+ contextLocked: PropTypes.bool.isRequired,
changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
startOpen: PropTypes.bool,
startRevealed: PropTypes.bool,
}
@@ -294,7 +296,9 @@ export default class RootController extends React.Component {
currentWorkDir={this.props.currentWorkDir}
getCurrentWorkDirs={getCurrentWorkDirs}
onDidChangeWorkDirs={onDidChangeWorkDirs}
+ contextLocked={this.props.contextLocked}
changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
/>
)}
@@ -311,7 +315,9 @@ export default class RootController extends React.Component {
currentWorkDir={this.props.currentWorkDir}
getCurrentWorkDirs={getCurrentWorkDirs}
onDidChangeWorkDirs={onDidChangeWorkDirs}
+ contextLocked={this.props.contextLocked}
changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
openCreateDialog={this.openCreateDialog}
openPublishDialog={this.openPublishDialog}
openCloneDialog={this.openCloneDialog}
diff --git a/lib/github-package.js b/lib/github-package.js
index f2fb23f72d..f0c88c6c99 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -26,6 +26,8 @@ import {reporterProxy} from './reporter-proxy';
const defaultState = {
newProject: true,
+ activeRepositoryPath: null,
+ contextLocked: false,
};
export default class GithubPackage {
@@ -71,6 +73,7 @@ export default class GithubPackage {
this.activeContextQueue = new AsyncQueue();
this.guessedContext = WorkdirContext.guess(criteria, this.pipelineManager);
this.activeContext = this.guessedContext;
+ this.lockedContext = null;
this.workdirCache = new WorkdirCache();
this.contextPool = new WorkdirContextPool({
window,
@@ -159,10 +162,10 @@ export default class GithubPackage {
}
async activate(state = {}) {
- this.savedState = {...defaultState, ...state};
+ const savedState = {...defaultState, ...state};
const firstRun = !await fileExists(this.configPath);
- const newProject = this.savedState.firstRun !== undefined ? this.savedState.firstRun : this.savedState.newProject;
+ const newProject = savedState.firstRun !== undefined ? savedState.firstRun : savedState.newProject;
this.startOpen = firstRun || newProject;
this.startRevealed = firstRun && !this.config.get('welcome.showOnStartup');
@@ -175,14 +178,9 @@ export default class GithubPackage {
return !!event.target.closest('.github-FilePatchListView').querySelector('.is-selected');
};
- const handleProjectPathsChange = () => {
- const activeRepository = this.getActiveRepository();
- const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;
- this.scheduleActiveContextUpdate({activeRepositoryPath});
- };
-
this.subscriptions.add(
- this.project.onDidChangePaths(handleProjectPathsChange),
+ this.workspace.getCenter().onDidChangeActivePaneItem(this.handleActivePaneItemChange),
+ this.project.onDidChangePaths(this.handleProjectPathsChange),
this.styleCalculator.startWatching(
'github-package-styles',
['editor.fontSize', 'editor.fontFamily', 'editor.lineHeight', 'editor.tabLength'],
@@ -243,16 +241,33 @@ export default class GithubPackage {
);
this.activated = true;
- this.scheduleActiveContextUpdate(this.savedState);
+ this.scheduleActiveContextUpdate({
+ usePath: savedState.activeRepositoryPath,
+ lock: savedState.contextLocked,
+ });
this.rerender();
}
- serialize() {
- const activeRepository = this.getActiveRepository();
- const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;
+ handleActivePaneItemChange = () => {
+ if (this.lockedContext) {
+ return;
+ }
+ const itemPath = pathForPaneItem(this.workspace.getCenter().getActivePaneItem());
+ this.scheduleActiveContextUpdate({
+ usePath: itemPath,
+ lock: false,
+ });
+ }
+
+ handleProjectPathsChange = () => {
+ this.scheduleActiveContextUpdate();
+ }
+
+ serialize() {
return {
- activeRepositoryPath,
+ activeRepositoryPath: this.getActiveWorkdir(),
+ contextLocked: Boolean(this.lockedContext),
newProject: false,
};
}
@@ -275,7 +290,11 @@ export default class GithubPackage {
}
const changeWorkingDirectory = workingDirectory => {
- this.scheduleActiveContextUpdate({activeRepositoryPath: workingDirectory});
+ return this.scheduleActiveContextUpdate({usePath: workingDirectory});
+ };
+
+ const setContextLock = (workingDirectory, lock) => {
+ return this.scheduleActiveContextUpdate({usePath: workingDirectory, lock});
};
this.renderFn(
@@ -304,7 +323,9 @@ export default class GithubPackage {
startRevealed={this.startRevealed}
removeFilePatchItem={this.removeFilePatchItem}
currentWorkDir={this.getActiveWorkdir()}
+ contextLocked={this.lockedContext !== null}
changeWorkingDirectory={changeWorkingDirectory}
+ setContextLock={setContextLock}
/>, this.element, callback,
);
}
@@ -497,71 +518,146 @@ export default class GithubPackage {
return this.switchboard;
}
- async scheduleActiveContextUpdate(savedState = {}) {
+ /**
+ * Enqueue a request to modify the active context.
+ *
+ * options:
+ * usePath - Path of the context to use as the next context, if it is present in the pool.
+ * lock - True or false to lock the ultimately chosen context. Omit to preserve the current lock state.
+ *
+ * This method returns a Promise that resolves when the requested context update has completed. Note that it's
+ * *possible* for the active context after resolution to differ from a requested `usePath`, if the workdir
+ * containing `usePath` is no longer a viable option, such as if it belongs to a project that is no longer present.
+ */
+ async scheduleActiveContextUpdate(options = {}) {
this.switchboard.didScheduleActiveContextUpdate();
- await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false});
+ await this.activeContextQueue.push(this.updateActiveContext.bind(this, options), {parallel: false});
}
/**
* Derive the git working directory context that should be used for the package's git operations based on the current
* state of the Atom workspace. In priority, this prefers:
*
- * - The preferred git working directory set by the user (This is also the working directory that was active when the
- * package was last serialized).
- * - A git working directory corresponding to "first" Project, whether or not there is a single project or multiple.
+ * - When activating: the working directory that was active when the package was last serialized, if it still a viable
+ * option. (usePath)
+ * - The working directory chosen by the user from the context tile on the git or GitHub tabs. (usePath)
+ * - The working directory containing the path of the active pane item.
+ * - A git working directory corresponding to "first" project, if any projects are open.
* - The current context, unchanged, which may be a `NullWorkdirContext`.
*
* First updates the pool of resident contexts to match all git working directories that correspond to open
- * projects.
+ * projects and pane items.
*/
- async getNextContext(savedState) {
+ async getNextContext(usePath = null) {
+ // Identify paths that *could* contribute a git working directory to the pool. This is drawn from
+ // the roots of open projects, the currently locked context if one is present, and the path of the
+ // open workspace item.
+ const candidatePaths = new Set(this.project.getPaths());
+ if (this.lockedContext) {
+ const lockedRepo = this.lockedContext.getRepository();
+ /* istanbul ignore else */
+ if (lockedRepo) {
+ candidatePaths.add(lockedRepo.getWorkingDirectoryPath());
+ }
+ }
+ const activeItemPath = pathForPaneItem(this.workspace.getCenter().getActivePaneItem());
+ if (activeItemPath) {
+ candidatePaths.add(activeItemPath);
+ }
+
+ let activeItemWorkdir = null;
+ let firstProjectWorkdir = null;
+
+ // Convert the candidate paths into the set of viable git working directories, by means of a cached
+ // `git rev-parse` call. Candidate paths that are not contained within a git working directory will
+ // be preserved as-is within the pool, to allow users to initialize them.
const workdirs = new Set(
await Promise.all(
- this.project.getPaths().map(async projectPath => {
- const workdir = await this.workdirCache.find(projectPath);
- return workdir || projectPath;
+ Array.from(candidatePaths, async candidatePath => {
+ const workdir = (await this.workdirCache.find(candidatePath)) || candidatePath;
+ // Note the workdirs associated with the active pane item and the first open project so we can
+ // prefer them later.
+ if (candidatePath === activeItemPath) {
+ activeItemWorkdir = workdir;
+ } else if (candidatePath === this.project.getPaths()[0]) {
+ firstProjectWorkdir = workdir;
+ }
+ return workdir;
}),
),
);
- // Update pool with the open projects
- this.contextPool.set(workdirs, savedState);
+ // Update pool with the identified projects.
+ this.contextPool.set(workdirs);
- if (savedState.activeRepositoryPath) {
- // Preferred git directory (the preferred directory or the last serialized directory).
- const stateContext = this.contextPool.getContext(savedState.activeRepositoryPath);
- // If the context exists chose it, else continue.
+ // 1 - Explicitly requested workdir. This is either selected by the user from a context tile or
+ // deserialized from package state. Choose this context only if it still exists in the pool.
+ if (usePath) {
+ const stateContext = this.contextPool.getContext(usePath);
if (stateContext.isPresent()) {
return stateContext;
}
}
- const projectPaths = this.project.getPaths();
+ // 2 - Use the currently locked context, if one is present.
+ if (this.lockedContext) {
+ return this.lockedContext;
+ }
- if (projectPaths.length >= 1) {
- // Single or multiple projects (just choose the first, the user can select after)
- const projectPath = projectPaths[0];
- const activeWorkingDir = await this.workdirCache.find(projectPath);
- return this.contextPool.getContext(activeWorkingDir || projectPath);
+ // 3 - Follow the active workspace pane item.
+ if (activeItemWorkdir) {
+ return this.contextPool.getContext(activeItemWorkdir);
}
- if (projectPaths.length === 0 && !this.activeContext.getRepository().isUndetermined()) {
- // No projects. Revert to the absent context unless we've guessed that more projects are on the way.
+ // 4 - The first open project.
+ if (firstProjectWorkdir) {
+ return this.contextPool.getContext(firstProjectWorkdir);
+ }
+
+ // No projects. Revert to the absent context unless we've guessed that more projects are on the way.
+ if (this.project.getPaths().length === 0 && !this.activeContext.getRepository().isUndetermined()) {
return WorkdirContext.absent({pipelineManager: this.pipelineManager});
}
- // It is only possible to reach here if there there was no preferred directory, there are no project paths and the
- // the active context's repository is not undetermined.
+ // It is only possible to reach here if there there was no preferred directory, there are no project paths, and the
+ // the active context's repository is not undetermined. Preserve the existing active context.
return this.activeContext;
}
- setActiveContext(nextActiveContext) {
+ /**
+ * Modify the active context and re-render the React tree. This should only be done as part of the
+ * context update queue; use scheduleActiveContextUpdate() to do this.
+ *
+ * nextActiveContext - The WorkdirContext to make active next, as derived from the current workspace
+ * state by getNextContext(). This may be absent or undetermined.
+ * lock - If true, also set this context as the "locked" one and engage the context lock if it isn't
+ * already. If false, clear any existing context lock. If null or undefined, leave the lock in its
+ * existing state.
+ */
+ setActiveContext(nextActiveContext, lock) {
if (nextActiveContext !== this.activeContext) {
if (this.activeContext === this.guessedContext) {
this.guessedContext.destroy();
this.guessedContext = null;
}
this.activeContext = nextActiveContext;
+ if (lock === true) {
+ this.lockedContext = this.activeContext;
+ } else if (lock === false) {
+ this.lockedContext = null;
+ }
+
+ this.rerender(() => {
+ this.switchboard.didFinishContextChangeRender();
+ this.switchboard.didFinishActiveContextUpdate();
+ });
+ } else if ((lock === true || lock === false) && lock !== (this.lockedContext !== null)) {
+ if (lock) {
+ this.lockedContext = this.activeContext;
+ } else {
+ this.lockedContext = null;
+ }
+
this.rerender(() => {
this.switchboard.didFinishContextChangeRender();
this.switchboard.didFinishActiveContextUpdate();
@@ -571,15 +667,22 @@ export default class GithubPackage {
}
}
- async updateActiveContext(savedState = {}) {
+ /**
+ * Derive the next active context with getNextContext(), then enact the context change with setActiveContext().
+ *
+ * options:
+ * usePath - Path of the context to use as the next context, if it is present in the pool.
+ * lock - True or false to lock the ultimately chosen context. Omit to preserve the current lock state.
+ */
+ async updateActiveContext(options) {
if (this.workspace.isDestroyed()) {
return;
}
this.switchboard.didBeginActiveContextUpdate();
- const nextActiveContext = await this.getNextContext(savedState);
- this.setActiveContext(nextActiveContext);
+ const nextActiveContext = await this.getNextContext(options.usePath);
+ this.setActiveContext(nextActiveContext, options.lock);
}
async refreshAtomGitRepository(workdir) {
@@ -594,3 +697,22 @@ export default class GithubPackage {
}
}
}
+
+function pathForPaneItem(paneItem) {
+ if (!paneItem) {
+ return null;
+ }
+
+ // Likely GitHub package provided pane item
+ if (typeof paneItem.getWorkingDirectory === 'function') {
+ return paneItem.getWorkingDirectory();
+ }
+
+ // TextEditor-like
+ if (typeof paneItem.getPath === 'function') {
+ return paneItem.getPath();
+ }
+
+ // Oh well
+ return null;
+}
diff --git a/lib/views/git-tab-header-view.js b/lib/views/git-tab-header-view.js
index 78a71a990c..7189782236 100644
--- a/lib/views/git-tab-header-view.js
+++ b/lib/views/git-tab-header-view.js
@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import path from 'path';
+
import {AuthorPropType} from '../prop-types';
+import Octicon from '../atom/octicon';
export default class GitTabHeaderView extends React.Component {
static propTypes = {
@@ -10,20 +12,36 @@ export default class GitTabHeaderView extends React.Component {
// Workspace
workdir: PropTypes.string,
workdirs: PropTypes.shape({[Symbol.iterator]: PropTypes.func.isRequired}).isRequired,
+ contextLocked: PropTypes.bool.isRequired,
+ changingWorkDir: PropTypes.bool.isRequired,
+ changingLock: PropTypes.bool.isRequired,
// Event Handlers
handleWorkDirSelect: PropTypes.func,
+ handleLockToggle: PropTypes.func,
}
render() {
+ const lockIcon = this.props.contextLocked ? 'lock' : 'unlock';
+ const lockToggleTitle = this.props.contextLocked ?
+ 'Change repository with the dropdown' :
+ 'Follow the active pane item';
+
return (
{this.renderCommitter()}
+
);
}
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index a5f10cc687..93c7fa46a0 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -61,7 +61,9 @@ export default class GitTabView extends React.Component {
attemptFileStageOperation: PropTypes.func.isRequired,
discardWorkDirChangesForPaths: PropTypes.func.isRequired,
openFiles: PropTypes.func.isRequired,
+ contextLocked: PropTypes.bool.isRequired,
changeWorkingDirectory: PropTypes.func.isRequired,
+ setContextLock: PropTypes.func.isRequired,
onDidChangeWorkDirs: PropTypes.func.isRequired,
getCurrentWorkDirs: PropTypes.func.isRequired,
};
@@ -126,11 +128,13 @@ export default class GitTabView extends React.Component {
// Workspace
currentWorkDir={this.props.workingDirectoryPath}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
+ contextLocked={this.props.contextLocked}
+ changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
// Event Handlers
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
onDidUpdateRepo={repository.onDidUpdate.bind(repository)}
- handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)}
/>
);
}
diff --git a/lib/views/github-tab-header-view.js b/lib/views/github-tab-header-view.js
index fdcfa69b8a..bd3c6c1895 100644
--- a/lib/views/github-tab-header-view.js
+++ b/lib/views/github-tab-header-view.js
@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import path from 'path';
+
import {AuthorPropType} from '../prop-types';
+import Octicon from '../atom/octicon';
export default class GithubTabHeaderView extends React.Component {
static propTypes = {
@@ -10,20 +12,34 @@ export default class GithubTabHeaderView extends React.Component {
// Workspace
workdir: PropTypes.string,
workdirs: PropTypes.shape({[Symbol.iterator]: PropTypes.func.isRequired}).isRequired,
-
- // Event Handlers
- handleWorkDirSelect: PropTypes.func,
+ contextLocked: PropTypes.bool.isRequired,
+ changingWorkDir: PropTypes.bool.isRequired,
+ changingLock: PropTypes.bool.isRequired,
+ handleWorkDirChange: PropTypes.func.isRequired,
+ handleLockToggle: PropTypes.func.isRequired,
}
render() {
+ const lockIcon = this.props.contextLocked ? 'lock' : 'unlock';
+ const lockToggleTitle = this.props.contextLocked ?
+ 'Change repository with the dropdown' :
+ 'Follow the active pane item';
+
return (
{this.renderUser()}
+
);
}
diff --git a/lib/views/github-tab-view.js b/lib/views/github-tab-view.js
index a0add89b17..266bf4c93a 100644
--- a/lib/views/github-tab-view.js
+++ b/lib/views/github-tab-view.js
@@ -28,6 +28,8 @@ export default class GitHubTabView extends React.Component {
workingDirectory: PropTypes.string,
getCurrentWorkDirs: PropTypes.func.isRequired,
changeWorkingDirectory: PropTypes.func.isRequired,
+ contextLocked: PropTypes.bool.isRequired,
+ setContextLock: PropTypes.func.isRequired,
repository: PropTypes.object.isRequired,
// Remotes
@@ -40,7 +42,7 @@ export default class GitHubTabView extends React.Component {
aheadCount: PropTypes.number,
pushInProgress: PropTypes.bool.isRequired,
- // Event Handelrs
+ // Event Handlers
handleWorkDirSelect: PropTypes.func,
handlePushBranch: PropTypes.func.isRequired,
handleRemoteSelect: PropTypes.func.isRequired,
@@ -137,10 +139,13 @@ export default class GitHubTabView extends React.Component {
// Workspace
currentWorkDir={this.props.workingDirectory}
+ contextLocked={this.props.contextLocked}
+ changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
// Event Handlers
- handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)}
+ // handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
/>
);
@@ -151,10 +156,12 @@ export default class GitHubTabView extends React.Component {
// Workspace
currentWorkDir={this.props.workingDirectory}
+ contextLocked={this.props.contextLocked}
+ changeWorkingDirectory={this.props.changeWorkingDirectory}
+ setContextLock={this.props.setContextLock}
getCurrentWorkDirs={this.props.getCurrentWorkDirs}
// Event Handlers
- handleWorkDirSelect={e => this.props.changeWorkingDirectory(e.target.value)}
onDidChangeWorkDirs={this.props.onDidChangeWorkDirs}
/>
);
diff --git a/styles/project.less b/styles/project.less
index e425cba4e6..143cd6a5a8 100644
--- a/styles/project.less
+++ b/styles/project.less
@@ -17,4 +17,39 @@
border-radius: @component-border-radius;
}
+ &-lock.btn {
+ width: 40px;
+ height: 20px;
+ padding: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: baseline;
+
+ border: none;
+ background-color: transparent;
+ background-image: none;
+
+ &:active, &:hover {
+ background-color: transparent;
+ background-image: none;
+ }
+ }
+
+ .icon {
+ padding: 0;
+ margin: 0;
+ line-height: 1em;
+ fill: @text-color;
+
+ &:hover {
+ fill: @text-color-highlight;
+ }
+
+ &.icon-unlock {
+ width: 21px;
+ height: 17px;
+ padding-left: 1px;
+ }
+ }
}
diff --git a/test/atom/octicon.test.js b/test/atom/octicon.test.js
new file mode 100644
index 0000000000..f7f373cc8a
--- /dev/null
+++ b/test/atom/octicon.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import Octicon from '../../lib/atom/octicon';
+
+describe('Octicon', function() {
+ it('defaults to rendering an octicon span', function() {
+ const wrapper = shallow();
+ assert.isTrue(wrapper.exists('span.icon.icon-octoface'));
+ });
+
+ it('renders SVG overrides', function() {
+ const wrapper = shallow();
+
+ assert.strictEqual(wrapper.find('svg').prop('viewBox'), '0 0 24 16');
+ });
+});
diff --git a/test/containers/github-tab-header-container.test.js b/test/containers/github-tab-header-container.test.js
index 15eb6eeb9b..db68e86492 100644
--- a/test/containers/github-tab-header-container.test.js
+++ b/test/containers/github-tab-header-container.test.js
@@ -27,7 +27,12 @@ describe('GithubTabHeaderContainer', function() {
null}
+ currentWorkDir={null}
+ contextLocked={false}
+ changeWorkingDirectory={() => {}}
+ setContextLock={() => {}}
+ getCurrentWorkDirs={() => new Set()}
+ onDidChangeWorkDirs={() => {}}
{...overrideProps}
/>
);
diff --git a/test/controllers/git-tab-header-controller.test.js b/test/controllers/git-tab-header-controller.test.js
index 5c66d545b9..d6619fedde 100644
--- a/test/controllers/git-tab-header-controller.test.js
+++ b/test/controllers/git-tab-header-controller.test.js
@@ -15,17 +15,16 @@ describe('GitTabHeaderController', function() {
function buildApp(overrides) {
const props = {
getCommitter: () => nullAuthor,
+ currentWorkDir: null,
getCurrentWorkDirs: () => createWorkdirs([]),
- onDidUpdateRepo: () => new Disposable(),
+ changeWorkingDirectory: () => {},
+ contextLocked: false,
+ setContextLock: () => {},
onDidChangeWorkDirs: () => new Disposable(),
- handleWorkDirSelect: () => null,
+ onDidUpdateRepo: () => new Disposable(),
...overrides,
};
- return (
-
- );
+ return ;
}
it('get currentWorkDirs initializes workdirs state', function() {
@@ -112,6 +111,62 @@ describe('GitTabHeaderController', function() {
assert.isTrue(getCommitter.calledOnce);
});
+ it('handles a lock toggle', async function() {
+ let resolveLockChange;
+ const setContextLock = sinon.stub().returns(new Promise(resolve => {
+ resolveLockChange = resolve;
+ }));
+ const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock}));
+
+ assert.isFalse(wrapper.find('GitTabHeaderView').prop('contextLocked'));
+ assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingLock'));
+
+ const handlerPromise = wrapper.find('GitTabHeaderView').prop('handleLockToggle')();
+ wrapper.update();
+
+ assert.isTrue(wrapper.find('GitTabHeaderView').prop('contextLocked'));
+ assert.isTrue(wrapper.find('GitTabHeaderView').prop('changingLock'));
+ assert.isTrue(setContextLock.calledWith('the/workdir', true));
+
+ // Ignored while in-progress
+ wrapper.find('GitTabHeaderView').prop('handleLockToggle')();
+
+ resolveLockChange();
+ await handlerPromise;
+
+ assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingLock'));
+ });
+
+ it('handles a workdir selection', async function() {
+ let resolveWorkdirChange;
+ const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => {
+ resolveWorkdirChange = resolve;
+ }));
+ const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory}));
+
+ assert.strictEqual(wrapper.find('GitTabHeaderView').prop('workdir'), 'original');
+ assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingWorkDir'));
+
+ const handlerPromise = wrapper.find('GitTabHeaderView').prop('handleWorkDirSelect')({
+ target: {value: 'work/dir'},
+ });
+ wrapper.update();
+
+ assert.strictEqual(wrapper.find('GitTabHeaderView').prop('workdir'), 'work/dir');
+ assert.isTrue(wrapper.find('GitTabHeaderView').prop('changingWorkDir'));
+ assert.isTrue(changeWorkingDirectory.calledWith('work/dir'));
+
+ // Ignored while in-progress
+ wrapper.find('GitTabHeaderView').prop('handleWorkDirSelect')({
+ target: {value: 'ig/nored'},
+ });
+
+ resolveWorkdirChange();
+ await handlerPromise;
+
+ assert.isFalse(wrapper.find('GitTabHeaderView').prop('changingWorkDir'));
+ });
+
it('unmounts without error', function() {
const wrapper = shallow(buildApp());
wrapper.unmount();
diff --git a/test/controllers/github-tab-controller.test.js b/test/controllers/github-tab-controller.test.js
index ad5e3c2fd2..c7be628195 100644
--- a/test/controllers/github-tab-controller.test.js
+++ b/test/controllers/github-tab-controller.test.js
@@ -45,6 +45,8 @@ describe('GitHubTabController', function() {
currentWorkDir={repo.getWorkingDirectoryPath()}
changeWorkingDirectory={() => {}}
+ setContextLock={() => {}}
+ contextLocked={false}
onDidChangeWorkDirs={() => {}}
getCurrentWorkDirs={() => []}
openCreateDialog={() => {}}
diff --git a/test/controllers/github-tab-header-controller.test.js b/test/controllers/github-tab-header-controller.test.js
index 1e4f8529f1..994f1b8752 100644
--- a/test/controllers/github-tab-header-controller.test.js
+++ b/test/controllers/github-tab-header-controller.test.js
@@ -15,10 +15,12 @@ describe('GithubTabHeaderController', function() {
function buildApp(overrides) {
const props = {
user: nullAuthor,
+ currentWorkDir: null,
+ contextLocked: false,
+ changeWorkingDirectory: () => {},
+ setContextLock: () => {},
getCurrentWorkDirs: () => createWorkdirs([]),
- onDidUpdateRepo: () => new Disposable(),
onDidChangeWorkDirs: () => new Disposable(),
- handleWorkDirSelect: () => null,
...overrides,
};
return (
@@ -73,6 +75,62 @@ describe('GithubTabHeaderController', function() {
assert.isTrue(getCurrentWorkDirs.calledTwice);
});
+ it('handles a lock toggle', async function() {
+ let resolveLockChange;
+ const setContextLock = sinon.stub().returns(new Promise(resolve => {
+ resolveLockChange = resolve;
+ }));
+ const wrapper = shallow(buildApp({currentWorkDir: 'the/workdir', contextLocked: false, setContextLock}));
+
+ assert.isFalse(wrapper.find('GithubTabHeaderView').prop('contextLocked'));
+ assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingLock'));
+
+ const handlerPromise = wrapper.find('GithubTabHeaderView').prop('handleLockToggle')();
+ wrapper.update();
+
+ assert.isTrue(wrapper.find('GithubTabHeaderView').prop('contextLocked'));
+ assert.isTrue(wrapper.find('GithubTabHeaderView').prop('changingLock'));
+ assert.isTrue(setContextLock.calledWith('the/workdir', true));
+
+ // Ignored while in-progress
+ wrapper.find('GithubTabHeaderView').prop('handleLockToggle')();
+
+ resolveLockChange();
+ await handlerPromise;
+
+ assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingLock'));
+ });
+
+ it('handles a workdir selection', async function() {
+ let resolveWorkdirChange;
+ const changeWorkingDirectory = sinon.stub().returns(new Promise(resolve => {
+ resolveWorkdirChange = resolve;
+ }));
+ const wrapper = shallow(buildApp({currentWorkDir: 'original', changeWorkingDirectory}));
+
+ assert.strictEqual(wrapper.find('GithubTabHeaderView').prop('workdir'), 'original');
+ assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingWorkDir'));
+
+ const handlerPromise = wrapper.find('GithubTabHeaderView').prop('handleWorkDirChange')({
+ target: {value: 'work/dir'},
+ });
+ wrapper.update();
+
+ assert.strictEqual(wrapper.find('GithubTabHeaderView').prop('workdir'), 'work/dir');
+ assert.isTrue(wrapper.find('GithubTabHeaderView').prop('changingWorkDir'));
+ assert.isTrue(changeWorkingDirectory.calledWith('work/dir'));
+
+ // Ignored while in-progress
+ wrapper.find('GithubTabHeaderView').prop('handleWorkDirChange')({
+ target: {value: 'ig/nored'},
+ });
+
+ resolveWorkdirChange();
+ await handlerPromise;
+
+ assert.isFalse(wrapper.find('GithubTabHeaderView').prop('changingWorkDir'));
+ });
+
it('disposes on unmount', function() {
const disposeSpy = sinon.spy();
const onDidChangeWorkDirs = () => ({dispose: disposeSpy});
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index c2eb72e890..9d61f815ca 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -58,13 +58,13 @@ describe('RootController', function() {
workspace={workspace}
commands={commands}
deserializers={deserializers}
- grammars={grammars}
notificationManager={notificationManager}
tooltips={tooltips}
+ keymaps={atomEnv.keymaps}
+ grammars={grammars}
config={config}
- confirm={confirm}
project={project}
- keymaps={atomEnv.keymaps}
+ confirm={confirm}
currentWindow={atomEnv.getCurrentWindow()}
loginModel={loginModel}
@@ -72,9 +72,14 @@ describe('RootController', function() {
repository={absentRepository}
resolutionProgress={emptyResolutionProgress}
+ currentWorkDir={null}
+
initialize={() => {}}
clone={() => {}}
+ contextLocked={false}
+ changeWorkingDirectory={() => {}}
+ setContextLock={() => {}}
startOpen={false}
startRevealed={false}
/>
diff --git a/test/fixtures/props/git-tab-props.js b/test/fixtures/props/git-tab-props.js
index 5a8cf2d72b..8628000ecf 100644
--- a/test/fixtures/props/git-tab-props.js
+++ b/test/fixtures/props/git-tab-props.js
@@ -27,8 +27,10 @@ export function gitTabItemProps(atomEnv, repository, overrides = {}) {
openFiles: noop,
openInitializeDialog: noop,
changeWorkingDirectory: noop,
+ contextLocked: false,
+ setContextLock: () => {},
onDidChangeWorkDirs: () => ({dispose: () => {}}),
- getCurrentWorkDirs: () => [],
+ getCurrentWorkDirs: () => new Set(),
...overrides
};
}
@@ -102,9 +104,11 @@ export async function gitTabViewProps(atomEnv, repository, overrides = {}) {
discardWorkDirChangesForPaths: () => {},
openFiles: () => {},
+ contextLocked: false,
changeWorkingDirectory: () => {},
+ setContextLock: () => {},
onDidChangeWorkDirs: () => ({dispose: () => {}}),
- getCurrentWorkDirs: () => [],
+ getCurrentWorkDirs: () => new Set(),
onDidUpdateRepo: () => ({dispose: () => {}}),
getCommitter: () => nullAuthor,
diff --git a/test/github-package.test.js b/test/github-package.test.js
index dfb0308011..67d50bcddd 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -412,12 +412,23 @@ describe('GithubPackage', function() {
});
describe('when removing a project', function() {
- beforeEach(async function() {
+ it('removes the project\'s context', async function() {
await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath1]));
- });
- it('removes the project\'s context', function() {
assert.isFalse(contextPool.getContext(workdirPath2).isPresent());
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
+
+ it('does nothing if the context is locked', async function() {
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath2,
+ lock: true,
+ });
+
+ await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath1]));
+
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2);
});
});
@@ -434,6 +445,100 @@ describe('GithubPackage', function() {
assert.isTrue(githubPackage.getActiveRepository().isAbsent());
});
});
+
+ describe('when changing the active pane item', function() {
+ it('follows the active pane item', async function() {
+ const itemPath2 = path.join(workdirPath2, 'b.txt');
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath2));
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2);
+ });
+
+ it('does nothing if the context is locked', async function() {
+ const itemPath2 = path.join(workdirPath2, 'c.txt');
+
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath1,
+ lock: true,
+ });
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath2));
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
+ });
+
+ describe('with a locked context', function() {
+ it('preserves the locked context in the pool', async function() {
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath1,
+ lock: true,
+ });
+
+ await contextUpdateAfter(githubPackage, () => project.setPaths([workdirPath2]));
+
+ assert.isTrue(contextPool.getContext(workdirPath1).isPresent());
+ assert.isTrue(contextPool.getContext(workdirPath2).isPresent());
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
+
+ it('may be unlocked', async function() {
+ const itemPath1a = path.join(workdirPath1, 'a.txt');
+ const itemPath1b = path.join(workdirPath2, 'b.txt');
+
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath2,
+ lock: true,
+ });
+
+ await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath1a));
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath2);
+
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath1,
+ lock: false,
+ });
+
+ await contextUpdateAfter(githubPackage, () => atomEnv.workspace.open(itemPath1b));
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
+
+ it('triggers a re-render when the context is unchanged', async function() {
+ sinon.stub(githubPackage, 'rerender');
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath1,
+ lock: true,
+ });
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ assert.isTrue(githubPackage.rerender.called);
+ githubPackage.rerender.resetHistory();
+
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath1,
+ lock: false,
+ });
+
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ assert.isTrue(githubPackage.rerender.called);
+ });
+ });
+
+ it('does nothing when the workspace is destroyed', async function() {
+ sinon.stub(githubPackage, 'rerender');
+ atomEnv.destroy();
+
+ await githubPackage.scheduleActiveContextUpdate({
+ usePath: workdirPath2,
+ });
+
+ assert.isFalse(githubPackage.rerender.called);
+ assert.strictEqual(githubPackage.getActiveWorkdir(), workdirPath1);
+ });
});
describe('with non-repository, no-conflict, and in-progress merge-conflict projects', function() {
@@ -455,7 +560,7 @@ describe('GithubPackage', function() {
let resolutionMergeConflict;
beforeEach(async function() {
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: workdirMergeConflict,
+ usePath: workdirMergeConflict,
});
resolutionMergeConflict = contextPool.getContext(workdirMergeConflict).getResolutionProgress();
});
@@ -477,7 +582,7 @@ describe('GithubPackage', function() {
let resolutionNoConflict;
beforeEach(async function() {
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: workdirNoConflict,
+ usePath: workdirNoConflict,
});
resolutionNoConflict = contextPool.getContext(workdirNoConflict).getResolutionProgress();
});
@@ -494,7 +599,7 @@ describe('GithubPackage', function() {
describe('when opening a non-repository project', function() {
beforeEach(async function() {
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: nonRepositoryPath,
+ usePath: nonRepositoryPath,
});
});
@@ -515,7 +620,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: workdirPath3,
+ usePath: workdirPath3,
});
context1 = contextPool.getContext(workdirPath1);
});
@@ -538,7 +643,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1]);
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: workdirPath2,
+ usePath: workdirPath2,
});
context1 = contextPool.getContext(workdirPath1);
});
@@ -561,7 +666,7 @@ describe('GithubPackage', function() {
project.setPaths([workdirPath1, workdirPath2]);
await githubPackage.scheduleActiveContextUpdate({
- activeRepositoryPath: workdirPath2,
+ usePath: workdirPath2,
});
});
diff --git a/test/views/git-tab-header-view.test.js b/test/views/git-tab-header-view.test.js
index 5e0d9e8f7b..c1074551dd 100644
--- a/test/views/git-tab-header-view.test.js
+++ b/test/views/git-tab-header-view.test.js
@@ -15,7 +15,13 @@ describe('GitTabHeaderView', function() {
function build(options = {}) {
const props = {
committer: nullAuthor,
+ workdir: null,
workdirs: createWorkdirs([]),
+ contextLocked: false,
+ changingWorkDir: false,
+ changingLock: false,
+ handleWorkDirSelect: () => {},
+ handleLockToggle: () => {},
...options,
};
return shallow();
@@ -29,7 +35,11 @@ describe('GitTabHeaderView', function() {
beforeEach(function() {
select = sinon.spy();
- wrapper = build({handleWorkDirSelect: select, workdirs: createWorkdirs(paths), workdir: path2});
+ wrapper = build({
+ handleWorkDirSelect: select,
+ workdirs: createWorkdirs(paths),
+ workdir: path2,
+ });
});
it('renders an option for all given working directories', function() {
@@ -49,6 +59,42 @@ describe('GitTabHeaderView', function() {
});
});
+ describe('context lock control', function() {
+ it('renders locked when the lock is engaged', function() {
+ const wrapper = build({contextLocked: true});
+
+ assert.isTrue(wrapper.exists('Octicon[icon="lock"]'));
+ });
+
+ it('renders unlocked when the lock is disengaged', function() {
+ const wrapper = build({contextLocked: false});
+
+ assert.isTrue(wrapper.exists('Octicon[icon="unlock"]'));
+ });
+
+ it('calls handleLockToggle when the lock is clicked', function() {
+ const handleLockToggle = sinon.spy();
+ const wrapper = build({handleLockToggle});
+
+ wrapper.find('button').simulate('click');
+ assert.isTrue(handleLockToggle.called);
+ });
+ });
+
+ describe('when changes are in progress', function() {
+ it('disables the workdir select while the workdir is changing', function() {
+ const wrapper = build({changingWorkDir: true});
+
+ assert.isTrue(wrapper.find('select').prop('disabled'));
+ });
+
+ it('disables the context lock toggle while the context lock is changing', function() {
+ const wrapper = build({changingLock: true});
+
+ assert.isTrue(wrapper.find('button').prop('disabled'));
+ });
+ });
+
describe('with falsish props', function() {
let wrapper;
diff --git a/test/views/git-tab-view.test.js b/test/views/git-tab-view.test.js
index 05671f2f4b..1acc2c2732 100644
--- a/test/views/git-tab-view.test.js
+++ b/test/views/git-tab-view.test.js
@@ -286,7 +286,7 @@ describe('GitTabView', function() {
it('calls changeWorkingDirectory when a project is selected', async function() {
const changeWorkingDirectory = sinon.spy();
const wrapper = shallow(await buildApp({changeWorkingDirectory}));
- wrapper.find('GitTabHeaderController').prop('handleWorkDirSelect')({target: {value: 'some-path'}});
+ wrapper.find('GitTabHeaderController').prop('changeWorkingDirectory')('some-path');
assert.isTrue(changeWorkingDirectory.calledWith('some-path'));
});
});
diff --git a/test/views/github-tab-header-view.test.js b/test/views/github-tab-header-view.test.js
index 43cf57d060..882d92f43e 100644
--- a/test/views/github-tab-header-view.test.js
+++ b/test/views/github-tab-header-view.test.js
@@ -15,7 +15,13 @@ describe('GithubTabHeaderView', function() {
function build(options = {}) {
const props = {
user: nullAuthor,
+ workdir: null,
workdirs: createWorkdirs([]),
+ contextLocked: false,
+ changingWorkDir: false,
+ changingLock: false,
+ handleWorkDirChange: () => {},
+ handleLockToggle: () => {},
...options,
};
return shallow();
@@ -29,7 +35,7 @@ describe('GithubTabHeaderView', function() {
beforeEach(function() {
select = sinon.spy();
- wrapper = build({handleWorkDirSelect: select, workdirs: createWorkdirs(paths), workdir: path2});
+ wrapper = build({handleWorkDirChange: select, workdirs: createWorkdirs(paths), workdir: path2});
});
it('renders an option for all given working directories', function() {
@@ -49,6 +55,42 @@ describe('GithubTabHeaderView', function() {
});
});
+ describe('context lock control', function() {
+ it('renders locked when the lock is engaged', function() {
+ const wrapper = build({contextLocked: true});
+
+ assert.isTrue(wrapper.exists('Octicon[icon="lock"]'));
+ });
+
+ it('renders unlocked when the lock is disengaged', function() {
+ const wrapper = build({contextLocked: false});
+
+ assert.isTrue(wrapper.exists('Octicon[icon="unlock"]'));
+ });
+
+ it('calls handleLockToggle when the lock is clicked', function() {
+ const handleLockToggle = sinon.spy();
+ const wrapper = build({handleLockToggle});
+
+ wrapper.find('button').simulate('click');
+ assert.isTrue(handleLockToggle.called);
+ });
+ });
+
+ describe('when changes are in progress', function() {
+ it('disables the workdir select while the workdir is changing', function() {
+ const wrapper = build({changingWorkDir: true});
+
+ assert.isTrue(wrapper.find('select').prop('disabled'));
+ });
+
+ it('disables the context lock toggle while the context lock is changing', function() {
+ const wrapper = build({changingLock: true});
+
+ assert.isTrue(wrapper.find('button').prop('disabled'));
+ });
+ });
+
describe('with falsish props', function() {
let wrapper;
diff --git a/test/views/github-tab-view.test.js b/test/views/github-tab-view.test.js
index d78ea5b44b..e507264c49 100644
--- a/test/views/github-tab-view.test.js
+++ b/test/views/github-tab-view.test.js
@@ -31,26 +31,31 @@ describe('GitHubTabView', function() {
return (
[]}
+ changeWorkingDirectory={() => {}}
+ contextLocked={false}
+ setContextLock={() => {}}
repository={repo}
- branches={new BranchSet()}
- currentBranch={nullBranch}
+
remotes={new RemoteSet()}
currentRemote={nullRemote}
manyRemotesAvailable={false}
- pushInProgress={false}
isLoading={false}
+ branches={new BranchSet()}
+ currentBranch={nullBranch}
+ pushInProgress={false}
+ handleWorkDirSelect={() => {}}
handlePushBranch={() => {}}
handleRemoteSelect={() => {}}
- changeWorkingDirectory={() => {}}
onDidChangeWorkDirs={() => {}}
- getCurrentWorkDirs={() => []}
openCreateDialog={() => {}}
openBoundPublishDialog={() => {}}
openCloneDialog={() => {}}
@@ -125,7 +130,7 @@ describe('GitHubTabView', function() {
const currentRemote = new Remote('aaa', 'git@github.com:aaa/bbb.git');
const changeWorkingDirectory = sinon.spy();
const wrapper = shallow(buildApp({currentRemote, changeWorkingDirectory}));
- wrapper.find('GithubTabHeaderContainer').prop('handleWorkDirSelect')({target: {value: 'some-path'}});
+ wrapper.find('GithubTabHeaderContainer').prop('changeWorkingDirectory')('some-path');
assert.isTrue(changeWorkingDirectory.calledWith('some-path'));
});
});