Skip to content

Instantly share code, notes, and snippets.

@jbontech
Created December 20, 2019 09:47
Show Gist options
  • Save jbontech/0b99ce831fccbbf88e34939abd748e36 to your computer and use it in GitHub Desktop.
Save jbontech/0b99ce831fccbbf88e34939abd748e36 to your computer and use it in GitHub Desktop.
#!/bin/bash
##########
# contents
##########
# contents
# notes
# script setup
# git config files
# local versioning within a single commit
# local versioning across commits
# local branches
# remote repos
# remote repos with multiple users
# commands that can change published commit history
# misc
# script cleanup
##########
# notes
##########
# this script creates repos in a sandbox directory and uses them for demos.
# add an exit command anywhere to stop the script and examine repo state.
# rerun the script freely. it cleans up the sandbox before each run.
# the sandbox dir is ignored so created files are not added to any uber repo.
# terminology
# working, staged, committed <=> filesystem, index, commit graph
# ref => a branch ref or a tag ref
# direction of git diff
# "git diff commitA commitB", where commitA predates commitB,
# will show the changes required to transform commitA into commitB.
# similarly, when diffing within a single commit
# (e.g., diff staged and committed),
# git shows the changes needed to transform older state into newer state.
# in this case, committed is "older",
# so git diff shows how to transform committed into staged.
# examining repo state
# suggest to set the git la alias described below and run it often.
# it's an easy way to visualize HEAD, branches, tags, and the commit graph.
##########
# script setup
##########
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# this gets the directory where the current script resides,
# as long as the last element in the path is not a symlink.
# https://stackoverflow.com/a/246128/2512141
cd $SCRIPT_DIR
mkdir -p $SCRIPT_DIR/sandbox
# if sandbox has unexpected content, stop script
EXP_CONT='\(repoA\|localA\|localB\|remoteUpstream\|remoteDownstream\)'
UNEXP_CONT=$(ls sandbox/ | sed "/$EXP_CONT/d" | wc -l)
if [[ $UNEXP_CONT -gt 0 ]]
then
echo 'found some unexpected content in sandbox dir.'
echo 'did you already have a directory named sandbox?'
echo 'this script will clean out sandbox. best to run it from an empty dir.'
exit
fi
rm -rf $SCRIPT_DIR/sandbox/*
# if no .gitignore or .gitignore doesn't have sandbox, ignore sandbox
if ! grep -q 'sandbox' ./.gitignore 2> /dev/null
then
printf "sandbox\n" >> ./.gitignore
fi
##########
# git config files
##########
# list git configs with sources.
git config --list --show-origin
#C:/Program Files/Git/mingw64/etc/gitconfig (system)
#C:/Users/$USER/.gitconfig (global)
#.git/config (local)
#"C:\\ProgramData/Git/config" (Windows)
echo
git config --list --show-origin --system
echo
git config --list --show-origin --global
echo
git config --list --show-origin --local
echo
# configs in C:\\ProgramData/Git/config (Windows specific) are not
# available through a flag.
# contents of global gitconfig
cat ~/.gitconfig
echo
# list aliases
git config --get-regexp alias
echo
# set alias.
#git config --global alias.la log --all --graph --oneline --decorate
# unset alias
#git config --global --unset alias.la
# add alias manually to global config.
# vim ~/.gitconfig
# [alias]
# la = log --all --graph --oneline --decorate
# set username/email
# git config --global user.name "john smith"
# git config --global user.email "[email protected]"
##########
# local versioning within a single commit
##########
REPA=$SCRIPT_DIR/sandbox/repoA
mkdir -p $REPA
cd $REPA
# initialize
git init
git status
# "No commits yet"
# working -> staged
printf "apple\npear\npeach\n" >> fruits.txt
git add -A
# prefer "-A" instead of "." because "-A" capture all changes in repo.
# "." only captures changes in current dir.
# staged -> committed
git commit -m 'added apple, pear, peach'
# working <- staged <- committed
# (copy commited to working and staged, and clean up untracked files and dirs)
# CAUTION: this will permanently erase files that are not committed.
# consider committing and then reverting to a previous commit instead.
printf "cherry\n" >> fruits.txt
git add fruits.txt
# now cherry is staged.
printf "apricot\n" >> fruits.txt
# apricot is not staged, only exists in working.
printf "sourdough\n" >> breads.txt
# file breads.txt is untracked.
mkdir other
printf "potato\n" >> other/vegetables.txt
# directory other is untracked.
git status --untracked-files=all
# -u for short. shows individual files, not just dirs.
git reset --hard HEAD
git clean -x -d -n
# -x: disregard .gitignore exclusions when cleaning up
# -d: clean directories as well
# -n: trial run only
# no way to list files inside of directories, unfortunately.
git clean -x -d -f
# -f: force
# staged <- committed
printf "cherry\n" >> fruits.txt
git add fruits.txt
printf "apricot\n" >> fruits.txt
# working has cherry and apricot, staged has cherry only.
git reset --mixed HEAD
# staged no longer has cherry.
# note that --mixed is the default mode for reset.
# working <- staged
git checkout .
# working no longer has cherry or apricot.
# note: checkout doesn't support -A flag like add.
# working <- staged, single file
printf "cherry\n" >> fruits.txt
git checkout fruits.txt
# compare working, staged
printf "lemon\n" >> fruits.txt
git add fruits.txt
# stages lemon
printf "lime\n" >> fruits.txt
# lime is only in working directory
git diff
# for this and other diff examples, append
# " -- fruits.txt" to limit diff to one file.
# compare staged, committed
git diff --cached
# compare working, committed
git diff HEAD
# remove file from working, staged, and committed
printf "shiitake\n" >> mushrooms.txt
git add -A
git commit -m 'added shiitake'
rm mushrooms.txt
git add -A
git commit -m 'removed mushrooms.txt'
# leave file in working, ignore it, and remove from staged and committed
# CAUTION: the file can be deleted by git clean.
printf "oat\n" >> milks.txt
git add -A
git commit -m 'added oat to milks.txt'
git rm --cached milks.txt
printf "milks.txt\n" >> .gitignore
git add -A
git commit -m 'moved milks.txt to gitignore'
git status
cat milks.txt
##########
# local versioning across commits
##########
# see which commit HEAD is pointed to
git rev-parse HEAD
git rev-parse master
git log --all --oneline --graph --decorate
# compare two commits
printf "strawberry\n" >> fruits.txt
git add -A
git commit -m 'added strawberry'
git diff HEAD^ HEAD
git diff HEAD~1 HEAD
git diff @~1 @
git diff @~1 @ -- fruits.txt
# -- fruits.txt limits diff to that file.
git diff master~1 master
# see what a file looked like in a previous commit
git show HEAD~2:fruits.txt > tmp.txt
cat tmp.txt
rm tmp.txt
# see what a file looked like at a certain date/time (must specify branch)
#git show master@{2019-10-02}:fruits.txt
#git show master@{"2019-10-02 20:39:04"}:fruits.txt
# create a commit which restores the state of a previous commit
# first, notes on what not do do.
# checkout and reset can modify history, so don't use those.
# https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting
# another caution against using git reset:
# https://www.atlassian.com/git/tutorials/undoing-changes/git-revert
# now, the correct way:
printf "banana\n" >> fruits.txt
git add -A
git commit -m 'added banana'
printf "kiwi\n" >> fruits.txt
git add -A
git commit -m 'added kiwi'
printf "pineapple\n" >> fruits.txt
git add -A
git commit -m 'added pineapple'
printf "mango\n" >> fruits.txt
git add -A
git commit -m 'added mango'
# we wish to revert all the changes that came after banana.
git revert --no-commit HEAD~3..HEAD
# --no-commit flag avoids generation of a commit for each reverted commit.
cat fruits.txt
git diff HEAD
# shows the changes we will make in the upcoming commit.
git commit -m 'reverting all changes after banana'
##########
# local branches
##########
# list local branches
git branch -l
# -l for local, -a for all, -r for remote. -l is default.
# create a branch
git branch sweet_fruits
# switch to a branch
git checkout sweet_fruits
# create and switch to a branch
git checkout -b sour_fruits
# rename a branch
git branch -m sweet_fruits sugary_fruits
git branch -l
git branch -m sugary_fruits sweet_fruits
git branch -l
# you can modify working and staged and later decide where to commit them.
printf "grape\n" >> fruits.txt
git add -A
printf "cantaloupe\n" >> fruits.txt
git status
git checkout master
git log --all --oneline --graph --decorate
git checkout sweet_fruits
git log --all --oneline --graph --decorate
git checkout sour_fruits
git log --all --oneline --graph --decorate
# commit changes to a particular branch
git checkout sweet_fruits
git add -A
git commit -m 'added grape and cantaloupe'
git log --all --oneline --graph --decorate
# diverged branches
git checkout sour_fruits
printf "grapefruit\n" >> fruits.txt
git add -A
git commit -m 'added grapefruit'
git log --all --oneline --graph --decorate
# merging and merge conflicts
git checkout master
git merge sweet_fruits
# fast-forward merge (master is ancestor of sweet_fruits in commit graph).
# no commit object is created, so no commit message required.
git log --all --oneline --graph --decorate
git merge sour_fruits
# conflict! sweet_fruits and sour_fruits both modified fruits.txt.
sed '/[<<<|>>>|===]/d' ./fruits.txt >> tmp.txt;
rm fruits.txt
mv tmp.txt fruits.txt
git add -A
git commit -m 'resolved conflicts and merged sour_fruits'
# this commit takes us out of conflict state and completes the merge.
git log --all --oneline --graph --decorate
# delete a local branch that has been merged
git branch -d sweet_fruits
git branch -d sour_fruits
# delete a local branch that has not been merged
git checkout -b savory_fruits
printf "???\n" >> fruits.txt
git add -A
git commit -m 'cannot think of any savory fruits'
git checkout master
# cannot delete the branch if it is checked out, so we switch.
git branch -d savory_fruits
# warning
git branch -D savory_fruits
git log --all --oneline --graph --decorate
# note: because a branch is simply a pointer to a commit object,
# deleting the branch does not delete the commit.
# we can still find the savory_fruits commit with git reflog.
# and, if we wish, we can reattach a branch to the commit and recover it.
# (not shown.)
# detached HEAD
# this is when we point HEAD directly to a commit instead of a branch ref.
# this can happen in several ways. for example:
# - point HEAD to a past commit which has no branch ref.
# - point HEAD to a commit where the branch ref was deleted.
# - point HEAD to a commit using a tag.
# to fix detached HEAD, just checkout a commit that has a branch ref.
# if you commit when in detached HEAD state, git will still create a new commit,
# but that commit object will have no branch ref
# and therefore won't be discoverable in the commit graph after you leave it.
# if you are in detached HEAD and have made changes you need to preserve,
# commit your changes and then create a branch at that commit.
printf "watermelon\n" >> fruits.txt
git add -A
git commit -m 'added watermelon'
git tag -a watermelonTag -m 'tagging watermelon'
printf "honeydew\n" >> fruits.txt
git add -A
git commit -m 'added honeydew'
git checkout watermelonTag
# detached HEAD warning
git checkout master
# detached HEAD warning gone
git checkout watermelonTag
# detached HEAD warning
printf "raisin\ncurrant\nprune\n" >> dried_fruits.txt
git add -A
git commit -m 'added raisin, currant, prune'
# succeeds, creates new commit. HEAD points to new commit.
# but new commit has no branch ref.
git checkout -b dried_fruits
# now commit has a branch ref.
# we can switch to master and merge it in.
git checkout master
git merge dried_fruits -m 'merged dried fruits'
# this is not a fast-forward merge, so a new commit object will be created.
# we must supply a message for that new commit.
# take single-file changes from another branch
git checkout -b dressings
printf "balsamic\nred\n" >> vinaigrettes.txt
printf "french\nitalian\n" >> sauces.txt
git add -A
git commit -m 'added balsamic, red, french, italian'
git checkout master
git checkout dressings -- vinaigrettes.txt
git add -A
git commit -m 'added balsamic, red'
##########
# remote repos
##########
# git allows us to treat a repo on local disk the same as
# a repo that lives in a hosting service like github.
# we will create bare repos on disk and
# pretend they are remote for demo purposes.
# (a bare repo has no working or staged area.)
REMUP=$SCRIPT_DIR/sandbox/remoteUpstream
LOCA=$SCRIPT_DIR/sandbox/localA
mkdir -p $REMUP
cd $REMUP
git init --bare
# two ways to create a local repo and associate remote:
# 1) init locally and add remote manually. (commented out)
# 2) clone remote. (uncommented)
# this 2nd approach copies down any repo content if it exists.
# also, it sets the default fetch/push location for the local master branch.
# 1) init locally
#mkdir -p $LOCA
#cd $LOCA
#git init
#ls -a
#git remote add remUp $REMUP
# # note: we are departing slightly from convention here.
# # normally, we name the remote repo "origin", not "remUp".
# 2) clone remote
cd $SCRIPT_DIR/sandbox
git clone -o remUp $REMUP $LOCA
# if we omit the -o <name> param, git will
# give the remote the default name, "origin".
cd $LOCA
# push some content to the remote repo
printf "pinto\n" >> legumes.txt
git add -A
git commit -m 'added pinto'
printf "lima\n" >> legumes.txt
git add -A
git commit -m 'added lima'
# 1) init locally
#git push remUp master
#git branch --set-upstream-to=remUp/master
# 2) clone remote
git push
# list remotes
git remote -v
git remote show remUp
# see which remote branch the local master branch is tracking
git branch -avv
# rename a remote
git remote rename remUp remFoo
git remote -v
git remote rename remFoo remUp
git remote -v
# add a remote
git remote add remBar ./../bar
# (dir does not exist)
# delete a remote
git remote remove remBar
# in most workflows, a coder will pull code from an upstream repo,
# edit it, and push it back to the upstream repo, possibly in a new branch.
# in other cases, it is useful to pull code from an upstream repo,
# edit it, and then push the edited code to a separate downstream repo.
# we demonstrate this latter workflow now so we can more fully explore
# manipulation of remotes.
REMDN=$SCRIPT_DIR/sandbox/remoteDownstream
mkdir -p $REMDN
cd $REMDN
git init --bare
# add a downstream remote
cd $LOCA
git remote add remDn $REMDN
# push to a downstream remote
git push remDn master
# configure master branch to push to downstream remote by default
git branch -avv
git branch --set-upstream-to=remDn/master
git branch -avv
# note: the terminology here is confusing.
# there is not really a good way to get around that.
# a branch may have only one remote associated as its default,
# and that remote is set with "--set-upstream-to", regardless of its use.
# the default remote is used for fetch and push commands run without args.
# it is possible to configure a remote to use different fetch and push urls.
# this means we could configure a branch to have a default remote which
# pulls from one repo and pushes to another repo.
# however, this breaks the convention that a remote represents one "repo".
# also, the convention used by github is to define a separate remote
# for each push or pull location.
# https://stackoverflow.com/a/47959803/2512141
# https://stackoverflow.com/q/9257533/2512141
# summarizing:
# a repo has multiple branches.
# a branch has one default remote (set with "--set-upstream-to").
# a remote has a fetch url and a push url.
# now that default is set, we can push to downstream remote without args
printf "chickpea\n" >> legumes.txt
git add -A
git commit -m 'added chickpea'
git push
# downstream demo is done, point master back to upstream by default
git branch --set-upstream-to=remUp/master
git push
# add a branch, push it to remote, and list local and remote branches
git checkout -b are_these_beans
printf "green bean\nsoybean\nsnap peas\n" >> legumes.txt
git add -A
git commit -m 'added green bean, soybean, snap pea'
git branch -avv
git push
# "fatal: No configured push destination"
# this is a new branch, so it doesn't have push configured yet.
git push remUp
# "fatal: The current branch are_these_beans has no upstream branch."
git push remUp are_these_beans
git branch -avv
git branch --set-upstream-to=remUp/are_these_beans
git branch -avv
# delete a local branch and the remote branch it tracks
# an aside: this is the highest-voted SO post i've ever seen:
# https://stackoverflow.com/q/2003505/2512141
git checkout master
git push remUp --delete are_these_beans
# delete the remote branch first (allows tab completion for remote).
git branch -D are_these_beans
##########
# remote repos with multiple users
##########
# create a second local repo.
LOCB=$SCRIPT_DIR/sandbox/localB
cd $SCRIPT_DIR/sandbox
git clone $REMUP $LOCB
# no name arg passed, so git will create remote with default name, "origin".
cd $LOCB
ls -a
git log --all --graph --oneline --decorate
cat legumes.txt
# merging another user's changes
cd $LOCA
printf "mung\n" >> legumes.txt
git add -A
git commit -m 'added mung'
git push
cd $LOCB
git status
git log --all --graph --oneline --decorate
# local B is not yet aware that anything has changed in the upstream repo.
git fetch
# makes local repo aware of all changes in remote repo, across all branches.
git status
git log --all --graph --oneline --decorate
# now the changes are visible. we can merge them into local master.
git merge
# merging another user's changes with conflict
cd $LOCA
printf "ricebean\n" >> legumes.txt
git add -A
git commit -m 'added ricebean'
git push
cd $LOCB
printf "lentil\n" >> legumes.txt
git add -A
git commit -m 'added lentil'
git fetch
git status
git log --all --graph --oneline --decorate
# notice repo localB has diverged from origin/master.
git merge
# conflict
sed '/[<<<|>>>|===]/d' ./legumes.txt >> tmp.txt;
rm legumes.txt
mv tmp.txt legumes.txt
git add -A
git commit -m 'resolved merge conflict'
git log --all --graph --oneline --decorate
git status
# "Your branch is ahead of 'origin/master' by 2 commits."
# the upstream repo and repoA are not aware of the resolved merge.
git push
cd $LOCA
git pull
# does a fetch and merge
cat legumes.txt
# adding a branch added by another user
cd $LOCA
git checkout -b grains
printf "millet\n" >> grains.txt
git add -A
git commit -m 'added millet'
git push remUp grains
cd $LOCB
git fetch
git branch -a
git log --all --graph --oneline --decorate
# note: although origin/grains is present, there is no local grains branch
git checkout -b bGrains origin/grains
# creates a local branch based on the newly copied remote branch.
# observe that local branch name is not required to match remote branch.
# however, by convention local branch name should match the remote.
git branch -m bGrains grains
# rename bGrains to grains
# cleaning up a branch deleted by another user
cd $LOCA
git checkout master
git push remUp --delete grains
git branch -D grains
cd $LOCB
git fetch
git branch -a
# note: origin/grains is still visible. fetch did not delete it.
git fetch --prune
git branch -a
# now origin/grains is gone.
git checkout master
git branch -D grains
# delete local branch that was tracking remote branch.
##########
# commands that can change published commit history
##########
# use these with caution.
# if working on a shared repo, changing and pushing commit history will
# break the repo for your collaborators.
cd $SCRIPT_DIR/sandbox/repoA
# reset --soft
# git reset --soft takes the branch tag that HEAD points to
# and moves it backwards in the graph without changing working or staged.
# if you reset --soft and then commit, it has the effect of "squashing"
# multiple commits into a single commit.
printf "almond\n" >> nuts.txt
git add -A
git commit -m 'added almond'
printf "cashew\n" >> nuts.txt
git add -A
git commit -m 'added cashew'
printf "pecan\n" >> nuts.txt
git add -A
git commit -m 'added pecan'
git log --all --graph --oneline --decorate
git reset --soft HEAD~3
git commit -m
git log --all --graph --oneline --decorate
# interactive rebase, squash
printf "celery\n" >> vegetables.txt
git add -A
git commit -m 'added celery'
printf "carrot\n" >> vegetables.txt
git add -A
git commit -m 'added carrot'
printf "plum\n" >> fruits.txt
git add -A
git commit -m 'added plum'
printf "tomato\n" >> vegetables.txt
git add -A
git commit -m 'added tomato'
printf "onion\n" >> vegetables.txt
git add -A
git commit -m 'added onion'
# we wish to rebase such that the fruit commit comes first,
# and the vegetable commits are squashed together.
# unfortunately, interactive rebase can't be done cleanly by script.
# steps are described instead:
# git rebase -i HEAD~5
# in editor, move plum to the top,
# leave celery as pick,
# and change carrot, tomato, and onion action to "squash"
# finally, in commit message editor,
# remove all lines and write a single commit line for the squashed veg commits:
# add carrot, tomato, onion
# amend
# affects only the most recent commit.
# useful if you forgot to include a file in the last commit or
# if you have a minor change which you want to add to the last commit.
printf "papaya\n" >> fruits.txt
printf "broccoli\n" >> vegetables.txt
git add fruits.txt
git commit -m 'added papaya'
git log --all --graph --oneline --decorate
git add vegetables.txt
git commit --amend -m 'added papaya, broccoli'
# if you don't want to change the commit message, use arg --no-edit.
git log --all --graph --oneline --decorate
##########
# misc
##########
# "*" vs "." vs "-A"
# "git add *" is intercepted by the shell and
# git gets a list of files in the current dir.
# the list will not include files that begin with ".".
# "git add ." is not intercepted by the shell,
# and git will interpret it to mean all files in the current dir and down.
# https://stackoverflow.com/q/26042390/2512141
# "git add -A" adds all changes from repo root down,
# regardless of where it is called.
# "origin/master" vs "origin master"
# roughly, "origin/master" is a branch ref which refers to a
# local copy of a remote branch and is used in local operations,
# while "origin master" is used in commands that
# actually change the remote repo.
# git checkout vs git reset
# git checkout moves only HEAD, and copies contents
# of addressed commit to staged and working.
# git reset moves HEAD and HEAD's tag, then copies to staged
# and working, depending on flags.
# git reset can alter pushed history (use caution).
# https://stackoverflow.com/q/3639342/2512141
# .gitignore syntax
# a "/" at the beginning or middle of an ignore pattern means the pattern
# will use the location of the .gitignore file as root.
# patterns without a "/" at the beginning or middle apply everywhere.
# patterns with "/" at the beginning or middle can be made to apply anywhere
# by preceding with "**".
# it is possible to use multiple .gitignore files in a repo.
# *.txt
# ignore all files that end with ".txt".
# foo
# ignore all files and directories named "foo".
# foo/
# ignore all directories named "foo".
# /foo
# ignore file or directory named "foo" in repo root.
# /foo/bar
# ignore file or directory "bar" in repo root directory "foo"
# **/foo/bar
# ignore all files and directories "bar" which live in a dir "foo".
# not covered
# stashing
# patching
# switch and restore
# experimental functions added Aug 2019 in git 2.23.
# https://github.blog/2019-08-16-highlights-from-git-2-23/
# https://www.infoq.com/news/2019/08/git-2-23-switch-restore/
# https://git-scm.com/docs/git-restore
# in short, functionality of git checkout is split:
# switch changes branches and restore reverts files.
# cherry pick
# was going to cover, then found this series of blog posts:
# https://devblogs.microsoft.com/oldnewthing/?p=98245
# omitting until i have understood the warning.
# create a tag and push to remote
cd $LOCA
git checkout master
printf "tangerine\n" >> fruits.txt
git add -A
git commit -m 'added tangerine'
git push
git tag -a tangerineTag -m 'tagging tangerine'
git push --tags
git log --all --graph --oneline --decorate
git ls-remote --tags remUp
# git tag -r would have been more natural here...
# remove a tag and delete from remote
git tag -d tangerineTag
git push --delete remUp tangerineTag
git log --all --graph --oneline --decorate
git ls-remote --tags remUp
##########
# script cleanup
##########
echo 'goodbye'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment