Skip to content

Commit

Permalink
#385 Push branches or tags to multiple remotes from their respective …
Browse files Browse the repository at this point in the history
…dialogs.
  • Loading branch information
mhutchie committed Sep 19, 2020
1 parent 7355d83 commit 6a7790d
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 19 deletions.
44 changes: 44 additions & 0 deletions src/dataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,50 @@ export class DataSource extends Disposable {
return this.runGitCommand(['push', remote, tagName], repo);
}

/**
* Push a branch to multiple remotes.
* @param repo The path of the repository.
* @param branchName The name of the branch to push.
* @param remotes The remotes to push the branch to.
* @param setUpstream Set the branches upstream.
* @param mode The mode of the push.
* @returns The ErrorInfo's from the executed commands.
*/
public async pushBranchToMultipleRemotes(repo: string, branchName: string, remotes: string[], setUpstream: boolean, mode: GitPushBranchMode): Promise<ErrorInfo[]> {
if (remotes.length === 0) {
return ['No remote(s) were specified to push the branch ' + branchName + ' to.'];
}

const results: ErrorInfo[] = [];
for (let i = 0; i < remotes.length; i++) {
const result = await this.pushBranch(repo, branchName, remotes[i], setUpstream, mode);
results.push(result);
if (result !== null) break;
}
return results;
}

/**
* Push a tag to multiple remotes.
* @param repo The path of the repository.
* @param tagName The name of the tag to push.
* @param remote The remotes to push the tag to.
* @returns The ErrorInfo's from the executed commands.
*/
public async pushTagToMultipleRemotes(repo: string, tagName: string, remotes: string[]): Promise<ErrorInfo[]> {
if (remotes.length === 0) {
return ['No remote(s) were specified to push the tag ' + tagName + ' to.'];
}

const results: ErrorInfo[] = [];
for (let i = 0; i < remotes.length; i++) {
const result = await this.pushTag(repo, tagName, remotes[i]);
results.push(result);
if (result !== null) break;
}
return results;
}


/* Git Action Methods - Branches */

Expand Down
4 changes: 2 additions & 2 deletions src/gitGraphView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ export class GitGraphView extends Disposable {
case 'pushBranch':
this.sendMessage({
command: 'pushBranch',
error: await this.dataSource.pushBranch(msg.repo, msg.branchName, msg.remote, msg.setUpstream, msg.mode)
errors: await this.dataSource.pushBranchToMultipleRemotes(msg.repo, msg.branchName, msg.remotes, msg.setUpstream, msg.mode)
});
break;
case 'pushStash':
Expand All @@ -475,7 +475,7 @@ export class GitGraphView extends Disposable {
case 'pushTag':
this.sendMessage({
command: 'pushTag',
error: await this.dataSource.pushTag(msg.repo, msg.tagName, msg.remote)
errors: await this.dataSource.pushTagToMultipleRemotes(msg.repo, msg.tagName, msg.remotes)
});
break;
case 'rebase':
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,11 +960,11 @@ export interface ResponsePullBranch extends ResponseWithErrorInfo {
export interface RequestPushBranch extends RepoRequest {
readonly command: 'pushBranch';
readonly branchName: string;
readonly remote: string;
readonly remotes: string[];
readonly setUpstream: boolean;
readonly mode: GitPushBranchMode;
}
export interface ResponsePushBranch extends ResponseWithErrorInfo {
export interface ResponsePushBranch extends ResponseWithMultiErrorInfo {
readonly command: 'pushBranch';
}

Expand All @@ -980,9 +980,9 @@ export interface ResponsePushStash extends ResponseWithErrorInfo {
export interface RequestPushTag extends RepoRequest {
readonly command: 'pushTag';
readonly tagName: string;
readonly remote: string;
readonly remotes: string[];
}
export interface ResponsePushTag extends ResponseWithErrorInfo {
export interface ResponsePushTag extends ResponseWithMultiErrorInfo {
readonly command: 'pushTag';
}

Expand Down
100 changes: 100 additions & 0 deletions tests/dataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4270,6 +4270,106 @@ describe('DataSource', () => {
});
});

describe('pushBranchToMultipleRemotes', () => {
it('Should push a branch to one remote', async () => {
// Setup
mockGitSuccessOnce();

// Run
const result = await dataSource.pushBranchToMultipleRemotes('/path/to/repo', 'master', ['origin'], false, GitPushBranchMode.Normal);

// Assert
expect(result).toStrictEqual([null]);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'master'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should push a branch to multiple remotes', async () => {
// Setup
mockGitSuccessOnce();
mockGitSuccessOnce();

// Run
const result = await dataSource.pushBranchToMultipleRemotes('/path/to/repo', 'master', ['origin', 'other-origin'], false, GitPushBranchMode.Force);

// Assert
expect(result).toStrictEqual([null, null]);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'master', '--force'], expect.objectContaining({ cwd: '/path/to/repo' }));
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'other-origin', 'master', '--force'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should push a branch to multiple remotes, stopping if an error occurs', async () => {
// Setup
mockGitSuccessOnce();
mockGitThrowingErrorOnce();

// Run
const result = await dataSource.pushBranchToMultipleRemotes('/path/to/repo', 'master', ['origin', 'other-origin', 'another-origin'], true, GitPushBranchMode.Normal);

// Assert
expect(result).toStrictEqual([null, 'error message']);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'master', '--set-upstream'], expect.objectContaining({ cwd: '/path/to/repo' }));
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'other-origin', 'master', '--set-upstream'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should return an error when no remotes are specified', async () => {
// Run
const result = await dataSource.pushBranchToMultipleRemotes('/path/to/repo', 'master', [], false, GitPushBranchMode.Normal);

// Assert
expect(result).toStrictEqual(['No remote(s) were specified to push the branch master to.']);
});
});

