Resources

If you’d prefer to reference Gists, I’ve provided the links below:

Git Tags

  • git tag: List all tags
  • git tag -l "v1.0.*": List all tags with a pattern
  • git tag <tagname>: Create a lightweight tag
  • git tag -a <tagname> -m "<tag message>": Create an annotated tag
  • git tag -a <tagname> <commit-hash> -m "<tag message>": Create an annotated tag on a commit
  • git push <remote> <tagname>: Push a tag to remote
  • git push <remote> --tags: Push all tags to remote
  • git checkout <tagname>: Checkout a tag
  • git tag -d <tagname>: Delete a tag locally
  • git push <remote> --delete <tagname>: Delete a remote tag
  • git show <tagname>: Show information about a tag
  • git 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.

  1. 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.
  2. 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.
  3. 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 repository
  • git remote rm [name]: Remove the connection to a remote repository
  • git remote rename [old_name] [new_name]: Rename a remote connection
  • git remote show [name]: Display information about a remote connection
  • git remote update: Fetch updates from all remotes

Git Branch Management

  • git branch [branch]: Create a new branch from the current branch without switching
  • git checkout -b [branch]: Create a new branch from the current branch and check it out
  • git branch -D [branch]: Delete a branch
  • git branch -M [new name]: Change the name of the current branch
  • git push origin --delete [branch]: Delete a remote branch
  • git fetch origin --prune: Update indexed remote branches (remove deleted remotes)
  • git branch: List local branches
  • git 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

  1. Removing sensitive data
  2. Changing author information
  3. 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