diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index ac5710fb69..331cf44912 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -37,6 +37,7 @@ export default class GitTabController {
workspace={this.props.workspace}
commandRegistry={this.props.commandRegistry}
notificationManager={this.props.notificationManager}
+ initializeRepo={this.props.initializeRepo}
didSelectFilePath={this.props.didSelectFilePath}
didDiveIntoFilePath={this.props.didDiveIntoFilePath}
didSelectMergeConflictFile={this.props.didSelectMergeConflictFile}
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index f18fe6e471..88910f8307 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -38,6 +38,8 @@ export default class RootController extends React.Component {
notificationManager: React.PropTypes.object.isRequired,
tooltips: React.PropTypes.object.isRequired,
confirm: React.PropTypes.func.isRequired,
+ activeProjectPath: React.PropTypes.string,
+ createRepositoryForProjectPath: React.PropTypes.func,
repository: React.PropTypes.object,
resolutionProgress: React.PropTypes.object,
statusBar: React.PropTypes.object,
@@ -151,6 +153,7 @@ export default class RootController extends React.Component {
commandRegistry={this.props.commandRegistry}
notificationManager={this.props.notificationManager}
repository={this.props.repository}
+ initializeRepo={this.initializeRepo}
resolutionProgress={this.props.resolutionProgress}
isAmending={this.state.amending}
didSelectFilePath={this.showFilePatchForPath}
@@ -238,6 +241,11 @@ export default class RootController extends React.Component {
}
}
+ @autobind
+ initializeRepo() {
+ this.props.createRepositoryForProjectPath(this.props.activeProjectPath);
+ }
+
@autobind
async showFilePatchForPath(filePath, stagingStatus, {activate, amending} = {}) {
if (!filePath) { return null; }
diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js
index ba71fbcd64..283896f63f 100644
--- a/lib/git-shell-out-strategy.js
+++ b/lib/git-shell-out-strategy.js
@@ -152,6 +152,10 @@ export default class GitShellOutStrategy {
}
}
+ init() {
+ return this.exec(['init', this.workingDir]);
+ }
+
/**
* Staging/Unstaging files and patches and committing
*/
diff --git a/lib/github-package.js b/lib/github-package.js
index 005d9656c5..2b7906b913 100644
--- a/lib/github-package.js
+++ b/lib/github-package.js
@@ -122,11 +122,13 @@ export default class GithubPackage {
notificationManager={this.notificationManager}
tooltips={this.tooltips}
confirm={this.confirm}
+ activeProjectPath={this.activeProjectPath}
repository={this.getActiveRepository()}
resolutionProgress={this.getActiveResolutionProgress()}
statusBar={this.statusBar}
savedState={this.savedState.gitController}
githubEnabled={this.config.get('github.githubEnabled')}
+ createRepositoryForProjectPath={this.createRepositoryForProjectPath}
/>, this.element,
);
}
@@ -155,6 +157,22 @@ export default class GithubPackage {
await this.destroyModelsForPaths(Array.from(removed));
}
+ @autobind
+ async createRepositoryForProjectPath(projectPath) {
+ if (!projectPath) {
+ throw new Error('Must specify a project path to create a repository for');
+ }
+ await Repository.init(projectPath);
+ this.modelsByProjectPath.delete(projectPath);
+ this.modelPromisesByProjectPath.delete(projectPath);
+ const promises = this.cacheModelsForPaths([projectPath]);
+ await Promise.all([
+ promises.changeObserversStartedPromise,
+ promises.modelsPromise,
+ ]);
+ await this.updateActiveModels();
+ }
+
@autobind
didChangeActivePaneItem() {
this.updateActiveModels();
@@ -168,6 +186,10 @@ export default class GithubPackage {
return this.initModelsPromise;
}
+ getActiveProjectPath() {
+ return this.activeProjectPath;
+ }
+
getActiveRepository() {
return this.activeRepository;
}
@@ -178,12 +200,13 @@ export default class GithubPackage {
async updateActiveModels() {
await this.initModelsPromise;
- let nextActiveRepository, nextActiveResolutionProgress;
+ let nextActiveRepository, nextActiveResolutionProgress, nextActiveProjectPath;
const activeItem = this.workspace.getActivePaneItem();
if (activeItem && typeof activeItem.getPath === 'function') {
const projectPath = this.projectPathForItemPath(activeItem.getPath());
if (projectPath) {
+ nextActiveProjectPath = projectPath;
await Promise.all([
this.getRepositoryForWorkdirPath(projectPath).then(r => { nextActiveRepository = r; }),
this.getResolutionProgressForWorkdirPath(projectPath).then(rp => { nextActiveResolutionProgress = rp; }),
@@ -192,20 +215,24 @@ export default class GithubPackage {
} else {
nextActiveRepository = this.activeRepository;
nextActiveResolutionProgress = this.activeResolutionProgress;
+ nextActiveProjectPath = this.activeProjectPath;
}
if (activeItem instanceof FilePatchController) {
if (!activeItem.props.repository.isDestroyed()) {
nextActiveRepository = activeItem.props.repository;
nextActiveResolutionProgress = activeItem.props.resolutionProgress;
+ nextActiveProjectPath = nextActiveRepository.getWorkingDirectoryPath();
}
}
if (!activeItem && this.project.getPaths().length === 1) {
- nextActiveRepository = await this.getRepositoryForWorkdirPath(this.project.getPaths()[0]);
+ nextActiveProjectPath = this.project.getPaths()[0];
+ nextActiveRepository = await this.getRepositoryForWorkdirPath(nextActiveProjectPath);
}
if (nextActiveRepository && nextActiveRepository !== this.activeRepository) {
+ this.activeProjectPath = nextActiveProjectPath || null;
this.setActiveModels(nextActiveRepository, nextActiveResolutionProgress);
if (this.destroyedRepositorySubscription) { this.destroyedRepositorySubscription.dispose(); }
@@ -215,9 +242,11 @@ export default class GithubPackage {
}
this.destroyedRepositorySubscription.dispose();
});
- }
-
- if (!nextActiveRepository) {
+ } else if (nextActiveRepository && this.activeProjectPath !== nextActiveProjectPath) {
+ this.activeProjectPath = nextActiveProjectPath || null;
+ this.rerender();
+ } else if (!nextActiveRepository) {
+ this.activeProjectPath = nextActiveProjectPath || null;
this.setActiveModels(null, null);
}
}
diff --git a/lib/models/repository.js b/lib/models/repository.js
index 3a3294b47d..ee84a3a51c 100644
--- a/lib/models/repository.js
+++ b/lib/models/repository.js
@@ -43,6 +43,12 @@ export default class Repository {
}
}
+ static async init(workingDirectoryPath, gitStrategy) {
+ const strategy = gitStrategy || CompositeGitStrategy.create(workingDirectoryPath);
+ await strategy.init(workingDirectoryPath);
+ return this.open(workingDirectoryPath, strategy);
+ }
+
static githubInfoFromRemote(remote) {
// proto login domain owner repo
const regex = /(?:.+:\/\/)?(?:.+@)?github\.com[:/]([^/]+)\/(.+)/;
@@ -122,6 +128,10 @@ export default class Repository {
/* eslint-enable no-console */
}
+ isGitRepository() {
+ return this.git.isGitRepository();
+ }
+
fetchStatusesForChangedFiles() {
return this.git.getStatusesForChangedFiles();
}
diff --git a/lib/views/git-logo.js b/lib/views/git-logo.js
new file mode 100644
index 0000000000..3af325c6f3
--- /dev/null
+++ b/lib/views/git-logo.js
@@ -0,0 +1,24 @@
+/** @jsx etch.dom */
+import etch from 'etch';
+
+export default class GitLogo {
+ constructor() {
+ etch.initialize(this);
+ }
+
+ update() {}
+
+ render() {
+ /* eslint-disable */
+ return (
+
+ )
+ /* eslint-enable */
+ }
+}
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 3005cf9e6c..607786c9ca 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -7,12 +7,14 @@ import etch from 'etch';
import {autobind} from 'core-decorators';
import StagingView from './staging-view';
+import GitLogo from './git-logo';
import CommitViewController from '../controllers/commit-view-controller';
export default class GitTabView {
constructor(props) {
this.props = props;
etch.initialize(this);
+ this.repoCreationInProgress = false;
this.element.addEventListener('focusin', this.rememberLastFocus);
@@ -35,7 +37,15 @@ export default class GitTabView {
if (!this.props.repository) {
return (
-
No repository for active pane item
+
+
+
+
+
Initialize this project directory with a Git repository
+
+
);
} else if (this.props.fetchInProgress) {
@@ -99,6 +109,19 @@ export default class GitTabView {
return etch.destroy(this);
}
+ @autobind
+ async initializeRepo(event) {
+ event.preventDefault();
+ this.repoCreationInProgress = true;
+ etch.update(this);
+ try {
+ await this.props.initializeRepo();
+ } finally {
+ this.repoCreationInProgress = false;
+ etch.update(this);
+ }
+ }
+
@autobind
blur() {
this.props.workspace.getActivePane().activate();
diff --git a/styles/git-panel.less b/styles/git-panel.less
index fc41ecd5ea..6c069df539 100644
--- a/styles/git-panel.less
+++ b/styles/git-panel.less
@@ -17,11 +17,27 @@
-webkit-user-select: none;
cursor: default;
- &.is-loading,
- &.no-repository {
+ &.is-loading, &.no-repository {
padding: @component-padding * 3;
text-align: center;
- color: @text-color-subtle;
+ }
+
+ &.is-loading {
font-style: italic;
+ color: @text-color-subtle;
+ }
+
+ &.no-repository {
+ display: flex;
+ justify-content: center;
+ font-size: 1.25em;
+
+ > * {
+ margin: @component-padding 0;
+ }
+
+ .git-logo-path {
+ fill: mix(@base-background-color, @text-color, 66%); // 2/3 of bg color
+ }
}
}
diff --git a/test/github-package.test.js b/test/github-package.test.js
index ff20f4c19f..2fa5dac660 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -47,10 +47,10 @@ describe('GithubPackage', function() {
});
describe('changing the project paths', function() {
- it('updates the active repository', async function() {
+ it('updates the active repository and project path', async function() {
const workdirPath1 = await cloneRepository('three-files');
const workdirPath2 = await cloneRepository('three-files');
- const nonRepositoryPath = temp.mkdirSync();
+ const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'change 1', 'utf8');
@@ -60,20 +60,24 @@ describe('GithubPackage', function() {
await workspace.open(path.join(workdirPath1, 'a.txt'));
const repository1 = await githubPackage.getRepositoryForWorkdirPath(workdirPath1);
await assert.async.strictEqual(githubPackage.getActiveRepository(), repository1);
+ await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath1);
await assert.async.equal(githubPackage.rerender.callCount, 1);
// Remove repository for open file
project.setPaths([workdirPath2, nonRepositoryPath]);
+ await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath1);
await assert.async.isNull(githubPackage.getActiveRepository());
await assert.async.equal(githubPackage.rerender.callCount, 2);
await workspace.open(path.join(workdirPath2, 'b.txt'));
const repository2 = await githubPackage.getRepositoryForWorkdirPath(workdirPath2);
await assert.async.strictEqual(githubPackage.getActiveRepository(), repository2);
+ await assert.async.equal(githubPackage.getActiveProjectPath(), workdirPath2);
await assert.async.equal(githubPackage.rerender.callCount, 3);
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await assert.async.isNull(githubPackage.getActiveRepository());
+ await assert.async.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
await assert.async.equal(githubPackage.rerender.callCount, 4);
});
@@ -130,7 +134,7 @@ describe('GithubPackage', function() {
it('updates the active repository based on the active item, setting it to null when the active item is not in a project repository', async function() {
const workdirPath1 = await cloneRepository('three-files');
const workdirPath2 = await cloneRepository('three-files');
- const nonRepositoryPath = temp.mkdirSync();
+ const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
await githubPackage.activate();
@@ -140,34 +144,42 @@ describe('GithubPackage', function() {
await githubPackage.updateActiveModels();
assert.isNotNull(githubPackage.getActiveRepository());
+ assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
await githubPackage.updateActiveModels();
+ assert.equal(githubPackage.getActiveProjectPath(), nonRepositoryPath);
assert.isNull(githubPackage.getActiveRepository());
await workspace.open(path.join(workdirPath1, 'a.txt'));
await githubPackage.updateActiveModels();
+ assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
workspace.getActivePane().activateItem({}); // such as when find & replace results pane is focused
await githubPackage.updateActiveModels();
+ assert.equal(githubPackage.getActiveProjectPath(), workdirPath1);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath1));
await workspace.open(path.join(workdirPath2, 'b.txt'));
await githubPackage.updateActiveModels();
+ assert.equal(githubPackage.getActiveProjectPath(), workdirPath2);
assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(workdirPath2));
project.removePath(workdirPath2);
await githubPackage.updateActiveModels();
+ assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
project.removePath(workdirPath1);
await githubPackage.updateActiveModels();
+ assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
await workspace.open(path.join(workdirPath1, 'a.txt'));
await githubPackage.updateActiveModels();
+ assert.isNull(githubPackage.getActiveProjectPath());
assert.isNull(githubPackage.getActiveRepository());
});
@@ -186,7 +198,7 @@ describe('GithubPackage', function() {
// Repository with a merge conflict, repository without a merge conflict, path without a repository
const workdirMergeConflict = await cloneRepository('merge-conflict');
const workdirNoConflict = await cloneRepository('three-files');
- const nonRepositoryPath = temp.mkdirSync();
+ const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
project.setPaths([workdirMergeConflict, workdirNoConflict, nonRepositoryPath]);
@@ -230,7 +242,7 @@ describe('GithubPackage', function() {
if (process.platform !== 'win32') {
it('handles symlinked project paths', async () => {
const workdirPath = await cloneRepository('three-files');
- const symlinkPath = temp.mkdirSync() + '-symlink';
+ const symlinkPath = fs.realpathSync(temp.mkdirSync()) + '-symlink';
fs.symlinkSync(workdirPath, symlinkPath);
project.setPaths([symlinkPath]);
await githubPackage.activate();
@@ -296,6 +308,23 @@ describe('GithubPackage', function() {
});
});
+ describe('#createRepositoryForProjectPath', function() {
+ it('creates and sets a repository for the given project path', async function() {
+ const workdirPath1 = await cloneRepository('three-files');
+ const workdirPath2 = await cloneRepository('three-files');
+ const nonRepositoryPath = fs.realpathSync(temp.mkdirSync());
+ fs.writeFileSync(path.join(nonRepositoryPath, 'c.txt'));
+ project.setPaths([workdirPath1, workdirPath2, nonRepositoryPath]);
+ await githubPackage.activate();
+
+ await workspace.open(path.join(nonRepositoryPath, 'c.txt'));
+ await assert.async.isNull(githubPackage.getActiveRepository());
+ await githubPackage.createRepositoryForProjectPath(nonRepositoryPath);
+ assert.isOk(githubPackage.getActiveRepository());
+ assert.equal(githubPackage.getActiveRepository(), await githubPackage.getRepositoryForWorkdirPath(nonRepositoryPath));
+ });
+ });
+
describe('#projectPathForItemPath', function() {
it('does not error when the path is falsy (e.g. new unsaved file)', function() {
sinon.stub(project, 'getDirectories').returns([new Directory('path')]);
diff --git a/test/models/repository.test.js b/test/models/repository.test.js
index fb8dd50093..c10988f27a 100644
--- a/test/models/repository.test.js
+++ b/test/models/repository.test.js
@@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import dedent from 'dedent-js';
+import temp from 'temp';
import Repository from '../../lib/models/repository';
@@ -39,6 +40,15 @@ describe('Repository', function() {
});
});
+ describe('init', function() {
+ it('creates a repository in the given dir and returns the repository', async function() {
+ const soonToBeRepositoryPath = fs.realpathSync(temp.mkdirSync());
+ const repo = await Repository.init(soonToBeRepositoryPath);
+ assert.isTrue(await repo.isGitRepository());
+ assert.equal(repo.getWorkingDirectoryPath(), soonToBeRepositoryPath);
+ });
+ });
+
describe('staging and unstaging files', function() {
it('can stage and unstage modified files', async function() {
const workingDirPath = await cloneRepository('three-files');