Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maintenance #30

Merged
merged 7 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ jobs:
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: build challenge
run: ./create_challenge.sh --verbose
- name: build
run: ./build.sh --verbose
- name: create tarball
# GitHub removes file permissions, but we need them to be there
# https://github.com/actions/upload-artifact/blob/cf8714cfeaba5687a442b9bcb85b29e23f468dfa/README.md#permission-loss
# Alternatively we could require the user to run a script that adds the permissions on first run, but that also feels broken...
run: tar -cvf challenge.tar ./challenge/
- name: upload challenge
run: tar -cvf tutorial.tar ./tutorial/
- name: upload tutorial
uses: actions/upload-artifact@v4
with:
name: challenge
path: ./challenge.tar
name: tutorial
path: ./tutorial.tar
if-no-files-found: error
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
/challenge*
/tutorial*
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
# What is this?

This is the (WIP) script for creating an interactive `git` repo that is a completely self-contained `git` tutorial (and hopefully at least a bit fun, too ;-) ).
The challenge itself is only in the folder "challenge" that can be build (see below) and NOT in this repository. Do NOT look around in this repository unless you want to develop this challenge, because otherwise you will be spoiled with the solutions. Also: don't snoop around in the .git-Folder unless explicitly told - it'll spoil the fun for you and as said before: you can find all flags by using git commands ;)
The tutorial itself is only in the folder "tutorial" that can be build (see below) and NOT in this repository. Do NOT look around in this repository unless you want to develop it, because otherwise you will be spoiled with the solutions. Also: don't snoop around in the .git-Folder unless explicitly told - it'll spoil the fun for you and as said before: you can find all flags by using git commands ;)

You can download a copy from GitHub, unzip it and (for some cursed reason thanks to GitHub it must also be in a tarball to remain playable) then you also need to extract the tarball.
On MacOS both can be achieved by double-clicking them in finder, but if you are using Linux or you already want to get warmed up using the terminal/command line, you can run those commands in your download folder:
```sh
unzip challenge.zip
tar --extract --file=challenge.tar
cd challenge
unzip tutorial.zip
tar --extract --file=tutorial.tar
cd tutorial
```
and then you are ready!

If instead you want to create a local "playable" copy, you can simply run `./create_challenge.sh` and you will find the folder "challenge".
If instead you want to create a local "playable" copy yourself, you can simply run `./build.sh` and you will find the folder "tutorial".

## What git functions are already explained?

- diff (--staged)
- (add) <= maybe needs to be improved?
- commit
- show
- switch (-c)
- push
- pull
- log
- rebase
- (merge) <= only explained, but not interactively

## debugging

By default `./create_challenge.sh` does not show the git output to avoid leaking information. If you get an error you can run it again with `-v`/`--verbose`.
By default `./build.sh` does not show the git output to avoid leaking information. If you get an error you can run it again with `-v`/`--verbose`.

# Developing

Expand All @@ -30,7 +43,7 @@ To run the tests you simply need to execute:
```
It has two levels of debugging, so by default it just prints the test cases, but if run with `-v` it will also output every expect. If run with `-v -v` it will additionally print every output of the commands it executed.

WARNING: It will build the challenge for you, since it has to reset the state inbetween to check incompatible "paths" (test with triggering the LocalCodeExecution trap, or not). That means it does not matter into which state you bring the challenge directory before the run - it will be deleted and replaced by a fresh build before the first test is run.
WARNING: It will build the tutorial for you, since it has to reset the state inbetween to check incompatible "paths" (test with triggering the LocalCodeExecution trap, or not). That means it does not matter into which state you bring the tutorial directory before the run - it will be deleted and replaced by a fresh build before the first test is run.

For writing tests there is a tiny testing framework (loosely inspired by `jest`). It is by far not feature complete. Example:
```sh
Expand Down
25 changes: 21 additions & 4 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

