The Foundation of Git Workflow
Git commands are like the vocabulary of a new language. While Git has over 150 commands with numerous options, mastering just a handful of essential commands will enable you to handle 90% of your version control needs. Today, we'll focus on five fundamental commands that form the backbone of the Git workflow: init, add, commit, status, and log.
Think of these commands as the basic movements in a dance. Just as a dancer must master the fundamental steps before creating elaborate choreography, a developer must understand these core Git commands before implementing complex version control strategies.
By the end of this lesson, you'll be able to create repositories, track changes, record snapshots of your code, check the status of your work, and review your project's history—essential skills for any developer working on projects of any size.
Git Init: Creating Your Repository
What is git init?
The git init command creates a new Git repository. It transforms a regular directory into a Git repository by creating a hidden .git subdirectory, which contains all the necessary metadata for the repository to function.
Think of git init as planting a seed that will grow into your project's history tree. It's the foundational step that enables Git to track the evolution of your project over time.
When to use git init
Use git init when:
- Starting a new project from scratch
- Converting an existing project to use Git version control
- Creating a new repository for experimentation
Basic usage
$ git init
Initializes a new Git repository in the current directory
$ git init my_project
Creates a new directory called "my_project" and initializes a Git repository inside it
What happens behind the scenes
When you run git init, Git creates a hidden .git directory containing several files and subdirectories:
- config: Repository-specific configuration settings
- description: Used for GitWeb (rarely modified)
- HEAD: Reference to the current branch
- hooks/: Directory for client or server-side hook scripts
- objects/: Directory where Git stores all content
- refs/: Directory containing references to commits (branches, tags, etc.)
Real-world example: Creating a Python project repository
Let's create a repository for a new Python web application:
$ mkdir flask_blog_app
$ cd flask_blog_app
$ git init
Initialized empty Git repository in /path/to/flask_blog_app/.git/
Now we can create the basic structure of our Flask blog application:
$ mkdir templates static
$ touch app.py requirements.txt README.md .gitignore
Best practices for git init
- Initialize early: Create a Git repository at the beginning of your project to track changes from the start
- Don't nest repositories: Avoid initializing a Git repository inside another Git repository to prevent confusion
- Create a .gitignore file: Before your first commit, create a .gitignore file to exclude temporary files, build artifacts, and sensitive information
- Add a README.md: Include a README file to document your project's purpose and setup instructions
Common mistakes and troubleshooting
- Initializing in the wrong directory: Always verify your current directory before running
git init - Re-initializing an existing repository: Running
git initin an existing repository is safe but unnecessary - Creating nested repositories: If you accidentally create a repository inside another, you can remove the nested
.gitdirectory
Git Add: Staging Changes
What is git add?
The git add command adds changes from your working directory to Git's staging area (also known as the index). It tells Git which changes you want to include in your next commit.
Think of git add as a photographer selecting which elements to include in a photograph. You're not taking the picture yet (that's what git commit does), but you're composing the shot by deciding what will be in the frame.
Understanding the staging area
Git's staging area is an intermediate step between your working directory and the repository. It allows you to group related changes into distinct, logical commits—even if you made those changes across multiple files or at different times.
The staging area gives you fine-grained control over what gets committed, ensuring each commit represents a coherent unit of work. This is like a chef preparing all ingredients for a dish before cooking begins—everything is measured, chopped, and ready to go.
Basic usage
$ git add filename.py
Stages changes to a specific file
$ git add directory/
Stages changes to all files in a directory (and its subdirectories)
$ git add .
Stages all changes in the current directory (and subdirectories)
$ git add -p
Interactively stages changes in chunks, allowing you to review each change before staging
What can be staged?
Git can stage various types of changes:
- New files: Files that have been created but not yet tracked by Git
- Modified files: Changes to files that are already tracked by Git
- Deleted files: Files that have been removed from the working directory
- Renamed files: Files that have been renamed (although Git often detects this automatically)
Real-world example: Starting our Flask blog application
Let's continue with our Flask blog application example. First, let's add some basic content to our files:
For app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html', title='Flask Blog')
if __name__ == '__main__':
app.run(debug=True)
For requirements.txt:
Flask==2.0.1
For README.md:
# Flask Blog Application
A simple blog application built with Flask.
## Setup
1. Install requirements: `pip install -r requirements.txt`
2. Run the application: `python app.py`
For .gitignore:
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.env
venv/
ENV/
# Flask
instance/
.webassets-cache
# IDE
.idea/
.vscode/
*.swp
*.swo
Now, let's stage these files:
$ git add app.py requirements.txt README.md .gitignore
This selectively adds our initial files to the staging area, ready to be committed.
Advanced git add techniques
Interactive staging
The -p (or --patch) option allows you to interactively select portions of a file to stage. This is useful when you've made multiple unrelated changes to a single file:
$ git add -p app.py
Git will break the changes into "hunks" and ask whether you want to stage each one
Staging by pattern
You can use wildcards to stage multiple files that match a pattern:
$ git add *.py
Stages all Python files in the current directory (but not subdirectories)
$ git add "*.py"
Stages all Python files in the current directory and all subdirectories
Best practices for git add
- Stage related changes together: Group changes that form a logical unit of work
- Review what you're staging: Use
git add -porgit statusto verify what's being staged - Be selective: Avoid using
git add .blindly; stage files intentionally - Stage early and often: Stage changes as you make them to avoid forgetting what you've modified
Common mistakes and troubleshooting
- Staging unwanted files: If you accidentally stage a file, use
git reset HEAD filenameto unstage it - Staging binary files: Be cautious when staging binary files; they can bloat your repository
- Ignored files not being ignored: If a file is already tracked by Git, adding it to .gitignore won't affect it; use
git rm --cached filenamefirst
Git Commit: Recording Changes
What is git commit?
The git commit command creates a snapshot of your staged changes, saving them permanently to your Git repository's history. Each commit is like a savepoint in your project, capturing the state of your files at a specific point in time.
Think of git commit as a photographer actually taking the picture after carefully composing the shot with git add. Once taken, this snapshot becomes a permanent part of your project's timeline that you can always return to.
The anatomy of a commit
A Git commit consists of:
- A snapshot of staged changes: The content that was in your staging area
- A commit message: A description of what the commit changes and why
- Metadata: Author information, timestamp, and a unique identifier (SHA-1 hash)
- Parent commit reference(s): Pointer(s) to the previous commit(s) in history
Basic usage
$ git commit -m "Add initial Flask application structure"
Creates a commit with the specified message
$ git commit
Opens a text editor for a more detailed commit message
$ git commit -a -m "Update Flask routes"
Automatically stages all modified (but not new) files and commits them
Crafting meaningful commit messages
A good commit message is like a well-written chapter title in a book—it helps readers understand what's inside without having to read the whole thing. Effective commit messages follow these guidelines:
- Be concise but descriptive: Aim for 50 characters or less in the first line
- Use the imperative mood: Write as if giving a command (e.g., "Add feature" not "Added feature")
- Start with a verb: Begin with actions like "Fix," "Add," "Update," "Remove," or "Refactor"
- Explain the what and why: Describe what changes were made and why they were necessary
- For larger changes: Include a more detailed message body after the summary line
For more significant commits, use a multi-line message format:
Add user authentication system
- Implement login and registration forms
- Add password hashing with bcrypt
- Create user model with validation
- Set up session management for logged-in users
Closes #42
Real-world example: Committing our Flask blog app
Let's commit the files we staged earlier for our Flask blog application:
$ git commit -m "Create initial Flask blog application structure"
[main (root-commit) a1b2c3d] Create initial Flask blog application structure
4 files changed, 28 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 app.py
create mode 100644 requirements.txt
Now let's continue building our application by creating a basic template:
$ mkdir -p templates static/css
$ touch templates/home.html static/css/style.css
For templates/home.html:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header>
<h1>Flask Blog</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<h2>Welcome to Flask Blog</h2>
<p>This is a simple blog built with Flask.</p>
</main>
<footer>
<p>© 2025 Flask Blog</p>
</footer>
</body>
</html>
For static/css/style.css:
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
color: #333;
}
header {
background: #333;
color: #fff;
padding: 1rem;
}
header h1 {
margin: 0;
}
nav ul {
padding: 0;
list-style: none;
display: flex;
}
nav ul li {
margin-right: 1rem;
}
nav a {
color: #fff;
text-decoration: none;
}
main {
padding: 1rem;
}
footer {
background: #333;
color: #fff;
text-align: center;
padding: 1rem;
position: fixed;
bottom: 0;
width: 100%;
}
Now let's stage and commit these new files:
$ git add templates/ static/
$ git commit -m "Add basic HTML template and CSS styling"
Advanced commit techniques
Amending commits
If you need to fix a mistake in your most recent commit or add forgotten changes, you can use the --amend option:
$ git commit --amend -m "Updated commit message"
Changes the message of the most recent commit
$ git add forgotten_file.py
$ git commit --amend --no-edit
Adds a forgotten file to the most recent commit without changing the commit message
Important: Never amend commits that have been pushed to a shared repository unless you're absolutely sure no one has based work on them.
Empty commits
Sometimes you might want to create a commit without any actual code changes, such as marking the start of a new development phase:
$ git commit --allow-empty -m "Begin refactoring phase"
Creates a commit with a message but no content changes
Best practices for git commit
- Commit logical units of work: Each commit should represent a single, cohesive change
- Commit frequently: Make small, focused commits rather than large, sweeping changes
- Write meaningful commit messages: Help your future self and others understand why changes were made
- Verify what you're committing: Review your staged changes before committing
- Don't commit generated files: Use .gitignore to exclude build artifacts and dependencies
- Don't commit broken code: Ensure your code compiles/runs before committing
Common mistakes and troubleshooting
- Committing with a typo in the message: Use
git commit --amendto fix the most recent commit message - Committing to the wrong branch: Use
git resetto undo commits if necessary, thengit checkoutto switch branches - Committing sensitive information: If you accidentally commit sensitive data, consider using
git filter-branchorBFG Repo-Cleanerto remove it - Forgetting what you're committing: Always use
git statusbefore committing to review staged changes
Git Status: Checking Repository State
What is git status?
The git status command shows the current state of your working directory and staging area. It lets you see which changes have been staged, which haven't, and which files aren't being tracked by Git.
Think of git status as a dashboard or control panel for your Git workflow. It provides real-time information about what's happening in your repository, helping you make informed decisions about what to do next.
Why git status is essential
Regular use of git status helps you:
- Understand what Git sees in your repository
- Avoid committing unwanted files or changes
- Identify files that need to be added to .gitignore
- Remember what you've been working on
- Get guidance on what commands to run next
Basic usage
$ git status
Shows the full status output
$ git status -s
Shows a simplified, compact status output
Understanding git status output
The output of git status includes several sections:
Branch information
On branch main
Shows which branch you're currently on
Changes to be committed
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: templates/home.html
modified: app.py
Green files are staged and ready to be committed
Changes not staged for commit
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: README.md
Red files have been modified but not yet staged
Untracked files
Untracked files:
(use "git add <file>..." to include in what will be committed)
static/js/main.js
.env.example
Red files that Git doesn't yet track
Real-world example: Checking status of our Flask blog app
Let's continue with our Flask blog application. Let's add an about page and check the status:
First, let's create templates/about.html:
<!DOCTYPE html>
<html>
<head>
<title>About - Flask Blog</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header>
<h1>Flask Blog</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<h2>About This Blog</h2>
<p>This is a simple demonstration blog built with Flask.</p>
<p>It was created as part of the Python Full Stack Developer Course.</p>
</main>
<footer>
<p>© 2025 Flask Blog</p>
</footer>
</body>
</html>
Also, let's modify app.py to add the about route:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('home.html', title='Flask Blog')
@app.route('/about')
def about():
return render_template('about.html')
if __name__ == '__main__':
app.run(debug=True)
Now, let's run git status to see what Git reports:
$ git status
On branch 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.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
templates/about.html
no changes added to commit (use "git add" and/or "git commit -a")
This tells us:
- We've modified app.py, but haven't staged those changes yet
- We've created a new file, templates/about.html, which is currently untracked
- No changes are staged for commit yet
Advanced git status techniques
Short status format
For a more compact view, especially useful in larger repositories:
$ git status -s
M app.py
?? templates/about.html
In the short format:
M= modifiedA= addedD= deletedR= renamedC= copied??= untracked- Left column = staging area, right column = working directory
Status with ignored files
To see ignored files that match .gitignore patterns:
$ git status --ignored
Shows status including files that are being ignored
Best practices for git status
- Check status frequently: Run
git statusbefore and after making changes - Use as a guide: Let the output guide your next actions (stage, commit, etc.)
- Review before committing: Always check status before committing to ensure you're committing what you intend
- Use short format for large repos:
git status -sprovides a cleaner view when working with many files
Common mistakes and troubleshooting
- Forgetting to add new files: New files must be explicitly added with
git add; they won't be included in commits otherwise - Confused by staged vs. unstaged: Green files are staged (will be included in the next commit), red files are not
- Files not showing up: If files are ignored via .gitignore, they won't appear in normal status output
- Too many untracked files: Consider updating your .gitignore file to exclude unwanted files
Git Log: Viewing Commit History
What is git log?
The git log command displays the commit history of your repository, showing who made changes, when they were made, and what was changed. It allows you to explore the evolution of your project over time.
Think of git log as a time machine that lets you travel through your project's past. It's like reading a journal of your project's development, with entries for each significant change along the way.
Why git log is powerful
Git log helps you:
- Understand how your project has evolved
- Identify when and by whom specific changes were introduced
- Find commits that might have introduced bugs
- Learn from past development decisions
- Document the progress of your project
Basic usage
$ git log
Shows the commit history with details
$ git log --oneline
Shows a compact commit history with one line per commit
$ git log -p
Shows the commit history with the diff (changes) for each commit
Understanding git log output
The standard git log output includes:
commit a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Author: Jane Doe <jane@example.com>
Date: Mon Apr 15 12:34:56 2025 -0700
Create initial Flask blog application structure
Each commit shows: unique hash identifier, author, date, and commit message
Real-world example: Viewing history of our Flask blog app
Let's add and commit our changes to the Flask blog app, then examine the history:
$ git add app.py templates/about.html
$ git commit -m "Add about page and route"
Now, let's view our commit history:
$ git log
commit f1e2d3c4b5a6978685746352413f2g1h (HEAD -> main)
Author: Jane Doe <jane@example.com>
Date: Mon Apr 15 14:30:00 2025 -0700
Add about page and route
commit b9c8d7e6f5a4b3c2d1e0f9g8h7i6j5k4l3m2n1
Author: Jane Doe <jane@example.com>
Date: Mon Apr 15 13:45:00 2025 -0700
Add basic HTML template and CSS styling
commit a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
Author: Jane Doe <jane@example.com>
Date: Mon Apr 15 12:34:56 2025 -0700
Create initial Flask blog application structure
For a more compact view:
$ git log --oneline
f1e2d3c (HEAD -> main) Add about page and route
b9c8d7e Add basic HTML template and CSS styling
a1b2c3d Create initial Flask blog application structure
Advanced git log techniques
Viewing changes in commits
To see the actual code changes in each commit:
$ git log -p
Shows commits with the diff of changes
$ git log -p filename.py
Shows commits that affected a specific file
Limiting the output
In larger repositories, you might want to limit the output:
$ git log -n 5
Shows only the 5 most recent commits
$ git log --since="2 weeks ago"
Shows commits from the last two weeks
$ git log --author="Jane"
Shows commits by a specific author
Graphical representations
For visualizing branching and merging:
$ git log --graph --oneline --all
Shows a text-based graph of the commit history, including all branches
Customizing the output format
You can create custom formats for the log output:
$ git log --pretty=format:"%h - %an, %ar : %s"
Shows a customized log format with abbreviated hash, author name, relative date, and subject
Best practices for git log
- Use filtering options: Narrow down results to find what you're looking for more efficiently
- Customize your view: Different formats are useful for different purposes
- Combine with other commands: Use
git showto examine specific commits found viagit log - Create aliases: Set up Git aliases for your frequently used log commands
Common aliases for git log
Many developers create aliases for common log formats:
$ git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
Creates a colorized, graphical log alias that you can use with git lg
Putting It All Together: A Basic Git Workflow
Now that we understand the five essential Git commands, let's see how they fit together in a typical workflow:
- Initialize a repository (once at the beginning):
$ mkdir new_project$ cd new_project$ git init - Create or modify files (make changes to your project):
# Work on your files using your text editor or IDE - Check status (see what's changed):
$ git status - Stage changes (prepare for commit):
$ git add filename.py# Or to stage all changes:$ git add . - Check status again (verify what's staged):
$ git status - Commit changes (save a snapshot):
$ git commit -m "Add user authentication system" - View history (review what's been done):
$ git log - Repeat steps 2-7 as you continue development.
Example: Complete workflow with our Flask blog
Let's add a new feature to our Flask blog: a contact page with a simple form.
- First, check the current status:
$ git statusOn branch mainnothing to commit, working tree clean - Create a new template for the contact page:
$ touch templates/contact.htmlAdd content to templates/contact.html:
<!DOCTYPE html> <html> <head> <title>Contact - Flask Blog</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <header> <h1>Flask Blog</h1> <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/contact">Contact</a></li> </ul> </nav> </header> <main> <h2>Contact Us</h2> <form method="POST" action="/contact"> <div> <label for="name">Name:</label> <input type="text" id="name" name="name" required> </div> <div> <label for="email">Email:</label> <input type="email" id="email" name="email" required> </div> <div> <label for="message">Message:</label> <textarea id="message" name="message" rows="5" required></textarea> </div> <button type="submit">Send Message</button> </form> </main> <footer> <p>© 2025 Flask Blog</p> </footer> </body> </html> - Update the app.py file to add the contact route:
from flask import Flask, render_template, request, redirect, url_for, flash app = Flask(__name__) app.secret_key = 'your_secret_key' # Required for flash messages @app.route('/') def home(): return render_template('home.html', title='Flask Blog') @app.route('/about') def about(): return render_template('about.html') @app.route('/contact', methods=['GET', 'POST']) def contact(): if request.method == 'POST': name = request.form.get('name') email = request.form.get('email') message = request.form.get('message') # In a real application, you would process the form data here # (e.g., send an email, save to database, etc.) flash('Thank you for your message! We will get back to you soon.') return redirect(url_for('contact')) return render_template('contact.html') if __name__ == '__main__': app.run(debug=True) - Also update the home.html and about.html templates to include the contact link in the nav menu.
- Check the status to see our changes:
$ git statusOn branch mainChanges 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.pymodified: templates/about.htmlmodified: templates/home.htmlUntracked files:(use "git add <file>..." to include in what will be committed)templates/contact.htmlno changes added to commit (use "git add" and/or "git commit -a") - Stage all the changes:
$ git add . - Check status again to verify what's staged:
$ git statusOn branch mainChanges to be committed:(use "git restore --staged <file>..." to unstage)modified: app.pymodified: templates/about.htmlmodified: templates/home.htmlnew file: templates/contact.html - Commit the changes:
$ git commit -m "Add contact page with form" - View the commit history:
$ git log --onelinea7b6c5d Add contact page with formf1e2d3c Add about page and routeb9c8d7e Add basic HTML template and CSS stylinga1b2c3d Create initial Flask blog application structure
Hands-on Exercises
Exercise 1: Basic Repository Setup
- Create a new directory called "python_exercises" on your computer
- Initialize a Git repository in this directory
- Create a README.md file with a description of what this repository will contain
- Create a simple Python script called calculator.py with a function to add two numbers
- Check the status of your repository
- Stage both files
- Commit the files with an appropriate message
- View your commit history
Exercise 2: Making Changes and Viewing Differences
- Continuing from the previous exercise, add a subtraction function to calculator.py
- Check the status to see the modified file
- Use
git diffto see what changes you've made - Stage the changes
- Commit with an appropriate message
- Add multiplication and division functions
- Stage and commit these changes
- Use
git log -pto see the changes in each commit
Exercise 3: Selective Staging
- Add a docstring at the top of calculator.py describing the module
- Add a new file called test_calculator.py with basic tests for your functions
- Check the status to see all changes
- Stage only the docstring changes in calculator.py using
git add -p - Commit these changes with a message about adding documentation
- Stage and commit the test file separately
- View your commit history in a compact format using
git log --oneline
Key Takeaways
- git init establishes a new repository, creating the framework for version control
- git add stages changes, allowing you to selectively prepare modifications for a commit
- git commit records a snapshot of your staged changes with a descriptive message
- git status provides a real-time view of your repository state, showing staged, unstaged, and untracked files
- git log reveals your project's history, allowing you to explore past changes and decisions
- These five commands form a cohesive workflow that enables effective version control
- Regular use of these commands creates a detailed, navigable history of your project
By mastering these fundamental Git commands, you've established a solid foundation for version control in your development workflow. As you continue learning, you'll build upon these basics to leverage Git's more advanced features for branching, collaboration, and complex project management.
Assignment: Basic Git Commands Practice
Create a simple Python project with multiple files and track it using Git:
- Initialize a new Git repository
- Create the following files:
- main.py - A simple menu-driven program that calls functions from other modules
- math_operations.py - A module with basic math functions (add, subtract, multiply, divide)
- string_operations.py - A module with basic string functions (reverse, count_vowels, is_palindrome)
- README.md - Documentation about your project
- .gitignore - Include patterns for Python-specific files to ignore
- Make at least 5 meaningful commits showing the progression of your project
- Use descriptive commit messages following best practices
- Include a screenshot or copy of your git log output showing your commit history
Bonus challenges:
- Use
git add -pfor at least one commit - Make an intentional "mistake" and fix it in a separate commit
- Use
git diffto show changes before staging
Additional Resources
- Pro Git Book - Comprehensive guide to Git
- Official Git Documentation
- Learn Git Branching - Interactive visualization tool
- Oh Shit, Git!?! - Solutions for common Git mistakes
- GitHub Git Cheat Sheet