Joshua's Cheatsheets - Git
Light
help

Git - Cheatsheet

Other Resources

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 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.

Meta cheatsheet - actually setting up Git

  • Checking installed version

    • git --version
  • Check installed path

    • git --exec-path
  • If you are getting auth issues ("logon failed", etc.), your version of git is probably out of date

Start from scratch (create empty repo in current folder)

git init

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

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)

  1. Add already existing (but empty) github URL as remote repo and set as remote

    • git remote add origin [REMOTE_ORIGIN_URL]
  2. Verify that it is linked

    • git remote -v
  3. Now push up

    • git push origin master

Setting upstream (for forks)

This is really the same steps as adding a new remote origin:

git remote add upstream [REMOTE_UPSTREAM_URL]

Making sure your fork is up to date before making a PR

  1. Make sure you have fresh data on upstream

    • git fetch upstream
  2. Merge the fresh upstream master, into:

    • Your feature branch

      • git merge upstream/master
    • Your master branch

      • git checkout master && git merge upstream/master

Here is an advanced version that will fetch upstream master, merge into local master, and then merge into feature you are on, without switching branches. (as long as it can fast-foward) (only works if you are not on master currently)

git fetch upstream master:master && git merge master

Or (works regardless if you are on master or not currently):

git pull upstream master:master && git merge master

details

Change the link to remote

(for example, if you change the repo name on Github)

  1. First check list of remotes

    • git remote -v
  2. Then remove the one you want to

    • git remote rm [NAME_OF_REMOTE||Example:Origin]
  3. Double check that it was removed

    • git remote -v

      Branch Linkage

  4. Show how local branches are linked up to origin (e.g. showing tracking links)

    • git branch -vv
  5. List only remote branches:

    • git branch -r

      • or:
    • git ls-remote --heads origin
  6. Fetch (and switch to) remote branch that does not exist locally (yet)

    • git checkout --track origin/[REMOTE_BRANCH]
  7. Link a local branch to a remote TRACKING branch, that DOES NOT EXIST YET - set upstream (very commmon 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]
  8. Link a local branch to a remote TRACKING branch that does ALREADY exist

    • git branch -u origin/[TRACKING_BRANCH] [LOCAL_BRANCH_NAME] - Or
    • git branch --set-upstream-to origin/[TRACKING_BRANCH]
  9. 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
  10. UNLINK a branch that is tracking remote

    • git branch --unset-upstream

Good S/O answer about upstream. And this

In get 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.

More 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"
  • 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 master into local master, and then merge that into yours, all without checking out!

      • git fetch origin master:master && git merge master
    • Extended example: merge origin master into local master, merge master into currrent branch feature, then merge current branch feature into local master, then push local master back up to origin. - Steps: - git fetch origin master:master && git merge master - git fetch . feature:master - git push origin master - This is basically the full update cycle for an org where master is source of truth

      • As one line: - git fetch origin master:master && git merge master && git fetch . feature:master && git push origin master

View all untracked files, not just dir names

git status -u

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 {branchA} ^{branchB}

Short

git log --oneline {branchA} ^{branchB}

Just the count (number of commits different between branches)

git rev-list --count {branchA} ^origin/master

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

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

Cherry-Picking / Selective Git Merging

cherry pick a commit 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

      Grab files from another branch and merge into yours, without branch merging

  • Specific files:

    • git checkout {branch} -- {filename(s)}
  • Grab everything:

    • git checkout {branch} -- .

Exclude a file from a diff

  • git diff [OTHER] . ":(exclude)[FILEPATH_TO_EXCLUDE]"
  • Sample:

    • git diff 7cc297d5baf2e305b709fcf93e3fe93284fb18e1 --stat -- . ":(exclude)package-lock.json"

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).

Revise the last commit (different from revert - this is essentially like undo)

  • 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
    • Else if you want to just nuke it

      • git reset --hard HEAD

Nuke it - reset to remote master

  1. git fetch origin
  2. git reset --hard origin/master

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 rever to the origin as source of truth.

This won't actually remove/delete untracked files. To do that, see below section.

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


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

