Repository: gelstudios/gitfiti Branch: main Commit: 2437272939af Files: 8 Total size: 25.3 KB Directory structure: gitextract_uqfzpv26/ ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── gitfiti.py └── tests/ ├── __init__.py ├── test_find_max_daily_commits.py └── test_str_to_sprite.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.pyc gitfiti.sh gitfiti.ps1 ================================================ FILE: .travis.yml ================================================ language: python python: - "2.7" - "3.4" - "3.5" - "3.5-dev" - "nightly" install: - "pip install pytest" script: - "py.test tests" sudo: false notifications: email: false ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2013 Eric Romano (@gelstudios). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/gelstudios/gitfiti.svg?branch=master)](https://travis-ci.org/gelstudios/gitfiti) **gitfiti** _noun_ : Carefully crafted graffiti in a github commit history calendar. An example of gitfiti in the wild: ![screenshot of gitfiti](https://raw.github.com/gelstudios/gitfiti/master/gitfiti-screenshot.png "screenshot") `gitfiti.py` is a tool to decorate your github account's commit history calendar by (blatantly) abusing git's ability to accept commits _in the past_. How? `gitfiti.py` generates a script (powershell or bash) that makes commits with the GIT_AUTHOR_DATE and GIT_COMMITTER_DATE environment variables set for each targeted pixel. Since this is likely to clobber repo's history, it is highly recommend that you create a _new_ github repo when using gitfiti. Also, the generated script assumes you are using public-key authentication with git. ### Pixel Art ![pixel art examples](https://raw.github.com/gelstudios/gitfiti/master/pixels-large.png "pixel art") Included "art" from left to right: kitty, oneup, oneup2, hackerschool, octocat, octocat2 ### Usage 1. Create a new github repo to store your handiwork. 2. Run `gitfiti.py` and follow the prompts for username, art selection, offset, and repo name. For Python 3, use `python3`. ```console $ python3 ./gitfiti.py _ __ _____ __ _ ____ _(_) /_/ __(_) /_(_) / __ `/ / __/ /_/ / __/ / / /_/ / / /_/ __/ / /_/ / \__, /_/\__/_/ /_/\__/_/ /____/ Enter GitHub URL (leave blank to use https://github.com/): ``` For Python 2, use `python2`. ```console $ python2 ./gitfiti.py _ __ _____ __ _ ____ _(_) /_/ __(_) /_(_) / __ `/ / __/ /_/ / __/ / / /_/ / / /_/ __/ / /_/ / \__, /_/\__/_/ /_/\__/_/ /____/ Enter GitHub URL (leave blank to use https://github.com/): ``` 3. Run the generated `gitfiti.sh` or `gitfiti.ps1` from your home directory (or any non-git tracked dir) and watch it go to work. 4. Wait... Seriously, you'll probably need to wait a day or two for the gitfiti to show in your commit graph. ### User Templates The file format for personal templates is the following: 1. Each template starts off with a ":" and then a name (eg. ":foo") 2. Each line after that is part of a json-recognizable array. 3. The array contain values 0-4, 0 being blank and 4 being dark green. 4. To add multiple templates, just add another name tag as described in 1. For example: ``` :center-blank [[1,1,1,1,1,1,1], [1,1,1,1,1,1,1], [1,1,1,1,1,1,1], [1,1,1,0,1,1,1], [1,1,1,1,1,1,1], [1,1,1,1,1,1,1], [1,1,1,1,1,1,1]] ``` This would output a 7 x 7 light green square with a single blank center square. Once you have a file with templates, enter its name when prompted and the templates will be added to the list of options. ### Removal Fortunately if you regret your gitfiti in the morning, removing it is fairly easy: delete the repo you created for your gitfiti (and wait). ### License gitfiti is released under [The MIT license (MIT)](http://opensource.org/licenses/MIT) --- #### Todo - ~~Remove 'requests' dependency~~ [_thanks empathetic-alligator_](https://github.com/empathetic-alligator) - ~~Web interface~~ See several web-based things below - ~~Load "art" from a file~~ [_thanks empathetic-alligator_](https://github.com/empathetic-alligator) - Load commit content from a file - Text/alphabet option - ~~powershell support!~~ [_thanks axzn_](https://github.com/axzn) - ... - Profit? #### Notable derivatives or mentions - [Vincent Van Git](https://github.com/jh3y/vincent-van-git) Vincent, which offers a [very slick web ui](https://vincent-van-git.netlify.app/) to generate a gitfiti script - [github-calendar-customerizer](https://github.com/ZachSaucier/github-calendar-customizer) from ZachSaucier, another very [nice web GUI](https://codepen.io/ZachSaucier/full/PzVRBy) for generating gitfiti templates - [git-art](https://github.com/jamesjarvis/git-art) from jamesjarvis, a work-alike web based [editor GUI](https://jamesjarvis.github.io/git-art/) that generates the script too - [Pikesley's](https://github.com/pikesley) Pokrovsky, which offers Github History Vandalism [as a Service!](http://pokrovsky.herokuapp.com/) - [PSVandalism](https://github.com/DenisBalan/PSVandalism) Wrapper around Pokrovsky, which makes possible vandalising Github History from Powershell - [github-board](https://github.com/bayandin/github-board) commits gitfiti from easy templates - [ghdecoy](https://github.com/tickelton/ghdecoy) fills the contribution graph with random data (sneaky!) - [Gitfiti Painter](http://codepen.io/cbas/pen/vOXeKV) visual drawing tool for artists to easily create templates - [git-draw](https://github.com/ben174/git-draw) a Chrome extension which will allow you to freely draw on your commit map(!) - [github-jack](https://github.com/tardypad/github-jack) a pure bash version with space invaders and shining creepypasta - [github-graffiti](https://github.com/mavrk/github-graffiti) a GUI editor with a bash script to allow custom designs on your commit map - [Paint GitHub](https://paintgithub.com/) is the most convenient way to paint your GitHub contribution graph! - [contribution-pixel-messages](https://github.com/abulvenz/contribution-pixel-messages) generates a date plan from an editable GUI - Seen something else? Submit a pull request or open an issue! ================================================ FILE: gitfiti.py ================================================ #!/usr/bin/env python # # Copyright (c) 2013 Eric Romano (@gelstudios) # released under The MIT license (MIT) http://opensource.org/licenses/MIT # """ gitfiti noun : Carefully crafted graffiti in a GitHub commit history calendar """ from datetime import datetime, timedelta import itertools import json import math import os try: # Python 3+ from urllib.error import HTTPError, URLError from urllib.request import urlopen except ImportError: # Python 2 from urllib2 import HTTPError, URLError, urlopen try: # Python 2 raw_input except NameError: # Python 3 (Python 2's `raw_input` was renamed to `input`) raw_input = input GITHUB_BASE_URL = 'https://github.com/' FALLBACK_IMAGE = 'kitty' TITLE = ''' _ __ _____ __ _ ____ _(_) /_/ __(_) /_(_) / __ `/ / __/ /_/ / __/ / / /_/ / / /_/ __/ / /_/ / \__, /_/\__/_/ /_/\__/_/ /____/ ''' KITTY = [ [0,0,0,4,0,0,0,0,4,0,0,0], [0,0,4,2,4,4,4,4,2,4,0,0], [0,0,4,2,2,2,2,2,2,4,0,0], [2,2,4,2,4,2,2,4,2,4,2,2], [0,0,4,2,2,3,3,2,2,4,0,0], [2,2,4,2,2,2,2,2,2,4,2,2], [0,0,0,3,4,4,4,4,3,0,0,0], ] ONEUP = [ [0,4,4,4,4,4,4,4,0], [4,3,2,2,1,2,2,3,4], [4,2,2,1,1,1,2,2,4], [4,3,4,4,4,4,4,3,4], [4,4,1,4,1,4,1,4,4], [0,4,1,1,1,1,1,4,0], [0,0,4,4,4,4,4,0,0], ] ONEUP2 = [ [0,0,4,4,4,4,4,4,4,0,0], [0,4,2,2,1,1,1,2,2,4,0], [4,3,2,2,1,1,1,2,2,3,4], [4,3,3,4,4,4,4,4,3,3,4], [0,4,4,1,4,1,4,1,4,4,0], [0,0,4,1,1,1,1,1,4,0,0], [0,0,0,4,4,4,4,4,0,0,0], ] HACKERSCHOOL = [ [4,4,4,4,4,4], [4,3,3,3,3,4], [4,1,3,3,1,4], [4,3,3,3,3,4], [4,4,4,4,4,4], [0,0,4,4,0,0], [4,4,4,4,4,4], ] OCTOCAT = [ [0,0,0,4,0,0,0,4,0], [0,0,4,4,4,4,4,4,4], [0,0,4,1,3,3,3,1,4], [4,0,3,4,3,3,3,4,3], [0,4,0,0,4,4,4,0,0], [0,0,4,4,4,4,4,4,4], [0,0,4,0,4,0,4,0,4], ] OCTOCAT2 = [ [0,0,4,0,0,4,0], [0,4,4,4,4,4,4], [0,4,1,3,3,1,4], [0,4,4,4,4,4,4], [4,0,0,4,4,0,0], [0,4,4,4,4,4,0], [0,0,0,4,4,4,0], ] HELLO = [ [0,1,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,4], [0,2,0,0,0,0,0,0,0,2,0,2,0,0,0,0,0,4], [0,3,3,3,0,2,3,3,0,3,0,3,0,1,3,1,0,3], [0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,3], [0,3,0,3,0,3,3,3,0,3,0,3,0,3,0,3,0,2], [0,2,0,2,0,2,0,0,0,2,0,2,0,2,0,2,0,0], [0,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,4], ] HEART1 = [ [0,1,1,0,1,1,0], [1,3,3,1,3,3,1], [1,3,4,3,4,3,1], [1,3,4,4,4,3,1], [0,1,3,4,3,1,0], [0,0,1,3,1,0,0], [0,0,0,1,0,0,0], ] HEART2 = [ [0,5,5,0,5,5,0], [5,3,3,5,3,3,5], [5,3,1,3,1,3,5], [5,3,1,1,1,3,5], [0,5,3,1,3,5,0], [0,0,5,3,5,0,0], [0,0,0,5,0,0,0], ] HIREME = [ [1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], [3,3,3,0,2,0,3,3,3,0,2,3,3,0,0,3,3,0,3,0,0,2,3,3], [4,0,4,0,4,0,4,0,0,0,4,0,4,0,0,4,0,4,0,4,0,4,0,4], [3,0,3,0,3,0,3,0,0,0,3,3,3,0,0,3,0,3,0,3,0,3,3,3], [2,0,2,0,2,0,2,0,0,0,2,0,0,0,0,2,0,2,0,2,0,2,0,0], [1,0,1,0,1,0,1,0,0,0,1,1,1,0,0,1,0,1,0,1,0,1,1,1], ] BEER = [ [0,0,0,0,0,0,0,3,3,3,0,0,3,3,3,0,3,3,3,0,3,3,3,0,0], [0,0,1,1,1,1,0,3,0,0,3,0,3,0,0,0,3,0,0,0,3,0,0,3,0], [0,2,2,2,2,2,0,3,0,0,3,0,3,0,0,0,3,0,0,0,3,0,0,3,0], [2,0,2,2,2,2,0,3,3,3,0,0,3,3,3,0,3,3,3,0,3,3,3,0,0], [2,0,2,2,2,2,0,3,0,0,3,0,3,0,0,0,3,0,0,0,3,0,3,0,0], [0,2,2,2,2,2,0,3,0,0,3,0,3,0,0,0,3,0,0,0,3,0,0,3,0], [0,0,2,2,2,2,0,3,3,3,0,0,3,3,3,0,3,3,3,0,3,0,0,3,0], ] GLIDERS = [ [0,0,0,4,0,4,0,0,0,0,4,0,0,0], [0,4,0,4,0,0,4,4,0,0,0,4,0,0], [0,0,4,4,0,4,4,0,0,4,4,4,0,0], [0,0,0,0,0,0,0,0,0,0,0,0,0,0], [0,4,0,4,0,0,0,4,0,0,0,0,0,0], [0,0,4,4,0,4,0,4,0,0,0,0,0,0], [0,0,4,0,0,0,4,4,0,0,0,0,0,0], ] HEART = [ [0,4,4,0,4,4,0], [4,2,2,4,2,2,4], [4,2,2,2,2,2,4], [4,2,2,2,2,2,4], [0,4,2,2,2,4,0], [0,0,4,2,4,0,0], [0,0,0,4,0,0,0], ] HEART_SHINY = [ [0,4,4,0,4,4,0], [4,2,0,4,2,2,4], [4,0,2,2,2,2,4], [4,2,2,2,2,2,4], [0,4,2,2,2,4,0], [0,0,4,2,4,0,0], [0,0,0,4,0,0,0], ] ASCII_TO_NUMBER = { '_': 0, '_': 1, '~': 2, '=': 3, '*': 4, } def str_to_sprite(content): # Break out lines and filter any excess lines = content.split('\n') def is_empty_line(line): return len(line) != 0 lines = filter(is_empty_line, lines) # Break up lines into each character split_lines = [list(line) for line in lines] # Replace each character with its numeric equivalent for line in split_lines: for index, char in enumerate(line): line[index] = ASCII_TO_NUMBER.get(char, 0) # Return the formatted str return split_lines ONEUP_STR = str_to_sprite(''' ******* *=~~-~~=* *~~---~~* *=*****=* **-*-*-** *-----* ***** ''') IMAGES = { 'kitty': KITTY, 'oneup': ONEUP, 'oneup2': ONEUP2, 'hackerschool': HACKERSCHOOL, 'octocat': OCTOCAT, 'octocat2': OCTOCAT2, 'hello': HELLO, 'heart1': HEART1, 'heart2': HEART2, 'hireme': HIREME, 'oneup_str': ONEUP_STR, 'beer': BEER, 'gliders': GLIDERS, 'heart' : HEART, 'heart_shiny' : HEART_SHINY, } SHELLS = { 'bash': 'sh', 'powershell': 'ps1', } def load_images(img_names): """loads user images from given file(s)""" if img_names[0] == '': return {} for image_name in img_names: with open(image_name) as img: loaded_imgs = {} img_list = '' img_line = ' ' name = img.readline().replace('\n', '') name = name[1:] while True: img_line = img.readline() if img_line == '': break img_line.replace('\n', '') if img_line[0] == ':': loaded_imgs[name] = json.loads(img_list) name = img_line[1:] img_list = '' else: img_list += img_line loaded_imgs[name] = json.loads(img_list) return loaded_imgs def retrieve_contributions_calendar(username, base_url): """retrieves the GitHub commit calendar data for a username""" base_url = base_url + 'users/' + username try: url = base_url + '/contributions' page = urlopen(url) except (HTTPError, URLError) as e: print('There was a problem fetching data from {0}'.format(url)) print(e) raise SystemExit return page.read().decode('utf-8') def parse_contributions_calendar(contributions_calendar): """Yield daily counts extracted from the embedded contributions SVG.""" for line in contributions_calendar.splitlines(): # a valid line looks like this: # 23 contributions on Sunday, February 26, 2023 if 'data-date=' in line: commit = line.split('>')[1].split()[0] # yuck if commit.isnumeric(): yield int(commit) def find_max_daily_commits(contributions_calendar): """finds the highest number of commits in one day""" daily_counts = parse_contributions_calendar(contributions_calendar) return max(daily_counts, default=0) def calculate_multiplier(max_commits): """calculates a multiplier to scale GitHub colors to commit history""" m = max_commits / 4.0 if m == 0: return 1 m = math.ceil(m) m = int(m) return m def get_start_date(): """returns a datetime object for the first sunday after one year ago today at 12:00 noon""" today = datetime.today() date = datetime(today.year - 1, today.month, today.day, 12) weekday = datetime.weekday(date) while weekday < 6: date = date + timedelta(1) weekday = datetime.weekday(date) return date def generate_next_dates(start_date, offset=0): """generator that returns the next date, requires a datetime object as input. The offset is in weeks""" start = offset * 7 for i in itertools.count(start): yield start_date + timedelta(i) def generate_values_in_date_order(image, multiplier=1): height = 7 width = len(image[0]) for w in range(width): for h in range(height): yield image[h][w] * multiplier def commit(commitdate, shell): template_bash = ( '''GIT_AUTHOR_DATE={0} GIT_COMMITTER_DATE={1} ''' '''git commit --allow-empty -m "gitfiti" > /dev/null\n''' ) template_powershell = ( '''$Env:GIT_AUTHOR_DATE="{0}"\n$Env:GIT_COMMITTER_DATE="{1}"\n''' '''git commit --allow-empty -m "gitfiti" | Out-Null\n''' ) template = template_bash if shell == 'bash' else template_powershell return template.format(commitdate.isoformat(), commitdate.isoformat()) def fake_it(image, start_date, username, repo, git_url, shell, offset=0, multiplier=1): template_bash = ( '#!/usr/bin/env bash\n' 'REPO={0}\n' 'git init $REPO\n' 'cd $REPO\n' 'touch README.md\n' 'git add README.md\n' 'touch gitfiti\n' 'git add gitfiti\n' '{1}\n' 'git branch -M main\n' 'git remote add origin {2}:{3}/$REPO.git\n' 'git pull origin main\n' 'git push -u origin main\n' ) template_powershell = ( 'cd $PSScriptRoot\n' '$REPO="{0}"\n' 'git init $REPO\n' 'cd $REPO\n' 'New-Item README.md -ItemType file | Out-Null\n' 'git add README.md\n' 'New-Item gitfiti -ItemType file | Out-Null\n' 'git add gitfiti\n' '{1}\n' 'git branch -M main\n' 'git remote add origin {2}:{3}/$REPO.git\n' 'git pull origin main\n' 'git push -u origin main\n' ) template = template_bash if shell == 'bash' else template_powershell strings = [] for value, date in zip(generate_values_in_date_order(image, multiplier), generate_next_dates(start_date, offset)): for _ in range(value): strings.append(commit(date, shell)) return template.format(repo, ''.join(strings), git_url, username) def save(output, filename): """Saves the list to a given filename""" with open(filename, 'w') as f: f.write(output) os.chmod(filename, 0o755) # add execute permissions def request_user_input(prompt='> '): """Request input from the user and return what has been entered.""" return raw_input(prompt) def main(): print(TITLE) ghe = request_user_input( 'Enter GitHub URL (leave blank to use {}): '.format(GITHUB_BASE_URL)) username = request_user_input('Enter your GitHub username: ') git_base = ghe if ghe else GITHUB_BASE_URL contributions_calendar = retrieve_contributions_calendar(username, git_base) max_daily_commits = find_max_daily_commits(contributions_calendar) m = calculate_multiplier(max_daily_commits) repo = request_user_input( 'Enter the name of the repository to use by gitfiti: ') offset = request_user_input( 'Enter the number of weeks to offset the image (from the left): ') offset = int(offset) if offset.strip() else 0 print(( 'By default gitfiti.py matches the darkest pixel to the highest\n' 'number of commits found in your GitHub commit/activity calendar,\n' '\n' 'Currently this is: {0} commits\n' '\n' 'Enter the word "gitfiti" to exceed your max\n' '(this option generates WAY more commits)\n' 'Any other input will cause the default matching behavior' ).format(max_daily_commits)) match = request_user_input() match = m if (match == 'gitfiti') else 1 print('Enter file(s) to load images from (blank if not applicable)') img_names = request_user_input().split(' ') loaded_images = load_images(img_names) images = dict(IMAGES, **loaded_images) print('Enter the image name to gitfiti') print('Images: ' + ', '.join(images.keys())) image = request_user_input() image_name_fallback = FALLBACK_IMAGE if not image: image = IMAGES[image_name_fallback] else: try: image = images[image] except: image = IMAGES[image_name_fallback] start_date = get_start_date() fake_it_multiplier = m * match if not ghe: git_url = 'git@github.com' else: git_url = request_user_input('Enter Git URL like git@site.github.com: ') shell = '' while shell not in SHELLS.keys(): shell = request_user_input( 'Enter the target shell ({}): '.format(' or '.join(SHELLS.keys()))) output = fake_it(image, start_date, username, repo, git_url, shell, offset, fake_it_multiplier) output_filename = 'gitfiti.{}'.format(SHELLS[shell]) save(output, output_filename) print('{} saved.'.format(output_filename)) print('Create a new(!) repo named {0} at {1} and run the script'.format(repo, git_base)) if __name__ == '__main__': main() ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_find_max_daily_commits.py ================================================ from gitfiti import find_max_daily_commits, parse_contributions_calendar CONTRIBUTIONS_CALENDAR_SVG = '''\ Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar Apr May M W F ''' def test_parse_contributions_calendar(): expected = [ 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 6, 84, 16, 4, 8, 0, 0, 0, 0, 25, 66, 20, 10, 0, 0, 33, 9, 0, 0, 7, ] actual = parse_contributions_calendar(CONTRIBUTIONS_CALENDAR_SVG) assert list(actual) == expected def test_find_max_daily_commits(): assert find_max_daily_commits(CONTRIBUTIONS_CALENDAR_SVG) == 84 ================================================ FILE: tests/test_str_to_sprite.py ================================================ from gitfiti import str_to_sprite, ONEUP_STR SYMBOLS = ''' ******* *=~~-~~=* *~~---~~* *=*****=* **-*-*-** *-----* ***** ''' NUMBERS = [ [0,4,4,4,4,4,4,4,0], [4,3,2,2,0,2,2,3,4], [4,2,2,0,0,0,2,2,4], [4,3,4,4,4,4,4,3,4], [4,4,0,4,0,4,0,4,4], [0,4,0,0,0,0,0,4,0], [0,0,4,4,4,4,4,0,0], ] def test_symbols_to_numbers(): actual = str_to_sprite(SYMBOLS) assert actual == NUMBERS