Resources
If you’d prefer to reference Gists, I’ve provided the links below:
Git Tags
git tag
: List all tagsgit tag -l "v1.0.*"
: List all tags with a patterngit tag <tagname>
: Create a lightweight taggit tag -a <tagname> -m "<tag message>"
: Create an annotated taggit tag -a <tagname> <commit-hash> -m "<tag message>"
: Create an annotated tag on a commitgit push <remote> <tagname>
: Push a tag to remotegit push <remote> --tags
: Push all tags to remotegit checkout <tagname>
: Checkout a taggit tag -d <tagname>
: Delete a tag locallygit push <remote> --delete <tagname>
: Delete a remote taggit show <tagname>
: Show information about a taggit tag -s <tagname> -m "<tag message>"
: Create a signed tag (must have a GPG setup)git tag -v <tagname>
: Verify a signed tag
Note: Always use annotated tags for marking releases or important points in history as they are checksummed and can include additional information.
Git Tagging Strategy Example
Here’s an example of how Git tags could be used for a blog website.
- Release Tags: Each release can be tagged with a version number. The numbering strategy should be determined
to suit your needs, for example
MAJOR.MINOR.PATCH
. - Post Publishing: Have a tag format for each post that’s published. An example of this would be
post/YYYY/MM/DD/post-title
. It might make sense to replicate your URL pattern for posts, or use the post slug in addition to the publish date. - Build Metadata: Typically used if you have a complicated development and release process that involves multiple stages of testing and deployment. You could use tags in this case to indicate that a version is in testing.
Don’t over-use tags, they should be meaningful. Think of a tag as a snapshot in time. If you have a simple project, it may not be necessary to use them at all.
Remotes and Branches
Git Remote Management
git remote add [name] [url]
: Create a new connection to a remote repositorygit remote rm [name]
: Remove the connection to a remote repositorygit remote rename [old_name] [new_name]
: Rename a remote connectiongit remote show [name]
: Display information about a remote connectiongit remote update
: Fetch updates from all remotes
Git Branch Management
git branch [branch]
: Create a new branch from the current branch without switchinggit checkout -b [branch]
: Create a new branch from the current branch and check it outgit branch -D [branch]
: Delete a branchgit branch -M [new name]
: Change the name of the current branchgit push origin --delete [branch]
: Delete a remote branchgit fetch origin --prune
: Update indexed remote branches (remove deleted remotes)git branch
: List local branchesgit branch -a
: List local and indexed remote branches
Upstreams
An upstream in Git is the default branch in the original repository from which your repository was cloned. This is where you pull changes from to update your project. A typical scenario in which you would use an upstream is contributing to an open source project. You would clone the project, make changes, and then submit a pull request
Forking a Repository and Creating a Pull Request
Fork and clone the forked repository to your machine. Set the upstream to the original repository:
git branch --set-upstream-to=[remote]/[branch] [local-branch]
Push your changes to the forked repository and create a pull request.
Git Rebase
A rebase is typically used in interactive mode by providing the -i
flag. This allows you to squash commits,
reorder commits, or remove them. You can also use the --autosquash
flag to automatically squash commits that
have the squash
or fixup
prefix.
When rebasing, you can rebase the entire branch, a certain number of commits, or by providing a commit hash.
# Rebase the last 3 commits
git rebase -i HEAD~3
# Rebase using a commit hash
git rebase -i 9fceb02
# Rebase the entire branch
git rebase -i [branch]
Squashing an Interactive Rebase
An example of the terminal view when squashing commits:
pick 1fc6c95 do something
s 6b2481b do something else
s dd1475d changed some things
# ... Instructions
Mirroring
While git push
is used to push a specific branch, git push --mirror
is used to push all branches and tags.
This ensures that the destination is updated to be exactly the same as the source repository.
Using --mirror
will also remove any branches or tags in the destination repository that don’t
exist in the source repository.
You can also clone a repository using the --mirror
flag. This will create a bare repository that is an exact copy
of the source repository.
A few cases where mirroring can be useful:
- Creating a backup of a repository.
- Creating a read-only copy of a repository.
- Migrating to a new server.
- Continuous deployment. Some systems may use
--mirror
to maintain a copy of the repository on the server.
The important point to remember is that a repository cloned with --mirror
is a bare clone, and does
not have a working directory. You cannot modify files or make commits. It’s simply a mirror.
Git Submodules
Think about Git submodules as managing nested repositories. You have an outer (parent) repository and one or
more inner (child) repositories. The child repositories are called submodules. When you register the child
repository as a submodule with the parent repository, it will register the presence of the submodule in a
.gitmodules
file.
Creating a Submodule
This section explains how to create an register a new submodule in your parent repository.
Create your outer repository and make at least one commit:
mkdir parent-repo
cd parent-repo
git init
touch README.md
git add .
git commit -m "initial commit"
From inside the parent repository, create a child repository and add at least one commit:
# from parent-repo directory
mkdir child-repo
cd child-repo
git init
touch README.md
git commit -m "initial commit"
Now we have one repository inside of another. Next, you’ll want to register the inner repository as a submodule.
# from parent-repo directory
git submodule add ./child-repo
This command created the .gitmodules
file and staged the changes. At this point you can commit the changes
to the parent repository. There will be a single file representing the submodule that you’ve added.
Cloning a Repo with Submodules
If you were to push the parent-repo
to a remote, you have the ability to clone this repo along with any
submodules. If you do a normal git clone
command, you’ll need to manually synchronize and download the submodule
with another set of commands. If you would like to clone the parent repo and all submodules, use the following:
git clone --recurse-submodules [url]
If you did not use --recurse-submodules
, you can use the following commands to pull in the submodule content:
git submodule init
git submodule update
git submodule init
initializes the submodules, which copies information from.gitmodules
to the.git/config
file.git submodule update
actually clones the submodule repositories and checks out the appropriate commits. This is the step that pulls the actual content from the submodule remotes.
Command Summary
git submodule add [url] [path]
: Add a new submodule to your project.git submodule init
: Initialize your submodule.git submodule update
: Fetch all the data from the submodule project.git submodule status
: Show the status of your submodule.git submodule foreach [command]
: Use this to run a command in each submodule.git submodule sync
: Synchronize submodules’ remote URL configuration setting.git submodule deinit [path]
: Remove a submodule from your project.git submodule update --remote [path]
: Update a submodule to the latest commit.git clone --recurse-submodules [url]
: Clone a project with all submodules.
Git Index
The Git index is known as the staging area. This is where you can manipulate tracked files.
We’ll focus on git update-index
to manage files that are assumed unchanged.
Mark a File as Unchanged
Use this to tell Git to assume that a file has not changed, even if it has. This is useful for modification of files locally without committing the changes.
git update-index --assume-unchanged [file]
Stop Assuming a File is Unchanged
Use this command to tell Git to stop assuming that a file is unchanged and start tracking it again.
git update-index --no-assume-unchanged [file]
View Files Assumed Unchanged
When you want to see a list of all files that are assumed unchanged, use this command.
git ls-files -v | grep '^h'
This command will show you a list of all files that are assumed unchanged. The -v
flag will show you the files that
are assumed unchanged. The grep
command will filter the output to only show files that are assumed unchanged.
Files that are assumed unchanged will have an h
in the first column of the output.
Stop Assuming All Files are Unchanged
The following helper script will stop assuming all files are unchanged.
for file in $(git ls-files -v | grep '^h' | awk '{print $2}'); do git update-index --no-assume-unchanged $file; done
Further Notes
Working with the Git index is a low-level operation. The index is typically interacted with through commands
like git add
and git reset
. Being aware of git update-index
operations can give you more control
over Git operations.
Git Filter
This is a command that is commonly used to remove sensitive data from a repository. It is a powerful command that can
have unintended consequences if used incorrectly. It’s important to understand how this command works before using it.
git filter-branch
applies filters to each commit in the branch history.
General Syntax:
git filter-branch --filter <options> -- <branch>
Types of Filters
--env-filter
: Allows you to modify environment variables for each commit--tree-filter
: Modifies the tree (contents) of each commit--index-filter
: Modifies the index (staging area) of each commit. Similar to--tree-filter
, but faster because it doesn’t check out the tree.--parent-filter
: Modifies the parent list of each commit--msg-filter
: Modifies the commit message of each commit--commit-filter
: Modifies the commit itself
Use Cases
- Removing sensitive data
- Changing author information
- Squashing commits
Note that for squashing commits, it’s generally advisable to use git merge --squash
or git rebase -i
to squash
instead of git filter-branch
. This is because git filter-branch
rewrites history.
Let’s break down a command, this one would be used to remove sensitive information.
Assume that I’ve accidentally committed the .env
file containing credentials. I could
simply run the following:
git filter-branch --index-filter 'git rm --cached --ignore-unmatch .env' HEAD
When you pass HEAD
as the final value, you’re instructing git to rewrite the history up to the current commit on
the current branch. You can also pass a commit hash to rewrite history up to a specific commit.
Fair Warning: git filter-branch
should probably be avoided when possible. This is especially true when working
on a repo with others.
Alternative Tools
It is recommended to use git-filter-repo as an alternative
to git filter-branch
. It is a much faster and safer tool for rewriting history.
Example - change the author information
Use with caution! - This will change the author information on all branches
git filter-branch --env-filter '
OLD_EMAIL="oldemail@example.com"
CORRECT_NAME="Correct Name"
CORRECT_EMAIL="correctemail@example.com"
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags
Command Explained
The –tag-name-filter cat – –branches –tags portion: This part of the command has two components:
--tag-name-filter cat
: This is used to rewrite tags.cat
takes the tag name as the input and outputs the same name. This keeps tag names the same. You would need to use a different command to alter the tag names.-- --branches --tags
: Operate on all branches and tags.
If you would like to change the author information on a single branch, you can modify the command to the following:
git filter-branch --env-filter '...' HEAD