Git · game guide

Githug levels and solutions: install it, then solve levels 1 to 55

Githug is a Ruby gem that hides a small Git exercise behind every level and checks your repository when you run it again. Version 0.5.1 has 56 levels, and it crashes on Ruby 3.2 or newer unless you work around one removed method.

LineGit
Read15 min · 6 stops
DatePublished Nov 14, 2022 · updated Nov 05, 2026
Tested withgithug 0.5.1 on Ruby 3.1.7 with git 2.39.5 (Docker ruby:3.1, Debian 12): levels 1 to 55 solved; crash and File.exists? shim on Ruby 3.3.13 with git 2.47.3 (Docker ruby:3.3)
Railway tracks splitting into many branches at night (AI-generated image)
AI-generated image
Guide6 stations on the Git line

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, reset and reset --soft, remotes, diff and blame;
  • levels 31 to 42 are about branches, merging, fetching and two kinds of rebase;
  • levels 43 to 48 rewrite history with cherry-pick and 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:

shell
gem install githug -v 0.5.1
output
Successfully 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 installed

The 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:

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:

~/file_exists_shim.rb
class File
  class << self
    alias_method :exists?, :exist? unless method_defined?(:exists?)
  end
end

Then start githug with Ruby told to load it first:

shell
RUBYOPT="-r$HOME/file_exists_shim.rb" githug

On 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:

output
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):

commandwhat it does
githugchecks the current level; on success it builds the next one
githug hintprints the hint stored in the level file
githug resetrebuilds the current level; githug reset 47 or githug reset merge_squash jumps to that level
githug levelslists 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:

output
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.
output
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.

Row of mechanical levers in an old railway signal box (AI-generated image)
AI-generated image

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.

#levelstars#levelstars
1init129diff2
2config130blame2
3add131branch1
4commit132checkout2
5clone133checkout_tag2
6clone_to_folder134checkout_tag_over_branch2
7ignore235branch_at3
8include236delete_branch2
9status137push_branch2
10number_of_files_committed138merge2
11rm239fetch2
12rm_cached240rebase2
13stash241rebase_onto2
14rename342repack2
15restructure343cherry-pick3
16log244grep2
17tag245rename_commit3
18push_tags246squash4
19commit_amend247merge_squash3
20commit_in_future248reorder4
21reset249bisect3
22reset_soft250stage_lines4
23checkout_file351find_old_branch4
24remote252revert4
25remote_url253restore4
26pull254conflict4
27remote_add255submodule2
28push356contribute3

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)

shell
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 include

Level 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.

output
A  rubyfile1.rb
M  rubyfile4.rb
 M rubyfile5.rb
?? rubyfile6.rb
?? rubyfile7.rb

The 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.

shell
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)

shell
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_file

Level 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)

shell
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 blame

Level 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:

output
@@ -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:

output
97bdd0cc (Spider Man 2012-03-08 23:08:15 +0000 5)     @password = password || "i<3evil"
Large tree with spreading branches inside a glass atrium (AI-generated image)
AI-generated image

Station 4: branches (levels 31 to 42)

shell
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 repack

Level 34 is the instructive one. With a branch and a tag both called v1.2, the plain command picks the branch:

output
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)

shell
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:

output
app.rb:3
config.rb:1

The 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:

shell
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 squash

For level 48, git rebase -i HEAD~3 shows this todo list:

output
pick 88c2328 First commit
pick dd349a2 Third commit
pick a351ee3 Second commit

Swap the last two lines, save, and the log reads Third, Second, First, Initial Setup from the top, which is the order the check expects.

Two highways merging into one at night, light trails (AI-generated image)
AI-generated image

Station 6: the advanced githug levels (49 to 56)

shell
git bisect start HEAD f608824      # 49 bisect: bad commit, then known good one
git bisect run make test
git bisect reset

The 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:

output
18ed2ac1522a014412d4303ce7c8db39becab076 is the first bad commit

Level 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:

output
(1/1) Stage this hunk [y,n,q,a,d,e,?]? Sorry, cannot split this hunk

Choose 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.

shell
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:

output
Humpty dumpty
<<<<<<< HEAD
Categorized shoes by color
=======
Sat on a wall
>>>>>>> mybranch
Humpty dumpty
Had a great fall

Keep "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:

shell
git submodule add https://github.com/jackmaney/githug-include-me

Level 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.

Last stops · FAQ

Questions people ask

What is githug and who is it for?

Githug is a command-line game distributed as a Ruby gem. Each level sets up a real Git repository in a folder called git_hug, describes a task, and checks the result when you run githug again. It suits people who already use add and commit but avoid rebase -i, reflog or bisect, because the later levels force exactly those commands on small, disposable repositories.

How many levels does githug have?

Version 0.5.1, the current gem, lists 56 levels in the LEVELS array of lib/githug/level.rb, from init to contribute. githug levels prints the same list with numbers. The last one, contribute, cannot be solved inside the game folder: it checks GitHub for a commit by your configured name and email, so in practice the playable run ends at level 55, submodule.

Why does githug fail with undefined method exists? for class File?

githug calls File.exists?, a deprecated alias that Ruby removed in version 3.2. On Ruby 3.3.13 the first githug call stops with a NoMethodError in cli.rb. Run the game on Ruby 3.1 (a version manager or the ruby:3.1 Docker image), or load a five-line file that restores the alias through RUBYOPT, as shown in the install section.

Can you skip a githug level or go back to one?

Yes. githug reset takes a level name or number, for example githug reset grep or githug reset 47, sets that level as the current one and rebuilds its repository. After you solve it, githug moves on to the first level that is not yet in your completed list, which is stored in .profile.yml inside git_hug. Deleting that file starts the game from zero.

Are these githug solutions the only correct ones?

No. The githug solutions on this page are one way through; each check looks at the final state of the repository, not at the commands you typed. The include level only reads .gitignore for the lines *.a and !lib.a; the restore level only checks that file3 exists again. Any route that produces that state passes: level 20 accepted both a fixed ISO date and $(date -d tomorrow) for --date.

Does githug need an internet connection?

For a few levels. clone, clone_to_folder, pull and submodule fetch real repositories from GitHub over HTTPS, and contribute clones the githug repository itself. The push and fetch levels do not: their setup copies the repository into a temporary directory and adds it as origin, so git push writes to a local path and needs no account or SSH key.

Next stop

Where this line continues