Table of Contents
Click to Expand ToC (large!)
- Resources
- Often used commands cheat table
- Get commit ___ # of commits ago
- Meta cheatsheet - actually setting up git / git configuration
- Dealing with Linkage
- Branches
- View all untracked files, not just dir names
- Add all untracked files to .gitignore file
- Viewing Changes / Diffing
- Order of References When Comparing
- Git Comparisons - Triple Dot vs Double Dot
- Get summary of lines changed by file
- Get just summary of total lines changed, etc.
- Get a list of changed files (filenames)
- Different Diff Targets and Views
- Exclude a file from a diff
- Better Diffs for Text-Based Files
- Better Diffs for Refactoring
- Diff Tools and Software
- Comparing File Similarity
- Amend the last commit (!!! - Danger - REWRITING HISTORY - !!!)
- Inspecting / Browsing files
- Cherry-Picking / Selective Git Merging
- Selective Git Staging / Interactive
- Undoing or Revising in Git
- Removing and Unstaging Files
- Submodules
- TEMPORARILY jump to a specific moment in time
- Merging a branch the default Github PR way
- Moving Files
- Show which files are being ignored:
- Explain why a file is ignored
- Dealing with Dirty Files / Conflicts
- Stashing / Temporary Staging
- Metadata (commits, files, etc.)
- Git Hooks
- Git Tags
- Dates in Git
- Rebasing
- What Does Rebasing Do?
- Rebase Commands
- References and Terminology while Rebasing
- Rebasing with --onto
- Rebasing: Quick Fixup Commits
- Rebasing: Editing a commit (without splitting)
- Rebasing: Splitting up Commits
- Rebasing Workflows - Example Scenarios
- Rebase vs ff merge
- Issues with dates and rebasing
- Rebase - Opinions
- Git styles, standards, best practices
- Finding a force push on Github
- Wiping Git History - Squashing to a Single Commit
- Git LFS
Resources
- My Github specific cheatsheet
- Other Cheatsheets
- git ready - Simple, easy to understand, tips at a time
- gitignore templates
- You can pull one of these in via the CLI, with
gitignore
NPM package:npx gitignore python
- You can pull one of these in via the CLI, with
- .gitconfig - handy git bash aliases
- agis/git-style-guide
- Zak Laughton - Three ways to time travel in Git to undo destructive mistakes
- Git History (pomber/git-history)
- Timeline / slideshow view of a single file's Git history
- Just replace
github.com
withgithub.githistory.xyz
- Just replace
- There is also a VSCode extension
- And a CLI!
npx git-file-history path/to/file.ext
- Timeline / slideshow view of a single file's Git history
- Git Command Explorer
Often used commands cheat table
Subset | Command | Does: |
---|---|---|
Branching | git checkout -b {BRANCH_NAME} |
Create and switch to a new local branch |
Branching | git checkout --track origin/{REMOTE_BRANCH_NAME} |
Create and switch to a local branch based off remote, that will be linked.git checkout {REMOTE_BRANCH_NAME} should also accomplish the same thing. |
Branching | git branch -u origin/[TRACKING_BRANCH] [LOCAL_BRANCH_NAME] |
Link a different branch (that already exists) to an already existing remote |
Branching | git branch -d {BRANCH_NAME} |
Delete a local branch |
Branching | git branch {BRANCH_NAME} {REF} |
Create a branch at a specific reference point (SHA, branch, etc.), without requiring you to move HEAD / working directory first. WARNING: If {REF} is a remote branch, this sets your branch to use it as tracking. Use --no-track if you don't want this. |
Staging | git add --patch {FILENAME} |
Stage parts of a file (in chunks / hunks) |
Staging | git reset |
Unstage all files / everything: using git reset without any other options simply unstages all files, while keeping those changes in your working directory (non destructive) |
Committing | git commit --no-verify |
Skip git hooks while committing |
Revising | git commit --amend |
Amend the last commit (& edit) |
Revising | git commit --amend --no-edit |
Amend the last commit, reuse message. |
Revising | git reset HEAD~1 |
Resets to the last commit, but keeps changes locally |
Revising | git reset --hard origin/main |
Hard reset to remote main |
Revising | git reset --hard {REF} |
Hard reset to specific commit |
Revising | git branch --force main origin/main |
Similar to reset ; forces a different commit reference. |
Revising | git rebase -i {baseRef} |
Rebase interactively to ref |
Tracking | git rm --cached {FILE} |
Remove a file from tracking after adding to gitignore |
Tracking | git ls-files |
Get a list of tracked files |
Tracking | git ls-files --others --exclude-standard |
Get a list of both tracked and untracked files, but still respect .gitignore and standard exclusions |
Merging | git merge --no-ff {BRANCH_TO_MERGE} |
Merge without fast-forward (GitHub standard) |
Merging | git merge --ff-only {BRANCH_TO_MERGE} |
Only merge via fast-forward merging, which avoids creating a merge commit. Will fail if there is not a shared history and FF can't work. |
Revising | git pull {BRANCH_TO_UPDATE_FROM} --ff-only --rebase --autostash |
If you have a dirty tree / merge conflicts, this is an alternative to git merge --ff-only that can handle that situation automatically. Details here. |
Merging | git fetch . origin/{branchToMergeFrom}:{localBranchToMergeInto} |
Merge branches without switching to them |
Tags | git tag {tagName} |
Create lightweight tag |
Tags | git push --tags |
Push all tags to remote / origin |
Tags | git push origin {tagName} |
Push specific tag. |
Forks | git fetch upstream && git merge upstream/main |
Fetch upstream and merge |
Analysis | git log --pretty=fuller |
A more verbose log output, which includes both Author and Committer info. |
Analysis | git log --all --decorate --oneline --graph |
Show a visualization of the git tree. There are more advanced formatting options available too. |
Analysis | git log --oneline --abbrev-commit |
More concise output (short hash, skip body) |
Analysis | git log --full-history -- {FILE_PATH} |
Find when a file was deleted, by viewing full history. |
Analysis | git log --full-history --reverse -- {FILE_PATH} |
Find when a file was created (even across renames) |
Analysis | git log -p {FILE_PATH} |
Scroll through the history of a file, viewing patches for each commit. Kind of like a scrollable git blame , in historical order rather than line order. |
Analysis | gitk {FILE_PATH} |
Pretty much the same as git log -p (patch view), but with a GUI. Can be slow to load for large history. |
Analysis | git diff -M{OPT_n} git diff --find-renames{OPT_n} |
Force git to try and follow renames. This is on by default with git diff , except in rare instances. |
Analysis | git diff --name-only |
Get list of changed files (filenames) |
Analysis | git diff --word-diff-regex="\w+" or --word-diff or word-diff=color or --color-words |
Better ways to view a diff for text-based files, like Markdown docs. |
Analysis | git diff --color-moved |
Color chunks that have moved differently. --color-moved can also be used with log , show , and other diff commands. |
Analysis | git diff {BRANCH} {FILE_PATH} |
Git diff a single file |
Analysis | git diff {REF_A_OPT}:{FILE_PATH} {REF_B_OPT}:FILE_PATH |
Git diff across two different paths, as if it were a single file that changed. Optionally, also across two different refs. |
Analysis | git diff --no-index {FILE_A} {FILE_B} |
Perform a diff on files outside of Git (or at least outside of index). |
Analysis | git diff -- . ':(exclude){GLOB_OR_FILENAME}' |
Perform a git diff, while excluding certain filenames or patterns of filenames from being included. (SO) Very common use-case: git diff -- . ':(exclude)package-lock.json' |
Analysis | git gui browser {REF} |
Launch an interactive file browser and inspector that shows the state of the repo at {REF} , without checking out that branch or commit. |
Analysis | git show {REF}:{FILE_PATH} Pipe to VSCode: git show {REF}:{FILE_PATH} | code - |
View the file contents at a certain state. Especially useful if you can't diff due to file not existing in HEAD. |
Analysis | git blame {FILE_PATH} |
View "blame" for a file; breaks down history of file line-by-line. Can be very slow on large files with lots of commit history, so you might want to use filters. |
Analysis | git blame {startLineNum},{endLineNum} {FILE_PATH} |
Run git blame for a specific part of a file. |
Analysis | git log --all --grep='{SEARCH_STRING}' |
Search through git commit messages for a string. This does not search code changes (use -G for that purpose, see below). |
Analysis | git log -G {SEARCH_STRING} |
Search through git history (code patches) for a string (tips). - Use git log -p -G {SEARCH_STRING} for patch view.- This can be used for replacements, additions, and removals. |
Analysis | git merge-base branch_a branch_b |
Find the common ancestor (shared commit before divergence) between two branches |
Analysis | git branch --all --contains {SHA} |
List all branches (local and remote) that contain a given commit SHA. Use with with -r instead of --all to just list just remote branches. |
Analysis | git ls-remote |
List references in a remote repository This is especially useful for verifying that you correctly configured read access to a repo, without actually cloning. |
Cloning | git clone --depth {DEPTH} {REMOTE} |
How many commits to retrieve when cloning. Useful for if you need to quickly explore a repo or fetch just for testing - you can use depth of 1 for just the last commit. |
Remotes | git push {REMOTE} {LOCAL_BRANCH_NAME}:{REMOTE_BRANCH_NAME} Or, if branch names are the same git push {REMOTE} {BRANCH_NAME} |
Push a branch to a specific remote |
Most of the above commands are covered more in-depth in subsections below.
Note:
git gui
andgitk
commands depend on the corresponding programs being installed (git-gui
andgitk
, respectively), which normally are bundled with the installation ofgit
, but might not be on your system.
Get commit ___ # of commits ago
HEAD~1
= second to last commit
You can change 1
to how many commits back you need to look
For example, to diff between the last commit and the one before that:
git diff HEAD HEAD~1
Get the last commit SHA hash, or priors
## Full SHA
git rev-parse HEAD
## Short SHA
git rev-parse --short HEAD
## 2nd to last SHA
git rev-parse HEAD~1
Relevant S/O: https://stackoverflow.com/q/949314
Meta cheatsheet - actually setting up git / git configuration
- Start from scratch (create empty repo in current folder):
git init
- Set committer name and email
git config --global user.name "Alan Turing"
git config --global user.email "aturing@example.com"
- Checking installed version
git --version
- Check installed path
git --exec-path
- If you are getting auth issues ("logon failed", etc.), there could be a few reasons (especially if you are using 2FA):
- your version of git is probably out of date
- For example, on Windows, a standard combo is Git Credential Manager for Windows + Git for Windows. There is a known issue with the credential manager and older versions of GFW.
- Windows credentials got "unset" as the cred provider, or something glitched with it
- Try
git config --global credential.helper manager
ANDgit config --system credential.helper manager
to reset Windows Credentials as the provider, then retry your operation, and if necessary, relogin- Note: This is no longer
wincred
! - See below about how to verify this setting took hold
- Note: This is no longer
- You can use
git config --list --{global || system}
to check settings (see this)
- Try
- your version of git is probably out of date
- Checking your full git config
git config --list
will show all, from all levels (system, global, and local)- To view just one level, include it as a flag:
git config --list --global
- You can use
git config --get
to view specific key-value pairs - Check contents of
~/.gitconfig
- Checking the user config object
- You can use the RegEx option to view the user config key-value pairs:
git config --get-regexp 'user\..+'
- You can use the RegEx option to view the user config key-value pairs:
- Bulk ignoring commits for git blame
- Commit and use a
.git-blame-ignore-revs
file (will be supported on GitHub's web UI soon)
- Commit and use a
Signing Your Commits / Creating Verified Commits
To sign your commits, there are multiple options, but the most popular are SSH or GPG. Unless you are already familiar with GPG, SSH is going to be the easier option for most users.
GitHub has great docs on configuring git to sign commits.
Dealing with Linkage
Adding a named remote
git remote add {name} [REMOTE_ORIGIN_URL]
Hint: You probably want to fetch after adding a remote. E.g.
git remote add steve-contrib [URL] && git fetch steve-contrib
Remotes are flexible too:
- You can mix-and-match providers. E.g., your local repo can have both GitHub and Azure as a remotes
- You can even use local directories as remotes
Renaming a named remote
git remote rename {current_remote_name} {new_remote_name}
Hooking an existing local repo to a remote origin
(example - created git repo from cmd instead of github.com gui, now want to link up to existing Github repo)
- Add already existing (but empty) github URL as remote repo and set as remote
git remote add origin [REMOTE_ORIGIN_URL]
- Verify that it is linked
git remote -v
- Now push up
git push origin main
Setting upstream (for forks)
This is really the same steps as adding a new remote
origin:
git remote add [NAME_FOR_UPSTREAM] [REMOTE_UPSTREAM_URL]
Making sure your fork is up to date before making a PR
- Make sure you have fresh data on
upstream
git fetch upstream
- Merge the fresh upstream main, into:
- Your feature branch
git merge upstream/main
- Your main branch
git checkout main && git merge upstream/main
- Your feature branch
Here is an advanced version that will fetch upstream main, merge into local main, and then merge into feature you are on, without switching branches. (as long as it can fast-forward) (only works if you are not on main currently):
git fetch upstream main:main && git merge main
Or (works regardless if you are on main
or not currently):
git pull upstream main:main && git merge main
Change the link to remote
(for example, if you change the repo name on Github, or fork a repo and want to set origin to your new fork.)
- First check list of remotes
git remote -v
- Then remove the one you want to
git remote rm [NAME_OF_REMOTE||Example:Origin]
- Double check that it was removed
git remote -v
- Add new link
git remote add [NAME_OF_REMOTE||Example:Origin] [REMOTE_URL]
Branch Linkage
- Show how local branches are linked up to origin (e.g. showing tracking links)
git branch -vv
- List only remote branches:
git branch -r
- or:
git ls-remote --heads origin
- Fetch (and switch to) remote branch that does not exist locally (yet)
git checkout --track origin/[REMOTE_BRANCH]
- Link a local branch to a remote TRACKING branch, that DOES NOT EXIST YET - set upstream (very common annoyance)
git push --set-upstream origin [LOCAL_BRANCH_NAME]:[NEW_REMOTE_BRANCH_NAME]
- Or, even shorter
git push -u origin [LOCAL_BRANCH_NAME]:[NEW_REMOTE_BRANCH_NAME]
- Or, even shorter (assuming same names)
git push -u origin [NEW_REMOTE_BRANCH_NAME]
- Link a local branch to a remote TRACKING branch that ALREADY exist
- Different branch (already exists):
git branch -u origin/[TRACKING_BRANCH] [LOCAL_BRANCH]
- Different branch (does not already exist)
git branch --track [LOCAL_BRANCH] origin/[TRACKING_BRANCH]
- Or
git checkout [TRACKING_BRANCH]
(notice did not useorigin/
before it)
- Current branch:
git branch --set-upstream-to origin/[TRACKING_BRANCH]
- Or
git branch -u origin/[TRACKING_BRANCH]
- Different branch (already exists):
- Push all local branches up to origin, regardless if they exist on origin or not yet
git push origin --all
- Or
git push origin --all -u
- Use the
-u
flag to set-upstream, which makes pulling from branches later easy
- Use the
- UNLINK a branch that is tracking remote
git branch --unset-upstream
Good S/O answer about upstream. And this
In git push, when you only specify one branch name, instead of both remote and local, git assumes the branch names are the same!!! See notes under linking a local branch to a remote that does not yet exist.
Branches
Miscellaneous Branch Stuff
- Push to origin
- Current branch
git push
- Another branch
git push origin {branch}
- All branches
git push origin --all
- See above notes under "branch linkage"
- Current branch
- Merge branches without switching to them! (Only for fast-forward merges, use with caution) (Details)
- Merge local into local
git fetch . {localBranchA}:{localBranchToMergeAInto}
- Merge remote branch into local branch
git fetch origin {remoteBranch}:{localBranchToMergeInto}
- Or:
git fetch . origin/{remoteBranch}:{localBranchToMergeInto}
- Practical example: merge
origin:main
intolocal:main
, and then merge that into yours, all without checking out!git fetch origin main:main && git merge main
- Extended example: merge
origin main
intolocal main
, merge main into current branchfeature
, then merge current branchfeature
intolocal main
, then pushlocal main
back up to origin.- Steps:
git fetch origin main:main && git merge main
git fetch . feature:main
git push origin main
- This is basically the full update cycle for an org where
main
is source of truth - As one line:
git fetch origin main:main && git merge main && git fetch . feature:main && git push origin main
- Steps:
- Merge local into local
Get commits between branches
It's important to note that you can pass
HEAD
instead of{branchA}
, if you just want to compare against the current branch you are on.
- Full git log / list commits different between branches
git log {branchA}..{branchB}
- Short version
git log --oneline {branchA}..{branchB}
- Just the count (number of commits different between branches)
git rev-list --count {branchA}..origin/main
- Just the behind commit count
git rev-list --count {AHEAD_REF} --not {BEHIND_REF}
- e.g., how many commits feature is behind main:
git rev-list --count HEAD --not origin/main
- e.g., how many commits feature is behind main:
git rev-list --count {BEHIND_REF}..{AHEAD_REF}
- e.g., how many commits feature is behind main:
git rev-list --count HEAD..origin/main
- e.g., how many commits feature is behind main:
Counting commits between branches
The easiest way to count commits between branches is with rev-list --count
:
git rev-list --count {REF_A}..{REF_B}
You can get different results by swapping the refs and/or using triple-dot comparisons instead of double-dot.
git rev-list --count {BEHIND_REF}..{AHEAD_REF}
- e.g., how many commits feature is behind main:
git rev-list --count HEAD..origin/main
- e.g., how many commits feature is behind main:
You can also use the --not
argument with rev-list
:
# Just the behind commit count
git rev-list --count {AHEAD_REF} --not {BEHIND_REF}
Get the current branch (name)
Couple of options:
git symbolic-ref --short HEAD
Or:
git rev-parse --abbrev-ref HEAD
Note: Neither of these options actually captures the name as a variable or does anything with it. If you wanted to use the branch name with another command, you would need to use piping / redirection / variable capture.
My notes on Bash variable capturing and redirection are here.
View all untracked files, not just dir names
git status -u
Add all untracked files to .gitignore file
git ls-files --others --exclude-standard >> .gitignore
Viewing Changes / Diffing
Order of References When Comparing
When comparing references with git diff
or other git tools, and given a "base" reference and something that has diverged from the base reference that you want to inspect (such as a feature-branch that is ahead), you want to pass the diverged thing of interest last.
For example, if you are trying to see how branch feature-a
has introduced changes on-top of branch main
that it was based off, you could use git diff main..feature-a
.
Git Comparisons - Triple Dot vs Double Dot
Triple-dot vs double-dot comparisons are a bit confusing in git, especially since the context matters - the implications are different when used with log
vs diff
(and, in fact, are almost reversed):
log
- Double dot (e.g.
git log A..B
):- Shows you only commits that B has that A doesn't
- Useful when you are only interested in work done on
B
and don't care about behind changes
- Triple dot (e.g.
git log A...B
):- Shows you both commits that B has that A doesn't, plus those in A that are not in B (aka behind)
- Omits commits shared by both (same as double dot)
- Useful for a more comprehensive look - e.g., ahead vs behind
- Double dot (e.g.
diff
- Double dot (e.g.
git diff A..B
):- This is actually the same as just using
git diff A B
- the..
is implied by default - Shows differences between the two tips, without any special consideration for common ancestor / point of divergence
- This is actually the same as just using
- Triple dot (e.g.
git diff A...B
):- Shows difference starting at point of common ancestor (or, put another way, since divergence)
- Answers the question "how has B changed since branching from A"
- Double dot (e.g.
Further reading: this StackOverflow for
git log
and this StackOverflow forgit diff
Get summary of lines changed by file
git diff [version A] [OPT Version B] --stat
Get just summary of total lines changed, etc.
git diff [version A] [OPT Version B] --stat | grep '^\s*.*files\schanged.*$'
Get a list of changed files (filenames)
- During pre-commit (staged files)
git diff --cached --name-only --diff-filter=ACMRTUXB
- You can modify the filters to change which files show up
- During post-commit / find the files changed in the very last commit
- With truly only names (credit):
git diff --name-only HEAD HEAD~1
- You can combine with diff-filter:
git diff --name-only --diff-filter=ACMRTUXB HEAD HEAD~1
- To see operations for each file (modified, deleted, etc.)
git diff --name-status HEAD HEAD~1
- Without using
diff
(for example, if there is only one commit in the repo):git show HEAD --name-only --format=%b
- Note that there will be spacing around the filenames
- With truly only names (credit):
To pipe filenames from Git that might contains spaces, you need to take some extra steps - use the
-z
with diff to get null terminators, and then use-0
or--null
withxargs
, to tell it that input items are demarcated by null termination characters.
Different Diff Targets and Views
- Get just ahead differences between a given branch and another:
git diff {BEHIND_BRANCH}...{AHEAD_BRANCH}
- To see how many commits behind vs ahead
git rev-list --left-right --count {BRANCH_A}...{BRANCH_B}
- Output will look like
14 3
, where{BRANCH_B}
is 14 commits behind{BRANCH_A}
and 3 commits behind - Explanation
- Output will look like
Exclude a file from a diff
git diff [OTHER] . ":(exclude)[FILEPATH_TO_EXCLUDE]"
- Sample:
git diff 7cc297d5baf2e305b709fcf93e3fe93284fb18e1 --stat -- . ":(exclude)package-lock.json"
Better Diffs for Text-Based Files
The bare-bones git diff
command works well for code, but often is almost unusable when trying to view the differences between revisions of a text-based / prose file, like in Markdown documents.
Aside from using a different diff'ing tool, their are some options for git diff
that can vastly improve the text diff viewing experience:
💡 If you are looking for the shortest command with the biggest impact, it might be
git diff --color-words="{REGEX}"
Example:
git diff --word-diff-regex="\w+"
- These commands are functionally equivalent
git diff --color-words
git diff --word-diff=color
- You can also customize the pattern for the word-based diff views:
git diff --word-diff-regex="\w+"
- You might want to combine this with the
color
mode, so either:git diff --color-words="\w+"
- Or...
git diff --word-diff-regex="\w+" --color-words
Better Diffs for Refactoring
One of my (many) complaints about the out-of-the-box git diff
experience is that it is pretty bad when it comes to showing refactoring changes. For example, if you are trying to review someone's pull request, where they have just moved a bunch of code around without changing it, a git diff
(including GitHub's PR diff view) will show all the code as "changed", even if all they did was copy and paste code from one file into another.
Blame, on the other hand, is much better at following code around as it moves within or between files, and one individual (David Szotten) even provides a helpful blog post on how to use blame for better diffs.
Here is the command they came up with:
git blame -s -w -C main..HEAD -- {FILE_PATH} |egrep --color=always '^[^\^]|^' |less -R
Diff Tools and Software
Comparing File Similarity
# This is pretty good, but drops files that are identical
git diff --no-index --ignore-all-space --numstat {FILE_A} {FILE_B}
Amend the last commit (!!! - Danger - REWRITING HISTORY - !!!)
- For if you just want to change the message
git commit --amend -m "my new commit message to replace old"
- For if you forgot to add files / stage (stage first with
add
before running)git commit --amend
- Same as above, but without interactive confirmation prompt to change message
git commit --amend --no-edit
Removing a file that was added in the last commit?
git rm {file}
and thengit commit --amend
like normal
Inspecting / Browsing files
In general, if you want to temporarily view file(s) at a point in time other than what you are currently looking at, git show
is the main command to utilize.
git show {ref}:{path}
Cherry-Picking / Selective Git Merging
cherry pick commit(s) from another branch to add to current
git cherry-pick [COMMIT_HASH]
- Use the
--no-commit
option if you want to manually merge changes, and/or combine multiple cherry-picks into one commit
- Use the
git cherry-pick A..B
- Cherry picks a range of commits, from A to B, where A is older than B
rebase --onto
might be a preferred alternative
git cherry-pick --edit -x {SHA}
This cherry-picks a commit, appends the original SHA to the commit message, and stops to let you edit the message
If you want to edit the content / code of the commit, use
--no-commit
, then edit the staged content before actually committing the change
Merge up to a specific commit / Cherry-pick multiple with shared history
git merge [COMMIT_FROM_OTHER_BRANCH_HASH]
- This is the automatic version of cherry-picking commits manually from where a branch diverged, up to the point you want
- Very cool feature; just pass in the hash of the last commit on the other branch!
- Unlike cherry-pick, this is going to require shared history, just like regular merging
- Relevant S/O
Cherry-pick, but for specific files
This is rather complicated, especially if you are trying to preserve history... in many cases, you are better off using the "grab files" approach (e.g. checkout
, see further down below).
In the case that you really want to preserve history, you basically only have two options:
- Manually find all the commits that affected the file and
cherry-pick
with them, removing the changes for other files as you go- You can use
git cherry-pick -n
to stage but not commit
- You can use
- Or, create a diff patch based on the target commit(s), and then git apply them
- This S/O answer shows how to do it
git show {SHA} -- {filename(s)} | git apply -
git add {filename(s)}
git commit -c {SHA}
- This S/O answer shows how to do it
Grab files from another branch (or commit) and merge into yours, without branch merging
This is basically like copying a file from a git ref to your local working directory, without having to cherry-pick commits and/or merge branches.
Keywords: Checkout files, copy files, restore files
- For specific files / copy files:
git checkout {branchOrSHA} -- {filename(s)}
- This supports directories as well (and expansion / globs)
- Interactively:
git checkout -p {branchOrSHA} -- {filename(s)}
git checkout -p {branchOrSHA}
- For everything (all files on branch or commit):
git checkout {branch} -- .
For complex reasons these methods will automatically stage the files that are brought in. There is a workaround, but the syntax is harder to remember.
Cherry-Picking From Stash
Cherry-picking or selective file merging can be done with stashes the same way it is done with branches or commits, as stashes actually have a normal reference / pointer / whatever you want to call it.
To reference a stash, you use stash@{STASH_NUMBER}
.
Therefore, here is an example of how you could apply the changes from a single file in a stash, essentially coping the file out of the stash without actually applying or popping the full stash:
git checkout stash@{0} -- my_file.txt
If you want to apply the changes, without completely replacing:
git diff --no-color HEAD...stash@{0} -- {filename(s)} | git apply -
# Or
git show --patch --no-color stash@{0} -- {filename(s)} | git apply -
If you want to see what is in the stash, you could use something like
git diff --stat HEAD...stash@{0}
Selective Git Staging / Interactive
A neat tip is that you don't always have to stage an entire file - you can add individual lines! This is great to remember when maybe you need to comment out something that breaks your local build, but needs to stay in the code base for someone else at the moment.
The easiest way to do this is with your IDE. In VSCode, all you have to do is select the lines you want to stage, then open the command palette (CTRL + P
) and select "Git: Stage Selected Ranges
".
From the CLI, you can do this by running git add --patch {filename}
. Details. You could alternatively run git add -i
and then select patch
(details).
Undoing or Revising in Git
Resource: Gitlab - numerous undo possibilities in git.
Reverting a Commit
If you want to revert a commit (like undoing a commit), there are multiple approaches you can use.
The safest way is to use git revert {HASH}
to create a new commit that automatically contains an opposing change diff to the commit corresponding with {HASH}
.
However, if don't care about revising past history (for example, if you are working by yourself on a feature branch) you could use rebase
and perform a drop
on the commit(s) you want to undo.
Reverting Parts of a Commit, Reverting Files
When talking about reverting individual files, it is important to clarify what "revert" means.
If that means resetting a file to its contents at a certain point in time, you can use the git checkout {SHA} -- {FILEPATH}
trick, but this will completely replace the contents with whatever it was at {SHA}
.
If you instead want to undo or revert the changes made to a particular file in a particular commit, there are considerable more diverging approaches, but the easiest way is:
git show {SHA} -- {FILEPATH} | git apply -R
If you get an error of error: unrecognized input with the above command, try:
- Using safer options:
git show --patch --no-color {SHA}
- Quoting your file path(s)
- Checking that the file was actually changed at the given commit and that the commit is not a merge commit
- Make sure
git show {SHA} -- {FILEPATH}
has non-empty output - Merge commits will not show a diff with
git show
by default, but there are workarounds- Example, use
--first-parent
:git show --first-parent {MERGE_COMMIT_SHA} -- {FILEPATH}
- Example, use
- Make sure
Revise the last commit
If you want to undo the last commit, but keep changes locally so you can edit and then re-commit
git reset HEAD~1
- If you want to re-commit with original message
- Interactive commit message editor
git commit -c ORIG_HEAD
- No editor
git commit -C ORIG_HEAD
- Interactive commit message editor
- Else if you want to just nuke it
git reset --hard HEAD
Nuke it - reset to remote main
git fetch origin
git reset --hard origin/main
You can use this to reset to head of any branch really. Just make sure you have fetched. For example,
git reset --hard
or git reset --hard HEAD
.
Useful for when you accidentally diverge and want to revert to the origin as source of truth.
This won't actually remove/delete untracked files. To do that, see below section.
🚨 However, it WILL blow away uncommitted changes to tracked files, and you CANNOT get these back, even via reflog. If you want to preserver these but still use --hard
, make sure you git stash
and then git stash pop
after reset.
Removing and Unstaging Files
Remove untracked files
- Interactively
git clean -i -fd
- Remove all untracked files and directories
git clean -fd
- Also smart to use this flag:
--dry-run [OR] -n
Good combo: git clean -fd -n
to preview, and then git clean -fd
to finalize
Remove a file from git tracking after adding it to the gitignore
- Use
--cached
flag withrm
git rm --cached <file>
"unstage" a file (do the reverse of git add)
- Single file
git reset -- [FILEPATH_TO_UNSTAGE]
- Note that the "--" is because git reset can also be used with branches instead of files, so "--" is to specify this is only for files
- To unstaged and checkout unchanged (non-dirty) version:
git checkout HEAD -- {FILEPATH_TO_RESET}
- All added files
- Same as above, but instead of
-- {FILEPATH}
use.
- Same as above, but instead of
Auto Stashing
For rebasing, there is a built-in autostash config option in git that makes it so you don't have to manually stash changes for an unclean working directory before rebasing.
git config rebase.autoStash true
or
git config --global rebase.autoStash true
For automatically stashing and popping on branch checkout, there is no built-in autostash option you can now use the git switch -m
command. You could also solve this with some bash aliases or custom scripts.
Submodules
- git-scm page
- Good submodule cheatsheet: vogella.com/tutorials/GitSubmodules
Add a submodule
- With SSH access
git submodule add git@github.com:joshuatz/j-prism-toolbar.git [LOCAL_FOLDER_PATH]/[NEW FOLDER THAT DOES NOT EXIST]
- With standard Github credentials
$ git submodule add https://github.com/joshuatz/j-prism-toolbar.git [LOCAL_FOLDER_PATH]/[NEW FOLDER THAT DOES NOT EXIST]
- If something goes wrong, don't be afraid to manually edit
.gitmodules
and.git/CONFIG
Update a submodule / init after clone
- To clone a repo, and include submodules from the get-go
git clone --recurse-submodules {originUrl}
- If you forgot to clone with recurse on, you can initialize submodules after the fact by using:
git submodule update --init
orgit submodule update --init --recursive
Updating (fetch)
- Easiest to remember is to just treat git subdir as real git repo
cd
to the directory, then rungit fetch
andgit merge origin/main
- Then
cd
back up, and commit the update to the parent repo
- Alternative is to use shorthand command
git submodule update --remote
Remove a submodule
Most complete option - https://stackoverflow.com/a/36593218
- Remove the submodule entry from .git/config
git submodule deinit -f path/to/submodule
- Remove the submodule directory from the superproject's .git/modules directory
rm -rf .git/modules/path/to/submodule
- Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule
git rm -f path/to/submodule
TEMPORARILY jump to a specific moment in time
- Very cool, you can just checkout the commit!
git checkout 778de63b25d66b576beba53b2ca0506ced9dded7
- If you want to jump back to tip after, just checkout the branch name again
git checkout main
- If your workspace is "dirty" you might need to use
--force
Merging a branch the default Github PR way
git merge [BRANCH_TO_MERGE] --no-ff
The reason why you can see commits grouped together with a specific PR / branch merge on Github is because when you click the "merge" button, instead of just doing git merge [BRANCH_TO_MERGE]
it uses git merge [BRANCH_TO_MERGE] --no-ff
--no-ff
means "no fast forward":- Default merge uses fast forward, which basically says that if the branch you are merging into shares a common history with the branch you are merging, it will "fast forward" the base branch until it points to the last commit on the branch you are merging.
- "no fast forward" means all the commits that make up the feature branch you are merging are kind of lumped together (or treated as children) as a new EXPLICIT merge commit. This is why when you merge a PR on github, it forces you to create a new specific merge commit.
Side Benefit: Merging this way means that you can point to a specific commit that brought in a set of feature changes (or an entire feature). This provides a bunch of different benefits:
- To undo the merge of a feature, you just need to revert one commit, instead of having to do some crazy stuff with finding the commit before the merge was done, or cherry picking commits, etc.
- You can easily visualize branch history, and see where a feature was specifically worked on separately
- Git GUI's (like Github) will treat it like a "true merge" if you do it this way
Moving Files
- Why use git mv?
- most of the time, Git can guess renames/moves vs new files based on contents and filename, but not 100% of the time. Git mv is a little more foolproof, since you are explicitly telling Git where your files are moving to / getting renamed
- git mv also automatically takes care of the "git rm" for the old file, and "git add" for the renamed/moved file
- most of the time, Git can guess renames/moves vs new files based on contents and filename, but not 100% of the time. Git mv is a little more foolproof, since you are explicitly telling Git where your files are moving to / getting renamed
- Methods:
- One by one
git mv oldfiledir/oldfile.h newfilepath/oldfile.h
- Bulk
- Generally, you can just move the files yourself, and when you "git add" or "git add -A", it should detect rename vs new files
- Make sure you do git add after moving the files, but BEFORE changing contents. Since the git rename detection works by content hash.
- One by one
Show which files are being ignored:
On git v1.7.7 and up (SO),
git status --ignored
Explain why a file is ignored
git check-ignore -v {FILE}
Dealing with Dirty Files / Conflicts
- Unmerged paths / both modified
- Discard changes completely
git checkout HEAD -- {FILE}
- Discard changes completely
Stashing / Temporary Staging
Docs:
Stashing - Reminders:
stash
will grab both staged and unstaged files, by default (but not untracked)- If you only want certain files (e.g. staged), you need to use
patch
and/orpush
. See "partial stashes" notes below. - NEW: Starting with git
2.35
, you can usestash --staged
to stash just staged changes
- If you only want certain files (e.g. staged), you need to use
- Stashes are NOT synced with origin / remote
Stashing - The basics
- Stash:
git stash
- Include untracked or ignored files:
git stash -u
git stash -a
- Just staged files (
git >= 2.35
)git stash --staged
- Pop:
git stash pop
- See all stashes:
git stash list
- Search stashes
git stash list -G"{REGEX}"
- This really comes from
git log
- This really comes from
- Apply stash without popping it / losing it from stack
git stash apply
- Warning: This will also stage the changes
- Using a stash as a reference
stash@{NUMBER}
- View the contents (changes) of a stash
- Very last stash:
git stash show -p
- Another stash:
git stash show -p NUMBER
git show stash@{NUMBER}
(might have to use-m
)
- Very last stash:
Partial stashes
- Just staged files (requires git version
>= 2.35
)git stash --staged
- Interactive (hunk picker)
git stash -p
(orgit stash --patch
)
- Single file:
- (Still interactive, but just hit
a
to stash all hunks in current displayed file)git stash -p index.html
- You can even include a message!
git stash -p index.html -m "trying out different CDN"
- Newer syntax (> v2.13) - non interactive
git stash push {flags} -- {pathspec}
- Example:
git stash push -m "trying out different CDN" -- index.html
-
Order matters!
-- {pathspec}
must come last.
- (Still interactive, but just hit
git stash -p
is an internal alias forgit stash push -p
Reminder: There is no "cost" to just using a temporary branch to stash changes, and is usually a better alternative to stashing. Or, explore using patches or actual temp files with arbitrary extension that is then gitignored (e.g. ".tempdump")
Dumping changes (git diff) as a patch file
If you want to quickly dump all the differences between two branches (lets say between feature
and main
), or just the changes in your current branch, as a backup, you could export the diff as a patch file.
git diff main > {filename}.patch
Or, to create a patch based on difference between current and staged, just leave off the branch:
git diff > {filename}.path
🚨 WARNING: This won't capture untracked changes. As a workaround, some might prefer to stage all their changes and then use
git diff --cached > {filename}.patch
.
🚨 WARNING: If you want to capture binary change (e.g. JPG files, EXEs, etc.), you need to use
--binary
when creating the patch
And to restore from that patch, you can either use patch
or git apply
:
-
git apply
:git apply {filename}.patch
- Use
--include={FILE_PATTERN}
to only apply to specific files. Useful if your patch file contains changes for a lot of files, and you only want to apply a subset.- This can be a glob
- Use
--stat
or--summary
to show some information about proposed changes, but don't apply
- Use
-
patch
:patch -p1 < {filename}.patch
- Use
--dry-run
withpatch
for a preview of changes.
Use
patch -p0
if--no-prefix
was used withgit diff
- Use
Metadata (commits, files, etc.)
Show BOTH the authorDate and commitDate
git log --format=fuller
Get 'last modified' timestamps:
git show -s --format=%at
-s
=--no-patch
= suppress diff info--format=%at
=--pretty[=<format>]
= pretty-print%at
= 'author date, UNIX timestamp'- Alternative:
%ct
= 'committer date, UNIX timestamp'
- Alternative:
- For a specific thing (commit, tree), just put it last:
git show -s --format=%at {thing}
git log --pretty=format:%at | head -n 1
- Gets the log, formats as UNIX stamps, sorts, then limits to one line
- This can be for a file, etc:
git log --pretty=format:%at -- myfile.js | head -n 1
- Alternative:
git log --pretty=format:%at | sort | tail -n 1
git log -1 --pretty=format:%at
- Same as above, but uses the
-{numLimit}
git option to pre-limit the result to one line- Now we don't have to use
head
ortail
to limit
- Now we don't have to use
- Same as above, but uses the
committer date
vsauthor date
- there should only ever be oneauthor
date, and it corresponds to when the code was actually first committed/authored withgit commit
. However, there can be multiplecommit
dates, and they correspond to when the commit was modified in the process of applying it through merge/rebase. See "Issues with dates and rebasing" on this page for details.
Get date 'created' timestamps:
git log --pretty=format:%at --follow -- {thing} | tail -n 1
--follow
will make sure it captures renames in history- SO
Warning: You cannot combine
--reverse
with--follow
; this is a known bug
Git Attributes (gitattributes)
Check Attributes
git check-attr {flag} {path}
Example (shows all attributes)
git check-attr -a cheatsheets/vue.md
Git Hooks
Reminder: Git hooks are not committed into your repo by default. The recommended way to share hooks in a repo is to create a checked-in folder, like
/.githooks
(but this can be named anything), and automate copying (or even better,symlinking
) the scripts to the real githooks location (.git/hooks/*
). This could be done with Bash scripts, BAT, MAKEFILE, etc. See this and this. Or use a dependency for automatic hook installation and management (some options further below)Reminder: Add "shebang" to file and make sure executable (
chmod +x
).
Git Hooks - Resources
Git Hooks - Management Tools
Git Hooks - Tips
- Pre-commit
- You can add files to the commit, without the user needing to interact or approve, by just calling
git add
within the git hook
- You can add files to the commit, without the user needing to interact or approve, by just calling
- Post-Commit
- Be very careful using further git commands within a post-commit git hook. Very easy to accidentally write an endless loop
- For example, if you touch a file and then amend it to the last commit, that will actually trigger the hook itself and you will end up looping
- Be very careful using further git commands within a post-commit git hook. Very easy to accidentally write an endless loop
Git Tags
As usual, Atlassian has one of the best guides: here
Tip: Something that a lot of tutorials gloss over or don't even mention, but what I feel like should be bullet point number #1, is that when you create a tag and don't explicitly attach it to a specific commit, by default it gets attached to the current commit that the HEAD is pointing at.
Creating a tag does not actually check new code into VC or save the "state" of your environment. It is more like a pointer to commit.
In brief:
Action | Command |
---|---|
List all tags | git tag |
Fetch all tags | git fetch --all --tags |
Create lightweight tag | git tag {tagName} |
Create annotated tag - Interactive | git tag -a {tagName} |
Create annotated tag - NON-interactive | git tag -a {tagName} -m "{tagMessageString}" |
Assign a new tag to different commit | git tag {tagName} {commitHash} git tag -a {tagName} {commitHash} |
List tags | git tag |
Delete tag | git tag -d {tagName} |
Checkout a tag (jump to state) | git checkout {tagName} |
Push tag to remote / origin | git push origin {tagName} |
Push all tags to remote / origin | git push --tags |
Push commits, plus relevant (e.g. attached) tags to remote / origin WARNING: Only works with annotated tags, not lightweight |
git push --follow-tags |
Find nearest tag that is related to current HEAD | git describe --tags (or just git describe if using annotated tags) |
git describe --tags
can return a tag in a format that looks likev2.4-28-a402cbee4
- that is: `{tag_name}-{number_of_commits_since_tag}-{commit_hash}
Finding commit that a tag points to
One command you can use is git rev-list -n 1 {tagName}
, which will get you the hash of the commit the tag points to. You can then use something like git show
to get the full commit details (author, changes). Or, as a one liner:
git rev-list -n 1 {tagName} | xargs git show
What do I use for a tag name?
Up to you! A lot of people use Semantic Versioning with tags to correspond to releases. In fact, Github will automatically create new releases under the corresponding tab of your repo if you add tags.
A sample tag might be something like v1.4.23
or v1.4.23-beta.1+autobuild24
.
How do I move a tag?
The short answer is you don't; make a new tag instead of moving an existing one.
The long answer is that you technically can with --force
, but in 99% of cases, you don't want to do this.
Annotated vs lightweight
Essentially, lightweights can only be the name of the tag, and nothing more. Whereas with annotated, you can add a message, sign with PGP key, and more.
Dates in Git
📘 Excellent reference: Peattie - Working with Dates in Git
Although we often talk about git only in terms of linked nodes, human-understandable dates have an impact on both writing and retrieving commits.
- Commits store both an
AuthorDate
(GIT_AUTHOR_DATE
) andCommitDate
(GIT_COMMITTER_DATE
)- When replaying or modifying commits (e.g. via amends or rebases), the Author Date will stay the same, but the Comitter Date will reflect whenever the commit was modified / replayed.
- See my section on Issues with Dates and Rebasing for details
- When replaying or modifying commits (e.g. via amends or rebases), the Author Date will stay the same, but the Comitter Date will reflect whenever the commit was modified / replayed.
- You can use git dates as references, using them for jumping to points or querying
git checkout main@{"30 minutes ago"}
git diff main@{"yesterday"} main@{"1 year 2 months ago"}
Date references also work in GitHub URLs (kinda), with
github.com/{USER}/{REPO}/tree/{BASE_REF}@{DATE_REF}
Rebasing
Some explanations / resources:
- https://www.youtube.com/watch?v=f1wnYdLEpgI
- https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase
- https://www.atlassian.com/git/tutorials/merging-vs-rebasing
💡 If you use rebase a lot, you might want to consider configuring rebase to always use
autostash
, so you don't have to manually stash changes for an unclean working directory before rebasing.
git config rebase.autostash true
or
git config --global rebase.autostash true
💡 Another helpful rebase config option is to set
rerere.enabled
totrue
. Details here
What Does Rebasing Do?
If (like me), you have trouble remembering and conceptualizing what rebase actually does, just think about the name... "re-base" - you are re-setting the base of the of a bunch of commits to a new one.
This is sometimes referred to as changing the parent, moving the base, etc.
Here are some guides:
However, the rebase
command is a lot more powerful than just moving the base of a branch - you can use it to edit past commit messages, reorder commits, drop commits, and much more.
Rebase Commands
** Where {baseRef}
is one of standard ref (id
, branchName
, tag
, HEAD
, etc.)
- Rebase non-interactively
git rebase {baseRef}
- Rebase interactively
git rebase -i {baseRef}
Example: Rebase branch alpha
onto main
Previously:
A---D---E (HEAD main)
\
B---C (HEAD alpha)
After:
A---D---E (HEAD main)
\
B*---C* (HEAD alpha)
* = same diffs, but SHAs have changed due to rebase
- Rebasing from the branch you are altering the parent of. Usually a feature branch.
git checkout alpha && git rebase main
- Rebasing while on the parent (usually
main
):git rebase main alpha
git rebase HEAD alpha
References and Terminology while Rebasing
This might be one of the most confusing things about rebasing - while in the middle of an interactive rebase, a lot of the references and terms get literally inverted from what they are during other git operations (like during merge
).
🚨 No really, theirs and ours have directly opposite meanings during
git merge
vsgit rebase
!
While rebasing:
- Branch / commit we are rebasing onto = Current / HEAD / Ours
- E.g.
origin/main
that we are rebasing our feature branch onto - Git ref:
HEAD
- To checkout a file from it:
git checkout --ours {FILE}
git checkout HEAD -- {FILE}
- E.g.
- The branch we started with / are rebasing = Incoming / Theirs
- E.g. a feature branch we working
- Git ref: Not provided, but the contents of
.git/rebase-merge/stopped-sha
contains the commit SHA for an interactive rebase.git/rebase-apply/stopped-sha
for non-interactive rebases
- To checkout a file from it:
git checkout --theirs {FILE}
🔗 📄 This post has a good explanation of how these references get flipped around between
git merge
andgit rebase
.
Rebasing with --onto
The onto (--onto
) argument is a powerful time-saver with git rebase, useful for scenarios in which you want to drop commits while moving the base of a branch. Without --onto
, rebase assumes that the base that should be moved is just the common ancestor with whatever the new base is, but with --onto
, you specify the exact base that should be moved.
git rebase --onto <newparent> <oldparent>
Where the SHA passed to --onto
as <oldparent>
should be for the commit right before the range you want to preserve.
Rebasing: Quick Fixup Commits
If you want to revise a specific commit instead of going through the normal full interactive rebase process, there is a slightly faster way to revise it using a "fixup" commit.
- Stage your changes, and then run
git commit --fixup={HASH_OF_COMMIT_YOU_WANT_TO_FIX}
- Have git handle moving and squashing for you:
git rebase --interactive --autosquash {HASH_OF_COMMIT_BEFORE_ONE_YOU_WANT_TO_FIX}
- You can use
{HASH_OF_COMMIT_YOU_WANT_TO_FIX}~1
instead of{HASH_OF_COMMIT_BEFORE_ONE_YOU_WANT_TO_FIX}
- You can use
- Save and quit when Git opens the editor
Rebasing: Editing a commit (without splitting)
- Use rebase to stop and
edit
the commit you want to edit. - Now you have two routes:
- Option A: make the changes immediately, and then use
git commit --amend
to save the edits - Option B: Run
git reset HEAD~
to unstage all the changes in the commit, then make your changes and rungit commit
- Option A: make the changes immediately, and then use
If you just need to edit the very last commit in your branch, it is much faster to skip rebasing by just making changes, staging them, and then using
git commit --amend
Rebasing: Splitting up Commits
With rebase, in addition to modifying past commits, you can also inject new ones and even take an existing past commit and split it into multiple commits. This is particularly useful if you end up doing too much work in a commit and decide you want to split it up later to make it easier to review and/or revert in chunks.
To split up a past commit:
- Use rebase to stop and
edit
the commit you want to split up. How you do this exactly is up to and depends on if you want to do anything else during this rebase operation- If all you want to do at this time is split up the commit, you can use
git rebase --interactive {HASH_OF_COMMIT_TO_SPLIT}~
, then selectedit
for that commit before continuing
- If all you want to do at this time is split up the commit, you can use
- When rebase stops on that commit to
edit
it, usegit reset HEAD~
to essentially undo the commit and unstage the changed files - Create your new split commits: for each new commit, stage the portions of code you want, then use
git commit
as you normally would. Repeat as needed. - Once you have made all the changes you wanted to (the original commit has been fully broken up into smaller ones), you can use
git rebase --continue
to finish up with the rebase workflow.
Rebasing Workflows - Example Scenarios
Rebasing a Stacked PR or Feature Branch After Trunk Experienced a Squash Merge
If squash-merges have been used in a branch (such as main
), it makes rebasing against the new history a lot trickier.
Let's say that you are working on feat-b
, which was based on feat-a
, but feat-a
was just squash-merged into origin/main
, and you want to rebase.
If you still have a reference to the branch that was just squash-merged (feat-a
), you can take a shortcut with:
git rebase --onto origin/main feat-a
This is really a shortcut for manually finding the
merge-base
between the trunk and the feature branch. E.g.,git merge-base
However, if you don't have a reference to the original branch (such as if you are on a different machine and the remote branch was deleted), you will need to manually find the last commit in your local feature branch that represents the starting point for your divergent work on the feature - i.e., the last commit from the PR that just got merged - then pass that to rebase
with git rebase --onto origin/main {COMMIT_SHA}
.
Finally, you can also always manually use interactive rebasing or cherry-picking.
Rebase vs ff merge
A rebase that is used just to reset the base (and not make other edits) is very similar, in results, to a "fast forward merge" (through git merge
(if it can ff and defaults have not been changed) or git merge --ff-only
). Both methods result in a linear history, that makes it look like the feature branch commits were applied directly to main (without merge commits).
Example rebasing of feature onto main
git checkout feature
git rebase main # Or, git rebase -i main
## One-liner
git rebase main feature
The main difference seems to be that rebase is really replaying or copying commits onto the new base, versus merge ff, which is more like a pointer move. The result is that, although the output looks the same, rebase can end up with different commit hashes, since copying a commit results in a slightly different hash.
Note: The one-liner syntax for rebase still performs a
checkout
first, changing your working directory; it just does it for you, behind the scenes. If you want to rebase without modifying working directory, you will need to use something likeworktree
Atlassian has an in-depth explainer on the differences: "Merging vs Rebasing"
Issues with dates and rebasing
Since you are technically re-commiting when you rebase (by re-writing history), the default thing that happens is that the Author
and AuthorDate
stay the same as before, but the Commit
(author) and CommitDate
reflect yourself and the current time of the rebase.
You can use
git log --pretty=fuller
to show bothAuthorDate
andCommitDate
at the same time, to verify if this has happened
This can make it look like old commits have just been made and can sometimes cause issues with different tools that integrate with Git.
Github used to go by
CommitDate
for ordering and display within a repo (1, 2) which could really mess with the commit order in a PR (at least for casual viewing). This was fixed in 2020.
To avoid this behavior, use the --committer-date-is-author-date
flag with git rebase
, but only when it really makes sense to use (see below for details).
In most cases, the default behavior makes sense to use. If changes are being rebased onto a new base, where the changes rely on functionality present within the new base, it doesn't make sense to say those changes happened before the functionality was implemented in base, so default rebase date behavior makes sense.
If we have already used rebase, and now our dates are messed up, we can use the same rewriting properties of rebase to fix the very problem it created, and edit the past commit dates, still using the --committer-date-is-author-date
flag, like so:
git rebase --committer-date-is-author-date {commitHash}
For picking the commitHash to rebase from, pick the first commit that has a correct date, where the author and commit dates match, and use it.
Credit goes to this S/O.
Warning 🚨: This flag is easy to misinterpret; it essentially sets the committer date to whatever the author date (usually this means pretending it happened earlier than it did).
If you want the opposite, to force the author date to match the committer date, you will want to use-i
,--ignore-date
, or--reset-author-date
(v > = 2.29). Similar to the above trick, you will also need to pick a commit hash that comes before the one you want to fix, if the commit you want to fix is the current HEAD.
Squashing on Github - Issues
Github has two advanced options for merging PRs - "squash and merge" and "rebase and merge" can both lead to complicated issues.
For example, squashing and rebasing often changes the metadata about who committed the code, and in the case of squashing, it can attribute large quantities of code by dozens of authors, to a new single author.
In late 2019, Github improved how attribution works with squash merges, by automatically adding everyone who touched the code in the PR as a co-author. You can see this automated text in the bottom of the squashed commit text.
Tools like Gitlens often do not play nice with complex code history, where there are multiple authors (co-authors), a different committer vs author, etc.
Github's "squash and merge" option basically leaves two commit trails. The branch that you merged from will have the full commit history. The branch you are merging into will have just a single commit that bundles up all the commits that made up the PR.
Rebase - Opinions
General Rule: Here is the general rule about rebase that seems to be the consensus: Rebasing on your own stuff (your branch of shared repo, feature, solo repo) that is not yet under active review or part of shared code is fine. Rebasing on shared branches, other people's branches, or commits that are already pushed and others rely on or are reviewing, is not (under most circumstances).
You will find that many well-established organizations and repositories follow this principle; here is GitHub mentioning it.
Something I find kind of funny is that rebase goes against a lot of best practices around source control (source of truth, preserving history, etc.), but can also make using it more enjoyable (in certain situations) and you will often find that developers have very strong opinions about it.
I personally think that the value from git rebasing is very contextual, similar to how I feel about squash merging. My general advice is:
- You will find less value in rebasing if:
- You already write very clean commit messages and follow a logical commit pattern
- You use
--amend
to fixup commits instead of things likecommit -m "Whoops, maybe it will work now"
- Reviewers or viewers of your code (including your future self) don't care much about a clean commit history
- Squash merging is being used, so you can cleanup your final commit message at the end.
- You will likely find more value in rebasing if:
- You feel writing well-crafted commit messages and ensuring each commit is logically contained slows your process down while working, and you would rather "tidy things up" at the end
- Reviewers or viewers of your code (including your future self) care about a clean commit history
- Squash merging is not being used - all commits are preserved
Also, when in doubt, make temp local backup branches before attempting anything complex with rebase 😅
Git styles, standards, best practices
Good reference: agis/git-style-guide.
Branch naming
Here is a good S/O thread on the topic. And here is a Dev discussion.
Easy, good rules to remember:
- No caps
- Use hyphens instead of spaces
- Try to keep branch names short
- Use "grouping" tokens and slashes to help
- Use issue / ticket IDs when applicable
Examples:
feature/OMT-4215/adding-gps-locator
feature/adding-gps-locator/joshua
joshua/OMT-4215/adding-gps-locator
You should try to avoid using
master
as your main branch, and use something likemain
,production
, or whatever else makes logical sense. "Master", at least in the origins of Git (and its intention to replace Bitkeeper), has roots in a "master / slave" relation context, and in addition, often isn't a great descriptor of what the branch is for anyways! And, it often doesn't take that much effort to change.
Writing Commit Messages
- To encourage writing good commit messages, you might want to avoid using
commit -m "my message"
, and switch to usingcommit
, which will launch your default text editor and encourage you to write a full commit message.- To set VSCode as the default commit editor, it should be as easy as:
git config --global core.editor "code --wait"
- To set VSCode as the default commit editor, it should be as easy as:
- There is no single "standard", but a very common format is one outlined by Tim Pope in a blog post. This is further summarized and outlined in many other posts:
- McKenzie: How to write a Git commit message properly
- Painsi: Painsi/commit-message-guideline.md (GH Gist)
- Ayodeji: How to Write Good Commit Messages: A Practical Git Guide
- Beams: How to Write a Git Commit Message
- Here is my own summary of the Tim Pope standard:
- Heading: Short (<= 50 chars), capitalized, but no period
Empty Line
- Body
- This can be a single paragraph, multiple paragraphs, bulleted lists, or just a few sentences. BUT, they should adhere to the following format rules:
- Lines should wrap at 72 characters (including bullet items)
- Bullets should use
*
or-
- Empty lines should be placed between multiple paragraphs, but are optional for between bullet list items
- Use imperative grammar:
Add test suite
, notAdded test suite
- This can be a single paragraph, multiple paragraphs, bulleted lists, or just a few sentences. BUT, they should adhere to the following format rules:
- My own personal preferences, and notes:
- If you are using a ticketing system, make sure to include the ticket ID either in the head or body
- Most systems, like Jira, will auto-link based on this
- Github actually does this as well
- if you write something like
addresses #4
in your commit message - where there is an issue with id #4 - pushing the commit up will auto-associate it with the issue (or PR) - You can even close issues via commit messages - once the commit is merged into your default branch (details).
- if you write something like
- I prefer short bullet lists over long paragraphs
- Bullet lists are also much easier to combine in rebase / merges, over paragraphs or sentences
- Easy way to avoid 72 char wraps
- If you did something "weird" in your commit (disabled a test, reverted critical code, etc.) - SAY SO
- If you are going to insist on using Emoji in your headings, try to be consistent; take a look at
emoji-log
- If you are using a ticketing system, make sure to include the ticket ID either in the head or body
Finding a force push on Github
- Generate a token to use with the Github API if you don't already have one handy
- Make a request to
https://api.github.com/repos/{OWNER}/{REPO}/events
- Scan / filter to events with
pushEvent
as the type - FURTHER filter to those where the new
payload.head
does not match any of the SHAs in thepayload.commits
array- Sample code:
const filtered = data.filter((obj)=> { let wasForcePush = false; if (obj.type === 'PushEvent') { const payload = obj.payload; if (Array.isArray(payload.commits) && payload.commits.length) { wasForcePush = payload.commits[payload.commits.length - 1].sha !== payload.head; } } return wasForcePush; });
- Once you have found the commit that forced a new diverged tip, you can create a new branch based off the commit right before that forced push (on Github.com via API)
- Finally, you can check out the newly created branch locally, and if you want to, merge back.
Finding a Force Push - Further reading:
- https://objectpartners.com/2014/02/11/recovering-a-commit-from-githubs-reflog/
- https://stackoverflow.com/a/28958418
- https://stackoverflow.com/a/48020000
Wiping Git History - Squashing to a Single Commit
In certain cases, you might want a branch or repo to be devoid of history. In this case, what you are usually looking to do is squash the entire codebase into a single commit.
There are several ways to do this, enumerated here and here. The "orphan branch" solution, described in this response seems the most straight-forward and least error-prone of all the solutions listed.
Git LFS
WARNING:
.gitattributes
does not work with directories! So trying to track a directory with LFS will lead to confusing behavior (e.g.,git lfs status
will correctly show objects to be commited, butgit lfs ls-files
will not show the correct files after commit).Instead of
my_dir filter=lfs
, usemy_dir/**/* filter=lfs ...