Git and GitHub Essential Commands
RELEASES Dec. 8, 2025, 5:30 p.m.

Git and GitHub Essential Commands

Git and GitHub have become the backbone of modern software development, yet many newcomers feel overwhelmed by the sheer number of commands. In this guide we’ll cut through the noise and focus on the essential commands you’ll use every day, from initializing a repository to collaborating on large teams. By the end, you’ll not only know *what* each command does, but also *when* to use it in real‑world scenarios, complete with practical code snippets and pro tips that will make your workflow smoother and more reliable.

Getting Started: Installation & Global Configuration

Before you type a single Git command, you need a working installation. On macOS you can use brew install git, on Windows the official installer, and on Linux most distributions ship Git in their default repositories. Once installed, set up your identity—Git attaches this information to every commit you create.

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

These settings are stored in ~/.gitconfig and apply to every repository on your machine. If you ever need a project‑specific override, drop the --global flag and run the same commands inside the repository.

Basic Workflow: From Zero to First Commit

The typical day starts with either creating a fresh repository or cloning an existing one. git init creates the .git folder that tracks all changes, while git clone copies a remote repo and sets up the default origin remote for you.

# Create a new repo locally
mkdir my‑project && cd my‑project
git init

# Or clone an existing repo
git clone https://github.com/user/example.git

Once the repo exists, you’ll frequently run git status to see what’s changed, git add to stage those changes, and git commit to record a snapshot. A good habit is to write concise, imperative commit messages that describe *what* changed, not *why*.

git status          # Shows staged vs. unstaged files
git add README.md   # Stage a single file
git add .           # Stage everything in the working tree
git commit -m "Add project overview to README"

Tip: Use git commit -v to open the editor with a diff of staged changes, helping you craft accurate messages.

Viewing History

Understanding the commit graph is crucial for debugging. git log offers a powerful, customizable view of history. The --oneline flag condenses each commit to a single line, while --graph draws an ASCII tree.

git log --oneline --graph --decorate

This one‑liner quickly reveals branch merges, rebases, and tags, making it easier to navigate complex histories.

Branching: Isolate Work Without Fear

Branches are Git’s most celebrated feature. They let you develop new features, fix bugs, or experiment without disturbing the main line of development. Creating a branch is as simple as git branch feature-x, but most developers prefer the combined checkout -b shortcut.

git checkout -b feature-login   # Create & switch to new branch

When you’re ready to share your work, push the branch to the remote. The -u flag sets the upstream tracking branch, so future git push and git pull commands know where to go.

git push -u origin feature-login

To merge your changes back into main, first switch to the target branch, then run git merge. If the feature branch is clean and linear, a fast‑forward merge will happen automatically.

git checkout main
git merge feature-login   # Fast‑forward if possible

Rebasing vs. Merging

Both git merge and git rebase integrate changes, but they do so differently. Merging preserves the full history, creating a merge commit that clearly shows when two lines of development converged. Rebasing rewrites the feature branch’s base, producing a linear history that can be easier to read.

# Rebase feature branch onto latest main
git checkout feature-login
git fetch origin
git rebase origin/main

Pro tip: Use rebase for local, private branches; avoid rebasing branches that are already shared with others, as it rewrites commit hashes and can cause confusion.

Remote Collaboration: Push, Pull, Fetch, and Fork

GitHub shines when you start collaborating. The typical workflow involves pulling changes from the central repository, making local commits, and pushing them back. git pull is essentially a fetch followed by a merge (or rebase if you configure it).

git pull origin main   # Fetches and merges remote main into local main

Sometimes you only want the latest objects without merging—this is where git fetch shines. After fetching, you can inspect the remote branches and decide how to integrate them.

git fetch origin
git checkout -b temp origin/feature-xyz   # Create a local branch from remote

Forking is a GitHub‑specific pattern for contributing to projects you don’t own. You fork the repo on GitHub, clone your fork locally, add the upstream repository as a remote, and keep your fork in sync.

git remote add upstream https://github.com/original/awesome.git
git fetch upstream
git checkout main
git merge upstream/main   # Keep your fork up‑to‑date

Pull Requests (PRs)

A Pull Request is GitHub’s way of asking the maintainer to review and merge your changes. After pushing a branch, you’ll see a “Compare & pull request” button on GitHub. The PR page lets you add a description, request reviewers, and run CI checks automatically.

Pro tip: Keep your PR titles short and descriptive (e.g., “Add OAuth2 login flow”). Use the description to explain the problem, the solution, and any testing steps you performed.

Advanced Commands: Cherry‑Pick, Stash, Revert, and Reflog

Once you master the basics, a handful of advanced commands become indispensable. git cherry-pick lets you apply a single commit from another branch onto your current branch—perfect for hot‑fixes that need to be propagated quickly.

