Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create a branch from origin owner, repo, branch name, and an op…
…tional primary branch (#27) This PR implements the basic functionality of creating a branch and has basic mock test. Given: * an owner * a repository * a branch name * an optional primary branch name Create a new GitHub reference based off the primary branch HEAD SHA. On successful reference creation, the GitHub API (wrapped by Octokit) returns the base SHA for new commits to be applied on top of. On GitHub API error, log error and re-throw the error. On primary branch not found error, throw that error. This PR also groups repository domain data together.
- Loading branch information
1 parent
362124e
commit fecaaba
Showing
9 changed files
with
398 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import {Logger, Octokit, RepoDomain} from '../types'; | ||
|
||
const REF_PREFIX = 'refs/heads/'; | ||
const DEFAULT_PRIMARY_BRANCH = 'master'; | ||
|
||
/** | ||
* Create a new branch reference with the ref prefix | ||
* @param {string} branchName name of the branch | ||
*/ | ||
function createRef(branchName: string) { | ||
return REF_PREFIX + branchName; | ||
} | ||
|
||
/** | ||
* get branch commit HEAD SHA of a repository | ||
* Throws an error if the branch cannot be found | ||
* @param {Logger} logger The logger instance | ||
* @param {Octokit} octokit The authenticated octokit instance | ||
* @param {RepoDomain} origin The domain information of the remote origin repository | ||
* @param {string} branch the name of the branch | ||
* @returns {Promise<string>} branch commit HEAD SHA | ||
*/ | ||
async function getBranchHead( | ||
logger: Logger, | ||
octokit: Octokit, | ||
origin: RepoDomain, | ||
branch: string | ||
): Promise<string> { | ||
const branchData = ( | ||
await octokit.repos.getBranch({ | ||
owner: origin.owner, | ||
repo: origin.repo, | ||
branch, | ||
}) | ||
).data; | ||
logger.info( | ||
`Successfully found primary branch HEAD sha \"${branchData.commit.sha}\".` | ||
); | ||
return branchData.commit.sha; | ||
} | ||
|
||
/** | ||
* Create a GitHub branch given a remote origin. | ||
* Throws an exception if octokit fails, or if the base branch is invalid | ||
* @param {Logger} logger The logger instance | ||
* @param {Octokit} octokit The authenticated octokit instance | ||
* @param {RepoDomain} origin The domain information of the remote origin repository | ||
* @param {string} name The branch name to create on the origin repository | ||
* @param {string} baseBranch the name of the branch to base the new branch off of. Default is master | ||
* @returns {Promise<string>} the base SHA for subsequent commits to be based off for the origin branch | ||
*/ | ||
async function branch( | ||
logger: Logger, | ||
octokit: Octokit, | ||
origin: RepoDomain, | ||
name: string, | ||
baseBranch: string = DEFAULT_PRIMARY_BRANCH | ||
): Promise<string> { | ||
// create branch from primary branch HEAD SHA | ||
try { | ||
const baseSHA = await getBranchHead(logger, octokit, origin, baseBranch); | ||
const refData = ( | ||
await octokit.git.createRef({ | ||
owner: origin.owner, | ||
repo: origin.repo, | ||
ref: createRef(name), | ||
sha: baseSHA, | ||
}) | ||
).data; | ||
logger.info(`Successfully created branch at ${refData.url}`); | ||
return baseSHA; | ||
} catch (err) { | ||
logger.error('Error when creating branch'); | ||
throw err; | ||
} | ||
} | ||
|
||
export {branch, getBranchHead, createRef}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './fork-handler'; | ||
export {branch} from './branch-handler'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import {describe, it, before} from 'mocha'; | ||
import {assert, expect} from 'chai'; | ||
import {logger, octokit, setup} from './util'; | ||
import * as sinon from 'sinon'; | ||
import { | ||
branch, | ||
getBranchHead, | ||
createRef, | ||
} from '../src/github-handler/branch-handler'; | ||
|
||
before(() => { | ||
setup(); | ||
}); | ||
|
||
describe('Branch module', async () => { | ||
const origin = {owner: 'octocat', repo: 'HelloWorld'}; | ||
const branchName = 'test-branch'; | ||
const branchResponseBody = await import( | ||
'./fixtures/create-branch-response.json' | ||
); | ||
const branchResponse = { | ||
headers: {}, | ||
status: 200, | ||
url: 'http://fake-url.com', | ||
data: branchResponseBody, | ||
}; | ||
|
||
describe('Get branch head', () => { | ||
it('is passed the correct parameters, invokes octokit correctly, and returns the HEAD sha', async () => { | ||
// setup | ||
const getBranchStub = sinon | ||
.stub(octokit.repos, 'getBranch') | ||
.resolves(branchResponse); | ||
// // tests | ||
const headSHA = await getBranchHead(logger, octokit, origin, 'master'); | ||
expect(headSHA).to.equal(branchResponse.data.commit.sha); | ||
sinon.assert.calledOnce(getBranchStub); | ||
sinon.assert.calledOnceWithExactly(getBranchStub, { | ||
owner: origin.owner, | ||
repo: origin.repo, | ||
branch: 'master', | ||
}); | ||
|
||
// restore | ||
getBranchStub.restore(); | ||
}); | ||
}); | ||
|
||
describe('The create branch function', () => { | ||
it('Returns the primary SHA when branching is successful', async () => { | ||
// setup | ||
const createRefResponse = { | ||
headers: {}, | ||
status: 200, | ||
url: 'http://fake-url.com', | ||
data: { | ||
ref: 'refs/heads/test-branch', | ||
node_id: 'MDM6UmVmMjc0NzM5ODIwOnJlZnMvaGVhZHMvVGVzdC1icmFuY2gtNQ==', | ||
url: | ||
'https://api.github.com/repos/fake-Owner/HelloWorld/git/refs/heads/Test-branch-5', | ||
object: { | ||
sha: 'f826b1caabafdffec3dc45a08e41d7021c68db41', | ||
type: 'commit', | ||
url: | ||
'https://api.github.com/repos/fake-Owner/HelloWorld/git/commits/f826b1caabafdffec3dc45a08e41d7021c68db41', | ||
}, | ||
}, | ||
}; | ||
const getBranchStub = sinon | ||
.stub(octokit.repos, 'getBranch') | ||
.resolves(branchResponse); | ||
const createRefStub = sinon | ||
.stub(octokit.git, 'createRef') | ||
.resolves(createRefResponse); | ||
// tests | ||
const sha = await branch(logger, octokit, origin, branchName, 'master'); | ||
expect(sha).to.equal(branchResponse.data.commit.sha); | ||
sinon.assert.calledOnce(createRefStub); | ||
sinon.assert.calledOnceWithExactly(createRefStub, { | ||
owner: origin.owner, | ||
repo: origin.repo, | ||
ref: `refs/heads/${branchName}`, | ||
sha: branchResponse.data.commit.sha, | ||
}); | ||
|
||
// restore | ||
createRefStub.restore(); | ||
getBranchStub.restore(); | ||
}); | ||
}); | ||
|
||
describe('Branching fails when', () => { | ||
const testErrorMessage = 'test-error-message'; | ||
it('Octokit get branch fails', async () => { | ||
const getBranchStub = sinon | ||
.stub(octokit.repos, 'getBranch') | ||
.rejects(Error(testErrorMessage)); | ||
try { | ||
await branch(logger, octokit, origin, branchName, 'master'); | ||
assert.fail(); | ||
} catch (err) { | ||
expect(err.message).to.equal(testErrorMessage); | ||
} finally { | ||
getBranchStub.restore(); | ||
} | ||
}); | ||
it('Octokit create ref fails', async () => { | ||
const getBranchStub = sinon | ||
.stub(octokit.repos, 'getBranch') | ||
.resolves(branchResponse); | ||
const createRefStub = sinon | ||
.stub(octokit.git, 'createRef') | ||
.rejects(Error(testErrorMessage)); | ||
try { | ||
await branch(logger, octokit, origin, branchName, 'master'); | ||
assert.fail(); | ||
} catch (err) { | ||
expect(err.message).to.equal(testErrorMessage); | ||
} finally { | ||
getBranchStub.restore(); | ||
createRefStub.restore(); | ||
} | ||
}); | ||
it('Primary branch specified did not match any of the branches returned', async () => { | ||
const listBranchesResponse = { | ||
headers: {}, | ||
status: 200, | ||
url: 'http://fake-url.com', | ||
data: [ | ||
{ | ||
name: 'master', | ||
commit: { | ||
sha: 'c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc', | ||
url: | ||
'https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc', | ||
}, | ||
protected: true, | ||
protection: { | ||
enabled: true, | ||
required_status_checks: { | ||
enforcement_level: 'non_admins', | ||
contexts: ['ci-test', 'linter'], | ||
}, | ||
}, | ||
protection_url: | ||
'https://api.github.com/repos/octocat/hello-world/branches/master/protection', | ||
}, | ||
], | ||
}; | ||
const getBranchStub = sinon | ||
.stub(octokit.repos, 'listBranches') | ||
.resolves(listBranchesResponse); | ||
try { | ||
await branch(logger, octokit, origin, branchName, 'non-master-branch'); | ||
assert.fail(); | ||
} catch (err) { | ||
assert.isOk(true); | ||
} finally { | ||
getBranchStub.restore(); | ||
} | ||
}); | ||
}); | ||
|
||
describe('Reference string parsing function', () => { | ||
it('correctly appends branch name to reference prefix', () => { | ||
assert.equal(createRef('master'), 'refs/heads/master'); | ||
assert.equal(createRef('foo/bar/baz'), 'refs/heads/foo/bar/baz'); | ||
assert.equal(createRef('+++'), 'refs/heads/+++'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.