My memory is astonishingly volatile and during the last years I’ve been under a heavy context switch, so I keep some dirty gists with recipes for different topics, from PostgreSQL to Linux. They need some refactoring, so maybe I can kill two birds with one stone and publish them here after the cleanup, they might be useful for somebody else.

Disclaimer: no, I don’t use (almost) any alias or small script. To save some keystrokes bash history is good enough, and learning the actual command will always be valuable. Currently, my only exception is gittree.

Workflow

Basic stuff, you probably want to skip this section, this is more a brain dump.

$ git status # First normal step, as a double-check for files not added to the repo or unwanted changes
$ git checkout -b my_new_branch # Let's begin
$ git add file_1 file_2
$ git commit -a -m "Message" # If no long explanation is required, or ...
$ git commit -a # ... if a long comment is convenient (see later)
$ git fetch master
$ git rebase master # If it's safe or ...
$ git merge master # ... if it's not and
$ git push # If there's no history rewrite or ...
$ git push --force-with-lease # ... if it's not (see later)

A must-have: just like in bash you can go to the “previous path” with cd -, in git you can go to the previous branch with git checkout -.

Commit messages: long or short?

Like many other things, this is often a matter of taste and there’s no best answer. Just adopting your team choice is good enough.

If I have to choose, I’d say short (single sentence, with -m), because better code or, more flexibly, a comment inside the code, is likely a better choice (seeing a comment and reading it is more convenient than traversing git history most of the times).

rebase vs merge, forcing

If you created a branch, rebasing the original one to keep yours up-to-date until the first push should be mandatory because it has little cost and a lot of return. The problems begin when you’ve already pushed.

If the branch has already been pushed and other people are also working on it, I always use merge. It pollutes the history, but alternatives can lead to messy repositories and even losing code.

That advice AFAIK is the normal practice. The fuzzy area is what to do when you’ve already pushed code. Pushing often is convenient both for safety reasons (to avoid losing work) and for communication (“ey, take a look at this WIP and tell me if…). Some people say that you should never, ever, rewrite history. I disagree. IMHO, even if you’ve pushed code, as long as anybody else has touched it, it’s right. And while you can’t know what are people doing, you can be sure that only you pushed, with --force-with-lease. It checks based on the expected references, if you want more details, this is a good explanation.

How to pull after a force? git reset origin/my_branch --hard.

WIP

There’s another common pattern in my workflow, pushing code that is still not finished:

$ git add .
$ git commit -a -m "WIP" # "This is work in progress and shouldn't get to master"
$ git push # Leaving today :wave:
...
$ git commit -a --amend # Continuing WIP, no relevant step that deserves its own commit
...
$ git commit -a --amend # Now change the message with the right one
$ git push --force-with-lease

The problem with that is that you must remember that you’re working on top of a WIP commit because if you don’t and add commits, then removing it through squashing can become tricky. In the past, I created some git hooks, but they were an annoyance (maybe I’ll eventually try again, who knows). Now have this in my .bashrc so the shell prompt display it:

    if [[ `git log -n 1 --pretty=oneline --abbrev-commit` =~ "WIP" ]]; then # if last commit is a WIP
        __wip_warn="${RED}WIP!!!"

Output:

juanignaciosl.github.io (git_recipes +) $ git commit -a -m "WIP"
[git_recipes c3f8af6] WIP
 2 files changed, 84 insertions(+)
 create mode 100644 _posts/2019-07-25-Git_recipes.markdown
 create mode 100644 img/workbench.jpg
juanignaciosl.github.io (git_recipes) WIP!!!$

WIP

Stashing

I use stashing a lot, specially when I have to move quickly between branches.

  • git stash: Stash current changes.
  • git stash pop: unstash and merge stored changes.
  • git stash apply: merge stored changes without unstashing (you can later remove it with git stash drop).

Anyway, I’m using the previous WIP strategy more and more. It’s easy to forget that you stashed something, and doing two consecutive git stash pop by mistake can lead to big messes :_)

Cleaning the history

I like doing a lot of commits, each of one encompassing a single step. Sometimes you discover steps that are strongly related but aren’t consecutive, so a simple --amend is not enough. In those cases, interactive rebase is useful and simple to use once you get used to. For example, with this command you can reorder and squash the last three commits:

$ git rebase -i HEAD~3

