├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── gitfiti-screenshot.png ├── gitfiti.py ├── pixels-large.png ├── pixels.png └── tests ├── __init__.py ├── test_find_max_daily_commits.py └── test_str_to_sprite.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | gitfiti.sh 3 | gitfiti.ps1 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.5-dev" 7 | - "nightly" 8 | install: 9 | - "pip install pytest" 10 | script: 11 | - "py.test tests" 12 | sudo: false 13 | notifications: 14 | email: false 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Eric Romano (@gelstudios). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gelstudios/gitfiti.svg?branch=master)](https://travis-ci.org/gelstudios/gitfiti) 2 | 3 | **gitfiti** _noun_ : Carefully crafted graffiti in a github commit history calendar. 4 | 5 | An example of gitfiti in the wild: 6 | ![screenshot of gitfiti](https://raw.github.com/gelstudios/gitfiti/master/gitfiti-screenshot.png "screenshot") 7 | 8 | `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_. 9 | 10 | 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. 11 | 12 | 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. 13 | 14 | ### Pixel Art 15 | 16 | ![pixel art examples](https://raw.github.com/gelstudios/gitfiti/master/pixels-large.png "pixel art") 17 | Included "art" from left to right: kitty, oneup, oneup2, hackerschool, octocat, octocat2 18 | 19 | ### Usage 20 | 21 | 1. Create a new github repo to store your handiwork. 22 | 2. Run `gitfiti.py` and follow the prompts for username, art selection, offset, and repo name. 23 | 24 | For Python 3, use `python3`. 25 | 26 | ```console 27 | $ python3 ./gitfiti.py 28 | 29 | _ __ _____ __ _ 30 | ____ _(_) /_/ __(_) /_(_) 31 | / __ `/ / __/ /_/ / __/ / 32 | / /_/ / / /_/ __/ / /_/ / 33 | \__, /_/\__/_/ /_/\__/_/ 34 | /____/ 35 | 36 | Enter GitHub URL (leave blank to use https://github.com/): 37 | ``` 38 | 39 | For Python 2, use `python2`. 40 | 41 | ```console 42 | $ python2 ./gitfiti.py 43 | 44 | _ __ _____ __ _ 45 | ____ _(_) /_/ __(_) /_(_) 46 | / __ `/ / __/ /_/ / __/ / 47 | / /_/ / / /_/ __/ / /_/ / 48 | \__, /_/\__/_/ /_/\__/_/ 49 | /____/ 50 | 51 | Enter GitHub URL (leave blank to use https://github.com/): 52 | ``` 53 | 54 | 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. 55 | 4. Wait... Seriously, you'll probably need to wait a day or two for the gitfiti to show in your commit graph. 56 | 57 | ### User Templates 58 | 59 | The file format for personal templates is the following: 60 | 61 | 1. Each template starts off with a ":" and then a name (eg. ":foo") 62 | 2. Each line after that is part of a json-recognizable array. 63 | 3. The array contain values 0-4, 0 being blank and 4 being dark green. 64 | 4. To add multiple templates, just add another name tag as described in 1. 65 | 66 | For example: 67 | 68 | ``` 69 | :center-blank 70 | [[1,1,1,1,1,1,1], 71 | [1,1,1,1,1,1,1], 72 | [1,1,1,1,1,1,1], 73 | [1,1,1,0,1,1,1], 74 | [1,1,1,1,1,1,1], 75 | [1,1,1,1,1,1,1], 76 | [1,1,1,1,1,1,1]] 77 | ``` 78 | 79 | This would output a 7 x 7 light green square with a single blank center square. 80 | 81 | Once you have a file with templates, enter its name when prompted and the templates will be added to the list of options. 82 | 83 | ### Removal 84 | 85 | Fortunately if you regret your gitfiti in the morning, removing it is fairly easy: delete the repo you created for your gitfiti (and wait). 86 | 87 | ### License 88 | 89 | gitfiti is released under [The MIT license (MIT)](http://opensource.org/licenses/MIT) 90 | 91 | --- 92 | 93 | #### Todo 94 | 95 | - ~~Remove 'requests' dependency~~ [_thanks empathetic-alligator_](https://github.com/empathetic-alligator) 96 | - ~~Web interface~~ See several web-based things below 97 | - ~~Load "art" from a file~~ [_thanks empathetic-alligator_](https://github.com/empathetic-alligator) 98 | - Load commit content from a file 99 | - Text/alphabet option 100 | - ~~powershell support!~~ [_thanks axzn_](https://github.com/axzn) 101 | - ... 102 | - Profit? 103 | 104 | #### Notable derivatives or mentions 105 | 106 | - [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 107 | - [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 108 | - [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 109 | - [Pikesley's](https://github.com/pikesley) Pokrovsky, which offers Github History Vandalism [as a Service!](http://pokrovsky.herokuapp.com/) 110 | - [PSVandalism](https://github.com/DenisBalan/PSVandalism) Wrapper around Pokrovsky, which makes possible vandalising Github History from Powershell 111 | - [github-board](https://github.com/bayandin/github-board) commits gitfiti from easy templates 112 | - [ghdecoy](https://github.com/tickelton/ghdecoy) fills the contribution graph with random data (sneaky!) 113 | - [Gitfiti Painter](http://codepen.io/cbas/pen/vOXeKV) visual drawing tool for artists to easily create templates 114 | - [git-draw](https://github.com/ben174/git-draw) a Chrome extension which will allow you to freely draw on your commit map(!) 115 | - [github-jack](https://github.com/tardypad/github-jack) a pure bash version with space invaders and shining creepypasta 116 | - [github-graffiti](https://github.com/mavrk/github-graffiti) a GUI editor with a bash script to allow custom designs on your commit map 117 | - [Paint GitHub](https://paintgithub.com/) is the most convenient way to paint your GitHub contribution graph! 118 | - [contribution-pixel-messages](https://github.com/abulvenz/contribution-pixel-messages) generates a date plan from an editable GUI 119 | - Seen something else? Submit a pull request or open an issue! 120 | -------------------------------------------------------------------------------- /gitfiti-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gelstudios/gitfiti/2437272939af339246f7f1330b4c016f1c8cb1f1/gitfiti-screenshot.png -------------------------------------------------------------------------------- /gitfiti.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2013 Eric Romano (@gelstudios) 4 | # released under The MIT license (MIT) http://opensource.org/licenses/MIT 5 | # 6 | """ 7 | gitfiti 8 | 9 | noun : Carefully crafted graffiti in a GitHub commit history calendar 10 | """ 11 | 12 | from datetime import datetime, timedelta 13 | import itertools 14 | import json 15 | import math 16 | import os 17 | try: 18 | # Python 3+ 19 | from urllib.error import HTTPError, URLError 20 | from urllib.request import urlopen 21 | except ImportError: 22 | # Python 2 23 | from urllib2 import HTTPError, URLError, urlopen 24 | 25 | try: 26 | # Python 2 27 | raw_input 28 | except NameError: 29 | # Python 3 (Python 2's `raw_input` was renamed to `input`) 30 | raw_input = input 31 | 32 | 33 | GITHUB_BASE_URL = 'https://github.com/' 34 | FALLBACK_IMAGE = 'kitty' 35 | 36 | 37 | TITLE = ''' 38 | _ __ _____ __ _ 39 | ____ _(_) /_/ __(_) /_(_) 40 | / __ `/ / __/ /_/ / __/ / 41 | / /_/ / / /_/ __/ / /_/ / 42 | \__, /_/\__/_/ /_/\__/_/ 43 | /____/ 44 | ''' 45 | 46 | 47 | KITTY = [ 48 | [0,0,0,4,0,0,0,0,4,0,0,0], 49 | [0,0,4,2,4,4,4,4,2,4,0,0], 50 | [0,0,4,2,2,2,2,2,2,4,0,0], 51 | [2,2,4,2,4,2,2,4,2,4,2,2], 52 | [0,0,4,2,2,3,3,2,2,4,0,0], 53 | [2,2,4,2,2,2,2,2,2,4,2,2], 54 | [0,0,0,3,4,4,4,4,3,0,0,0], 55 | ] 56 | 57 | ONEUP = [ 58 | [0,4,4,4,4,4,4,4,0], 59 | [4,3,2,2,1,2,2,3,4], 60 | [4,2,2,1,1,1,2,2,4], 61 | [4,3,4,4,4,4,4,3,4], 62 | [4,4,1,4,1,4,1,4,4], 63 | [0,4,1,1,1,1,1,4,0], 64 | [0,0,4,4,4,4,4,0,0], 65 | ] 66 | 67 | ONEUP2 = [ 68 | [0,0,4,4,4,4,4,4,4,0,0], 69 | [0,4,2,2,1,1,1,2,2,4,0], 70 | [4,3,2,2,1,1,1,2,2,3,4], 71 | [4,3,3,4,4,4,4,4,3,3,4], 72 | [0,4,4,1,4,1,4,1,4,4,0], 73 | [0,0,4,1,1,1,1,1,4,0,0], 74 | [0,0,0,4,4,4,4,4,0,0,0], 75 | ] 76 | 77 | HACKERSCHOOL = [ 78 | [4,4,4,4,4,4], 79 | [4,3,3,3,3,4], 80 | [4,1,3,3,1,4], 81 | [4,3,3,3,3,4], 82 | [4,4,4,4,4,4], 83 | [0,0,4,4,0,0], 84 | [4,4,4,4,4,4], 85 | ] 86 | 87 | OCTOCAT = [ 88 | [0,0,0,4,0,0,0,4,0], 89 | [0,0,4,4,4,4,4,4,4], 90 | [0,0,4,1,3,3,3,1,4], 91 | [4,0,3,4,3,3,3,4,3], 92 | [0,4,0,0,4,4,4,0,0], 93 | [0,0,4,4,4,4,4,4,4], 94 | [0,0,4,0,4,0,4,0,4], 95 | ] 96 | 97 | OCTOCAT2 = [ 98 | [0,0,4,0,0,4,0], 99 | [0,4,4,4,4,4,4], 100 | [0,4,1,3,3,1,4], 101 | [0,4,4,4,4,4,4], 102 | [4,0,0,4,4,0,0], 103 | [0,4,4,4,4,4,0], 104 | [0,0,0,4,4,4,0], 105 | ] 106 | 107 | HELLO = [ 108 | [0,1,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,4], 109 | [0,2,0,0,0,0,0,0,0,2,0,2,0,0,0,0,0,4], 110 | [0,3,3,3,0,2,3,3,0,3,0,3,0,1,3,1,0,3], 111 | [0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,4,0,3], 112 | [0,3,0,3,0,3,3,3,0,3,0,3,0,3,0,3,0,2], 113 | [0,2,0,2,0,2,0,0,0,2,0,2,0,2,0,2,0,0], 114 | [0,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,4], 115 | ] 116 | 117 | HEART1 = [ 118 | [0,1,1,0,1,1,0], 119 | [1,3,3,1,3,3,1], 120 | [1,3,4,3,4,3,1], 121 | [1,3,4,4,4,3,1], 122 | [0,1,3,4,3,1,0], 123 | [0,0,1,3,1,0,0], 124 | [0,0,0,1,0,0,0], 125 | ] 126 | 127 | HEART2 = [ 128 | [0,5,5,0,5,5,0], 129 | [5,3,3,5,3,3,5], 130 | [5,3,1,3,1,3,5], 131 | [5,3,1,1,1,3,5], 132 | [0,5,3,1,3,5,0], 133 | [0,0,5,3,5,0,0], 134 | [0,0,0,5,0,0,0], 135 | ] 136 | 137 | HIREME = [ 138 | [1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 139 | [2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], 140 | [3,3,3,0,2,0,3,3,3,0,2,3,3,0,0,3,3,0,3,0,0,2,3,3], 141 | [4,0,4,0,4,0,4,0,0,0,4,0,4,0,0,4,0,4,0,4,0,4,0,4], 142 | [3,0,3,0,3,0,3,0,0,0,3,3,3,0,0,3,0,3,0,3,0,3,3,3], 143 | [2,0,2,0,2,0,2,0,0,0,2,0,0,0,0,2,0,2,0,2,0,2,0,0], 144 | [1,0,1,0,1,0,1,0,0,0,1,1,1,0,0,1,0,1,0,1,0,1,1,1], 145 | ] 146 | 147 | BEER = [ 148 | [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], 149 | [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], 150 | [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], 151 | [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], 152 | [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], 153 | [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], 154 | [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], 155 | ] 156 | 157 | GLIDERS = [ 158 | [0,0,0,4,0,4,0,0,0,0,4,0,0,0], 159 | [0,4,0,4,0,0,4,4,0,0,0,4,0,0], 160 | [0,0,4,4,0,4,4,0,0,4,4,4,0,0], 161 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0], 162 | [0,4,0,4,0,0,0,4,0,0,0,0,0,0], 163 | [0,0,4,4,0,4,0,4,0,0,0,0,0,0], 164 | [0,0,4,0,0,0,4,4,0,0,0,0,0,0], 165 | ] 166 | 167 | HEART = [ 168 | [0,4,4,0,4,4,0], 169 | [4,2,2,4,2,2,4], 170 | [4,2,2,2,2,2,4], 171 | [4,2,2,2,2,2,4], 172 | [0,4,2,2,2,4,0], 173 | [0,0,4,2,4,0,0], 174 | [0,0,0,4,0,0,0], 175 | ] 176 | 177 | HEART_SHINY = [ 178 | [0,4,4,0,4,4,0], 179 | [4,2,0,4,2,2,4], 180 | [4,0,2,2,2,2,4], 181 | [4,2,2,2,2,2,4], 182 | [0,4,2,2,2,4,0], 183 | [0,0,4,2,4,0,0], 184 | [0,0,0,4,0,0,0], 185 | ] 186 | 187 | ASCII_TO_NUMBER = { 188 | '_': 0, 189 | '_': 1, 190 | '~': 2, 191 | '=': 3, 192 | '*': 4, 193 | } 194 | 195 | 196 | def str_to_sprite(content): 197 | # Break out lines and filter any excess 198 | lines = content.split('\n') 199 | def is_empty_line(line): 200 | return len(line) != 0 201 | lines = filter(is_empty_line, lines) 202 | 203 | # Break up lines into each character 204 | split_lines = [list(line) for line in lines] 205 | 206 | # Replace each character with its numeric equivalent 207 | for line in split_lines: 208 | for index, char in enumerate(line): 209 | line[index] = ASCII_TO_NUMBER.get(char, 0) 210 | 211 | # Return the formatted str 212 | return split_lines 213 | 214 | 215 | ONEUP_STR = str_to_sprite(''' 216 | ******* 217 | *=~~-~~=* 218 | *~~---~~* 219 | *=*****=* 220 | **-*-*-** 221 | *-----* 222 | ***** 223 | ''') 224 | 225 | 226 | IMAGES = { 227 | 'kitty': KITTY, 228 | 'oneup': ONEUP, 229 | 'oneup2': ONEUP2, 230 | 'hackerschool': HACKERSCHOOL, 231 | 'octocat': OCTOCAT, 232 | 'octocat2': OCTOCAT2, 233 | 'hello': HELLO, 234 | 'heart1': HEART1, 235 | 'heart2': HEART2, 236 | 'hireme': HIREME, 237 | 'oneup_str': ONEUP_STR, 238 | 'beer': BEER, 239 | 'gliders': GLIDERS, 240 | 'heart' : HEART, 241 | 'heart_shiny' : HEART_SHINY, 242 | } 243 | 244 | SHELLS = { 245 | 'bash': 'sh', 246 | 'powershell': 'ps1', 247 | } 248 | 249 | def load_images(img_names): 250 | """loads user images from given file(s)""" 251 | if img_names[0] == '': 252 | return {} 253 | 254 | for image_name in img_names: 255 | with open(image_name) as img: 256 | loaded_imgs = {} 257 | img_list = '' 258 | img_line = ' ' 259 | name = img.readline().replace('\n', '') 260 | name = name[1:] 261 | 262 | while True: 263 | img_line = img.readline() 264 | if img_line == '': 265 | break 266 | 267 | img_line.replace('\n', '') 268 | if img_line[0] == ':': 269 | loaded_imgs[name] = json.loads(img_list) 270 | name = img_line[1:] 271 | img_list = '' 272 | else: 273 | img_list += img_line 274 | 275 | loaded_imgs[name] = json.loads(img_list) 276 | 277 | return loaded_imgs 278 | 279 | 280 | def retrieve_contributions_calendar(username, base_url): 281 | """retrieves the GitHub commit calendar data for a username""" 282 | base_url = base_url + 'users/' + username 283 | 284 | try: 285 | url = base_url + '/contributions' 286 | page = urlopen(url) 287 | except (HTTPError, URLError) as e: 288 | print('There was a problem fetching data from {0}'.format(url)) 289 | print(e) 290 | raise SystemExit 291 | 292 | return page.read().decode('utf-8') 293 | 294 | 295 | def parse_contributions_calendar(contributions_calendar): 296 | """Yield daily counts extracted from the embedded contributions SVG.""" 297 | for line in contributions_calendar.splitlines(): 298 | # a valid line looks like this: 299 | # 23 contributions on Sunday, February 26, 2023 300 | if 'data-date=' in line: 301 | commit = line.split('>')[1].split()[0] # yuck 302 | 303 | if commit.isnumeric(): 304 | yield int(commit) 305 | 306 | 307 | def find_max_daily_commits(contributions_calendar): 308 | """finds the highest number of commits in one day""" 309 | daily_counts = parse_contributions_calendar(contributions_calendar) 310 | return max(daily_counts, default=0) 311 | 312 | 313 | def calculate_multiplier(max_commits): 314 | """calculates a multiplier to scale GitHub colors to commit history""" 315 | m = max_commits / 4.0 316 | 317 | if m == 0: 318 | return 1 319 | 320 | m = math.ceil(m) 321 | m = int(m) 322 | return m 323 | 324 | 325 | def get_start_date(): 326 | """returns a datetime object for the first sunday after one year ago today 327 | at 12:00 noon""" 328 | today = datetime.today() 329 | date = datetime(today.year - 1, today.month, today.day, 12) 330 | weekday = datetime.weekday(date) 331 | 332 | while weekday < 6: 333 | date = date + timedelta(1) 334 | weekday = datetime.weekday(date) 335 | 336 | return date 337 | 338 | 339 | def generate_next_dates(start_date, offset=0): 340 | """generator that returns the next date, requires a datetime object as 341 | input. The offset is in weeks""" 342 | start = offset * 7 343 | for i in itertools.count(start): 344 | yield start_date + timedelta(i) 345 | 346 | 347 | def generate_values_in_date_order(image, multiplier=1): 348 | height = 7 349 | width = len(image[0]) 350 | 351 | for w in range(width): 352 | for h in range(height): 353 | yield image[h][w] * multiplier 354 | 355 | 356 | def commit(commitdate, shell): 357 | template_bash = ( 358 | '''GIT_AUTHOR_DATE={0} GIT_COMMITTER_DATE={1} ''' 359 | '''git commit --allow-empty -m "gitfiti" > /dev/null\n''' 360 | ) 361 | 362 | template_powershell = ( 363 | '''$Env:GIT_AUTHOR_DATE="{0}"\n$Env:GIT_COMMITTER_DATE="{1}"\n''' 364 | '''git commit --allow-empty -m "gitfiti" | Out-Null\n''' 365 | ) 366 | 367 | template = template_bash if shell == 'bash' else template_powershell 368 | 369 | return template.format(commitdate.isoformat(), commitdate.isoformat()) 370 | 371 | 372 | def fake_it(image, start_date, username, repo, git_url, shell, offset=0, multiplier=1): 373 | template_bash = ( 374 | '#!/usr/bin/env bash\n' 375 | 'REPO={0}\n' 376 | 'git init $REPO\n' 377 | 'cd $REPO\n' 378 | 'touch README.md\n' 379 | 'git add README.md\n' 380 | 'touch gitfiti\n' 381 | 'git add gitfiti\n' 382 | '{1}\n' 383 | 'git branch -M main\n' 384 | 'git remote add origin {2}:{3}/$REPO.git\n' 385 | 'git pull origin main\n' 386 | 'git push -u origin main\n' 387 | ) 388 | 389 | template_powershell = ( 390 | 'cd $PSScriptRoot\n' 391 | '$REPO="{0}"\n' 392 | 'git init $REPO\n' 393 | 'cd $REPO\n' 394 | 'New-Item README.md -ItemType file | Out-Null\n' 395 | 'git add README.md\n' 396 | 'New-Item gitfiti -ItemType file | Out-Null\n' 397 | 'git add gitfiti\n' 398 | '{1}\n' 399 | 'git branch -M main\n' 400 | 'git remote add origin {2}:{3}/$REPO.git\n' 401 | 'git pull origin main\n' 402 | 'git push -u origin main\n' 403 | ) 404 | 405 | template = template_bash if shell == 'bash' else template_powershell 406 | 407 | strings = [] 408 | for value, date in zip(generate_values_in_date_order(image, multiplier), 409 | generate_next_dates(start_date, offset)): 410 | for _ in range(value): 411 | strings.append(commit(date, shell)) 412 | 413 | return template.format(repo, ''.join(strings), git_url, username) 414 | 415 | 416 | def save(output, filename): 417 | """Saves the list to a given filename""" 418 | with open(filename, 'w') as f: 419 | f.write(output) 420 | os.chmod(filename, 0o755) # add execute permissions 421 | 422 | 423 | def request_user_input(prompt='> '): 424 | """Request input from the user and return what has been entered.""" 425 | return raw_input(prompt) 426 | 427 | 428 | def main(): 429 | print(TITLE) 430 | 431 | ghe = request_user_input( 432 | 'Enter GitHub URL (leave blank to use {}): '.format(GITHUB_BASE_URL)) 433 | 434 | username = request_user_input('Enter your GitHub username: ') 435 | 436 | git_base = ghe if ghe else GITHUB_BASE_URL 437 | 438 | contributions_calendar = retrieve_contributions_calendar(username, git_base) 439 | 440 | max_daily_commits = find_max_daily_commits(contributions_calendar) 441 | 442 | m = calculate_multiplier(max_daily_commits) 443 | 444 | repo = request_user_input( 445 | 'Enter the name of the repository to use by gitfiti: ') 446 | 447 | offset = request_user_input( 448 | 'Enter the number of weeks to offset the image (from the left): ') 449 | 450 | offset = int(offset) if offset.strip() else 0 451 | 452 | print(( 453 | 'By default gitfiti.py matches the darkest pixel to the highest\n' 454 | 'number of commits found in your GitHub commit/activity calendar,\n' 455 | '\n' 456 | 'Currently this is: {0} commits\n' 457 | '\n' 458 | 'Enter the word "gitfiti" to exceed your max\n' 459 | '(this option generates WAY more commits)\n' 460 | 'Any other input will cause the default matching behavior' 461 | ).format(max_daily_commits)) 462 | match = request_user_input() 463 | 464 | match = m if (match == 'gitfiti') else 1 465 | 466 | print('Enter file(s) to load images from (blank if not applicable)') 467 | img_names = request_user_input().split(' ') 468 | 469 | loaded_images = load_images(img_names) 470 | images = dict(IMAGES, **loaded_images) 471 | 472 | print('Enter the image name to gitfiti') 473 | print('Images: ' + ', '.join(images.keys())) 474 | image = request_user_input() 475 | 476 | image_name_fallback = FALLBACK_IMAGE 477 | 478 | if not image: 479 | image = IMAGES[image_name_fallback] 480 | else: 481 | try: 482 | image = images[image] 483 | except: 484 | image = IMAGES[image_name_fallback] 485 | 486 | start_date = get_start_date() 487 | fake_it_multiplier = m * match 488 | 489 | if not ghe: 490 | git_url = 'git@github.com' 491 | else: 492 | git_url = request_user_input('Enter Git URL like git@site.github.com: ') 493 | 494 | shell = '' 495 | while shell not in SHELLS.keys(): 496 | shell = request_user_input( 497 | 'Enter the target shell ({}): '.format(' or '.join(SHELLS.keys()))) 498 | 499 | output = fake_it(image, start_date, username, repo, git_url, shell, offset, 500 | fake_it_multiplier) 501 | 502 | output_filename = 'gitfiti.{}'.format(SHELLS[shell]) 503 | save(output, output_filename) 504 | print('{} saved.'.format(output_filename)) 505 | print('Create a new(!) repo named {0} at {1} and run the script'.format(repo, git_base)) 506 | 507 | 508 | if __name__ == '__main__': 509 | main() 510 | -------------------------------------------------------------------------------- /pixels-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gelstudios/gitfiti/2437272939af339246f7f1330b4c016f1c8cb1f1/pixels-large.png -------------------------------------------------------------------------------- /pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gelstudios/gitfiti/2437272939af339246f7f1330b4c016f1c8cb1f1/pixels.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gelstudios/gitfiti/2437272939af339246f7f1330b4c016f1c8cb1f1/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_find_max_daily_commits.py: -------------------------------------------------------------------------------- 1 | from gitfiti import find_max_daily_commits, parse_contributions_calendar 2 | 3 | 4 | CONTRIBUTIONS_CALENDAR_SVG = '''\ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Jun 51 | Jul 52 | Aug 53 | Sep 54 | Oct 55 | Nov 56 | Dec 57 | Jan 58 | Feb 59 | Mar 60 | Apr 61 | May 62 | 63 | M 64 | 65 | W 66 | 67 | F 68 | 69 | 70 | 71 | ''' 72 | 73 | 74 | def test_parse_contributions_calendar(): 75 | expected = [ 76 | 0, 0, 0, 0, 6, 0, 0, 77 | 0, 0, 0, 0, 0, 0, 6, 78 | 84, 16, 4, 8, 0, 0, 0, 79 | 0, 25, 66, 20, 10, 0, 0, 80 | 33, 9, 0, 0, 7, 81 | ] 82 | 83 | actual = parse_contributions_calendar(CONTRIBUTIONS_CALENDAR_SVG) 84 | 85 | assert list(actual) == expected 86 | 87 | 88 | def test_find_max_daily_commits(): 89 | assert find_max_daily_commits(CONTRIBUTIONS_CALENDAR_SVG) == 84 90 | -------------------------------------------------------------------------------- /tests/test_str_to_sprite.py: -------------------------------------------------------------------------------- 1 | from gitfiti import str_to_sprite, ONEUP_STR 2 | 3 | 4 | SYMBOLS = ''' 5 | ******* 6 | *=~~-~~=* 7 | *~~---~~* 8 | *=*****=* 9 | **-*-*-** 10 | *-----* 11 | ***** 12 | ''' 13 | 14 | NUMBERS = [ 15 | [0,4,4,4,4,4,4,4,0], 16 | [4,3,2,2,0,2,2,3,4], 17 | [4,2,2,0,0,0,2,2,4], 18 | [4,3,4,4,4,4,4,3,4], 19 | [4,4,0,4,0,4,0,4,4], 20 | [0,4,0,0,0,0,0,4,0], 21 | [0,0,4,4,4,4,4,0,0], 22 | ] 23 | 24 | 25 | def test_symbols_to_numbers(): 26 | actual = str_to_sprite(SYMBOLS) 27 | assert actual == NUMBERS 28 | --------------------------------------------------------------------------------