Skip to content

Instantly share code, notes, and snippets.

@koreno
Last active April 1, 2020 10:44
Show Gist options
  • Star 95 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save koreno/5893d2d969ccb6b8341d to your computer and use it in GitHub Desktop.
Save koreno/5893d2d969ccb6b8341d to your computer and use it in GitHub Desktop.
'rebaser' improves on 'git rebase -i' by adding information per commit regarding which files it touched.

Prebase

git-prebase improves on 'git rebase -i' by adding information per commit regarding which files it touched.

  • 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)
  • 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).

Installation

Add the executable to your path and git will automatically expose it as

git prebase <commit-ref>

The flow is exactly like an interactive rebase, only that the 'todo' list will now contain the additional information.

Example

Below is an example of the 'todo' list created by git-prebase.

pick d0d13d0    1:removed some bullshit from __entrypoint.d      _________9______
pick a44e878    2:Improvements to object.d and __entrypoint.d    ________89______
pick 12c5b47    3:Add usage to build.d                           ___3____________
pick 318af43    4:Added .gitignore                               ______________e_
pick eb9ad0f    5:Attempting to add more array support           _1_3_56_89abcd__
pick 8b8df05    6:Added some special support for ldc to object.d ________8_______
pick e630300    7:Removed imports from build                     ___3____________
pick 69ae673    8:c-main to d-main                               __2345_7_9______
pick c00b344    9:Implemented write an exit system calls         _1_345678_______
pick 3901cca   10:Add wscript_build file                         0__3____________
pick 349bec4   11:WAF: fix build script                          0_______________
pick 70e1d26   12:Make main module qualified                     __2_____________
pick f22cca0   13:Update to 2.067                                _1______________
pick 06cb727   14:revert to compiling under 2.066                01______________
pick 25c13c4   15:WAF: remove unneeded post()s                   0_______________

# [0] wscript_build                                              0_______________
# [1] ports/posix.d                                              _1______________
# [2] app/main.d                                                 __2_____________
# [3] build.d                                                    ___3____________
# [4] include/__entrypoint.di                                    ____4___________
# [5] ports/linux.d                                              _____5__________
# [6] source/array.d                                             ______6_________
# [7] source/dmain.d                                             _______7________
# [8] source/object.d                                            ________8_______
# [9] source/__entrypoint.d                                      _________9______
# [a] ports/windows.d                                            __________a_____
# [b] source/ports/linux.d                                       ___________b____
# [c] source/ports/posix.d                                       ____________c___
# [d] source/ports/windows.d                                     _____________d__
# [e] .gitignore                                                 ______________e_

# Rebase 9c75315..25c13c4 onto 9c75315
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
#!/usr/bin/env python
"Improve on 'git rebase -i' by adding information per commit regarding which files it touched."
from __future__ import print_function
import sys
import os
from subprocess import CalledProcessError, check_call, check_output
from itertools import count, chain
from collections import defaultdict
from string import digits, ascii_letters
SYMBOLS = dict(enumerate(chain(digits, ascii_letters)))
SPACER = "_"
def parse_log(first, last):
gitlog = check_output([
'git', 'log', '--name-only', '--oneline', '--no-color',
'--format=#commit %h {idx:4}:%s',
"%s^..%s" % (first, last)],
universal_newlines=True)
lines = iter(gitlog.splitlines())
line = next(lines)
while True:
prefix, _, commit = line.partition(" ")
assert prefix == "#commit"
files = set()
for line in lines:
if line.startswith("#commit"):
yield (commit, sorted(files))
break # for
elif line:
files.add(line)
else:
yield (commit, sorted(files))
break # while
def compact(line, length, ellipsis="....", suffix_length=10):
if len(line) <= length:
return line
return line[:length-len(ellipsis)-suffix_length] + ellipsis + line[-suffix_length:]
def symbol(idx):
return SYMBOLS[idx % len(SYMBOLS)]
def write_todo(file, first, last, comments, sort_file_list=False):
c = count(0)
file_indices = defaultdict(lambda: next(c))
lines = []
log = list(parse_log(first, last))
width = min(120, max(len(c) for (c, _) in log) if log else 80)
for commit, files in log:
indices = {file_indices[f] for f in files}
placements = "".join(symbol(i) if i in indices else SPACER for i in range(max(indices)+1)) if indices else ""
lines.append((compact(commit, width).ljust(width), placements))
lines.reverse()
placements_width = max(file_indices.values()) + 2
for i, (commit, placements) in enumerate(lines, 1):
print("pick", commit.format(idx=i), placements.ljust(placements_width, SPACER), file=file)
print("", file=file)
sortby = 0 if sort_file_list else 1
for f, i in sorted(file_indices.items(), key=lambda p: p[sortby]):
pos = symbol(i).rjust(1+i, SPACER).ljust(placements_width, SPACER)
f = "[%s] %s" % (symbol(i), f)
fname = compact("# %s" % f, width+2).ljust(width+2)
print(fname, pos, file=file)
print("", file=file)
for line in comments:
print(line, file=file, end="")
def usage():
print("usage: %s [options] <branch>\n\n"
"Options:\n"
" -F, --sort-file-list Show file list sorted by file name, instead of order of appearance\n"
% os.path.basename(sys.argv[0]))
sys.exit(1)
if __name__ == '__main__':
if len(sys.argv) <= 1:
usage()
if 'GIT_ORIG_EDITOR' not in os.environ:
base_commit = None
for arg in sys.argv[1:]:
if arg.startswith("-"):
if arg in ("-F", "--sort-file-list"):
os.environ['GIT_PREBASE_SORT_FILE_LIST'] = "1"
else:
usage()
elif base_commit:
usage()
else:
base_commit = arg
if not base_commit:
usage()
git_editor = os.environ.get("GIT_SEQUENCE_EDITOR")
if not git_editor:
try:
git_editor = check_output(["git", "config", "--get", "sequence.editor"], universal_newlines=True).strip()
except CalledProcessError:
pass
if not git_editor:
try:
git_editor = check_output(["git", "var", "GIT_EDITOR"], universal_newlines=True).strip()
except CalledProcessError:
pass
os.environ['GIT_ORIG_EDITOR'] = os.path.expanduser(git_editor)
os.environ['GIT_SEQUENCE_EDITOR'] = __file__
os.execlpe("git", "git", "rebase", "-i", base_commit, os.environ)
todo_file = sys.argv[1]
os.environ['GIT_EDITOR'] = editor = os.environ['GIT_ORIG_EDITOR']
sort_file_list = bool(int(os.getenv("GIT_PREBASE_SORT_FILE_LIST", 0)))
if not todo_file.endswith("git-rebase-todo"):
os.execlpe(editor, editor, todo_file, os.environ)
commits = []
with open(todo_file) as f:
for line in f:
if line.strip() == "noop":
break
if not line.strip():
comments = list(f)
break
commits.append(line.split()[1])
if commits:
first, last = commits[0], commits[-1]
with open(todo_file, "w") as file:
write_todo(file, first, last, comments, sort_file_list=sort_file_list)
sh = os.getenv("SHELL")
assert sh, "Is this windows?... it'd be nice if someone can make this work :)"
check_call([sh, "-c", "%s %s" % (editor, todo_file)])
@koreno
Copy link
Author

koreno commented Jul 24, 2015

Running:

>> rebaser.py <commit-ref>

Each file gets an alpha-numeric character at a particular column, a list of which appears below the commit list.
Commits can be moved up and down safely (no conflicts) as long as they don't have any clashing columns.

You can add it to you ~/.gitconfig:

[alias]
    prebase = !rebaser.py $*

and then:

>> git prebase <commit-ref>

@kavu
Copy link

kavu commented Jul 24, 2015

@koreno, or you can add script to your PATH, name it git-rebaser for example and make executable with chmod +x. It will also work out.

@koreno
Copy link
Author

koreno commented Jul 24, 2015

Sweet!, I figured there's a better way...

@junkblocker
Copy link

Thanks for this! Very useful!

You should os.path.expanduser(...) for the editor since it can be set to ~/bin/vim etc. I just did a quick and dirty python2 port https://gist.github.com/junkblocker/c4b7f2417e62f0893021 .

@koreno
Copy link
Author

koreno commented Jul 24, 2015

Thank @junkblocker! I've updated mine as well

@koreno
Copy link
Author

koreno commented Jul 24, 2015

Fixed a bug with having >62 files modified.

@kornelski
Copy link

This is awesome! Could you turn it into a project?

One problem I've ran into is:

git var GIT_EDITOR
"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w

it works with standard git rebase, but rebaser interprets this as a filename rather than shell command:

No such file or directory: '"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" -w'

@koreno
Copy link
Author

koreno commented Aug 2, 2015

Now it is a project: https://github.com/koreno/prebase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment