├── README.md └── git-prebase /README.md: -------------------------------------------------------------------------------- 1 | # Prebase 2 | 3 | *git-prebase* improves on 'git rebase -i' by adding information per commit regarding which files it touched. 4 | - Each file gets an alpha-numeric identifier at a particular column, a list of which appears below the commit list. (The identifiers wrap around after the 62nd file) 5 | - Commits can be moved up and down safely (without conflicts) as long as their columns don't clash (they did not touch the same file). 6 | 7 | 8 | # Installation 9 | 10 | Add the executable to your path and git will automatically expose it as 11 | 12 | git prebase 13 | 14 | The flow is exactly like an interactive rebase, only that the 'todo' list will now contain the additional information. 15 | 16 | 17 | # Example 18 | 19 | Below is an example of the 'todo' list created by git-prebase. 20 | 21 | ``` 22 | pick d0d13d0 1:removed some bullshit from __entrypoint.d _________9______ 23 | pick a44e878 2:Improvements to object.d and __entrypoint.d ________89______ 24 | pick 12c5b47 3:Add usage to build.d ___3____________ 25 | pick 318af43 4:Added .gitignore ______________e_ 26 | pick eb9ad0f 5:Attempting to add more array support _1_3_56_89abcd__ 27 | pick 8b8df05 6:Added some special support for ldc to object.d ________8_______ 28 | pick e630300 7:Removed imports from build ___3____________ 29 | pick 69ae673 8:c-main to d-main __2345_7_9______ 30 | pick c00b344 9:Implemented write an exit system calls _1_345678_______ 31 | pick 3901cca 10:Add wscript_build file 0__3____________ 32 | pick 349bec4 11:WAF: fix build script 0_______________ 33 | pick 70e1d26 12:Make main module qualified __2_____________ 34 | pick f22cca0 13:Update to 2.067 _1______________ 35 | pick 06cb727 14:revert to compiling under 2.066 01______________ 36 | pick 25c13c4 15:WAF: remove unneeded post()s 0_______________ 37 | 38 | # [0] wscript_build 0_______________ 39 | # [1] ports/posix.d _1______________ 40 | # [2] app/main.d __2_____________ 41 | # [3] build.d ___3____________ 42 | # [4] include/__entrypoint.di ____4___________ 43 | # [5] ports/linux.d _____5__________ 44 | # [6] source/array.d ______6_________ 45 | # [7] source/dmain.d _______7________ 46 | # [8] source/object.d ________8_______ 47 | # [9] source/__entrypoint.d _________9______ 48 | # [a] ports/windows.d __________a_____ 49 | # [b] source/ports/linux.d ___________b____ 50 | # [c] source/ports/posix.d ____________c___ 51 | # [d] source/ports/windows.d _____________d__ 52 | # [e] .gitignore ______________e_ 53 | 54 | # Rebase 9c75315..25c13c4 onto 9c75315 55 | # 56 | # Commands: 57 | # p, pick = use commit 58 | # r, reword = use commit, but edit the commit message 59 | # e, edit = use commit, but stop for amending 60 | # s, squash = use commit, but meld into previous commit 61 | # f, fixup = like "squash", but discard this commit's log message 62 | # x, exec = run command (the rest of the line) using shell 63 | # 64 | # These lines can be re-ordered; they are executed from top to bottom. 65 | # 66 | # If you remove a line here THAT COMMIT WILL BE LOST. 67 | # 68 | # However, if you remove everything, the rebase will be aborted. 69 | # 70 | # Note that empty commits are commented out 71 | ``` 72 | -------------------------------------------------------------------------------- /git-prebase: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | "Improve on 'git rebase -i' by adding information per commit regarding which files it touched." 4 | 5 | from __future__ import print_function 6 | 7 | import sys 8 | import os 9 | from subprocess import check_call, check_output 10 | from itertools import count, chain 11 | from collections import defaultdict 12 | from string import digits, ascii_letters 13 | 14 | SYMBOLS = dict(enumerate(chain(digits, ascii_letters))) 15 | SPACER = "_" 16 | 17 | 18 | def parse_log(first, last): 19 | 20 | gitlog = check_output([ 21 | 'git', 'log', '--name-only', '--oneline', '--no-color', 22 | '--format=#commit %h {idx:4}:%s', 23 | "%s^..%s" % (first, last)], 24 | universal_newlines=True) 25 | 26 | lines = iter(gitlog.splitlines()) 27 | line = next(lines) 28 | 29 | while True: 30 | prefix, _, commit = line.partition(" ") 31 | assert prefix == "#commit" 32 | 33 | files = set() 34 | for line in lines: 35 | if line.startswith("#commit"): 36 | yield (commit, sorted(files)) 37 | break # for 38 | elif line: 39 | files.add(line) 40 | else: 41 | yield (commit, sorted(files)) 42 | break # while 43 | 44 | 45 | def compact(line, length, ellipsis="....", suffix_length=10): 46 | if len(line) <= length: 47 | return line 48 | return line[:length-len(ellipsis)-suffix_length] + ellipsis + line[-suffix_length:] 49 | 50 | 51 | def symbol(idx): 52 | return SYMBOLS[idx % len(SYMBOLS)] 53 | 54 | 55 | def write_todo(file, first, last, comments, sort_file_list=False): 56 | c = count(0) 57 | file_indices = defaultdict(lambda: next(c)) 58 | lines = [] 59 | log = list(parse_log(first, last)) 60 | width = min(120, max(len(c) for (c, _) in log) if log else 80) 61 | for commit, files in log: 62 | indices = {file_indices[f] for f in files} 63 | placements = "".join(symbol(i) if i in indices else SPACER for i in range(max(indices)+1)) if indices else "" 64 | lines.append((compact(commit, width).ljust(width), placements)) 65 | lines.reverse() 66 | placements_width = max(file_indices.values()) + 2 67 | for i, (commit, placements) in enumerate(lines, 1): 68 | print("pick", commit.format(idx=i), placements.ljust(placements_width, SPACER), file=file) 69 | 70 | print("", file=file) 71 | 72 | sortby = 0 if sort_file_list else 1 73 | for f, i in sorted(file_indices.items(), key=lambda p: p[sortby]): 74 | pos = symbol(i).rjust(1+i, SPACER).ljust(placements_width, SPACER) 75 | f = "[%s] %s" % (symbol(i), f) 76 | fname = compact("# %s" % f, width+2).ljust(width+2) 77 | print(fname, pos, file=file) 78 | 79 | print("", file=file) 80 | for line in comments: 81 | print(line, file=file, end="") 82 | 83 | 84 | def usage(): 85 | print("usage: %s [options] \n\n" 86 | "Options:\n" 87 | " -F, --sort-file-list Show file list sorted by file name, instead of order of appearance\n" 88 | % os.path.basename(sys.argv[0])) 89 | sys.exit(1) 90 | 91 | 92 | if __name__ == '__main__': 93 | 94 | if len(sys.argv) <= 1: 95 | usage() 96 | 97 | if 'GIT_ORIG_EDITOR' not in os.environ: 98 | base_commit = None 99 | 100 | for arg in sys.argv[1:]: 101 | if arg.startswith("-"): 102 | if arg in ("-F", "--sort-file-list"): 103 | os.environ['GIT_PREBASE_SORT_FILE_LIST'] = "1" 104 | else: 105 | usage() 106 | elif base_commit: 107 | usage() 108 | else: 109 | base_commit = arg 110 | 111 | if not base_commit: 112 | usage() 113 | 114 | git_editor = ( 115 | os.environ.get("GIT_SEQUENCE_EDITOR") 116 | or check_output(["git", "config", "--get", "sequence.editor"], universal_newlines=True).strip() 117 | or check_output(["git", "var", "GIT_EDITOR"], universal_newlines=True).strip()) 118 | 119 | os.environ['GIT_ORIG_EDITOR'] = os.path.expanduser(git_editor) 120 | os.environ['GIT_SEQUENCE_EDITOR'] = __file__ 121 | os.execlpe("git", "git", "rebase", "-i", base_commit, os.environ) 122 | 123 | todo_file = sys.argv[1] 124 | os.environ['GIT_EDITOR'] = editor = os.environ['GIT_ORIG_EDITOR'] 125 | sort_file_list = bool(int(os.getenv("GIT_PREBASE_SORT_FILE_LIST", 0))) 126 | 127 | if not todo_file.endswith("git-rebase-todo"): 128 | os.execlpe(editor, editor, todo_file, os.environ) 129 | 130 | commits = [] 131 | 132 | with open(todo_file) as f: 133 | for line in f: 134 | if line.strip() == "noop": 135 | break 136 | if not line.strip(): 137 | comments = list(f) 138 | break 139 | commits.append(line.split()[1]) 140 | 141 | if commits: 142 | first, last = commits[0], commits[-1] 143 | with open(todo_file, "w") as file: 144 | write_todo(file, first, last, comments, sort_file_list=sort_file_list) 145 | 146 | sh = os.getenv("SHELL") 147 | assert sh, "Is this windows?... it'd be nice if someone can make this work :)" 148 | check_call([sh, "-c", "%s %s" % (editor, todo_file)]) 149 | --------------------------------------------------------------------------------