Creating such an interactive repo that references itself everywhere is a bit tedious, since you basically have to think in reverse: The creation script has to finish where the player starts their journey. So when adding new "chapters" you (usually) need to add them at earlier point in the creation script, so that you can reference them. In general it is a good idea to mention only things that happened earlier in the script (even if you could mention e.g. a tag and only later create it), so basically to insert more or less at the top, but there are exceptions like the remote, where we want upstream mostly to have the current state and even if we could distinguish creating the repo and pushing to it, this does not make sense and so we want to do that last, even if we reference it before.

## How it works - Overview
# Overview of custom files/folders in .git

### How to refer to other commits
(see below for more detailed explanation)

If you want to link to a different commit, make sure that you don't do this absolute (as in run the creation script and then copy the hash), but instead put in a placeholder that can be dynamically replaced if the hash of that commit changes (e.g. if someone fixes a typo, adds a new commit before, ...). In ./lib.sh there is a function `replace_placeholders` that gets a file as an input and writes the content to stdout with the known placeholders replaced.
- `nuggits` (pseudo branch for tracking redeemed nuggits)
- `my-origin/` (remote for pushing/pulling)
- `another-downstream/` (another clone of my-origin used for adding changes to pull)
- `objects/*` (<= a few loose objects for keeping track of nuggits & the credits)
- `hooks/*.orig` (<= work horses to react to certain actions like commit/push/... - ".orig" will be removed by:)
- `hooks/*[^.orig]` (<= hooks that delete all non-".orig" hooks for the "paranoia flag" LocalCodeExecution, also then renaming the ".orig" hooks so that they are called in the future)

# How it works - in more details

### How nuggits are hidden
## How nuggits are hidden

- Some of the nuggits are static, as in the very first one `git diff`, `git show`, ... - those use "the normal builtin features" as the player knows them.
- Others like `git commit` adding a nuggit to the commit message make use of hooks (in this case `pre-commit-message`) to inject themselves when the player does an action
Expand All @@ -33,3 +40,13 @@ So what do we do then? Easy: We use the plumbing commands to create the commit a
Once a nuggit is redeemed, we write another object to the "database" with the format `'$nuggit' already redeemed`. That way we can look it up in our "database" before we tell the user they redeemed a new one.

As a side-note, we don't just rely on the number of commits in our nuggits branch, since the user could easily tinker with that. But with each newly redeemed nuggit we also write the total number of redeemed nuggits to the objects. That way we can at least cross reference a tiny bit if the player has tinkered with our system (but of course they could just open the script and reverse that as well...)

## Pushing & Pulling

The remote is not actually a server, but a bare repo (meaning a repo that does not have a workspace, but is basically just the content of a .git/ folder) that is found under .git/my-origin. That means a player can play this totally locally.
But that is not the only other git repository that is needed in this game: When pushing on the branch "working-with-others" we want the user to also be able to do a meaningful `git pull`, and so in the my-origin hook "post-update", which is triggered after someone pushed to it, we want to add commits. But since a bare repo cannot be used (easily) to create commits, we shell out and have another clone under .git/another-downstream that is used to push again to .git/my-origin, so that next time the player runs `git pull` there are actually changes to fetch

# How to refer to other commits

If you want to link to a different commit, make sure that you don't do this absolute (as in run the creation script and then copy the hash), but instead put in a placeholder that can be dynamically replaced if the hash of that commit changes (e.g. if someone fixes a typo, adds a new commit before, ...). In ./lib.sh there is a function `replace_placeholders` that gets a file as an input and writes the content to stdout with the known placeholders replaced.

17 changes: 9 additions & 8 deletions create_challenge.sh → build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
. ./lib.sh

parse_opts "$@"
: "${destination:=tutorial}"

# ------------------------------------------------------------------------------------------- #
# setup for the script
Expand All @@ -20,20 +21,20 @@ trap on_error TERM ABRT QUIT ERR EXIT

shopt -s extglob