Initializing

  • 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 or git submodule update --init --recursive Updating (fetch)
  • Easiest to remember is to just treat git subdir as real git repo

    • cd to the directory, then run git fetch and git merge origin/master
    • 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

    1. Remove the submodule entry from .git/config

      • git submodule deinit -f path/to/submodule
    2. Remove the submodule directory from the superproject's .git/modules directory

      • rm -rf .git/modules/path/to/submodule
    3. Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule

      • git rm -f path/to/submodule

Remove a file from git tracking after adding it to the gitignore

  • Use --cached flag with rm

    • git rm --cached <file>

Jump to a specific moment in time, and commit

[!!! - Danger - !!!]

  • git reset --hard [COMMIT_HASH]

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 master

"unstage" a file (do the reverse of git add)

Unstage 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

      unstage all added files

  • git reset .

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
  • 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.

Show which files are being ignored:

On git v1.7.7 and up (SO),

git status --ignored

Stashing / Temporary Staging

Docs:

  • git-scm

    The basics

  • Stash:

    • git stash
    • Include untracked or ignored files:

      • git stash -u
      • git stash -a
  • Pop:

    • git stash pop
  • See all stashes:

    • git stash list

      Partial stashes

      SO answers: 1, 2, 3

  • Interactive (hunk picker)

    • git stash -p (or git 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.

git stash -p is an internal alias for git stash push -p

Reminder: There is not "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 branch changes (git diff) as a patch file

If you want to quickly dump all the differences between two branches (lets say between feature and master) as a backup, you could export the diff as a patch file.

git diff --no-prefix master > {filename}.patch

And to restore from that patch, you could use:

git checkout master && git patch -p0 < {filename}.patch

Credit: S/O answer

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'
    • For a specific thing (commit, tree), just put it last:

      • git show -s --format=%at {thing}
  • git log --pretty=format:%at | sort | tail -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 | sort | tail -n 1

committer date vs author date - there should only ever be one author date, and it corresponds to when the code was actually first committed/authored with git commit. However, there can be multiple commit dates, and they correspond to when the commit was modified in the process of applying it through merge/rebase.

Get date 'created' timestamps:

  • git log --pretty=format:%at --follow -- {thing} | tail -n 1

    • --follow will make sure it captures renames in history
    • SO

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-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. Reminder: Add "shebang" to file and make sure executable (chmod +x).

Resources

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
  • 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

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
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 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 git push --follow-tags

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.

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.


Some common workflow approaches

@TODO


Rebasing

Some explanations / resources:

What it does

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.

Here are some guides:

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 master

  • git checkout alpha && git rebase master

    • Or, if you to rebase while on master:
  • git rebase master alpha

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 master.

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.

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.

This can make it look like old commits have just been made, and since Github goes by CommitDate for ordering and display within a repo (1, 2, 3), this can really mess with the commit order in a PR!

Fixing date order

We can use the same rewriting properties of rebase to fix the very problem it created, and edit the past commit dates.

If we have already used rebase, and now our dates are messed up, we can run rebase again, but this time 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.

Opinions

Something I find funny is rebase goes against a lot of best practices around source control (source of truth, preserving history, etc.), and yet so many devs advocate for using it. I understand its value, but it also seems like it is often more trouble than it can be worth. Not to mention that rewriting history and permanently screwing up a shared history is a great way to make devs you are working with irate.

Even on Github, you'll find pages that discourage using git rebase.

When in doubt, make temp local backup branches before attempting anything complex.

General Rule: Here is the general rule about rebase that seems to be the concensus: Rebasing on your own stuff (your branch of shared repo, feature, solo repo) that is not yet part of shared code is fine. Rebasing on shared branches or commits that are already pushed, or other people's branches, is not.


Git styles, standards, best practices

Good reference: agis/git-style-guide.

Branch naming

Here is a good S/O thread on the topic.

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
Markdown Source Last Updated:
Sun Nov 17 2019 06:35:58 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 19 2019 17:06:24 GMT+0000 (Coordinated Universal Time)