When working on a software project, it’s important to know how to manage your changes effectively. Git provides a structured way to do this through the commands git add, git commit, and git push. Each command has a distinct role in the process of taking your local edits and ensuring they become part of your codebase’s official history.
Git manages a directed acyclic graph of snapshots. Every commit represents a snapshot of your project’s files at a certain point in time. Before something becomes a commit, it first resides in something known as the staging area. The git add command moves changes from your working directory (the place where you edit files) into this staging area (an index, or record of what’s about to be committed). Then git commit converts whatever is staged into a new snapshot. Finally, git push transfers those snapshots from your local repository to a remote repository, making them visible to your team.
In other words, think of git add as preparing your changes, git commit as finalizing them into a checkpoint in your project’s history, and git push as publishing that checkpoint for others to pull.
How git add works
git add takes your modified files and places them into the staging area, also known simply as “the index.” This staging area is an intermediate state. Files remain staged until you commit them. Without this step, Git won’t know which changes you intend to include in your next commit.
For example, say you’ve edited a file named app.js. When you run:
git add app.js
Git scans and records the changes you’ve made to app.js in its staging area. If you want to stage multiple files at once, you can run:
git add .
This command stages all changed files, excluding those ignored by .gitignore. Internally, Git is creating a list of changes (a record of new content and removed lines) that will be included in the next commit. Think of it as organizing the papers on your desk before placing them into a binder. The staging area ensures you have control over what goes into each snapshot, allowing you to split your changes into meaningful chunks and maintain a clean commit history.
How git commitfinalizes changes
After staging your changes with git add, the next step is git commit. Running git commit transforms what’s in the staging area into an immutable snapshot stored within Git’s repository history. Each commit is given a unique hash (a cryptographic checksum) to identify it. This hash is basically an ID tied to that exact version of your files.
A typical git commit command looks like this:
git commit -m "Add new feature to handle user input"
The -m flag allows you to include a message directly in the command. Commits should always have a descriptive message that explains what changes were made and why. This is important for future reference, especially when you or your teammates need to understand how the code has evolved over time. Under the hood, when you run git commit, Git:
- Captures the current state of the staged files
 - Links this snapshot to the previous commit, forming a chain (the project’s history)
 - Saves a reference (a pointer) to this commit in the repository
 
When you look at your repository’s commit history later using git log, you’ll see these messages along with commit hashes and authorship details.
How git push shares changes with others
Up until now, all changes you’ve made have been local. Even if you create multiple commits, those commits live only on your machine, inside your local Git repository. To share these changes with others, you use git push.
git push sends your local commits to a remote repository (often hosted on platforms like GitHub, GitLab, or Bitbucket). A typical command is:
git push origin main
Here, origin is the default name for the remote repository you cloned from, and main is the branch you’re pushing to. When you run this command, Git compares your local branch’s commits to the remote’s commits and uploads any commits that are missing on the remote side.
Under the hood, git push communicates with the remote repository through the Git protocol, SSH, or HTTPS. It transfers your commits, along with any objects (such as file changes and directory snapshots) that the remote repository doesn’t have yet. Once complete, your new commits appear on the remote repository’s history. This means your teammates can now git pull those changes into their own local environments and continue from where you left off.
Example workflow
Let’s say you are working on a new feature. You’ve cloned a repository from GitHub and made some changes to app.js and styles.css. Here’s what you might do:
Check your current status
Terminalgit statusGit will show you which files are modified and which are untracked. For example:
TerminalOn branch mainYour branch is up to date with 'origin/main'.Changes not staged for commit:(use "git add <file>..." to update what will be committed)(use "git restore <file>..." to discard changes in working directory)modified: app.jsmodified: styles.cssStage your changes
Terminalgit add app.js styles.cssNow these files are in the staging area. Running
git statusagain will show:TerminalChanges to be committed:(use "git restore --staged <file>..." to unstage)modified: app.jsmodified: styles.cssCommit your changes
Terminalgit commit -m "Improve UI and add input validation in app.js"Now a snapshot of these changes is recorded. Running
git logwill show your new commit at the top:Terminalcommit d4c3f2a (HEAD -> main)Author: Your Name <your.email@example.com>Date: Wed Dec 11 14:28:23 2024 -0500Improve UI and add input validation in app.jsPush your changes
Terminalgit push origin mainThis sends your commits to the
mainbranch on the remote repository. After pushing, teammates can pull these changes and benefit from your latest updates.
Best practices for managing code changes
- Commit frequently: Commit small units of work often. This makes it easier to understand what changed and why.
 - Write descriptive commit messages: Good commit messages make it much easier to navigate project history and troubleshoot issues.
 - Use feature branches: Instead of working directly on 
main, create separate branches for new features, then merge them back when ready. - Check your status often: Use 
git statusto see what’s changed and what’s staged. This helps you stay organized and ensures you don’t commit unintended files. 
Using the Graphite CLI for streamlined Git workflows
The Graphite CLI enhances Git workflows by simplifying commands and introducing pull request (PR) stacking.
Simplified Git commands
Graphite CLI reduces complexity in tasks like staging, committing, and pushing changes. For example, this single command stages all files, creates a commit, and pushes a branch:
gt create --all --message "feat: Implement new API"
Submit it as a pull request with:
gt submit
Pull request stacking
PR stacking lets you break large tasks into manageable layers. Start with a base PR:
gt create --all --message "feat(api): Add base endpoint"gt submit
Then stack a dependent PR:
gt create --all --message "feat(ui): Add UI for feature"gt submit --stack
If a lower PR needs updates, gt modify propagates changes throughout the stack:
gt modify --all --commit --message "fix: Address feedback"
Keeping stacks in sync
Stay aligned with the latest changes from main using:
gt sync
This pulls updates, rebases stacks, and resolves conflicts.
The Graphite CLI simplifies workflows, enabling faster, incremental development. Learn more at Graphite's CLI documentation.
Summary
By combining git add, git commit, and git push, you create a powerful workflow for maintaining a clean, traceable record of your project’s evolution. For even greater efficiency, the Graphite CLI builds on this foundation by streamlining these operations and adding advanced features like pull request stacking and automated syncing. Together, these tools ensure every version of your code is stored, identified, and easily accessible, supporting seamless collaboration and long-term maintainability.