├── requirements.txt ├── process.py ├── README.md └── playback.py /requirements.txt: -------------------------------------------------------------------------------- 1 | GitPython==3.1.1 2 | whatthepatch==0.0.6 3 | keyboard==0.13.5 4 | -------------------------------------------------------------------------------- /process.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import git 4 | import whatthepatch 5 | 6 | # git hash-object -t tree /dev/null 7 | EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 8 | 9 | repo = git.Repo(".") 10 | 11 | file_name = sys.argv[1:] 12 | 13 | commits = list(repo.iter_commits(paths=file_name)) 14 | commits.reverse() 15 | 16 | # first commit hack 17 | patch = commits[0].diff(EMPTY_TREE_SHA, file_name, True)[0].diff.decode("utf-8") 18 | for diff in whatthepatch.parse_patch(patch): 19 | for (i, d, t, _) in diff.changes: 20 | print((d, i, t)) 21 | 22 | print((commits[0].author.name, commits[0].message)) 23 | 24 | for i, c in enumerate(commits): 25 | 26 | if i==0: continue 27 | 28 | patch = commits[i-1].diff(c, file_name, True)[0].diff.decode("utf-8") 29 | 30 | for diff in whatthepatch.parse_patch(patch): 31 | for (d, i, t, _) in diff.changes: 32 | if d==None or i==None: 33 | print((d, i, t)) 34 | 35 | print((c.author.name, c.message)) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-animate 2 | 3 | Reads through a git repo and turns each commit into a series of vim keystrokes. The result is a pseudo-timelapse which almost looks like it's being typed out by a real person. Results vary depending on how frequent the commits were. It's also still only doing linewise diffs, so even a single byte change means the whole line is re-typed. 4 | 5 | Only works on a single file. It is a good idea to rebase any merge commits so that the history reads as if it was written by a single author. 6 | 7 | `pip install -r requirements.txt` 8 | 9 | `process.py` reads the git history and outputs the patches as a list of tuples representing changes. If the repo contains multiple files, specify a filename to filter as an argument. E.g. `./process.py gbjs.htm > patches.txt` Multiple filenames can be provided if the file was renamed. 10 | 11 | `playback.py` reads the patches file and turns it into keystrokes. Run the script and then focus on a new terminal window. Hold esc to abort. 12 | 13 | These scripts were used to produce [this video](https://www.youtube.com/watch?v=i08S5qolgvc). 14 | -------------------------------------------------------------------------------- /playback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import keyboard 3 | import ast 4 | import time, sys 5 | 6 | filename=sys.argv[1] 7 | 8 | # stop after this many commits 9 | stopearly=9999 10 | 11 | 12 | 13 | def vimcmd(x): 14 | keyboard.write(x) 15 | keyboard.press_and_release('enter') 16 | 17 | def window(lr): 18 | keyboard.press_and_release('ctrl+w, '+lr) 19 | 20 | time.sleep(3) 21 | 22 | vimcmd("vim") 23 | time.sleep(0.3) 24 | 25 | # probably should have grabbed this from the repo 26 | vimcmd(":set syntax=html") 27 | vimcmd(":set timeoutlen=1000 ttimeoutlen=0") #not sure if this helps 28 | 29 | lines_added=0 30 | lines_removed=0 31 | current_line=0 32 | msgwindow=0 33 | 34 | def navigate(to): 35 | global current_line 36 | 37 | jump=abs(current_line-to) 38 | 39 | vimcmd(":"+str(to)) 40 | current_line=to 41 | if jump>3: 42 | keyboard.write('zz') # centre scroll 43 | if jump>15: 44 | vimcmd(':syntax sync fromstart') 45 | vimcmd(':') 46 | 47 | time.sleep(min(0.3,jump*0.005)) 48 | 49 | 50 | for line in open(filename): 51 | try: 52 | (d,i,t)= ast.literal_eval(line) 53 | except: 54 | try: 55 | (name,msg) = ast.literal_eval(line) 56 | except: 57 | continue; 58 | if (msgwindow==0): 59 | vimcmd(':set splitright') 60 | vimcmd(":vnew") 61 | keyboard.write('40') 62 | keyboard.press_and_release('ctrl+w') 63 | keyboard.write('|') 64 | vimcmd(':set linebreak') 65 | vimcmd(":set syntax=xml") 66 | vimcmd(':file commit_messages') 67 | msgwindow=1 68 | else: 69 | window('right') 70 | keyboard.write('i<'+name+'>: '+msg) 71 | keyboard.press_and_release('esc') 72 | window('left') 73 | 74 | stopearly=stopearly-1 75 | if stopearly<=0: exit() 76 | 77 | lines_added=0 78 | lines_removed=0 79 | continue; 80 | 81 | if keyboard.is_pressed('esc'): 82 | exit() 83 | 84 | time.sleep(0.01) 85 | if d==None: #insertion 86 | navigate(i) 87 | keyboard.write("O"+t, delay=0.001 ) 88 | keyboard.press_and_release('esc') 89 | lines_added=1+lines_added 90 | elif i==None: 91 | navigate(d+lines_added-lines_removed) 92 | 93 | keyboard.write("dd") 94 | lines_removed=1+lines_removed 95 | 96 | 97 | --------------------------------------------------------------------------------