if [ -e challenge ]; then
if [ -e "$destination" ]; then
if [ "$delete_existing_dir" = true ]; then
rm -rf challenge
rm -rf "$destination"
else
warn "'challenge' already exists. moving to challenge2..."
rm -rf challenge2
mv challenge challenge2
warn "'$destination' already exists. moving to ${destination}2..."
rm -rf "${destination}2"
mv "$destination" "${destination}2"
fi
fi

# ------------------------------------------------------------------------------------------- #
create_chapter initial setup
git init --initial-branch=main challenge
cd challenge
git init --initial-branch=main tutorial
cd tutorial
reproducibility_setup

# ------------------------------------------------------------------------------------------- #
Expand All @@ -58,7 +59,7 @@ ALMOST_CREDITS_HASH="$(git hash-object -w "$DOCDIR/almost_credits.txt")"
CREDITS_HASH="$(tr 'A-Za-z' 'N-ZA-Mn-za-m' < "$DOCDIR/credits.txt" | git hash-object -w --stdin)"
NUMBER_OF_NUGGITS="$(wc -l <"$DOCDIR/nuggits")"

replace_placeholders "$DOCDIR/redeem.nuggit" > ./.git/redeem.nuggit
replace_placeholders "$DOCDIR/redeem-nuggit.sh" > ./.git/redeem.nuggit
chmod a=rx ./.git/redeem.nuggit

# ------------------------------------------------------------------------------------------- #
Expand Down
4 changes: 2 additions & 2 deletions lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ parse_opts() {
-v|--verbose)
verbose=$((verbose + 1))
;;
-f|--force|--delete-existing-challenge)
-f|--force)
delete_existing_dir=true
;;
*)
echo "ERROR! Unknown option '$opt'. Useage: $0 [-v|--verbose] [-f|--force|--delete-existing-challenge]" >&2
echo "ERROR! Unknown option '$opt'. Useage: $0 [-v|--verbose] [-f|--force]" >&2
exit 1
;;
esac
Expand Down
4 changes: 2 additions & 2 deletions src/01_init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is an explanation/game on how to use the version control system `git`. It is not intended that you use other tools like `grep` to find the "nuggits" (little strings of text to show you that you make progress learning git ;) - otherwise known as flags in CTF challenges), but instead find them with the builtin git commands. For instructions on how to redeem them, see the FAQ below.

For many day-to-day tasks the graphical user interfaces for it might well work, but it will definitely get to its limits on this challenge, since it will get into the nitty-gritty details (if you want) so it is recommended to use the command line instead.
For many day-to-day tasks the graphical user interfaces for it might well work, but it will definitely get to its limits on this tutorial, since it will get into the nitty-gritty details (if you want) so it is recommended to use the command line instead.

