Skip to content

Commit

Permalink
Verify result repository
Browse files Browse the repository at this point in the history
Added a function to quickly verify a repository's mystery.

Please do NOT look at the code unless you already solved the mystery,
it'll just spoil the fun.
  • Loading branch information
nivbend committed Mar 7, 2020
1 parent 7edd195 commit 092cecb
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 3 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ To generate a new repository at `/tmp/gitstery`:
gitstery generate /tmp/gitstery
```

To verify a repository:
```
gitstery verify /tmp/gitstery
gitstery verify https://github.com/nivbend/gitstery.git
gitstery verify git@github.com:nivbend/gitstery.git
```

If you have your own fork of the gitstery repository you'd like to update:
```
gitstery push /tmp/gitstery <your repository URL>
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

setup(
name='gitstery-generator',
version='0.21.0',
version='1.0.0',
description='A Git murder mystery',
long_description=Path('README.md').read_text(),
long_description_content_type='text/markdown',
Expand Down
43 changes: 42 additions & 1 deletion src/gitstery/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import sys
from os import environ, urandom
from urllib.parse import urlparse
from itertools import chain
from pathlib import Path
from contextlib import contextmanager, nullcontext
from datetime import timedelta
from random import seed, choice, choices, randrange
from shutil import rmtree
from tempfile import TemporaryDirectory
from csv import DictWriter
from inflect import engine
from click import (Path as ClickPath, group, pass_context, argument, option, prompt, confirm, echo,
Expand All @@ -14,7 +18,14 @@
from .fillers import random_people
from .git_utils import git_commit
from .phases import PHASES_COUNT, build_phase_1, build_phase_2, build_phase_3
from .solution import build_solution
from .solution import build_solution, verify_repository

@contextmanager
def clone_repository(url):
with TemporaryDirectory() as temporary_directory:
echo(f'Cloning {url} to {temporary_directory}')
cloned = Repo.clone_from(url, temporary_directory)
yield cloned

@group()
def cli():
Expand Down Expand Up @@ -176,3 +187,33 @@ def push(repo, url):
echo(f' {ref.local_ref} {summary}')
finally:
repo.delete_remote('origin')

@cli.command()
@argument('repository', envvar='GITSTERY_TARGET_REPO')
def verify(repository):
"""Verify a mystery REPOSITORY was built properly."""
uri = urlparse(repository)
if uri.scheme.lower() in ('http', 'https') or uri.path.startswith('git@'):
repo_context = clone_repository(repository)
else:
try:
# This also takes care of `file://` URIs.
repo_context = nullcontext(Repo(uri.path))
except:
echo(f'{repository}: Not a git repository')
sys.exit(1)

with repo_context as repo:
if 'master' != repo.head.ref.name:
echo('Checking out `master`')
repo.heads.master.checkout()

if repo.head.is_detached:
echo("Repository's head is detached")
sys.exit(1)
if repo.is_dirty():
echo('Repository is dirty')
sys.exit(1)

if not verify_repository(repo):
sys.exit(1)
127 changes: 126 additions & 1 deletion src/gitstery/solution.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import re
from datetime import datetime
from subprocess import check_output
from pathlib import Path
from tempfile import NamedTemporaryFile
from .people import SUSPECTS
from click import echo, secho
from .defines import (DATE_REPORT, DATE_REPORT_WEEK_START, DATE_REPORT_WEEK_END, POLICE_BRANCH,
ACCESS_POINT_OF_INTEREST)
from .people import MAIN_DETECTIVE, SUSPECTS
from .utils import rot13

def build_solution(repo, addresses):
# First we generate a hash for the solution to store as the contents of the `solution` tag.
Expand All @@ -20,3 +28,120 @@ def build_solution(repo, addresses):
solution_ref = repo.git.hash_object('-w', solution.name)

repo.create_tag('solution', ref=solution_ref)

def verify_repository(repo):
### NOTE - SPOILER ALERT!!!!
# This function is used to verify the repository generated during development. If you haven't
# solved the mystery yet, don't spoil the way to the solution for yourself by reading it.
repo_root = Path(repo.working_tree_dir)
try:
(police_branch, ) = (branch for branch in repo.refs if branch.name.endswith(POLICE_BRANCH))
except ValueError as err:
echo(f'Missing police archive branch: {err}')
return False

try:
(detective_branch, ) = (branch for branch in repo.refs
if branch.name.endswith(f'detectives/{MAIN_DETECTIVE.username}'))
except ValueError as err:
echo(f"Missing main detective's branch: {err}")
return False

if not repo_root.joinpath('README.md').exists():
echo("Can't find 'README.md'")
return False
instructions = repo_root / 'instructions.txt'
if not instructions.exists():
echo(f"Can't find '{instructions.name}'")
return False
instructions_text = instructions.read_text()
if MAIN_DETECTIVE.name not in instructions_text:
echo(f"The main detective's name ({MAIN_DETECTIVE.name}) isn't in '{instructions.name}'")
return False
elif POLICE_BRANCH not in instructions_text:
echo(f"The policy branch name ({POLICE_BRANCH}) isn't in '{instructions.name}'")
return False

secho('Finding main report commit', fg='magenta')
regex_commit_log = re.compile('\n'.join([
r'commit [0-9a-f]+',
r'Author: +(.+) <.+>',
r'Date: +(.+)',
r'',
r'(.*)(?:\n((?: +.*\n*)+))?',
]), re.MULTILINE)
commits = regex_commit_log.findall(repo.git.log(
'--since', f'{DATE_REPORT_WEEK_START:%d/%m/%Y}',
'--before', f'{DATE_REPORT_WEEK_END:%d/%m/%Y}',
'--author', f'{MAIN_DETECTIVE.name}',
police_branch))
if not commits:
echo('No commits match for main report')
return False
elif 1 < len(commits):
echo(f'Multiple commits ({len(commits)}) match main report')
return False
((author, timestamp, title, main_report, ), ) = commits
timestamp = datetime.strptime(timestamp, '%a %b %d %H:%M:%S %Y %z').replace(tzinfo=None)
assert MAIN_DETECTIVE.name == author
assert 'Crime scene report' in title
assert DATE_REPORT == timestamp
assert ACCESS_POINT_OF_INTEREST in main_report
assert f'detectives/{MAIN_DETECTIVE.username}' in main_report

secho('Finding suspects in access log', fg='magenta')
commits = regex_commit_log.findall(repo.git.log(
f'-S{ACCESS_POINT_OF_INTEREST}',
detective_branch))
if not len(SUSPECTS) == len(commits):
echo(f'Expected number {len(SUSPECTS)} commits, got {len(commits)}')
return False
suspect_names = set(author for (author, _, _, _) in commits)
current_suspects = set(s.name for s in SUSPECTS)
assert current_suspects == suspect_names

secho("Finding suspects' addresses", fg='magenta')
addresses = []
regex_address = re.compile('^residents.txt:.+\t([0-9]+) (.+)$')
for suspect_name in suspect_names:
match = regex_address.match(repo.git.grep('-w', suspect_name, '--', 'residents.txt'))
if not match:
echo(f"Failed to find {suspect_name}'s address")
return False
(number, street_name, ) = match.groups()
addresses.append((suspect_name, street_name, int(number)))

secho('Interviewing suspects', fg='magenta')
lead = rot13('terra Ulhaqnv')
for (suspect_name, street_name, number) in addresses:
street_tag_name = street_name.lower().replace(' ', '_')
street_ref = f'street/{street_tag_name}~{number}'
(_, _, _, commit_msg) = regex_commit_log.match(repo.git.show('-s', street_ref)).groups()
if lead in commit_msg:
echo(' Got a lead by interviewing a suspect')
current_suspects.remove(suspect_name)
continue
investigate_ref = repo.git.show(f'{street_ref}:investigate')
investigation = repo.git.show(investigate_ref)
if lead not in investigation:
echo(' Suspect dropped due to lead')
current_suspects.remove(suspect_name)

try:
(suspect, ) = current_suspects
except ValueError:
echo(f"Couldn't reduce number of suspects ({len(current_suspects)})")
return False

secho('Checking suspect', fg='magenta')
solution_hash = check_output(
f'echo "{suspect}" | git hash-object --stdin',
cwd=repo_root,
shell=True,
universal_newlines=True)
if solution_hash.strip('\n') != repo.git.show('solution'):
echo('Found the wrong suspect!')
return False

secho('Good!', fg='green')
return True

0 comments on commit 092cecb

Please sign in to comment.