Committing Folders to GitHub from Your Browser with GitHub.js: A Full-Stack Developer‘s Guide
As a full-stack developer who has worked extensively with git and GitHub, I‘m always on the lookout for new ways to interact with repositories that can streamline workflows and enable new possibilities. Traditionally, working with repositories has meant either using the official command line interface, a GUI app like GitHub Desktop, or scripts that make raw API calls. But what if you could commit entire directories to GitHub without ever leaving your browser?
That‘s exactly what the GitHub.js library allows you to do. In this in-depth guide, we‘ll explore how to leverage this powerful capability to build web apps that can programmatically read from and write to GitHub repositories. As a professional coder, I‘ll share my insights on the technical concepts, tradeoffs, and best practices to be aware of, as well as some potential real-world use cases.
How It Compares to Other Approaches
First, let‘s contextualize this browser-based approach against the other common methods for interacting with GitHub repositories:
Method | Pros | Cons |
---|---|---|
Command Line | Full-featured, high performance | Requires local installation, steeper learning curve |
GitHub Desktop | User-friendly GUI | Limited to basic workflows, separate from web environment |
GitHub.com Web Interface | No setup required | Limited to individual file operations, no API access |
Direct API Calls | Full access to API capabilities | Requires manual HTTP request handling and authentication |
GitHub.js | Can be integrated into web app workflows | Limited by browser environment and library API coverage |
As we can see, using GitHub.js strikes a balance between the power of the command line/API and the convenience of the web interface. According to GitHub, their API receives over 1 billion requests per day, indicating a strong demand for programmatic repository interactions. By wrapping the API in a JavaScript library, GitHub.js makes it more accessible to frontend developers.
Key GitHub API Concepts
To understand how GitHub.js works, it‘s important to grasp some key concepts from the GitHub API data model:
- Repository: The top-level container for a project‘s files, issues, pull requests, etc. Identified by owner and name.
- Reference: A pointer to a commit, usually a branch or tag name. For example,
heads/master
points to the latest commit on the master branch. - Commit: A snapshot of the repository at a point in time. Each commit has a tree, parent(s), author, message, and SHA.
- Tree: A hierarchical representation of the files in a repository at a given commit. Each tree contains tree entries and references blobs for file contents.
- Blob: The raw content of a file in the repository, identified by its SHA.
Here‘s a simplified visualization of how these objects relate:
Repository
│
└───heads/master (ref)
│
└───Commit A (commit)
│
└───Tree A (tree)
│
├───file1.txt (blob)
└───file2.txt (blob)
When we use git
on the command line, these concepts are largely abstracted away. But when interacting with the API, we need to work with these primitives directly. Let‘s see how GitHub.js helps us do that.
Authenticating with GitHub.js
Before we can access a repository with GitHub.js, we need to authenticate our requests. The library supports both OAuth tokens and personal access tokens. Here‘s how to instantiate the client with a token:
import { Octokit } from ‘@octokit/rest‘;
const octokit = new Octokit({
auth: ‘YOUR_TOKEN_HERE‘
});
It‘s critical that you keep your token secure and never expose it in client-side code. If you‘re building a web app, the authentication should be handled on the backend. One approach is to use OAuth to obtain a token for the logged-in user, then proxy API requests through your server to attach the token.
When using a personal access token, be sure to limit its scope to only the necessary permissions (e.g. repo
for private repositories). Also be aware of the API rate limits, which are more restrictive for unauthenticated or OAuth-authenticated requests.
Committing a Directory
Now let‘s walk through the steps to commit an entire directory to a repository branch using GitHub.js. We‘ll break it down into several sub-steps and provide code samples along the way.
1. Get References to the Repository and Branch
First, we need to get a reference to the repository object:
const owner = ‘orgName‘;
const repo = ‘repoName‘;
const repoObj = await octokit.repos.get({ owner, repo });
Next, we retrieve a reference to the branch we want to commit to:
const ref = `heads/master`;
const { data: refObj } = await octokit.git.getRef({
owner,
repo,
ref
});
const commitSha = refObj.object.sha;
This gives us the SHA of the latest commit on the branch.
2. Get the Current Commit Tree
Every commit references a tree object that represents the state of the repository at that point. Let‘s fetch the tree for our current commit:
const { data: commitObj } = await octokit.git.getCommit({
owner,
repo,
commit_sha: commitSha
});
const treeSha = commitObj.tree.sha;
We now have a reference to the root tree object for the repository.
3. Create Blobs for New Files
For each file we want to add or update, we need to create a new blob object:
async function createBlob(filePath, content) {
const { data } = await octokit.git.createBlob({
owner,
repo,
content,
encoding: ‘utf-8‘
});
return {
path: filePath,
mode: ‘100644‘, // regular file mode
type: ‘blob‘,
sha: data.sha
};
}
const newBlobs = await Promise.all(Object.entries(files).map(async ([path, content]) => {
return createBlob(path, content);
}));
This gives us an array of objects representing the new blobs, which we‘ll add to our new tree.
4. Create a New Tree
To create a new tree, we construct an array of tree entries containing the existing untouched entries from the current tree, as well as entries for the new and updated files:
const { data: currentTree } = await octokit.git.getTree({
owner,
repo,
tree_sha: treeSha
});
const newTree = currentTree.tree
.filter(entry => !files[entry.path])
.concat(newBlobs);
const { data } = await octokit.git.createTree({
owner,
repo,
tree: newTree,
base_tree: treeSha
});
const newTreeSha = data.sha;
This gives us the SHA for our new tree object.
5. Create a New Commit
We now have all the pieces to create a new commit that points to our new tree:
const { data: newCommit } = await octokit.git.createCommit({
owner,
repo,
message: ‘Add new files programatically‘,
tree: newTreeSha,
parents: [commitSha]
});
The new commit is now part of the repository‘s Git history.
6. Update the Branch Reference
Finally, we need to update the branch reference to point to our new commit:
await octokit.git.updateRef({
owner,
repo,
ref: `heads/master`,
sha: newCommit.sha
});
And with that, we‘ve programatically committed an entire directory to our repository!
Real-World Use Cases
So what can we build with this capability? Here are a few ideas:
- A web-based code editor that allows committing changes directly to GitHub
- An app for non-developers to submit content to a documentation repo
- Integration with a static site CMS for content editors
- Scripts to sync data from external sources to a repo
- Tools for managing repositories and files in bulk through a web UI
For example, consider a blogging app that allows authors to write posts in Markdown. With GitHub.js, we could add a "Publish" button that would commit the Markdown file to a designated repository, which could then trigger a static site rebuild to update the live blog. This would provide a seamless publishing flow without the author needing to touch git directly.
Another potential use case is an app that allows non-technical users to submit content to a repository through a forms-based UI. For instance, an open source project could use this to crowdsource documentation contributions, or a company could use it to gather feedback and feature requests from customers.
Limitations and Tradeoffs
While committing directories through GitHub.js is powerful, there are some significant limitations and tradeoffs to consider:
-
It‘s not a full git implementation, so many features like pulling, branching, and history are not available. Tools like Isomorphic-git aim to provide a more complete in-browser git experience.
-
The need for separate API calls for each operation can be inefficient for very large commits or repositories. There are also stringent rate limits to stay within.
-
Storing tokens and interacting with repositories entirely in the browser comes with security risks. Be sure to follow best practices around authentication and consider the threat model for your application.
-
The GitHub.js library only wraps a subset of the GitHub API, so some use cases may require direct API calls or other tooling. For example, the contents API only allows files up to 1MB. Larger files would need to use the Git LFS API. Most of the repo management features like permissions and webhooks are also not supported.
-
Working with binary files requires base64 encoding the content, which can be cumbersome. Handling large assets like images or videos is generally better suited for a dedicated media hosting solution.
Conclusion
GitHub.js provides a compelling bridge between the web development world and the git-centric universe of GitHub. By allowing us to commit entire directories (and so much more) through a JavaScript API, it unlocks potential for all sorts of interesting integrations and automations.
However, as professional coders, we must carefully weigh the tradeoffs and limitations of leaning too heavily on this approach. For many use cases, the robustness and performance of native git tooling is still preferable. And of course, we must always prioritize security and scalability in our applications.
That said, I believe the trends around cloud-based development environments and low-code platforms point to a future where the lines between local, web-based, and API-driven development experiences are increasingly blurred. As the tooling matures and APIs become more expansive, I expect we‘ll see a proliferation of novel use cases and apps that remix the best of git-based workflows with the reach and accessibility of the web.
I hope this deep dive has given you a solid foundation for working with GitHub.js to build out your own ideas. Remember to experiment, share with the community, and always strive to build tools that empower and delight users. The world of software development is constantly evolving, and it‘s an exciting time to be a builder on the web.