What is githug, and what does it teach?
So what is githug? A game in which the only controller is Git. You run githug, it creates a folder called git_hug, prints a task, and waits. You solve the task with ordinary Git commands, then run githug again. A Ruby block in the level file inspects the repository and either prints "Congratulations, you have solved the level!" and builds the next one, or repeats the task.
None of it is simulated: the clone level clones from GitHub, the push levels push into a second repository that the setup creates in a temporary directory, and the bisect level hands you twenty commits, a program that prints the wrong number and a makefile. Browser tutorials draw a picture of a repository. Here the repository you break is real, and git status reports on it truthfully. That is the whole answer to what is githug: a real repository, a task and a check.
Version 0.5.1, released on 11 August 2024, is still the current gem. Its LEVELS list holds 56 githug levels, grouped roughly like this:
- levels 1 to 15 cover setup and the staging area:
init,config,clone,.gitignore,rm,stash,mv; - levels 16 to 30 cover the log, tags, amending,
resetandreset --soft, remotes,diffandblame; - levels 31 to 42 are about branches, merging, fetching and two kinds of rebase;
- levels 43 to 48 rewrite history with
cherry-pickand interactive rebase; - levels 49 to 56 bring
bisect, partial staging,reflog,revert, a merge conflict and a submodule.
Each level file also carries a difficulty from one to four stars, which githug prints under the level name. The stars are only loosely related to effort. rename and restructure get three stars for little more than git mv, while rebase_onto gets two.
Someone who commits every day will clear the first dozen levels quickly. Levels 40 to 54 are the reason to install it: rebase, reflog and bisect are commands people tend to paste from a search result without knowing what they do.
Githug install: the gem, the Ruby trap and Docker
githug is a plain gem with three pinned runtime dependencies: grit ~> 2.3.0 (an old pure-Ruby Git library that the level checks use), thor ~> 0.14.6 for the command line and a rake older than version 11. A githug install on Ruby 3.1 pulls in six gems:
gem install githug -v 0.5.1Successfully installed thor-0.14.6
Successfully installed rake-10.5.0
Successfully installed mime-types-1.25.1
Successfully installed diff-lcs-1.6.2
Successfully installed grit-2.3.0
Successfully installed githug-0.5.1
6 gems installedThe README still says that githug "requires Ruby 1.8.7 or higher". That was true when it was written; it stopped being true with Ruby 3.2.
There are two ways around it. One is to play on Ruby 3.1, where File.exists? still works and only prints a deprecation warning when Ruby runs with -W. A version manager such as rbenv or asdf can install 3.1 next to your system Ruby; that route was not tested for this page. The Docker route was, and it is the githug install every solution below was typed into. This one-liner mounts the current directory, installs the gem in a throwaway ruby:3.1 container and drops you into a shell:
docker run -it --rm -v "$PWD:/work" -w /work ruby:3.1 bash -c 'gem install githug -v 0.5.1 --no-document && exec bash'The image ships git 2.39.5, so nothing else needs installing. Progress survives the container, because it lives in git_hug/.profile.yml on the mounted directory. Your Git identity does not: git config --global writes to the container's home, so set user.name and user.email again after every new docker run, or the commit levels fail.
The other way keeps your current Ruby and puts the missing method back. Save this as ~/file_exists_shim.rb:
class File
class << self
alias_method :exists?, :exist? unless method_defined?(:exists?)
end
endThen start githug with Ruby told to load it first:
RUBYOPT="-r$HOME/file_exists_shim.rb" githugOn Ruby 3.3.13 this was enough to play levels 1 to 4 and to pass the five levels whose own checks call File.exists?. It is a patch on top of code from the Ruby 1.8 and 1.9 era, so treat any other strange failure on a new Ruby as a reason to fall back to 3.1.
How a level works: githug, hint, reset and levels
The first run asks a question and creates the game folder:
No githug directory found, do you wish to create one? [yn] Welcome to Githug!
Name: init
Level: 1
Difficulty: *
A new directory, `git_hug`, has been created; initialize an empty repository in it.From then on every command runs inside git_hug. Started from the parent folder, githug only prints "Please change into the git_hug directory". Four subcommands matter for playing (a fifth, githug test, loads a level from a file path and is meant for level authors):
| command | what it does |
|---|---|
githug | checks the current level; on success it builds the next one |
githug hint | prints the hint stored in the level file |
githug reset | rebuilds the current level; githug reset 47 or githug reset merge_ jumps to that level |
githug levels | lists all 56 levels with their numbers |
Every level setup deletes everything in git_hug except .profile.yml, so do not keep anything there. Ten levels ask questions instead of only checking a state (config, status, number_of_files_committed, log, remote, remote_url, diff, blame, grep, bisect); you type the answer at the prompt that appears after githug. A wrong attempt looks like this, followed by the hint for level 8:
Sorry, this solution is not quite right!
Name: include
Level: 8
Difficulty: **
Notice a few files with the '.a' extension. We want git to ignore all the files except the 'lib.a' file.Using `git help ignore`, read about the optional prefix to negate a pattern.After every third failed attempt githug adds a reminder that githug hint and githug reset exist. That reset earned its place during testing for this page. The setup of push_tags died once with Grit::Git::GitTimeout halfway through, and githug reset rebuilt the level cleanly in under a second.

All 56 githug levels at a glance
The numbers are the ones githug levels prints for 0.5.1; the stars come from each level file.
| # | level | stars | # | level | stars |
|---|---|---|---|---|---|
| 1 | init | 1 | 29 | diff | 2 |
| 2 | config | 1 | 30 | blame | 2 |
| 3 | add | 1 | 31 | branch | 1 |
| 4 | commit | 1 | 32 | checkout | 2 |
| 5 | clone | 1 | 33 | checkout_ | 2 |
| 6 | clone_ | 1 | 34 | checkout_ | 2 |
| 7 | ignore | 2 | 35 | branch_ | 3 |
| 8 | include | 2 | 36 | delete_ | 2 |
| 9 | status | 1 | 37 | push_ | 2 |
| 10 | number_ | 1 | 38 | merge | 2 |
| 11 | rm | 2 | 39 | fetch | 2 |
| 12 | rm_ | 2 | 40 | rebase | 2 |
| 13 | stash | 2 | 41 | rebase_ | 2 |
| 14 | rename | 3 | 42 | repack | 2 |
| 15 | restructure | 3 | 43 | cherry-pick | 3 |
| 16 | log | 2 | 44 | grep | 2 |
| 17 | tag | 2 | 45 | rename_ | 3 |
| 18 | push_ | 2 | 46 | squash | 4 |
| 19 | commit_ | 2 | 47 | merge_ | 3 |
| 20 | commit_ | 2 | 48 | reorder | 4 |
| 21 | reset | 2 | 49 | bisect | 3 |
| 22 | reset_ | 2 | 50 | stage_ | 4 |
| 23 | checkout_ | 3 | 51 | find_ | 4 |
| 24 | remote | 2 | 52 | revert | 4 |
| 25 | remote_ | 2 | 53 | restore | 4 |
| 26 | pull | 2 | 54 | conflict | 4 |
| 27 | remote_ | 2 | 55 | submodule | 2 |
| 28 | push | 3 | 56 | contribute | 3 |
Githug solutions, station by station
Each of these githug solutions was typed into a freshly built level on Ruby 3.1.7 and git 2.39.5 and then confirmed with githug. The comments give the level number. Hashes in the output differ on your machine except where a level ships its own history (cherry-pick, bisect).
Station 1: the basics (levels 1 to 15)
cd git_hug
git init # 1 init
git config --global user.name "Alice Example" # 2 config
git config --global user.email [email protected]
git add README # 3 add
git commit -m "Add README" # 4 commit
git clone https://github.com/Gazler/cloneme # 5 clone
git clone https://github.com/Gazler/cloneme my_cloned_repo # 6 clone_to_folder
echo "*.swp" >> .gitignore # 7 ignore
printf "*.a\n!lib.a\n" >> .gitignore # 8 includeLevel 2 then asks for your name and email and compares the answers with the config, so type them exactly as set. Level 8 works because a pattern starting with ! re-includes what an earlier line excluded. For levels 9 and 10, git status is the whole solution: the untracked file is database.yml, and the short form shows what will be committed.
A rubyfile1.rb
M rubyfile4.rb
M rubyfile5.rb
?? rubyfile6.rb
?? rubyfile7.rbThe left column is the index, the right one the working tree. Two letters stand on the left, so the answer is 2; rubyfile5.rb is modified but not staged.
git rm deleteme.rb # 11 rm
git rm --cached deleteme.rb # 12 rm_cached
git stash # 13 stash
git mv oldfile.txt newfile.txt # 14 rename
mkdir src # 15 restructure
git mv *.html src/Level 11 records a deletion that already happened on disk; level 12 removes the file from the index and leaves it on disk, which is exactly what the check tests.
Station 2: history and undo (levels 16 to 23)
git log --oneline # 16 log: type the 7-character hash
git tag new_tag # 17 tag
git push --tags origin # 18 push_tags
git add forgotten_file.rb # 19 commit_amend
git commit --amend --no-edit
git commit --date="2030-01-01T12:00:00" -m "Commit from the future" # 20 commit_in_future
git reset HEAD to_commit_second.rb # 21 reset
git reset --soft HEAD^ # 22 reset_soft
git checkout -- config.rb # 23 checkout_fileLevel 19 wants one commit that contains both files, so amending is required; a second commit fails the check. Level 22 is the soft reset: the commit disappears, newfile.rb stays staged. Without --no-edit, git commit --amend opens your editor, which on many systems is Vim; :wq saves the unchanged message.
Station 3: remotes, diff and blame (levels 24 to 30)
git remote # 24 remote: answer my_remote_repo
git remote -v # 25 remote_url: answer https://github.com/githug/not_a_repo
git pull origin master # 26 pull
git remote add origin https://github.com/githug/githug # 27 remote_add
git rebase origin/master # 28 push
git push origin master
git diff # 29 diff
git blame -L 5,5 config.rb # 30 blameLevel 28 is the first case where local and remote history have diverged: the rebase replays the three local commits on top of origin/master, after which the push is a plain fast-forward. The remotes in these levels are local paths and public HTTPS URLs; on a real server you would usually push over SSH, where a host alias in ~/.ssh/config saves typing.
Level 29 asks for a line number, and the diff only shows a hunk:
@@ -23,7 +23,7 @@ get '/yet_another' do
erb :success
end
get '/another_page' do
- @message = get_response('data.json')
+ @message = get_response('server.json')The hunk starts at line 23, and the changed line is the fourth one in it, so the answer is 26. For level 30, git blame -L 5,5 limits the output to the line with the password:
97bdd0cc (Spider Man 2012-03-08 23:08:15 +0000 5) @password = password || "i<3evil"
Station 4: branches (levels 31 to 42)
git branch test_code # 31 branch
git checkout -b my_branch # 32 checkout
git checkout v1.2 # 33 checkout_tag
git checkout tags/v1.2 # 34 checkout_tag_over_branch
git branch test_branch HEAD^ # 35 branch_at
git branch -d delete_me # 36 delete_branch
git push origin test_branch # 37 push_branch
git merge feature # 38 merge
git fetch origin # 39 fetch
git checkout feature # 40 rebase
git rebase master
git rebase --onto master wrong_branch readme-update # 41 rebase_onto
git repack -d # 42 repackLevel 34 is the instructive one. With a branch and a tag both called v1.2, the plain command picks the branch:
warning: refname 'v1.2' is ambiguous.
Switched to branch 'v1.2'The tags/ prefix removes the doubt. In level 37 the check counts remote branches afterwards, so git push --all fails it by sending more than test_branch. A plain fetch is enough for level 39: it records origin/new_branch and leaves local branches alone. The three-argument rebase in level 41 reads as "take the commits of readme-update that are not in wrong_branch and replay them on master".
Station 5: rewriting history (levels 43 to 48)
git log --oneline new-feature # 43 cherry-pick: find the README commit
git cherry-pick ca32a6d
git grep -c TODO # 44 grep
git merge --squash long-feature-branch # 47 merge_squash
git commit -m "Merge long-feature-branch as one commit"The cherry-pick hash is fixed because the level ships its own history; the commit is "Filled in README.md with proper input". For level 44, -c counts per file, so you add the numbers:
app.rb:3
config.rb:1The answer is 4. Levels 45, 46 and 48 use git rebase -i, which opens the todo list in your editor. For the tests on this page, sed stood in for the editor through GIT_SEQUENCE_EDITOR and made the same change you would make by hand:
GIT_SEQUENCE_EDITOR="sed -i \"1s/^pick/edit/\"" git rebase -i HEAD~2 # 45 rename_commit
git commit --amend -m "First commit"
sleep 2
git rebase --continue
GIT_SEQUENCE_EDITOR="sed -i \"2,4s/^pick/squash/\"" GIT_EDITOR=true git rebase -i HEAD~4 # 46 squashFor level 48, git rebase -i HEAD~3 shows this todo list:
pick 88c2328 First commit
pick dd349a2 Third commit
pick a351ee3 Second commitSwap the last two lines, save, and the log reads Third, Second, First, Initial Setup from the top, which is the order the check expects.

Station 6: the advanced githug levels (49 to 56)
git bisect start HEAD f608824 # 49 bisect: bad commit, then known good one
git bisect run make test
git bisect resetThe makefile runs ruby prog.rb 5 | ruby test.rb, so bisect run can decide good and bad on its own. After a few test runs it prints:
18ed2ac1522a014412d4303ce7c8db39becab076 is the first bad commitLevel 50 is partial staging, and the obvious tool does not quite work. git add -p offers s to split, but the two new lines are adjacent:
(1/1) Stage this hunk [y,n,q,a,d,e,?]? Sorry, cannot split this hunkChoose e instead and delete the line +This change belongs to the second feature in the editor. The first line goes to the index, the second stays in the working tree.
git reflog | grep checkout # 51 find_old_branch
git checkout solve_world_hunger
git revert --no-edit HEAD^ # 52 revert
git reflog | head -3 # 53 restore
git reset --hard HEAD@{1}For level 51 the most recent line in the reflog reads checkout: moving from solve_world_hunger to kill_the_batman; the branch you left is the one you forgot. The commits in level 52 count as "pushed", so history must not be rewritten; revert adds a new commit that undoes "Bad commit". In level 53, HEAD@{1} is the state before the hard reset, and resetting to it brings the commit back.
Level 54 is a real merge conflict in poem.txt:
Humpty dumpty
<<<<<<< HEAD
Categorized shoes by color
=======
Sat on a wall
>>>>>>> mybranch
Humpty dumpty
Had a great fallKeep "Sat on a wall", delete the three marker lines and the wrong line, then git add poem.txt and git commit --no-edit. The check wants a merge commit with two parents on master, and it rejects the file if a single <, >, = or | character is left anywhere in it. Level 55 takes one command:
git submodule add https://github.com/jackmaney/githug-include-meLevel 56 and the limits of the checker
contribute is the odd one. Its check clones the githug repository from GitHub into /tmp/githug and looks through the master history for a commit whose author name and email match your Git config. No command in git_hug can satisfy it; only a pull request that the maintainers merge can. The level text asks for new levels, bug fixes or documentation.
That would start at the githug repository on GitHub, and replacing the File.exists? calls with File.exist? is the obvious first patch. Whether anyone still merges pull requests into a gem whose last release dates from August 2024 is a question the level itself cannot answer.