# Apply commit abc123 from feature branch onto main
git checkout main
git cherry-pick abc123

If you’re in the middle of a feature and need to switch contexts, git stash saves your uncommitted changes on a stack, letting you revert to a clean working tree. Later, you can pop the stash to restore the changes.

git stash save "WIP: login UI"
git checkout main           # Switch branches safely
# ... do other work ...
git checkout feature-login
git stash pop               # Bring back the saved work

When a commit turns out to be a mistake, git revert creates a new commit that undoes the changes, preserving history. This is safer than git reset on shared branches.

git revert f5e8d2a   # Generates a new commit that reverses f5e8d2a

Finally, git reflog is a lifesaver when you think you’ve lost commits. It records every move of the HEAD pointer, allowing you to recover dangling commits or undo accidental resets.

git reflog
git checkout HEAD@{2}   # Jump back to a previous state

Cleaning Up

Over time, branches pile up and your local repository can become cluttered. Use git branch -d for safely deleting merged branches, and git branch -D for force‑deleting unmerged ones.

git branch -d feature-login   # Deletes if fully merged
git branch -D old-experiment  # Force delete

Real‑World Example: Automating Version Bumps with a Git Hook

Imagine you maintain a Python library and want to enforce semantic versioning. A pre‑commit hook can automatically bump the version in setup.py based on the commit message prefix (feat:, fix:, etc.). The hook runs locally before each commit, ensuring consistency without manual edits.

# .git/hooks/pre-commit
#!/usr/bin/env python3
import re, sys, pathlib

msg = pathlib.Path('.git/COMMIT_EDITMSG').read_text()
if msg.startswith('feat:'):
    bump = 'minor'
elif msg.startswith('fix:'):
    bump = 'patch'
else:
    sys.exit(0)   # No version bump needed

def bump_version(bump):
    import toml
    pyproject = pathlib.Path('pyproject.toml')
    data = toml.loads(pyproject.read_text())
    major, minor, patch = map(int, data['tool']['poetry']['version'].split('.'))
    if bump == 'minor':
        minor += 1; patch = 0
    elif bump == 'patch':
        patch += 1
    new_version = f"{major}.{minor}.{patch}"
    data['tool']['poetry']['version'] = new_version
    pyproject.write_text(toml.dumps(data))
    print(f"🔧 Bumped version to {new_version}")

bump_version(bump)
sys.exit(0)

Make the script executable (chmod +x .git/hooks/pre-commit) and every time you commit a feat: or fix: message, the version updates automatically. This tiny automation reduces human error and keeps your release process smooth.

Real‑World Example: CI Integration with GitHub Actions

GitHub Actions provides a YAML‑based pipeline that reacts to Git events. Below is a minimal workflow that runs tests on every push to main and automatically creates a release tag when a commit message contains #release.

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest

  release:
    needs: test
    if: contains(github.event.head_commit.message, '#release')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Create Tag
        run: |
          VERSION=$(git describe --tags --abbrev=0 || echo "v0.0.0")
          NEW=$(echo $VERSION | awk -F. '{print $1"."$2"."$3+1}')
          git tag $NEW
          git push origin $NEW
      - name: Publish Release
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ env.NEW }}
          name: Release ${{ env.NEW }}
          body: "Automated release generated by CI."

This workflow demonstrates how Git commands (git tag, git push) blend seamlessly with CI automation. Every successful test run can trigger a new version tag, eliminating manual release steps.

Pro Tips & Common Pitfalls

  • Never commit secrets. Use git ignore and tools like git‑secret or git‑crypt to keep API keys out of history.
  • Keep commits atomic. Each commit should represent a single logical change; this makes git bisect far more effective when tracking bugs.
  • Leverage aliases. Shorten repetitive commands in your .gitconfig:
    [alias]
      co = checkout
      br = branch
      st = status -sb
      ci = commit
    
  • Use signed commits. Enable GPG signing (git config --global commit.gpgsign true) for added security, especially in open‑source projects.
  • Regularly prune stale branches. Run git remote prune origin to clean up remote-tracking references that no longer exist on the server.
Pro tip: When collaborating across time zones, adopt a “no force‑push” policy on shared branches. If you must rewrite history, do it on a short‑lived feature branch and open a fresh PR.

Conclusion

Mastering Git is less about memorizing every flag and more about understanding the mental model behind snapshots, branches, and distributed collaboration. The commands covered here—init, clone, add, commit, branch, merge, rebase, push, pull, and the advanced utilities like cherry-pick, stash, and reflog—form a solid foundation for any developer. By integrating these commands into everyday practice, automating repetitive tasks with hooks, and tying Git into CI pipelines, you’ll move from “Git beginner” to “Git power user” with confidence. Happy committing!

Share this article