describe('pushTagToMultipleRemotes', () => {
it('Should push a tag to one remote', async () => {
// Setup
mockGitSuccessOnce();

// Run
const result = await dataSource.pushTagToMultipleRemotes('/path/to/repo', 'tag-name', ['origin']);

// Assert
expect(result).toStrictEqual([null]);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'tag-name'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should push a tag to multiple remotes', async () => {
// Setup
mockGitSuccessOnce();
mockGitSuccessOnce();

// Run
const result = await dataSource.pushTagToMultipleRemotes('/path/to/repo', 'tag-name', ['origin', 'other-origin']);

// Assert
expect(result).toStrictEqual([null, null]);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'tag-name'], expect.objectContaining({ cwd: '/path/to/repo' }));
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'other-origin', 'tag-name'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should push a tag to multiple remotes, stopping if an error occurs', async () => {
// Setup
mockGitSuccessOnce();
mockGitThrowingErrorOnce();

// Run
const result = await dataSource.pushTagToMultipleRemotes('/path/to/repo', 'tag-name', ['origin', 'other-origin', 'another-origin']);

// Assert
expect(result).toStrictEqual([null, 'error message']);
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'origin', 'tag-name'], expect.objectContaining({ cwd: '/path/to/repo' }));
expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['push', 'other-origin', 'tag-name'], expect.objectContaining({ cwd: '/path/to/repo' }));
});

it('Should return an error when no remotes are specified', async () => {
// Run
const result = await dataSource.pushTagToMultipleRemotes('/path/to/repo', 'tag-name', []);

// Assert
expect(result).toStrictEqual(['No remote(s) were specified to push the tag tag-name to.']);
});
});

