Skip to content

Commit

Permalink
feat: commit and push changes onto a SHA to a target remote (#30)
Browse files Browse the repository at this point in the history
This PR implements the basic functionality of creating GitHub trees, a commit for that tree, and updating the latest reference HEAD to point to that tree. It also has basic mock tests.

Given:
- current reference HEAD
- an owner
- a repository
- a branch name
- changes
- commit message

On success, create a tree with those changes, a commit pointing to that tree, and update the reference branch HEAD to point to that new commit.
On error re-throw Octokit Error.

Towards #19
  • Loading branch information
TomKristie committed Jul 17, 2020
1 parent f268414 commit 8bf1782
Show file tree
Hide file tree
Showing 9 changed files with 688 additions and 8 deletions.
198 changes: 198 additions & 0 deletions src/github-handler/commit-and-push-handler.ts
@@ -0,0 +1,198 @@
// 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 {
Changes,
FileData,
TreeObject,
Logger,
Octokit,
RepoDomain,
} from '../types';

/**
* Generate and return a GitHub tree object structure
* containing the target change data
* See https://developer.github.com/v3/git/trees/#tree-object
* @param {Changes} changes the set of repository changes
* @returns {TreeObject[]} The new GitHub changes
*/
function generateTreeObjects(changes: Changes): TreeObject[] {
const tree: TreeObject[] = [];
changes.forEach((fileData: FileData, path: string) => {
if (fileData.content == null) {
// if no file content then file is deleted
tree.push({
path,
mode: fileData.mode,
type: 'blob',
sha: null,
});
} else {
// update file with its content
tree.push({
path,
mode: fileData.mode,
type: 'blob',
content: fileData.content,
});
}
});
return tree;
}

/**
* Upload and create a remote GitHub tree
* and resolves with the new tree SHA.
* Rejects if GitHub V3 API fails with the GitHub error response
* @param {Octokit} octokit The authenticated octokit instance
* @param {RepoDomain} origin the the remote repository to push changes to
* @param {string} refHead the base of the new commit(s)
* @param {TreeObject[]} tree the set of GitHub changes to upload
* @returns {Promise<string>} the GitHub tree SHA
*/
async function createTree(
logger: Logger,
octokit: Octokit,
origin: RepoDomain,
refHead: string,
tree: TreeObject[]
): Promise<string> {
const oldTreeSha = (
await octokit.git.getCommit({
owner: origin.owner,
repo: origin.repo,
commit_sha: refHead,
})
).data.tree.sha;
logger.info('Got the latest commit tree');
const treeSha = (
await octokit.git.createTree({
owner: origin.owner,
repo: origin.repo,
tree,
base_tree: oldTreeSha,
})
).data.sha;
logger.info(
`Successfully created a tree with the desired changes with SHA ${treeSha}`
);
return treeSha;
}

/**
* Create a commit with a repo snapshot SHA on top of the reference HEAD
* and resolves with the SHA of the commit.
* Rejects if GitHub V3 API fails with the GitHub error response
* @param {Logger} logger The logger instance
* @param {Octokit} octokit The authenticated octokit instance
* @param {RepoDomain} origin the the remote repository to push changes to
* @param {string} refHead the base of the new commit(s)
* @param {string} treeSha the tree SHA that this commit will point to
* @param {string} message the message of the new commit
* @returns {Promise<string>} the new commit SHA
*/
async function createCommit(
logger: Logger,
octokit: Octokit,
origin: RepoDomain,
refHead: string,
treeSha: string,
message: string
): Promise<string> {
const commitData = (
await octokit.git.createCommit({
owner: origin.owner,
repo: origin.repo,
message,
tree: treeSha,
parents: [refHead],
})
).data;
logger.info(`Successfully created commit. See commit at ${commitData.url}`);
return commitData.sha;
}

/**
* Update a reference to a SHA
* Rejects if GitHub V3 API fails with the GitHub error response
* @param {Logger} logger The logger instance
* @param {Octokit} octokit The authenticated octokit instance
* @param {RepoDomain} origin the the remote repository to push changes to
* @param {string} refName the name of the branch ref
* @param {string} newSha the ref to update the commit HEAD to
* @returns {Promise<void>}
*/
async function updateRef(
logger: Logger,
octokit: Octokit,
origin: RepoDomain,
refName: string,
newSha: string
): Promise<void> {
await octokit.git.updateRef({
owner: origin.owner,
repo: origin.repo,
ref: `heads/${refName}`,
sha: newSha,
});
logger.info(`Successfully updated reference ${refName} to ${newSha}`);
}

/**
* Given a set of changes, apply the commit(s) on top of the given branch's head and upload it to GitHub
* Rejects if GitHub V3 API fails with the GitHub error response
* @param {Logger} logger The logger instance
* @param {Octokit} octokit The authenticated octokit instance
* @param {string} refHead the base of the new commit(s)
* @param {Changes} changes the set of repository changes
* @param {RepoDomain} origin the the remote repository to push changes to
* @param {string} originBranchName the remote branch that will contain the new changes
* @param {string} commitMessage the message of the new commit
* @returns {Promise<void>}
*/
async function commitAndPush(
logger: Logger,
octokit: Octokit,
refHead: string,
changes: Changes,
origin: RepoDomain,
originBranchName: string,
commitMessage: string
) {
try {
const tree = generateTreeObjects(changes);
const treeSha = await createTree(logger, octokit, origin, refHead, tree);
const commitSha = await createCommit(
logger,
octokit,
origin,
refHead,
treeSha,
commitMessage
);
await updateRef(logger, octokit, origin, originBranchName, commitSha);
} catch (err) {
logger.error('Error while creating a tree and updating the ref');
throw err;
}
}

export {
commitAndPush,
createCommit,
generateTreeObjects,
createTree,
updateRef,
};
1 change: 1 addition & 0 deletions src/github-handler/index.ts
@@ -1,3 +1,4 @@
export * from './fork-handler';
export {branch} from './branch-handler';
export {commitAndPush} from './commit-and-push-handler';
export * from './pr-handler';
39 changes: 35 additions & 4 deletions src/types/index.ts
Expand Up @@ -15,11 +15,40 @@
import {Level, Logger} from '../logger';
import {Octokit} from '@octokit/rest';

// a flat object of the path of the file as the key, and the text contents as a value
interface Files {
[index: string]: string;
type FileMode = '100644' | '100755' | '040000' | '160000' | '120000';

/**
* GitHub definition of tree
*/
declare interface TreeObject {
path: string;
mode: FileMode;
type: 'blob' | 'tree' | 'commit';
sha?: string | null;
content?: string;
}

/**
* The content and the mode of a file.
* Default file mode is a text file which has code '100644'.
* If `content` is not null, then `content` must be the entire file content.
* See https://developer.github.com/v3/git/trees/#tree-object for details on mode.
*/
class FileData {
readonly mode: FileMode;
readonly content: string | null;
constructor(content: string | null, mode: FileMode = '100644') {
this.mode = mode;
this.content = content;
}
}

/**
* The map of a path to its content data.
* The content must be the entire file content.
*/
type Changes = Map<string, FileData>;

/**
* The domain of a repository
*/
Expand Down Expand Up @@ -76,9 +105,11 @@ interface GitHubContext {
}

export {
Changes,
FileData,
TreeObject,
BranchDomain,
Description,
Files,
GitHubContextParam,
GitHubContext,
Level,
Expand Down

0 comments on commit 8bf1782

Please sign in to comment.