Introduction

Agents write all the code, but I still make the Git commits. That’s my last remaining contribution. (T_T)

Treat this as a Git quick reference, though it could also serve as a tutorial.

The official cheat sheet is much better than mine.

Working tree, index, and HEAD

flowchart TD
    W["Working tree: files on disk"] -->|"git add"| I["Index: staged snapshot"]
    I -->|"git commit"| C["Commits"]
    I -->|"git restore"| W
    subgraph LOCAL["Local repository"]
        H["HEAD"] -.->|"normally refers to"| B["Current branch"]
        B -.->|"points to"| C
    end
  • Working tree: the files you edit on disk.
  • Index (staging area): the snapshot prepared for the next commit.
  • Local repository: stored objects, including commits, and references such as branches.
  • HEAD: normally refers to the current branch, which points to a commit. With a detached HEAD, it points directly to a commit.

Lazygit

I recommend Lazygit, a terminal user interface (TUI) for convenient Git operations.

Configuration

Inspect global configuration

git config --global --list

Edit the configuration file

Open the global configuration file in Git’s editor:

git config --global --edit

The file is usually ~/.gitconfig; Git also supports ~/.config/git/config. Add or update the relevant sections without replacing unrelated settings:

[user]
    name = Your Name
    email = you@example.com
[core]
    editor = vim

Replace vim with your preferred installed editor.

Configure GitHub credentials with GitHub CLI

Install GitHub CLI (gh) if needed. With Homebrew:

brew install gh

Authenticate through the browser and configure Git’s HTTPS credential helper:

gh auth login
gh auth setup-git
gh auth status

Create or obtain a repository

Clone a repository

Common remote cloning methods are HTTPS and SSH.

# HTTPS
git clone https://github.com/<username>/<repo_name>.git

# SSH, needs a configured SSH key
git clone git@github.com:<username>/<repo_name>.git

Pick either one. The SSH form only works when the host already knows your public key.

For SSH key creation, see my SSH notes.

Initialize a repository

git init -b main

Commits and recovery

Commit changes

git commit -a -m "Stylize the nav bar"

-a stages modifications and deletions of tracked files. New files still need git add before they can be committed.

Compare commits

# the latest commit against its parent
git diff HEAD^ HEAD

HEAD^ is the first parent. A root commit has no parent; for a merge commit, this compares against its first parent.

You can also compare two commits by their commit hashes (commit IDs):

# list abbreviated commit hashes
# press q to exit the pager
git log --oneline

# show the changes from the older snapshot to the newer one
git diff <older_commit> <newer_commit>

Use either full hashes or abbreviations that uniquely identify the commits in this repository. Reversing the arguments reverses the diff direction.

Reference: git-diff

Modify commits

Rewriting published commits changes their IDs and may disrupt collaborators. Please use with caution.

Amend the latest commit

git commit --amend

Optional parameters:

  • --no-edit: keep the existing commit message without opening an editor.
  • -m "New message": replace the commit message directly, without opening an editor.
  • --date="Wed Dec 18 14:46 2024 +0800": set the author date, not the committer date.

Staged changes go into the replacement commit. Without -m or --no-edit, Git opens an editor for the message.

Reference: git-commit

Modify earlier commits

Start with a clean working tree and index.

# list all commits
git log

# include the target commit and its descendants
# For the root commit, use `git rebase -i --root` instead, since it has no parent.
git rebase -i <commit>^

Git will open a list of commits.

Change the message: replace pick with reword on the target line. Save and close the rebase list; Git opens an editor for the new message and continues when it is saved and closed.

Change the author date: replace pick with edit on the target line. Save and close the list. When Git stops at that commit, run:

git commit --amend --date="Wed Dec 18 14:46 2024 +0800" --no-edit
git rebase --continue

git rebase --abort cancels an in-progress rebase and returns the branch to its original state.

Reference: git-rebase, git-commit

Force push

git push --force-with-lease origin <branch_name>

--force-with-lease compares the remote with the expected state before replacing it, but the remote history is still overwritten.

Restore uncommitted deletions

File deleted in the working tree, deletion not yet staged — restore it from the index:

git checkout -- <file_name>

git rm was run but the deletion is not committed — unstage the deletion first:

git reset HEAD -- index.html
git checkout -- index.html

Both cases overwrite whatever is at that path in the working tree. If the deletion is already committed, take the file from a commit that still contains it:

git checkout <commit> -- <file_name>

Reset the current branch

git reset <commit> moves the current branch to <commit>. The mode decides what happens to the index and the working tree.

ModeIndexWorking tree
--softkeptkept
--mixed (default)reset to the target commitkept
--hardreset to the target commitreset to the target commit

