Introduction to Git Workflow
Now that we have Git installed and configured, let's dive into the core commands that form the foundation of a typical Git workflow. Understanding these commands and how they fit together will enable you to effectively manage your code throughout its lifecycle.
A typical Git workflow involves:
- Creating or cloning a repository
- Making changes to files
- Reviewing those changes
- Staging changes for commit
- Committing changes to the repository
- Sharing changes with others
- Incorporating others' changes
Let's visualize this workflow:
In this lecture, we'll explore each of these steps and the commands that make them possible. We'll also look at how to manage your history, undo mistakes, and establish an effective workflow.
Basic Git Workflow Commands
Let's start with the essential commands you'll use in your day-to-day Git workflow. These commands are like the basic dance steps that, once mastered, allow you to perform more complex choreography with confidence.
Checking Repository Status
The git status command shows the current state of your working directory and staging area. Think of it as a snapshot of what's happening right now:
git status
This will show:
- The current branch
- Changes that are staged for the next commit
- Changes that are not staged
- Untracked files
Pro tip: Use git status -s for a more concise output that uses a two-column display with status symbols.
Tracking New Files
When you create a new file in your repository, Git initially sees it as "untracked." To start tracking it:
git add filename.txt
This stages the file for the next commit. You can also add multiple files or directories:
git add file1.txt file2.txt
git add directory/
git add . # Add all files in the current directory and subdirectories
Remember: git add doesn't just add new files—it also stages modified files for the next commit.
Viewing Changes
Before staging or committing changes, it's good practice to review what's changed. The git diff command shows you the differences between various states:
git diff # Shows unstaged changes
git diff --staged # Shows staged changes that will go into the next commit
git diff HEAD # Shows all changes (staged and unstaged) since the last commit
git diff commit1 commit2 # Shows changes between two commits
The output shows removed lines with a minus sign (-) and added lines with a plus sign (+).
Committing Changes
Once you've staged your changes, you commit them to the repository with a descriptive message:
git commit -m "Add user authentication feature"
For more complex commit messages, omit the -m flag to open your configured editor:
git commit
This opens an editor where you can write a detailed commit message. The first line should be a short summary (50 characters or less), followed by a blank line, and then a more detailed explanation if necessary.
Shortcut: You can stage and commit modified (not new) files in one step:
git commit -am "Fix typo in homepage"
This doesn't work for new (untracked) files—those still need to be added first.
Viewing Commit History
To see the history of commits in your repository:
git log
This shows all commits, starting with the most recent, including:
- Commit hash (the unique identifier)
- Author name and email
- Date and time
- Commit message
There are many useful options for git log:
git log --oneline # Compact view with one line per commit
git log --graph # Shows an ASCII graph of the branch and merge history
git log --stat # Shows which files were changed and how many lines added/removed
git log -p # Shows the actual changes (patch) introduced by each commit
git log --author="John" # Filter by author name
git log --since="2 weeks ago" # Filter by date
git log -n 5 # Show only the last 5 commits
Pro tip: Create an alias for your favorite log format:
git config --global alias.lg "log --graph --oneline --decorate --all"
Then you can just use git lg to get a nice graphical view of your history.
Undoing Changes
Everyone makes mistakes, and Git provides several ways to undo changes depending on what stage they're in. These commands are like your "undo" and "redo" buttons, but with more precision and power.
Unstaging Files
If you've staged a file but want to unstage it (without losing the changes):
git restore --staged filename.txt
In older versions of Git, you might see:
git reset HEAD filename.txt
Both do the same thing: they move the file from the staging area back to the working directory.
Discarding Changes in Working Directory
If you've made changes to a file but want to discard them and revert to the last committed version:
git restore filename.txt
In older versions of Git, you might see:
git checkout -- filename.txt
Warning: This permanently discards your changes—they cannot be recovered. Use with caution.
Amending the Last Commit
If you've just made a commit but want to change something (like the commit message or add more changes):
git commit --amend
This opens your editor to modify the commit message. If you've staged additional changes, they'll be included in the amended commit.
To amend without changing the message:
git commit --amend --no-edit
Important: Only amend commits that haven't been pushed to a shared repository, as it rewrites history.
Reverting a Commit
If you want to undo the changes introduced by a specific commit while keeping the history intact:
git revert commit-hash
This creates a new commit that undoes the changes from the specified commit. It's safe to use on commits that have been pushed to shared repositories because it doesn't rewrite history.
Resetting to a Previous State
If you want to move your branch pointer to a previous commit:
git reset commit-hash
There are three main options:
git reset --soft commit-hash: Moves HEAD to the specified commit, but keeps your changes in the staging areagit reset --mixed commit-hash: Default option; moves HEAD and unstages changesgit reset --hard commit-hash: Moves HEAD and discards all changes
Warning: Hard reset permanently discards changes. Also, avoid resetting commits that have been pushed to shared repositories, as it rewrites history.
Finding Lost Commits
If you've performed operations that seem to have lost commits (like a hard reset), you can often recover them using the reflog:
git reflog
The reflog shows a history of all the previous positions of HEAD, allowing you to find the commit hash of lost commits. You can then create a new branch at that commit:
git branch recovery-branch commit-hash
Working with Remotes
Git's distributed nature means you'll often work with remote repositories, which are versions of your project hosted elsewhere (like GitHub, GitLab, or Bitbucket). These remote connections are like bridges that let your local repository communicate with repositories on other computers.
Adding a Remote
To connect your local repository to a remote:
git remote add origin https://github.com/username/repository.git
Here, "origin" is a shortname you'll use to refer to this remote. You can add multiple remotes with different names.
Viewing Remotes
To see the remotes you have configured:
git remote -v
This shows the shortnames and URLs of your remotes, for both fetching and pushing.
Pushing to a Remote
To send your commits to a remote repository:
git push origin branch-name
For example, to push to the main branch:
git push origin main
If your branch is set up to track a remote branch, you can use:
git push
First push: When pushing a branch for the first time, use the -u flag to set up tracking:
git push -u origin branch-name
This sets up a tracking relationship, so future git push commands know where to go.
Fetching from a Remote
To retrieve changes from a remote repository without automatically merging them:
git fetch origin
This updates your local references to the remote branches but doesn't modify your working directory or local branches. It's like checking what's changed before deciding what to do.
Pulling from a Remote
To fetch and then integrate changes into your current branch:
git pull origin branch-name
This is equivalent to:
git fetch origin
git merge origin/branch-name
If your branch is set up to track a remote branch, you can simply use:
git pull
Pro tip: You can use git pull --rebase to apply your local commits on top of the fetched changes, which can create a cleaner history in some cases.
Handling Remote Branches
To see all remote branches:
git branch -r
To create a local branch based on a remote branch:
git checkout -b local-branch-name origin/remote-branch-name
In newer Git versions, you can simply checkout the remote branch:
git checkout remote-branch-name
Git will automatically create a local tracking branch for you.
Remote Workflow Example
Let's walk through a common collaboration scenario:
- Clone a repository:
git clone https://github.com/team/project.git cd project - Create a branch for your feature:
git checkout -b feature-login - Make changes and commit them:
git add . git commit -m "Implement login form" - Push your branch to the remote:
git push -u origin feature-login - Later, fetch updates from the team:
git fetch origin - Update your local main branch:
git checkout main git pull - Incorporate those updates into your feature branch:
git checkout feature-login git merge main - Resolve any merge conflicts, then push your updated branch:
git push
Git Repository Lifecycle
Now let's put everything together and look at the typical lifecycle of a Git repository from creation to collaboration. This walkthrough demonstrates how the commands we've learned are used in practice.
Creating a Repository
You can start a new repository in two ways:
Option 1: Initialize Locally First
- Create a directory for your project:
mkdir my-project cd my-project - Initialize a Git repository:
git init - Create your initial files:
echo "# My Project" > README.md - Stage and commit the files:
git add README.md git commit -m "Initial commit" - Create a repository on GitHub/GitLab/Bitbucket
- Add the remote:
git remote add origin https://github.com/username/my-project.git - Push your code:
git push -u origin main
Option 2: Clone an Existing Repository
- Create a repository on GitHub/GitLab/Bitbucket
- Clone it locally:
git clone https://github.com/username/my-project.git cd my-project - Create your initial files:
echo "# My Project" > README.md - Stage and commit the files:
git add README.md git commit -m "Initial commit" - Push your code:
git push
Making Changes
Once your repository is set up, you'll follow this cycle for making changes:
- Ensure you're on the right branch:
git checkout main - Create a new branch for your feature/fix:
git checkout -b feature-xyz - Make changes to your files
- Check what's changed:
git status git diff - Stage your changes:
git add filename.txt # or git add . to add all changes - Commit your changes:
git commit -m "Implement feature XYZ" - Push your branch:
git push -u origin feature-xyz
Incorporating Others' Changes
When collaborating with others, you'll need to integrate their changes:
- Fetch the latest changes:
git fetch origin - Update your main branch:
git checkout main git pull - Merge the main branch into your feature branch:
git checkout feature-xyz git merge main - Resolve any merge conflicts if they arise
- Commit the merge:
git commit -m "Merge main into feature-xyz" - Push the updated branch:
git push
Completing a Feature
Once your feature is complete and tested, you'll want to merge it back into the main branch:
- Ensure your feature branch is up to date with main (as above)
- Checkout the main branch:
git checkout main - Merge your feature branch:
git merge feature-xyz - Push the updated main branch:
git push - Optionally, delete your feature branch:
git branch -d feature-xyz # Delete local branch git push origin --delete feature-xyz # Delete remote branch
Note: In many team environments, you wouldn't directly merge to the main branch. Instead, you'd create a pull request (GitHub) or merge request (GitLab) for code review before merging.
Visual Representation of a Repository Lifecycle
Understanding File States in Git
To work effectively with Git, it's important to understand the different states a file can be in. This is like knowing the different phases of a manufacturing process, where raw materials transform into finished products through several stages.
The Four States of a File
Files in your Git repository can exist in one of four states:
- Untracked: Git doesn't know about the file yet
- Tracked - Unmodified: File is tracked and hasn't changed since the last commit
- Tracked - Modified: File is tracked but has been changed since the last commit
- Tracked - Staged: Modified file has been added to the staging area for the next commit
How to View File States
The git status command shows you which state your files are in:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: staged-file.txt
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: modified-file.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
untracked-file.txt
For a more concise view:
$ git status -s
M staged-file.txt
M modified-file.txt
?? untracked-file.txt
In the short status:
- Left column: staging area status
- Right column: working directory status
- M: modified
- A: added
- D: deleted
- R: renamed
- C: copied
- ??: untracked
Moving Between States
Here's how files move between states:
Untracked → Staged
git add untracked-file.txt
Modified → Staged
git add modified-file.txt
Staged → Committed
git commit -m "Add/modify files"
Modified → Committed (skipping staging)
git commit -am "Commit modified files"
Staged → Modified
git restore --staged staged-file.txt
Modified → Unmodified
git restore modified-file.txt
Tracked → Untracked
git rm --cached tracked-file.txt
Removing a file completely
git rm tracked-file.txt
Understanding these state transitions gives you precise control over what goes into each commit.
Working with .gitignore
When working on projects, there are often files you don't want to include in version control, such as build artifacts, dependency directories, or files containing sensitive information. The .gitignore file helps you specify patterns for files that Git should ignore.
Creating a .gitignore File
A .gitignore file is a plain text file in the root of your repository that lists patterns for files and directories to ignore:
# .gitignore
# Ignore build output
/build/
/dist/
# Ignore dependencies
/node_modules/
/vendor/
# Ignore log files
*.log
# Ignore environment files with secrets
.env
.env.local
# Ignore system files
.DS_Store
Thumbs.db
Pattern Syntax
The .gitignore file uses a simple but powerful pattern matching syntax:
#begins a comment line- Blank lines are ignored
*matches zero or more characters (except/)?matches exactly one character (except/)[abc]matches any one character inside the brackets**matches nested directories (e.g.,a/**/bmatchesa/b,a/x/b,a/x/y/b, etc.)/at the start of a pattern matches files/directories in the repository root/at the end of a pattern matches directories!negates a pattern (files that match this pattern will NOT be ignored)
Examples
Here are some examples of pattern matching:
*.log: Ignores all files with the .log extension/build/: Ignores the build directory in the root of the repositorybuild/: Ignores all directories named "build" anywhere in the repository**/*.class: Ignores all .class files in any directory!important.log: Doesn't ignore important.log, even if it matches a previous patterndocs/**/*.pdf: Ignores all PDF files in the docs directory and its subdirectories
Best Practices for .gitignore
Follow these best practices when setting up your .gitignore file:
- Create it early: Set up your .gitignore before your first commit
- Be specific: Ignore only what needs to be ignored
- Use templates: GitHub provides language and framework-specific templates
- Include global patterns: For OS-specific files (like .DS_Store), consider using a global gitignore file
- Document why: Add comments explaining why certain patterns are ignored
- Check for ignored files: Use
git status --ignoredto see which files are being ignored
Handling Already Tracked Files
If you've already committed a file that you now want to ignore, adding it to .gitignore won't work—Git will continue to track it. To stop tracking it:
git rm --cached filename.txt
This removes the file from the repository without deleting it from your working directory. After this, git will respect the .gitignore pattern for this file.
Sample .gitignore for Common Projects
Here's a sample .gitignore file for a typical web development project:
# Dependencies
/node_modules
/bower_components
/.pnp
.pnp.js
# Build outputs
/dist
/build
/out
/.next
# Testing
/coverage
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.log
# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.tmp
*.temp
*.bak
Git Command Reference Sheet
Here's a quick reference of the Git commands we've covered, organized by category. Think of this as your Git cheat sheet that you can refer back to as needed.
Repository Setup
| Command | Description |
|---|---|
git init |
Initialize a new Git repository |
git clone <url> |
Clone an existing repository |
git config --global user.name "Name" |
Set your username |
git config --global user.email "email" |
Set your email |
Making Changes
| Command | Description |
|---|---|
git status |
Show status of working directory and staging area |
git add <file> |
Add file to staging area |
git add . |
Add all changes to staging area |
git commit -m "message" |
Commit staged changes with a message |
git commit -am "message" |
Add and commit all modified files (doesn't include new files) |
git diff |
Show unstaged changes |
git diff --staged |
Show staged changes |
git rm <file> |
Remove file from working directory and staging area |
git rm --cached <file> |
Remove file from staging area but keep in working directory |
git mv <old-name> <new-name> |
Rename a file in Git |
Viewing History
| Command | Description |
|---|---|
git log |
Show commit history |
git log --oneline |
Show compact commit history |
git log --graph |
Show commit history with branch graph |
git log -p |
Show commit history with patches (changes) |
git log --stat |
Show commit history with file statistics |
git show <commit> |
Show details of a specific commit |
git blame <file> |
Show who changed each line in a file and when |
Undoing Changes
| Command | Description |
|---|---|
git restore <file> |
Discard changes in working directory |
git restore --staged <file> |
Unstage a file |
git commit --amend |
Modify the last commit |
git revert <commit> |
Create a new commit that undoes changes from a specific commit |
git reset <commit> |
Move branch pointer to a different commit (--soft, --mixed, --hard) |
git reflog |
Show history of HEAD movements (useful for recovery) |
Working with Remotes
| Command | Description |
|---|---|
git remote add <name> <url> |
Add a remote repository |
git remote -v |
List remote repositories |
git fetch <remote> |
Download changes from remote, but don't integrate them |
git pull <remote> <branch> |
Fetch and merge changes from remote |
git push <remote> <branch> |
Upload local branch to remote |
git push -u <remote> <branch> |
Upload local branch and set it to track remote branch |
git push <remote> --delete <branch> |
Delete a remote branch |
Practice Exercises
Let's reinforce what we've learned with some practical exercises. These will help you internalize the Git workflow and commands.
Exercise 1: Basic Git Workflow
Create a new repository and practice the basic Git workflow:
- Initialize a new Git repository
- Create a simple HTML file with a "Hello, World!" message
- Stage and commit the file
- Modify the file to add a heading and some CSS
- View the differences between your working directory and the last commit
- Stage and commit the changes
- View the commit history
Exercise 2: Undoing Changes
Practice different ways to undo changes:
- Create a new file and add some content
- Stage the file
- Unstage the file
- Modify an existing file
- Discard the changes to revert to the last commit
- Make and commit a change, then amend the commit with additional changes
- Create a commit, then revert it with a new commit
Exercise 3: Working with Remotes
Set up a remote repository and practice synchronizing:
- Create a repository on GitHub/GitLab/Bitbucket
- Add it as a remote to your local repository
- Push your local commits to the remote
- Make a change directly on the remote (through the web interface)
- Fetch and merge the remote changes
- Make conflicting changes in both repositories and practice resolving conflicts
Exercise 4: Creating a .gitignore File
Set up a proper .gitignore file for a project:
- Create a new repository for a simple web project
- Add a package.json file and run npm install to create a node_modules directory
- Create a .env file with dummy "secrets"
- Create a build directory with some output files
- Create a proper .gitignore file to exclude the appropriate files and directories
- Verify which files are ignored with git status --ignored
- Stage and commit only the appropriate files
Exercise 5: Git Command Challenge
For each scenario, identify the correct Git command(s) to use:
- You've made changes to a file but want to discard them
- You've staged a file but want to unstage it
- You want to change your last commit message
- You want to see who made changes to a specific line in a file
- You've accidentally committed to the wrong branch
- You want to temporarily save changes without committing them
- You want to see all branches, including remote ones
- You want to delete a remote branch
Further Reading
- Pro Git Book - Comprehensive, free book about Git
- Git Reference - Official Git documentation
- Oh Shit, Git!?! - Solutions for common Git mistakes
- Learn Git Branching - Interactive visualization tool for learning Git
- GitHub's collection of .gitignore templates
- Atlassian Git Tutorials - In-depth tutorials on Git concepts