describe('checkoutBranch', () => {
it('Should checkout a local branch', async () => {
// Setup
Expand Down
6 changes: 6 additions & 0 deletions web/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ class Dialog {
], actionName, (values) => actioned(<string>values[0]), target);
}

public showMultiSelect(message: string, defaultValues: ReadonlyArray<string>, options: ReadonlyArray<DialogSelectInputOption>, actionName: string, actioned: (value: string[]) => void, target: DialogTarget | null) {
this.showForm(message, [
{ type: DialogInputType.Select, name: '', options: options, defaults: defaultValues, multiple: true }
], actionName, (values) => actioned(<string[]>values[0]), target);
}

public showForm(message: string, inputs: ReadonlyArray<DialogInput>, actionName: string, actioned: (values: DialogInputValue[]) => void, target: DialogTarget | null, secondaryActionName: string = 'Cancel', secondaryActioned: ((values: DialogInputValue[]) => void) | null = null, includeLineBreak: boolean = true) {
const multiElement = inputs.length > 1;
const multiCheckbox = multiElement && inputs.every((input) => input.type === DialogInputType.Checkbox);
Expand Down
29 changes: 16 additions & 13 deletions web/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ class GitGraphView {
title: 'Push Branch' + ELLIPSIS,
visible: visibility.push && this.gitRemotes.length > 0,
onClick: () => {
let multipleRemotes = this.gitRemotes.length > 1, inputs: DialogInput[] = [
const multipleRemotes = this.gitRemotes.length > 1;
const inputs: DialogInput[] = [
{ type: DialogInputType.Checkbox, name: 'Set Upstream', value: true },
{
type: DialogInputType.Radio,
Expand All @@ -931,15 +932,17 @@ class GitGraphView {

if (multipleRemotes) {
inputs.unshift({
type: DialogInputType.Select, name: 'Push to Remote',
default: (this.gitRemotes.includes('origin') ? this.gitRemotes.indexOf('origin') : 0).toString(),
options: this.gitRemotes.map((remote, index) => ({ name: remote, value: index.toString() }))
type: DialogInputType.Select,
name: 'Push to Remote(s)',
defaults: [this.gitRemotes.includes('origin') ? 'origin' : this.gitRemotes[0]],
options: this.gitRemotes.map((remote) => ({ name: remote, value: remote })),
multiple: true
});
}

dialog.showForm('Are you sure you want to push the branch <b><i>' + escapeHtml(refName) + '</i></b>' + (multipleRemotes ? '' : ' to the remote <b><i>' + escapeHtml(this.gitRemotes[0]) + '</i></b>') + '?', inputs, 'Yes, push', (values) => {
let remote = this.gitRemotes[multipleRemotes ? parseInt(<string>values.shift()) : 0];
runAction({ command: 'pushBranch', repo: this.currentRepo, branchName: refName, remote: remote, setUpstream: <boolean>values[0], mode: <GG.GitPushBranchMode>values[1] }, 'Pushing Branch');
const remotes = multipleRemotes ? <string[]>values.shift() : [this.gitRemotes[0]];
runAction({ command: 'pushBranch', repo: this.currentRepo, branchName: refName, remotes: remotes, setUpstream: <boolean>values[0], mode: <GG.GitPushBranchMode>values[1] }, 'Pushing Branch');
}, target);
}
}
Expand Down Expand Up @@ -1342,13 +1345,13 @@ class GitGraphView {
onClick: () => {
if (this.gitRemotes.length === 1) {
dialog.showConfirmation('Are you sure you want to push the tag <b><i>' + escapeHtml(tagName) + '</i></b> to the remote <b><i>' + escapeHtml(this.gitRemotes[0]) + '</i></b>?', 'Yes, push', () => {
runAction({ command: 'pushTag', repo: this.currentRepo, tagName: tagName, remote: this.gitRemotes[0] }, 'Pushing Tag');
runAction({ command: 'pushTag', repo: this.currentRepo, tagName: tagName, remotes: [this.gitRemotes[0]] }, 'Pushing Tag');
}, target);
} else if (this.gitRemotes.length > 1) {
let defaultRemote = (this.gitRemotes.includes('origin') ? this.gitRemotes.indexOf('origin') : 0).toString();
let remoteOptions = this.gitRemotes.map((remote, index) => ({ name: remote, value: index.toString() }));
dialog.showSelect('Are you sure you want to push the tag <b><i>' + escapeHtml(tagName) + '</i></b>? Select the remote to push the tag to:', defaultRemote, remoteOptions, 'Yes, push', (remoteIndex) => {
runAction({ command: 'pushTag', repo: this.currentRepo, tagName: tagName, remote: this.gitRemotes[parseInt(remoteIndex)] }, 'Pushing Tag');
const defaults = [this.gitRemotes.includes('origin') ? 'origin' : this.gitRemotes[0]];
const options = this.gitRemotes.map((remote) => ({ name: remote, value: remote }));
dialog.showMultiSelect('Are you sure you want to push the tag <b><i>' + escapeHtml(tagName) + '</i></b>? Select the remote(s) to push the tag to:', defaults, options, 'Yes, push', (remotes) => {
runAction({ command: 'pushTag', repo: this.currentRepo, tagName: tagName, remotes: remotes }, 'Pushing Tag');
}, target);
}
}
Expand Down Expand Up @@ -2812,13 +2815,13 @@ window.addEventListener('load', () => {
refreshOrDisplayError(msg.error, 'Unable to Pull Branch');
break;
case 'pushBranch':
refreshOrDisplayError(msg.error, 'Unable to Push Branch');
refreshAndDisplayErrors(msg.errors, 'Unable to Push Branch');
break;
case 'pushStash':
refreshOrDisplayError(msg.error, 'Unable to Stash Uncommitted Changes');
break;
case 'pushTag':
refreshOrDisplayError(msg.error, 'Unable to Push Tag');
refreshAndDisplayErrors(msg.errors, 'Unable to Push Tag');
break;
case 'rebase':
if (msg.error === null) {
Expand Down

0 comments on commit 6a7790d

Please sign in to comment.