Core Git Commands and Workflow

Module 2: Version Control & Containerization

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:

  1. Creating or cloning a repository
  2. Making changes to files
  3. Reviewing those changes
  4. Staging changes for commit
  5. Committing changes to the repository
  6. Sharing changes with others
  7. Incorporating others' changes

Let's visualize this workflow:

flowchart TD A[Working Directory] -->|"git add"| B[Staging Area] B -->|"git commit"| C[Local Repository] C -->|"git push"| D[Remote Repository] D -->|"git fetch/pull"| C C -->|"git checkout"| A style A fill:#e1f5fe,stroke:#0288d1 style B fill:#fff3e0,stroke:#ff9800 style C fill:#e8f5e9,stroke:#43a047 style D fill:#f3e5f5,stroke:#8e24aa

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:

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:

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:

Warning: Hard reset permanently discards changes. Also, avoid resetting commits that have been pushed to shared repositories, as it rewrites history.

flowchart LR A[Working Directory] --> B[Staging Area] --> C[Repository] C --> D[Remote Repository] E[git restore] --> A F[git restore --staged] --> B G[git reset --soft] --> C H[git reset --mixed] --> B I[git reset --hard] --> A J[git revert] --> D style A fill:#e1f5fe,stroke:#0288d1 style B fill:#fff3e0,stroke:#ff9800 style C fill:#e8f5e9,stroke:#43a047 style D fill:#f3e5f5,stroke:#8e24aa style E fill:#ffcdd2,stroke:#d32f2f style F fill:#ffcdd2,stroke:#d32f2f style G fill:#ffcdd2,stroke:#d32f2f style H fill:#ffcdd2,stroke:#d32f2f style I fill:#ffcdd2,stroke:#d32f2f style J fill:#ffcdd2,stroke:#d32f2f

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:

  1. Clone a repository:
    git clone https://github.com/team/project.git
    cd project
  2. Create a branch for your feature:
    git checkout -b feature-login
  3. Make changes and commit them:
    git add .
    git commit -m "Implement login form"
  4. Push your branch to the remote:
    git push -u origin feature-login
  5. Later, fetch updates from the team:
    git fetch origin
  6. Update your local main branch:
    git checkout main
    git pull
  7. Incorporate those updates into your feature branch:
    git checkout feature-login
    git merge main
  8. 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

  1. Create a directory for your project:
    mkdir my-project
    cd my-project
  2. Initialize a Git repository:
    git init
  3. Create your initial files:
    echo "# My Project" > README.md
  4. Stage and commit the files:
    git add README.md
    git commit -m "Initial commit"
  5. Create a repository on GitHub/GitLab/Bitbucket
  6. Add the remote:
    git remote add origin https://github.com/username/my-project.git
  7. Push your code:
    git push -u origin main

Option 2: Clone an Existing Repository

  1. Create a repository on GitHub/GitLab/Bitbucket
  2. Clone it locally:
    git clone https://github.com/username/my-project.git
    cd my-project
  3. Create your initial files:
    echo "# My Project" > README.md
  4. Stage and commit the files:
    git add README.md
    git commit -m "Initial commit"
  5. Push your code:
    git push

Making Changes

Once your repository is set up, you'll follow this cycle for making changes:

  1. Ensure you're on the right branch:
    git checkout main
  2. Create a new branch for your feature/fix:
    git checkout -b feature-xyz
  3. Make changes to your files
  4. Check what's changed:
    git status
    git diff
  5. Stage your changes:
    git add filename.txt
    # or git add . to add all changes
  6. Commit your changes:
    git commit -m "Implement feature XYZ"
  7. Push your branch:
    git push -u origin feature-xyz

Incorporating Others' Changes

When collaborating with others, you'll need to integrate their changes:

  1. Fetch the latest changes:
    git fetch origin
  2. Update your main branch:
    git checkout main
    git pull
  3. Merge the main branch into your feature branch:
    git checkout feature-xyz
    git merge main
  4. Resolve any merge conflicts if they arise
  5. Commit the merge:
    git commit -m "Merge main into feature-xyz"
  6. 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:

  1. Ensure your feature branch is up to date with main (as above)
  2. Checkout the main branch:
    git checkout main
  3. Merge your feature branch:
    git merge feature-xyz
  4. Push the updated main branch:
    git push
  5. 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

gitGraph commit branch feature-xyz checkout feature-xyz commit commit checkout main merge feature-xyz checkout feature-xyz commit checkout main merge feature-xyz checkout main

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:

  1. Untracked: Git doesn't know about the file yet
  2. Tracked - Unmodified: File is tracked and hasn't changed since the last commit
  3. Tracked - Modified: File is tracked but has been changed since the last commit
  4. Tracked - Staged: Modified file has been added to the staging area for the next commit
stateDiagram-v2 [*] --> Untracked: Create new file Untracked --> Staged: git add Staged --> Committed: git commit Committed --> Modified: Edit file Modified --> Staged: git add Staged --> Committed: git commit Modified --> Committed: git commit -a Committed --> Untracked: git rm state Tracked { Committed Modified Staged }

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:

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:

Examples

Here are some examples of pattern matching:

Best Practices for .gitignore

Follow these best practices when setting up your .gitignore file:

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:

  1. Initialize a new Git repository
  2. Create a simple HTML file with a "Hello, World!" message
  3. Stage and commit the file
  4. Modify the file to add a heading and some CSS
  5. View the differences between your working directory and the last commit
  6. Stage and commit the changes
  7. View the commit history

Exercise 2: Undoing Changes

Practice different ways to undo changes:

  1. Create a new file and add some content
  2. Stage the file
  3. Unstage the file
  4. Modify an existing file
  5. Discard the changes to revert to the last commit
  6. Make and commit a change, then amend the commit with additional changes
  7. Create a commit, then revert it with a new commit

Exercise 3: Working with Remotes

Set up a remote repository and practice synchronizing:

  1. Create a repository on GitHub/GitLab/Bitbucket
  2. Add it as a remote to your local repository
  3. Push your local commits to the remote
  4. Make a change directly on the remote (through the web interface)
  5. Fetch and merge the remote changes
  6. 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:

  1. Create a new repository for a simple web project
  2. Add a package.json file and run npm install to create a node_modules directory
  3. Create a .env file with dummy "secrets"
  4. Create a build directory with some output files
  5. Create a proper .gitignore file to exclude the appropriate files and directories
  6. Verify which files are ignored with git status --ignored
  7. Stage and commit only the appropriate files

Exercise 5: Git Command Challenge

For each scenario, identify the correct Git command(s) to use:

  1. You've made changes to a file but want to discard them
  2. You've staged a file but want to unstage it
  3. You want to change your last commit message
  4. You want to see who made changes to a specific line in a file
  5. You've accidentally committed to the wrong branch
  6. You want to temporarily save changes without committing them
  7. You want to see all branches, including remote ones
  8. You want to delete a remote branch

Further Reading