git architecture
- git directories
- working dir - single checkout of one version of the project
- staging area - a simple file in git dir, stores info about next commit
- git dir - git metadata and object database for projects, most important part of git
sequenceDiagram
participant WD as working dir
participant SA as staging area
participant GD as git dir
GD->>WD: checkout the project
WD->>SA: stage files
SA->>GD: commit
- file states
- untracked - not any of the 3 below, totally outside of git control
- unmodified - data is committed and safely stored in git dir
- modified - file from git dir is in working folder and has been modified, but not staged or committed
- staged - file from git dir is in working folder and has been modified, and is marked in current version to go into next commit snapshot
sequenceDiagram
participant UT as untracked
participant UM as unmodified
participant MO as modified
participant ST as staged
UT->>UM: add the file
UM->>MO: edit the file
MO->>ST: stage the file
UM->>UT: remove the file
ST->>UM: commit
configuration files/commands
# /etc/gitconfig - contains values for every user and all their repositories - "--system" option
# ~/.gitconfig - specific to your user - "--global" option
# .git/config - specific to single repository
# config identity:
git config --global user.name "First Last"
git config --global user.email "firstlast@example.com"
# config editor:
git config --global core.editor vim
# config diff tool:
git config --global merge.tool vimdiff
# show config options:
git config --list or git config -l
git config user.name
creating repositories
# create initial repository
git init
# create a directory called 'grit'
# initialize .git directory inside of it
# pull all the data from repository
# and check out working copy of latest version
git clone git://github.com/schacon/grit.git
# or use a different dir name:
git clone git://github.com/schacon/grit.git mygrit
# clone from a dir/project in current dir called "project1" to a new dir/project
# called "project1-temp":
git clone project1 project2
adding files
# add - dual purpose command to add new files or stage modified files
# - any time a file is modified it has to be staged again
# - staging area only contains file versions from when files were last staged
git add hello_world.c
# add directories recursively
git add some_directory
git add .
git add *.c
git add README
git commit -m 'initial project version'
# each additional -m and corresponding message adds a new paragraph to the commit
# stage all new, modified and deleted files
git add -A
# almost same as git add -A, as follows:
# if in working directory, "git add -A" and "git add ." work the same
# if in a subdirectory of working directory, "git add -A" will add all files from the entire working directory,
# "git add ." will only add files from your current directory and its sub-dirs
git add .
unstaging files
# restoring a modified file to most recent commit version
git restore /path/to/file
# unstage a file:
git reset HEAD hello_world.c
# remove a file from git and remove it from directory also (still have to run commit to complete removal)
git rm hello_world.c
# remove a file from git but leave it in directory
# for example, if it's a file that should be an ignored file but was accidentally added
git rm --cached hello_world.c
git rm -r --cached _admin/ # recursively remove an entire dir from git, but leave it in main dir
renaming files
# rename a file in the directory and in git (still have to commit afterwards)
git mv hello_world.c hello_world2.c
committing files
# commit - takes snapshot of the staging area
# - any files changed since last git add don't capture those changes
# - only changes in staging area are captured
# - each commit represents a new snapshot of the project
# commit anything in staging area (launches editor of choice for commit message entry):
git commit
# the contents of editor contains latest output of git status in comments, can leave comments or take them out
# include diff of changes in comments:
git commit -v
# to type commit message inline:
git commit -m "commit message"
# stage every file that is already tracked and commit it, all in one step:
git commit -a -m "some commit message"
# add a file that should have been included with most recent commit:
git add hello_world.c
git commit --amend
# change message of most recent commit, if no other files added:
git commit --amend -m 'updated message'
# rollback most recent commit:
git reset HEAD~
simple workflow
# configure settings:
git config --global user.email "rvigil@roylu.com"
git config --global user.email "rvigil@roylu.com"
# add first file:
git add file1.txt
# commit first file:
git commit -m "initial commit"
# status shows 3 types of files: tracked, ignored, untracked:
git status
viewing commit history
# list commits in reverse chronological order: (each commit is represented by a sha-1 checksum)
git log
# limit output to last 2 entries: (can enter any number here)
git log -2
# list each commit on one line (limit to last 25 commits):
git log --pretty=oneline -25
# show graph of merge/branch activity, with one commit per line:
git log --pretty=oneline --graph
git log --graph --oneline --decorate --all
# search thru all commits
git log --pretty=oneline -S search_term
git log --pretty=oneline -S 'powered by'
# show all files in a given commit (using first 8 char's of commit hash)
git show --pretty="" --name-only bd61ad98
ignoring files
file patterns in file .gitignore will be ignored by git, each line in file can be a file pattern: *.[oa] *~
common .gitignore patterns
# Build output
build/
dist/
# Python
__pycache__/
*.pyc
.venv/
# OS junk
.DS_Store
Thumbs.db
# Editors
*.swp
*.swo
*~
.vscode/
.idea/
three levels of ignoring files
.gitignore— committed to the repo, visible to everyone. Use for generic patterns (build output, OS junk, editor files)..git/info/exclude— local-only, never committed. Use for project-specific private patterns you don’t want others to see.~/.gitignore_global— applies across all repos, lives outside any project.- use for sensitive file patterns (work documents, credentials) that should never appear in any repo or
.gitignore. .gitignoreis tracked, so any patterns and comments in it are visible in a public repo.- avoid putting private filenames or paths in
.gitignore— use.git/info/excludeor the global gitignore instead.
- use for sensitive file patterns (work documents, credentials) that should never appear in any repo or
# one-time setup for global gitignore
git config --global core.excludesFile ~/.gitignore_global # then add patterns to ~/.gitignore_global
# verifying .gitignore before first commit
git add --dry-run # to preview exactly what would be tracked, before actually staging anything:
# 1. create .gitignore first, then init the repo
git init
# 2. dry-run — shows what WOULD be staged (respects .gitignore)
git add -A --dry-run
# 3. same thing but as a tree view (pipe file list into tree)
git add -A --dry-run | sed "s/^add '//;s/'$//" | tree --fromfile -a
# 4. if unwanted files appear, edit .gitignore and re-run the dry-run (no need to delete the repo — nothing has been staged yet)
# 5. once the list looks right, stage and commit for real
git add -A
git commit -m "initial commit"
checking out files
# checkout a particular commit:
git checkout (sha-1 checksum)
# replace a modified file with most recent version in git:
git checkout hello_world.c
tags
- tag - a meaningful name given to a specific commit, for example: v1, v2
- tags are lightweight, they’re just a pointer to a specific commit
- tags are exactly like branches but tags don’t move, branches do
# list all tags:
git tag
# list tags that match a pattern:
git tag -l 'v1.4*'
# annotated tags - stored as full objects in git db,
# contain more meta data (like a message) than lightweight tags
# crating an annotated tag:
git tag -a v1.4 -m 'this is version 1.4'
# show all data for the annotated tag:
git show v1.4
# create a lightweight tag:
git tag v1.4
# create a tag on an earlier commit, just specify enough of the checksum
git tag v1.3 9fcdb02
# show message along with tag
$git tag -n
branches
-
branching - diverting from the main line of development and making changes without messing up the main line
-
branch - a pointer to some commit object, the branch can be moved to other commit objects
-
commit - a lightweight, movable pointer to a snapshot, the commit pointer also points to the previous parent commit (or commits)
-
MASTER - the default branch, with every commit, the master branch (pointer) moves to the most recent commit
-
HEAD - a special pointer called HEAD that points to the currently active branch (which itself points to a specific commit)
- Usually points to MASTER unless HEAD is redirected
- The active branch automatically follows new commits as they’re committed
-
MASTER - a branch that automatically moves from commit to commit as commits happen
- A user created branch points to the commit desired by user and can be moved to other commits
- note: you can only switch branches with a clean working directory, there can be no pending changes
#create new branch that points to same commit as master branch:
git branch (new-branch)
# create new branch pointing to a specific commit:
git branch (new-branch) (commit)
# note that creating a branch to a specific commit doesn't automatically switch to that branch
# the HEAD pointer doesn't automatically point to the new branch
# switch to the new or different branch:
git checkout (new-branch)
# HEAD now points to (new-branch)
# but if we change a file and commit, master and (new-branch)
# will no longer point to the same commit,
# master will be one commit behind
# (new-branch) branch will be pointing to most recent commit,
# and HEAD will be pointing to (new-branch)
# switch back to master branch:
git checkout master
# this will move HEAD back to master, and will revert the working directory with
# the snapshot from the commit that master points to
# making a change at this point will create a divergence in the history "tree" in git db
# this can be resolved with a merge
# create a branch and switch to it:
git branch -b (new-branch)
# create a branch pointing to a specific commit and switch to it:
git branch -b (new-branch) (commit)
# delete a branch:
git branch -d (new-branch)
# sometimes you just need a branch temporarily, like when you've merged changes back into master,
# may not need branch anymore
# merge into current branch the diff's from (branch-name):
git merge (branch-name)
# get list of current branches:
git branch
# the branch with the asterisk (*) will be the currently checked out branch
# show last commit on each branch:
git branch -v
# show all the commits that HEAD has pointed to:
git reflog
rebasing
- rebasing - taking the patch of a change for a given branch, and re-applying it to another branch
- Take all the changes that were committed on one branch, and apply them to another branch
- There’s always 2 branches involved, the source branch you’re on with the changes, and the target branch that will be rebased
- First rebase goes to the common ancestor commit of the 2 branches and it gets all the diffs of the commits from the ancestor to the current source branch.
- then it applies those commits to the target branch
- rebasing just makes for a cleaner history than with a regular merge. The history looks linear. It makes all the work appear as though it happened in series, although it may have happened in parallel branches
other
# view all tags
git tag
# view all remotes
git remote -v
# view all branches
git branch -a
# view current branch
git rev-parse --abbrev-ref HEAD
# delete a remote
git remote rm
# delete a branch
git branch -d
# tig - text mode interface for git
Github
github workflow
new/from scratch repo
# create empty github repo
# local
git init
git add -A
git commit -m 'initial commit'
# push to new github repo
git remote add origin git@github.com:robertvigil/markdown-to-docs.git
# force-rename current branch to main, shorthand for --move --force — renames even if a branch called main already exists (overwrites it)
# typically used after git init to rename the default master branch to main
git branch -M main
git push -u origin main # the -u flag from initial push sets up tracking, git push knows where to go from now on
# ongoing after initial push
git add <file>
git commit -m "message"
git push # push to github
clonsed repo
# Clone the public repo into the new local directory
git clone git@github.com:robertvigil/markdown-to-docs-actions.git markdown-to-docs-private
cd markdown-to-docs-private
# Point origin to your new private repo
git remote set-url origin git@github.com:robertvigil/markdown-to-docs-private.git
# Push (sets up tracking)
git push -u origin main
# Optional: add upstream to pull future workflow updates
git remote add upstream git@github.com:robertvigil/markdown-to-docs-actions.git
# get public repo updates, only touches files that exist in the public repo (basically just .github/workflows/build.yml and README.md)
# documents in src/ won't be affected since they don't exist upstream
# if both repos changed same file (like README.md), you'd get merge conflict to resolve manually, but wouldn't need README in private repo
git fetch upstream
git merge upstream/main
gitHub gh client
# https://cli.github.com/manual/
#
$ git config --global user.name "firstlast"
$ git config --global user.email "firstlast@example.com"
$ git config -l
# settings -> developer settings -> personal access tokens -> classic -> generate token
# permissions: checked all boxes for: repo, admin.org, and workflow
# install github client
$ sudo apt install gh
$ gh auth login # sets github credentials/to avoid constant git credential requests
# these not in strict order, just for reference, some may need sudo based on location
$ 'echo "hello there!!"' > sayhello.sh
$ git init
$ git config --global --add safe.directory /mnt/c/users/rvigi/dropbox/code/github/helloworld
$ git add sayhello.sh
$ git commit -m 'initial commit'
$ gh repo create helloworld --private
$ git remote add origin https://github.com/robertvigil/helloworld
$ git push --set-upstream origin master
$ git push
$ gh repo list # list all repos
$ gh repo delete <repo>
quick setup — if you’ve done this kind of thing before
- https or ssh:
- git@github.com:robertvigil/notes.git
- Get started by creating a new file or uploading an existing file. We recommend every repository include a README, LICENSE, and .gitignore.
…or create a new repository on the command line (from GitHub)
echo "# notes" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
# create an empty "notes" repository on github first
git remote add origin https://github.com/robertvigil/notes.git
# need to generate a personal token on github, and use it as the password when pushing
git push -u origin main
…or push an existing repository from the command line (from GitHub)
git remote add origin git@github.com:robertvigil/notes.git
git branch -M main
git push -u origin main
…or import code from another repository (from GitHub)
- You can initialize this repository with code from a Subversion, Mercurial, or TFS project.
github/gitpod/repl.it
Take any github repo, prepend the url with https://gitpod.io/#, and hit Enter
https://github.com/robertvigil/hello-world -> https://gitpod.io/#https://github.com/robertvigil/hello-world
That’ll open up an instance of VSCode in a web browser and you can start coding
Repl.it - the URL structure is repl.it/github/(GitHub path)
https://repl.it/github/robertvigil/hello-world/test.py
(You can even run it on your phone!)
github codespaces
- Default codespaces container
- Custom codespaces container
github pages
# create repo called <user>.github.io -> robertvigil.github.io
cd /media/sf_dropbox/code/_production
git clone https://github.com/robertvigil/robertvigil.github.io
cd robertvigil.github.io
# add index.html, styles.css, update .gitconfig..
git init
git add .
git commit -m "initial commit"
git remote add origin https://github.com/robertvigil/robertvigil.github.io.git
git push -u origin master
git ssh workflow
# - ssh keys are stored globally (not per-repository)
# - copy ~/.ssh/id_rsa.pub contents to github profile ssh keys
# - location: ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub (or similar)
# - these stay on your system regardless of what you do with git repos
# - when you clone again, git automatically:
# - detects the ssh url in the clone command
# - uses your existing ssh keys
# - sets up origin automatically
# example workflow:
# delete old folder:
rm -rf /path/to/othri
# clone again (ssh url)
git clone git@github.com:robertvigil/othri.git
# check remotes - already set up automatically!
cd othri
git remote -v
# origin git@github.com:robertvigil/othri.git (fetch)
# origin git@github.com:robertvigil/othri.git (push)
# push works immediately
git push origin main
# only need to reconfigure SSH if:
# 1. you clone with HTTPS URL instead of SSH (wrong - uses HTTPS)
git clone https://github.com/robertvigil/othri.git
git remote set-url origin git@github.com:robertvigil/othri.git # fix it
# 2. you lose your SSH keys (computer crashes, new machine, etc.)
# - then you'd need to generate new SSH keys and add them to GitHub
ssh -T git@github.com # verify your SSH keys are working:
# expected output:
# Hi robertvigil! You've successfully authenticated, but GitHub does not provide shell access.
# TL;DR: SSH keys are global, origin is set automatically when cloning. Delete and re-clone as many times as you want - it just works.
squash repo
# clone repo
git clone ssh://git@github.com/robertvigil/othri.git othri # example
# 1. create a new orphan branch (fresh start with no history)
# --orphan - new unborn branch, switch to it; first commit made on new branch will has no parents,
# is root of new history, and disconnected from all other branches and commits
git checkout --orphan new-main
# 2. stage all files
git add -A
# 3. create single commit for repo
git commit -m "initial commit"
# or
git commit -m "initial commit: Wiki.js Secure Assets System
Group-based authentication for Wiki.js assets using JWT tokens,
PostgreSQL, and Nginx auth_request module."
# or
git commit -m "initial commit: Wiki.js JWT Authentication Service
Generates Wiki.js-compatible JWT tokens for external applications
using RS256 signing, PostgreSQL authentication, and Express REST API."
# 4. delete old branch
git branch -D main
# 5. rename new branch to main
git branch -m main
# 6. force push to GitHub
git push -f origin main
# - important notes:
# - this completely rewrites history, so use -f (force) when pushing
# - make sure you're in the repository directory: /dir1/dir2/some-repo
# - the -f flag is safe here since this is your repo and you want to replace the messy history
# alternative method (if you want to keep some history):
# interactive rebase to squash last N commits
git rebase -i HEAD~N # Replace N with number of commits
# In the editor that opens, change "pick" to "squash" (or "s")
# for all commits except the first one, then save
# The orphan branch method is cleaner for a complete fresh start.