By default in most Projects there exists a README file, so with opening this file you already made the first step (I promise: even if you know git you probably get to know a thing or two about `git` that you didn't hear about. And if you did: please get in touch so that I can learn more from you and maybe even add some more ideas!)

Expand All @@ -23,7 +23,7 @@ A: That is a good question and you will figure out the answer the more you get i
Q: Can I use my favourite git GUI tool?
A: Well... For a few of the nuggits, yes, but some are well hidden in the stranger parts of git, so this project assumes running git from the command line from the beginning.

NOTE: This challenge uses git hooks extensively for it to work. Because of the way you downloaded this repository they are enabled by default and could run arbitrary code. I promise you they are just here for the benefit of learning git and don't do anything malicious. If it makes you feel better: For testing this challenge I have a test suite that I am regularly running on my machine that triggers all of them.
NOTE: This tutorial uses git hooks extensively for it to work. Because of the way you downloaded this repository they are enabled by default and could run arbitrary code. I promise you they are just here for the benefit of learning git and don't do anything malicious. If it makes you feel better: For testing this tutorial I have a test suite that I am regularly running on my machine that triggers all of them.
If you are still paranoid, you should never download repositories like this! Instead you can come and take a look how this was created and help to complete it - with git there are infinitely many things to learn and we will never be done...


Expand Down
2 changes: 1 addition & 1 deletion src/hooks/prepare-commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SHA1=$3

# add a nuggit to the commit message
git interpret-trailers --in-place \
--trailer "This is the challenge nuggit speaking. Please don't mind me. I am injecting myself in a few places (like this one), so if you ever see anything starting with nuggit in this repo, I injected it and it usually wouldn't be there in a normal git repo." \
--trailer "This is the tutorial nuggit speaking. Please don't mind me. I am injecting myself in a few places (like this one), so if you ever see anything starting with nuggit in this repo, I injected it and it usually wouldn't be there in a normal git repo." \
--trailer "nuggit: CommitMirInsAbendteuerland" \
"$COMMIT_MSG_FILE"
if test -z "$COMMIT_SOURCE"
Expand Down
61 changes: 61 additions & 0 deletions src/redeem-nuggit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# This file is not really intended for you to look into too much - but I like how curious you are, so here is a nuggit for you: CuriosityKilledTheCat

nuggit="$1"

success() {
echo "Success! What a nice nuggit for your collection! 🏅 $1 looks really good!"
}

already_redeemed() {
echo "'$1' already redeemed"
}

if [ -z "$nuggit" ]; then
echo "no nuggit passed in..." >&2
echo "Useage: \`$0 TestNuggit\`" >&2
exit
fi

if [ "$nuggit" = TestNuggit ]; then
echo "This is a test. You passed it! 👍"
exit
fi

already_redeemed=0
git cat-file -e "$(already_redeemed "$nuggit" | git hash-object --stdin)" 2>/dev/null && already_redeemed=1
redeemed_nuggits="$(git rev-list --count nuggits)"
redeemed_nuggits=$((redeemed_nuggits - already_redeemed))

[ "$redeemed_nuggits" -ne $((NUMBER_OF_NUGGITS - 1 )) ] || git cat-file -p ALMOST_CREDITS_HASH;

[ "$redeemed_nuggits" -ne $(( NUMBER_OF_NUGGITS + 0 )) ] || {
git cat-file -e "$(git hash-object --stdin <<< "$((NUMBER_OF_NUGGITS - 1))" | git hash-object --stdin)" 2>/dev/null || { echo Noughty boy!; exit 1; }
git cat-file -p CREDITS_HASH | tr 'A-Za-z' 'N-ZA-Mn-za-m';
}

git cat-file -p "$(already_redeemed "$nuggit" | git hash-object --stdin)" 2>/dev/null && exit
git cat-file -p "$(echo "You tried '$nuggit' before. It still isn't a valid answer... 🙄" | git hash-object --stdin)" 2>/dev/null && exit 1

if git cat-file -e "$(echo "$nuggit" | git hash-object --stdin | git hash-object --stdin)"; then
success "$nuggit"
else
echo "Unfortunately that is not a valid nuggit :/ Try again!" >&2
echo "You tried '$nuggit' before. It still isn't a valid answer... 🙄" | git hash-object --stdin -w >/dev/null 2>&1
exit 1
fi

commit_nuggit() { # Manage our own little "branch" manually
local tree
# get the tree object from the last commit in nuggits
tree="$(git rev-parse "nuggits^{tree}")"
# add an empty commit with the parent being nuggits and "reset nuggits to that new commit"
git commit-tree "$tree" -p "$(cat .git/nuggits)" -m "$1" > .git/nuggits.bak
# We can't directly pipe it into the file, because it will empty it before we read it...
mv .git/nuggits.bak .git/nuggits
}
commit_nuggit "$nuggit"

echo "Number of redeemed nuggits: $redeemed_nuggits of $((NUMBER_OF_NUGGITS))"
already_redeemed "$nuggit" | git hash-object --stdin -w >/dev/null 2>&1
git hash-object --stdin <<< "$redeemed_nuggits"| git hash-object --stdin -w >/dev/null 2>&1
60 changes: 0 additions & 60 deletions src/redeem.nuggit

This file was deleted.

Loading