If you want to rebase to master and at the same time rearrange the commits in your branch (thanks, @matallo:

$ git rebase -i master

If you need to rebase back to the first commit:

git rebase -i --root master

Finding bugs

I always said that if you find a new bug, reasoning based on changes (“what has changed since the last time that I think that it worked”) was a suboptimal strategy, but git provides a great tool to chase bugs that removes the mental load: git-bisect. It provides a binary search through git commits, from the one that you know that worked. Here’s a great tutorial about it.

GitHub

Hub is “an extension to command-line git that helps you do everyday GitHub tasks without ever leaving the terminal.”

Commit automation

If you navigate through a file, the address bar contains the reference inside a branch, which can change, so it’s not a true permalink. If you want to share a GitHub link, press y before, the URL will change to the commit hash.

Misc stuff

Credentials

Forwarding

Sometimes you want your local keys available in a server (for example, when you create a transient instance in a cloud provider and you just want to pull a git repo). Using ssh -A, if the remote system allows it, works. Keep in mind ssh man page warning:

     -A      Enables forwarding of the authentication agent connection.  This can also be specified on a per-host basis in a configuration file.

             Agent forwarding should be enabled with caution.  Users with the ability to bypass file permissions on the remote host (for the agent's UNIX-domain socket) can access the local agent through
             the forwarded connection.  An attacker cannot obtain key material from the agent, however they can perform operations on the keys that enable them to authenticate using the identities loaded
             into the agent.

macOS

I’m currently using Linux (Ubuntu 19.04) and I think that it worked right out of the box (I didn’t write anything in my guide and there’s nothing at my configuration files).

In macOS things were different:

  1. eval "$(ssh-agent -s)"
  2. git config --global credential.helper osxkeychain
  3. GitHub SSH Keys
  4. ssh-add -K ~/.ssh/id_rsa_github http://superuser.com/a/1155833

Instructions for macOS Sierra

ssh-add -K ~/.ssh/id_rsa Note: change the path to where your id_rsa key is located.
ssh-add -A

Create the following ~/.ssh/config file:

Host *
  UseKeychain yes
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_rsa

Viewing changes

  • git log -5 --pretty --oneline: single line per change.
  • git log --all --graph --decorate --oneline --simplify-by-decoration: pretty branch current status.
  • git reflog: displays every change.
  • git diff --ignore-space-at-eol: ignore spaces (workaround for Atom formatter).
  • git diff --cached: changes in files that you’ve already added.
  • git diff mybranch master -- file.ext: compare file between branches.

Comparing changes

  • git diff --name-status master..6953-Vizjson_3_generation: list changed files.
  • git diff master..6953-Vizjson_3_generation

  • git shortlog master..6953-Vizjson_3_generation

  • git log master..6953-Vizjson_3_generation
  • git log __branchA__ ^__branchB__ : commits in branch A that aren’t in branch B.
  • git log origin/__branchname__..__branchname__ : show diff between local commits and remote commits.

Searching

  • git log -S puppy: looks for “puppy” in the contents of the commit history.
  • git log --grep $string: looks for $string pattern (regexp) at messages (thanks to @stbnrivas).
  • git log --since=$(date --date="15 day ago" +"%Y-%m-%d"): filter by date.

  • git blame -M: blames original commit, not the move commit.
  • git blame -CCC: looks at all commits in history.

Branching

  • git branch -r: list remote branches known by local git.
  • git remote update origin --prune: update remote branches list.

Viewing the branching tree (this is my only current alias):

alias gittree='git log --graph --pretty=oneline --abbrev-commit'

Adding changes

  • git add -p: add step by step (hunks).
  • git rebase -i HEAD~4: interactive rebase.

Cherry-picking:

  • git cherry-pick [commit]: picks one commit.

Reverting changes

Atlassian tutorial.

  • git revert <commit>: applies the inverse of a commit.
  • Single file: git checkout 1b4df5b13e5c76aa50b9ba1fd0167a947250c298~1 -- file.rb.
  • git reset: reverts git add.
  • git reset <commit>:
    • --mixed (default): move the current branch HEAD and index backward to .
    • --soft: moves HEAD, index unchanged.
    • --hard: moves head, changes index and removes changes.

Reverting ammend

Source:

# Move the current head so that it's pointing at the old commit
# Leave the index intact for redoing the commit.
# HEAD@{1} gives you "the commit that HEAD pointed at before 
# it was moved to where it currently points at". Note that this is
# different from HEAD~1, which gives you "the commit that is the
# parent node of the commit that HEAD is currently pointing to."
git reset --soft HEAD@{1}

# commit the current tree using the commit details of the previous
# HEAD commit. (Note that HEAD@{1} is pointing somewhere different from the
# previous command. It's now pointing at the erroneously amended commit.)
git commit -C HEAD@{1}

Fixing things

  • git commit --amend: change last comment, but with -a you can merge current changes with the last commit.
  • Change commit author.
  • Recover deleted file: git checkout $(git rev-list -n 1 HEAD -- "$file")^ -- "$file"

Teamwork

Productivity

  • You can add a [alias] section at .gitconfig.
  • git diff --shortstat "@{0 day ago}": how many lines you’ve written (today).

Cleanup

  • git remote prune origin [--dry-run]: remove local branches deleted at remote.
  • git clean -n -df : displays local uncommited directory and file modifications. Remove -n to actually delete them.

Patching

  • Create patch: git diff > file.patch
  • Apply patch: git apply file.patch

--patch also works with add, checkout and log (@pvrrgrammer):

  • With log it’s like -p, for seeing the diff of the change.
  • With add, it interactively asks for action on each change hunk.
  • With checkout, it interactively asks for discard action on each change hunk.

Ignoring local files

Source.

The .git/info/exclude file has the same format as any .gitignore file. You can also set core.excludesfile to the name of a file containing global patterns.

If you already have unstaged changes you must then run:

git update-index --assume-unchanged [<file>...]

Copying/moving…

  • Clone branch to another location: git archive master | tar x -C /path/to/target/dir.

Submodules

  • git submodule init && git submodule update : retrieve and update all submodules (alt).

References