Chapter 1 · Git and GitHub Like a Professional
Advanced Git: stash, rebase, cherry-pick, reflog and tags
- Page 1 of 22
- 25 min read
In the beginner tutorial you learned the daily loop: edit, add, commit, branch, merge, push. That loop is enough until real work gets messy. You are halfway through an experiment when an urgent fix arrives. Your branch has nine commits called "wip" and a teammate will review it tomorrow. You ran git reset --hard and the last two hours of work vanished. A model went into production and nobody can say which commit produced it.
This page is the toolbox for those moments: stash, interactive rebase, cherry-pick, reflog, tags and safe history rewriting. In AI projects history is also part of your experiment log: which prompt, which chunk size, which embedding model gave which result. A clean, trustworthy history is worth the effort. Every command here runs for real, so follow along in a terminal (Git Bash or WSL on Windows).
What you will learn
- Park unfinished work with
git stash(push, list, apply, pop, drop) and bring it back. - Clean a branch with interactive
rebase: reorder, squash/fixup and reword commits, and run it without an editor. - Copy single commits between branches with
cherry-pick. - Recover "lost" commits and deleted branches with
reflog. - Mark releases with annotated tags, and rewrite pushed history safely with
--force-with-lease. - Decide between merge and rebase with a simple rule.
A small repository to practise on
We need a repository with some history. It is a tiny retrieval project for a RAG (retrieval-augmented generation) service. Nothing here needs a remote yet.
mkdir -p ~/projects/rag-notes
cd ~/projects/rag-notes
git init -q
cat > retriever.py <<'EOF'
def top_k(scores, k=3):
return sorted(scores, reverse=True)[:k]
EOF
echo "# rag-notes" > README.md
echo "embedding_model = embed-small" > config.txt
git add .
git commit -q -m "Add retriever, README and config"
echo "chunk_size = 500" >> config.txt
git commit -q -am "Set chunk size"
git log --oneline0d752d3 Set chunk size
1ab9c13 Add retriever, README and configYour hashes will differ from the ones printed here, because a hash depends on the author, the time and the parent. Always use your own.
git stash: park work without committing
You are changing the embedding model and adding a reranker when someone asks for a one-line fix in the README. Your changes are not ready to commit, and you do not want them in the fix. git stash puts every modified tracked file away and gives you a clean working tree.
sed -i 's/embed-small/embed-large/' config.txt
printf 'def rerank(hits):\n return hits\n' >> retriever.py
echo "scratch notes" > notes.tmp
git status --short
git stash push -m "try large embeddings"
git status --short
git stash list M config.txt
M retriever.py
?? notes.tmp
Saved working directory and index state On main: try large embeddings
?? notes.tmp
stash@{0}: On main: try large embeddingsTwo things to notice. The stash carries the message you gave it, and notes.tmp was not stashed: by default only tracked files go away (add -u to include untracked files, -a for ignored ones too). Now do the urgent fix, then bring the work back:
echo "Run: python retriever.py" >> README.md
git commit -q -am "Document how to run"
git stash apply -q
git stash list
git stash show
git status --shortstash@{0}: On main: try large embeddings
config.txt | 2 +-
retriever.py | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
M config.txt
M retriever.py
?? notes.tmpapply restores the changes but keeps the stash, which is handy when you want the same change on two branches. git stash pop is apply followed by drop. Drop the stash now that it is applied:
git stash drop -q
git stash list
git status --short M config.txt
M retriever.py
?? notes.tmpThe empty stash list shows nothing is parked. Now the same with an untracked file, and pop. The first status prints nothing (the tree is clean, even notes.tmp is parked); the second shows everything back:
git stash push -u -q -m "embedding experiment with notes"
git status --short
git stash pop -q
git status --short M config.txt
M retriever.py
?? notes.tmp| Command | What it does |
|---|---|
git stash push -m "text" | Stash tracked changes with a label (plain git stash works too, without a label) |
git stash push -u | Include untracked files; -a also ignored files |
git stash push -- path | Stash only the given files |
git stash list | Show all stashes, newest first as stash@{0} |
git stash show -p stash@{1} | Show the contents of a stash as a diff |
git stash apply [stash@{n}] | Re-apply, keep the stash |
git stash pop | Re-apply the newest stash and drop it (if it conflicts, the stash is kept) |
git stash drop / git stash clear | Delete one stash / all stashes |
git stash branch new-name | Make a branch from the commit the stash was made on and apply it there (useful when pop conflicts) |
A stash is a safe place for hours, not weeks. Stashes belong to your local repository, are never pushed, and are easy to forget. If the work matters, commit it on a branch (
git switch -c wip/embeddings); you can rewrite that commit later.
That is what we do with the experiment now: commit it on its own branch, which we need for the next section.
git switch -q -c feature/reranker
git add retriever.py config.txt
git commit -q -m "Add reranker stub"
git switch -q main
git branch feature/reranker
* mainInteractive rebase: tidy a branch before others see it
While working, commits are small and honest: "wip", "fix typo", "try again". Reviewers and your future self want a story: one commit per idea. git rebase -i (interactive rebase) lets you edit the story of a branch: reorder, merge and reword commits. It does so by replaying the commits on top of a base and creating new commits, so hashes change. That is why you only rewrite commits that nobody else has built on.
Let us add some messy commits to the reranker branch:
git switch -q feature/reranker
echo "TOP_N = 5" > rerank_config.py
git add rerank_config.py && git commit -q -m "wip"
printf 'def test_rerank():\n assert rerank([1, 2]) == [1, 2]\n' > test_rerank.py
git add test_rerank.py && git commit -q -m "Add rerank tests"
sed -i 's/rerank(hits)/rerank(hits, top_n=5)/' retriever.py
git commit -q -am "fix typo in reranker"
echo "TOP_N = 8" > rerank_config.py
git commit -q -am "wip again"
git log --oneline main..HEAD80ba815 wip again
f79fb55 fix typo in reranker
5c58e67 Add rerank tests
c81ef69 wip
f39e1d4 Add reranker stubFive commits, but the story is "add the reranker" and "add its tests". When you run git rebase -i main, Git opens your editor on a todo list: one line per commit, oldest first (the opposite of git log), each starting with a command. You can print the lines Git builds:
git log --reverse --format='pick %h %s' main..HEADpick f39e1d4 Add reranker stub
pick c81ef69 wip
pick 5c58e67 Add rerank tests
pick f79fb55 fix typo in reranker
pick 80ba815 wip againThe real file also has a block of comment lines explaining the commands. You edit the list, save and close, and Git carries it out from top to bottom. The commands you need every day:
| Command | Short | Effect |
|---|---|---|
pick | p | Keep the commit as it is |
reword | r | Keep the changes, stop to edit the message |
squash | s | Merge into the commit above; combine both messages in the editor |
fixup | f | Merge into the commit above and throw this message away |
edit | e | Stop after this commit so you can change it or split it |
drop | d | Delete the commit (or delete the line) |
Reordering means moving lines. Squashing means changing pick to squash or fixup on the line of the commit that should melt into the one above it. In a real session you do this by hand in the editor. To make every step reproducible here, we replace the editor with a one-line sed script through GIT_SEQUENCE_EDITOR (the editor for the todo list; GIT_EDITOR is the editor for commit messages). On macOS, sed -i needs an empty suffix argument (sed -i ''); Git Bash on Windows uses GNU sed and works as written. We add -q to the rebase only to hide its progress lines.
Step 1: reorder
The "fix typo" commit belongs next to the first reranker commit, but a "wip" commit and the tests sit between them. The todo list has five lines, and we want the order 1, 4, 2, 3, 5: take lines 2 and 3 (hold them) and put them after line 4.
GIT_SEQUENCE_EDITOR="sed -i -e '2,3{H;d}' -e '4G'" git rebase -q -i main
git log --oneline main..HEAD467cfc4 wip again
4d4839f Add rerank tests
928ba4d wip
ae6ccbd fix typo in reranker
f39e1d4 Add reranker stubStep 2: squash the noise
Now "fix typo" follows the first commit. Mark both "wip" commits and the typo fix as fixup, so they melt into the commit above them:
GIT_SEQUENCE_EDITOR="sed -i -e '/ wip/s/^pick/fixup/' -e '/ fix typo/s/^pick/fixup/'" git rebase -q -i main
git log --oneline main..HEAD67891bd Add rerank tests
5f06968 Add reranker stubStep 3: reword
The first commit now contains the stub, the config and the typo fix, so its old message is too small. Change it with reword. Git opens the message editor; here a second sed replaces the message:
GIT_SEQUENCE_EDITOR="sed -i '1s/^pick/reword/'" GIT_EDITOR="sed -i '1s/.*/Add reranker with config and top-n parameter/'" git rebase -q -i main
git log --oneline main..HEAD[detached HEAD a787fdf] Add reranker with config and top-n parameter
Date: Thu Jan 15 10:00:00 2026 +0600
3 files changed, 4 insertions(+), 1 deletion(-)
create mode 100644 rerank_config.py
f1a9944 Add rerank tests
a787fdf Add reranker with config and top-n parameterTwo clean commits instead of five. If you get stuck mid-rebase (a conflict while replaying), the three exits are: fix the files, git add them and git rebase --continue; skip the commit with git rebase --skip; or go back to where you started with git rebase --abort. The safety net is always the same: nothing is lost until the reflog expires, as you will see shortly.
Rebase also works to update a branch:
git rebase mainreplays your commits on top of the newestmain, which keeps history in a straight line.git commit --fixup <hash>plusgit rebase -i --autosquash mainautomates the "fix typo" pattern: Git places the fixup line for you.
Merge the tidy branch into main. Git could simply move main forward (a fast-forward), but --no-ff forces a merge commit that records where the feature began and ended; this is also what the green "Merge" button on GitHub does.
git switch -q main
git merge -q --no-ff --no-edit feature/reranker
git log --oneline --graph* d7477e6 Merge branch 'feature/reranker'
|\
| * f1a9944 Add rerank tests
| * a787fdf Add reranker with config and top-n parameter
|/
* f9d7ed2 Document how to run
* 0d752d3 Set chunk size
* 1ab9c13 Add retriever, README and configcherry-pick: take one commit, not the whole branch
Sometimes only one commit of another branch is wanted: a bug fix hidden in an experiment, or a hotfix that must land on a release branch. git cherry-pick copies a commit's changes and creates a new commit (new hash) on your current branch. Here an experiment branch contains a BM25 baseline (a classic keyword-search scorer) and a chunk-size change; we want only the chunk size.
git switch -q -c experiment
printf 'def bm25(query, docs):\n return docs\n' > bm25.py
git add bm25.py && git commit -q -m "Add BM25 baseline"
sed -i 's/chunk_size = 500/chunk_size = 300/' config.txt
git commit -q -am "Tune chunk size to 300"
git switch -q main
git cherry-pick -x experiment
git log --oneline -3
git show -s --format=%B HEAD[main a4e95a1] Tune chunk size to 300
Date: Thu Jan 15 10:00:00 2026 +0600
1 file changed, 1 insertion(+), 1 deletion(-)
a4e95a1 Tune chunk size to 300
d7477e6 Merge branch 'feature/reranker'
f9d7ed2 Document how to run
Tune chunk size to 300
(cherry picked from commit 54f24a418301449f518e2d1548e4d7b752961d9d)Notice the extra line (cherry picked from commit ...) added by -x: it records where the commit came from, which helps when you backport a fix to old release branches. Plain cherry-pick leaves no such trace, so use -x when the source commit is public.
| Form | Use |
|---|---|
git cherry-pick A B C | Several commits, in the order given |
git cherry-pick A^..B | The range from A to B, inclusive |
git cherry-pick -n A | Apply the change but do not commit (to combine several) |
git cherry-pick --continue / --abort | After a conflict: finish, or undo the whole attempt |
git cherry-pick -m 1 <merge-commit> | A merge commit needs -m to say which parent is the mainline |
Cherry-picking duplicates a change: the same change now exists as two commits. If you later merge the branches Git usually copes, but do not make a habit of it. If a whole branch is needed, merge it.
reflog: the undo button for lost commits
Git keeps a private diary, the reflog, of every place HEAD and each branch has pointed to in this repository during the last weeks. A commit that no branch contains any more is not deleted at once; the reflog still knows its hash. This is why Git is hard to break. Let us break it:
git reset --hard HEAD~2
git log --oneline
git reflog -6HEAD is now at f9d7ed2 Document how to run
f9d7ed2 Document how to run
0d752d3 Set chunk size
1ab9c13 Add retriever, README and config
f9d7ed2 HEAD@{0}: reset: moving to HEAD~2
a4e95a1 HEAD@{1}: cherry-pick: Tune chunk size to 300
d7477e6 HEAD@{2}: checkout: moving from experiment to main
54f24a4 HEAD@{3}: commit: Tune chunk size to 300
a851e07 HEAD@{4}: commit: Add BM25 baseline
d7477e6 HEAD@{5}: checkout: moving from main to experimentThe reset threw away the cherry-picked commit and the merge of the reranker. The log looks sad, but the reflog shows HEAD@{1}, where we stood one move ago. Each line is a move: HEAD@{n} means "where HEAD was n moves ago". Go back:
git reset --hard 'HEAD@{1}'
git log --oneline -4HEAD is now at a4e95a1 Tune chunk size to 300
a4e95a1 Tune chunk size to 300
d7477e6 Merge branch 'feature/reranker'
f9d7ed2 Document how to run
f1a9944 Add rerank testsEverything is back. The same trick rescues a deleted branch, the most common panic:
git switch -q -c scratch-eval
echo "eval_set = qa_v1.jsonl" > eval.cfg
git add eval.cfg && git commit -q -m "Add evaluation set config"
git switch -q main
git branch -D scratch-eval
git reflog -3
git branch scratch-eval 'HEAD@{1}'
git log --oneline -1 scratch-evalDeleted branch scratch-eval (was d92bd3b).
a4e95a1 HEAD@{0}: checkout: moving from scratch-eval to main
d92bd3b HEAD@{1}: commit: Add evaluation set config
a4e95a1 HEAD@{2}: checkout: moving from main to scratch-eval
d92bd3b Add evaluation set configYou can also use any hash from the reflog directly: git branch rescue 1a2b3c4 or git cherry-pick 1a2b3c4. The boundaries are important:
- The reflog is local. A fresh clone has no history of your mistakes, and it is not pushed.
- Entries expire: 90 days for commits still reachable from a branch, 30 days for unreachable ones (
gc.reflogExpire,gc.reflogExpireUnreachable). After thatgit gcmay delete them. - Only committed work (and stashed work) can be recovered. A file you never added or committed and then overwrote with
git restoreorreset --hardis gone. Commit early, even if you rewrite it later. - For a dropped stash or an orphaned object with no reflog entry,
git fsck --lost-foundlists dangling commits.
Tags and releases
A branch name moves with every commit. A tag is a name that stays on one commit forever, the right tool for "this is version 0.2.0" or "this is what was deployed on Monday". Git has two kinds:
git tag v0.1.0 main~3
git tag -a v0.2.0 -m "Release 0.2.0: reranker and chunk size 300"
git tag
git cat-file -t v0.1.0
git cat-file -t v0.2.0
git tag -nv0.1.0
v0.2.0
commit
tag
v0.1.0 Set chunk size
v0.2.0 Release 0.2.0: reranker and chunk size 300A lightweight tag is only a name pointing to a commit (type commit). An annotated tag (-a) is a real Git object (type tag) with a tagger, a date and a message, and it can be signed. Releases should be annotated:
git show v0.2.0 -s
git describe
git describe --tags HEAD~1tag v0.2.0
Tagger: Nadia Rahman <nadia@example.com>
Date: Thu Jan 15 10:00:00 2026 +0600
Release 0.2.0: reranker and chunk size 300
commit a4e95a1eb6123fb38885eec926847de7b16c82db
Author: Nadia Rahman <nadia@example.com>
Date: Thu Jan 15 10:00:00 2026 +0600
Tune chunk size to 300
(cherry picked from commit 54f24a418301449f518e2d1548e4d7b752961d9d)
v0.2.0
v0.1.0-4-gd7477e6git describe turns a commit into a human-readable version: the nearest tag, the number of commits since it, and the short hash (an exact match prints only the tag). Put it in your build and your /health endpoint, and a bug report can always be traced back to an exact commit. Without --tags it only sees annotated tags.
| Lightweight | Annotated | |
|---|---|---|
| Create | git tag v1 | git tag -a v1 -m "message" |
| Stores | A name for a commit | Tagger, date, message (and optionally a signature) |
| Use for | Private bookmarks | Releases, deployments, model versions |
Sent by git push --follow-tags | No | Yes |
Tags are not pushed by a plain git push. Send one with git push origin v0.2.0, all tags with git push --tags, or only the annotated tags that point into the pushed commits with git push --follow-tags. Delete a wrong tag locally with git tag -d v0.2.0 and on the remote with git push origin --delete v0.2.0; but a tag that others have fetched should be treated as permanent. Release a fixed version instead of moving the tag.
A release on GitHub is a tag plus a title, notes and optional downloadable files. You create it in the web page under Releases then Draft a new release, or with the GitHub CLI (not run in this tutorial because it needs a real GitHub account):
gh release create v0.2.0 --title "0.2.0" --generate-notesVersions follow semantic versioning MAJOR.MINOR.PATCH: bump the major number for breaking changes (an API field removed), the minor for compatible features (a new endpoint), the patch for fixes. For machine-learning projects it also helps to tag what a release contains: model-2026-01 next to the code tag, and the data version if you use DVC (two pages from now).
Rewriting history safely: force-with-lease
Everything above rewrote only local history. Once a commit is pushed, rewriting it means the remote branch must be replaced, and a normal push refuses. GitHub is, from Git's point of view, just another repository you push to; so we use a local bare repository as the server (a bare repository has no working files, only history, which is exactly what a GitHub repository is).
git init -q --bare ~/remotes/rag-notes.git
git remote add origin ~/remotes/rag-notes.git
git push -q -u origin main --tags
git switch -q -c feature/hybrid-search
echo "alpha = 0.5" > hybrid.cfg
git add hybrid.cfg && git commit -q -m "Add hybrid search weights"
echo "alpha = 0.7" > hybrid.cfg
git commit -q -am "wip"
git push -q -u origin feature/hybrid-search
git status -sb## feature/hybrid-search...origin/feature/hybrid-search
?? notes.tmpThe branch is shared now (Nadia pushed it for a pull request). Reviewers do not like a "wip" commit, so squash it, then try a normal push:
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/fixup/'" git rebase -q -i main
git log --oneline main..HEAD
git push origin feature/hybrid-search 2>&1 | grep -v '^hint'e89c33e Add hybrid search weights
To /home/nadia/remotes/rag-notes.git
! [rejected] feature/hybrid-search -> feature/hybrid-search (non-fast-forward)
error: failed to push some refs to '/home/nadia/remotes/rag-notes.git'Git refuses: the remote branch has two commits, your branch has a different one, so this is not a fast-forward. The cure that comes to mind, git push --force, overwrites the remote branch whatever is on it. If a colleague pushed in the meantime, you would erase their work without knowing. --force-with-lease is the safer cousin: it forces only if the remote branch is still where your last fetch saw it. Let us see it protect a colleague. Sam (a teammate with their own clone) pushes a review fix to the same branch:
git clone -q ~/remotes/rag-notes.git ~/projects/rag-notes-sam
cd ~/projects/rag-notes-sam
git switch -q feature/hybrid-search
echo "# alpha is tuned on qa_v1" >> hybrid.cfg
git commit -q -am "Document alpha"
git push -q origin feature/hybrid-search
cd ~/projects/rag-notes
git push --force-with-lease origin feature/hybrid-search 2>&1To /home/nadia/remotes/rag-notes.git
! [rejected] feature/hybrid-search -> feature/hybrid-search (stale info)
error: failed to push some refs to '/home/nadia/remotes/rag-notes.git'Rejected with stale info: Nadia's idea of the remote was out of date, so the lease refused. Fetch, look at what is there, carry Sam's commit over, then push with the lease again:
git fetch -q
git log --oneline origin/feature/hybrid-search
git cherry-pick origin/feature/hybrid-search
git push --force-with-lease origin feature/hybrid-search 2>&10b07427 Document alpha
f065b39 wip
3155e98 Add hybrid search weights
a4e95a1 Tune chunk size to 300
d7477e6 Merge branch 'feature/reranker'
f9d7ed2 Document how to run
f1a9944 Add rerank tests
0d752d3 Set chunk size
a787fdf Add reranker with config and top-n parameter
1ab9c13 Add retriever, README and config
[feature/hybrid-search c8cee9d] Document alpha
Date: Thu Jan 15 10:00:00 2026 +0600
1 file changed, 1 insertion(+)
To /home/nadia/remotes/rag-notes.git
+ 0b07427...c8cee9d feature/hybrid-search -> feature/hybrid-search (forced update)The push went through, and Sam's commit survived. A plain --force would have succeeded at the first attempt and erased it. Rules of thumb for rewriting history:
- Rewrite freely on your own branches, before or during review. Never rewrite
mainor a branch others build on; protectmainon GitHub so a force-push is refused (next page). - Use
--force-with-lease, never bare--force. Newer Git also offers--force-if-includesnext to it, which refuses if you have not integrated the remote commits locally. - Tell your team before rewriting a shared branch. They fix their copy with
git fetchthengit reset --hard origin/<branch>(after saving local commits). - Do not rewrite to hide a leaked secret. The old commits live on in clones, forks and caches. Rotate the secret; a later page explains cleaning history properly.
Merge or rebase?
Both integrate work from two lines of history. The difference is the shape of the result, not the content.
merge: rebase:
A---B---C main A---B---C main
\ \ \
D---E---M feature D'--E' feature
(history keeps both lines, (history is one straight line;
plus a merge commit M) D and E are new commits D' and E')| Situation | Use | Why |
|---|---|---|
Update your feature branch with the newest main | Rebase (git rebase main) if the branch is yours alone; otherwise merge main into it | Straight history, no noise merge commits; rebasing a shared branch breaks teammates |
Bring a finished feature into main | Merge (or the "squash and merge" button on GitHub) | The merge commit records when and what was integrated |
| A branch has messy "wip" commits | Interactive rebase, then merge | One commit per idea |
| Commits already pushed and used by others | Merge, never rebase | Rebasing rewrites commits they already have |
| Pulling teammates' work into your local branch | git pull --rebase keeps your local commits on top; plain git pull makes a merge | Fewer pointless merge commits |
| Undo a change that is already public | git revert | Adds a new commit; nothing is rewritten |
The golden rule: never rebase commits that exist outside your repository, unless everyone who has them knows. Most teams settle on one convention (for example "squash and merge" for every pull request) and keep to it.
Real-world use
- Experiments: try a new chunking strategy on a branch, cherry-pick only the evaluation-script fix back to
main, and stash the half-finished idea when production needs a patch. - Reviews: before opening a pull request, squash noise so the reviewer reads "Add reranker" and "Add reranker tests", not nine states of the same file.
- Releases: an annotated tag per deployment, with
git describeinside the app, answers "which code is running?" in one second. Tag model releases too. - Disaster recovery: the reflog turns "I deleted two days of work" into a two-minute fix, as long as you committed.
Common mistakes
- Forgetting that
stashskips untracked files. New files stay behind, and later you think they were stashed. Usegit stash push -u, and checkgit statusafter stashing. - Rebasing a branch others use. Your colleagues get duplicate commits and baffling conflicts. Rebase only your own unpublished (or review-only) branches.
- Using
git push --force. It silently deletes whatever a teammate pushed. Use--force-with-lease, and never force-pushmain. - Panicking after a bad reset or rebase. Do not run more commands in a hurry: read
git reflog, find the line before the mistake, andgit reset --hardto it. For a rebase that is still running,git rebase --abort. - Tagging with a lightweight tag and expecting a release. Plain tags carry no message, and
--follow-tagsignores them. Create releases withgit tag -a.
Try it yourself
- Easy: in a new repository
~/projects/ex-stashwith one committed fileprompt.txt, edit the file and create a new untracked filedraft.txt. Stash both with the message "prompt draft", check that the tree is clean, then restore them. - Easy: create the annotated tag
v1.0.0with a message in that repository and print its object type and its message. - Medium: in that repository create a branch
ideawith one commit, switch tomain, deleteideawith-D, and bring the branch back using the reflog. - Medium: make three commits "step 1", "oops", "step 2 final" on a branch
tidyand use an interactive rebase (no editor) so the branch has two commits: "step 1" with "oops" folded in, and "step 2 final". - Hard: create a bare remote
~/remotes/ex.git, pushtidy, then squash its two commits into one with the message "Add steps" (usesquashand a scripted message editor) and publish the rewrite with--force-with-lease. Show that the remote branch now has one commit.
Answers
1 and 2.
mkdir -p ~/projects/ex-stash && cd ~/projects/ex-stash
git init -q
echo "Answer briefly." > prompt.txt
git add prompt.txt && git commit -q -m "Add prompt"
echo "Answer briefly, in Bangla." > prompt.txt
echo "ideas" > draft.txt
git stash push -u -q -m "prompt draft"
git status --short
git stash list
git stash pop -q
git status --short
git tag -a v1.0.0 -m "First prompt release"
git cat-file -t v1.0.0
git tag -nstash@{0}: On main: prompt draft
M prompt.txt
?? draft.txt
tag
v1.0.0 First prompt release3. The reflog line "moving from idea to main" shows the tip of idea as HEAD@{1}:
git switch -q -c idea
echo "tone: friendly" > style.txt
git add style.txt && git commit -q -m "Add style"
git switch -q main
git branch -D idea
git branch idea 'HEAD@{1}'
git log --oneline -1 ideaDeleted branch idea (was 7fabfaa).
7fabfaa Add style4. The rebase base is main, and the three commits are on top of it. Line 2 of the todo list is "oops":
git switch -q -c tidy
echo 1 > steps.txt && git add steps.txt && git commit -q -m "step 1"
echo 2 > steps.txt && git commit -q -am "oops"
echo 3 > steps.txt && git commit -q -am "step 2 final"
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/fixup/'" git rebase -q -i main
git log --oneline main..HEAD0610e6d step 2 final
4d868aa step 15. Push first, then rewrite, then push with the lease. The message editor of squash is replaced by a sed that sets the first line and deletes the rest:
git init -q --bare ~/remotes/ex.git
git remote add origin ~/remotes/ex.git
git push -q -u origin tidy
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/squash/'" GIT_EDITOR="sed -i -e '1s/.*/Add steps/' -e '2,\$d'" git rebase -q -i main
git push -q --force-with-lease origin tidy
git log --oneline main..origin/tidy
git --git-dir ~/remotes/ex.git log --oneline tidy[detached HEAD 6601923] Add steps
Date: Thu Jan 15 10:00:00 2026 +0600
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 steps.txt
6601923 Add steps
6601923 Add steps
ba5217b Add promptSummary
git stash push -u -mparks unfinished work;poprestores and drops it,applyrestores and keeps it. Stashes are local and temporary.git rebase -i <base>rewrites a branch: reorder lines,fixup/squashto merge,rewordto rename; abort anytime with--abort. UseGIT_SEQUENCE_EDITORto script it.git cherry-pick -xcopies one commit to the current branch.git reflogrecords where HEAD has been, and rescues commits after a bad reset or a deleted branch, for roughly 30 to 90 days, but only for committed work.- Annotated tags (
git tag -a) mark releases;git describenames any commit by its nearest tag; tags need an explicit push. - Rewrite only your own branches, publish with
--force-with-lease, and prefer merge for shared history.
Next: Working as a Team on GitHub moves from your own repository to the shared one: forks, pull requests, reviews, branch protection and releases.