Example:

# drop the latest commit together with its changes
git reset --hard HEAD^

Move recent commits to a new branch

Move the most recent commit(s) to a new branch with Git

Branch

A branch is a movable reference to a commit. Its history consists of the commits reachable through parent links.

# list local branches
git branch

# show all branches and their commit relationships
git show-branch -a
# create from the current commit
git branch <branch_name>

# create from another branch
git branch <branch_name> <base_branch>

# switch to a branch
git switch <branch_name>
# delete the remote branch
git push -d origin <branch_name>

# delete the local branch
git branch -d <branch_name>

Remote collaboration

Change a remote URL

git remote set-url origin <repo_url>
git remote -v

Configure an upstream remote for a fork

git remote add upstream https://github.com/ORIGINAL-OWNER/ORIGINAL-REPOSITORY.git

upstream here is a remote name. You can use another name.

Push changes

# existing tracking relationship
git push
# specify remote and branch
git push origin main
# set tracking on the first push
git push --set-upstream origin main

Fetch, merge, and pull

# download remote updates without changing the local branch
git fetch origin <branch_name>

# download and integrate into the current branch
# in merge mode, pull = fetch + merge; --rebase uses rebase instead
git pull origin <branch_name>

# merge <branch_name> into the current branch
git merge <branch_name>

# after the merge succeeded and the branch is no longer needed
git branch -d <branch_name>

Stash uncommitted changes

Useful when local changes have to be set aside before pulling remote updates. By default stash leaves untracked files alone.

git stash
git pull
git stash pop

Pull requests

Push the topic branch to a repository you can write to:

git push -u origin <branch_name>

Then open a pull request on GitHub. The target repository and branch go in base, the topic branch in compare. Read the diff, write a title and description, and create it.

Worktree

I only learned about worktrees after seeing Codex use them.

git worktree gives one repository multiple working directories for parallel development, fixes, or reviews on different branches. Each has its own files, index, and HEAD, while sharing commit objects and branch refs. This avoids repeated stashing, branch switching, or cloning.

Examples:

# list working directories and their branches
git worktree list

# create feature from main and check it out in a new directory
git worktree add -b feature ../project-feature main

# alternatively, check out an existing hotfix branch in a new directory
git worktree add ../project-hotfix hotfix

# after committing the feature changes in ../project-feature,
# merge them into main from the original working directory
git switch main
git merge feature

# after a successful merge, remove the clean feature working directory
git worktree remove ../project-feature

# clean up administrative records for missing working directories
git worktree prune

Reference: git-worktree

Submodule and Subtree

To place repository B inside a subdirectory of repository A, choose how B should be maintained:

flowchart TD
    A["Place B inside A"] --> B{"How should B be managed?"}
    B -->|"Independent repository, pinned to a commit"| C["Submodule"]
    B -->|"Files tracked by A as a normal directory"| D["Subtree"]
    D --> E{"Include B's full commit history in A?"}
    E -->|"Yes"| F["Without --squash"]
    E -->|"Only a summary of each import"| G["With --squash"]

Submodule

B keeps its own history. A records B’s URL, directory, and a specific commit ID. Updates to B’s remote branch do not automatically change the version referenced by A.

Add a submodule

Run in A, replacing <dir-B> with a directory path that does not yet exist and main with B’s actual branch:

git submodule add -b main <B_repo_url> <dir-B>
git commit -m "Add B as a submodule"

To clone A together with its submodules:

git clone --recurse-submodules <A_repo_url>

Update a submodule

After cloning A or pulling its updates, check out the submodule versions recorded by A:

git submodule update --init --recursive

To upgrade to the latest commit on B’s remote main branch, run in A:

git submodule update --init --remote -- <dir-B>
git diff --submodule
git add <dir-B>
git commit -m "Update B submodule"

Without --remote, Git checks out A’s pinned version. With it, Git uses B’s configured remote branch.

To modify B, commit and push independently in B, then commit its new pointer in A.

Reference: gitsubmodules, git-submodule

Subtree

B’s files are tracked as a normal directory in A. A regular clone of A includes them.

Import

Run in the destination repository with a clean working tree and index. Replace <dir-B> with a directory path that does not yet exist:

git subtree add --prefix=<dir-B> <B_repo_url> main

This imports the files and creates a merge commit automatically.

Pull updates

git subtree pull --prefix=<dir-B> <B_repo_url> main

Import without the full history

Use --squash for both the initial import and later updates to merge a summary commit for each import rather than B’s full history:

git subtree add --prefix=<dir-B> <B_repo_url> main --squash
git subtree pull --prefix=<dir-B> <B_repo_url> main --squash

Reference: git-subtree