Last active
May 18, 2019 10:43
-
-
Save mnieber/b88a2f3136aa1f733d96314507935c8d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# Usage: git fixdown [-vfd] | |
# | |
# Finds all commits that are corrected by the staged changes. If there is only one | |
# such commit, makes a fixup commit targetting that commit. | |
# | |
# Note: place this script on the path, so git can find it, | |
# and make it executable (chmod +x git-fixdown). | |
# | |
# Author: Maarten Nieber | |
# Url: https://gist.github.com/mnieber/b88a2f3136aa1f733d96314507935c8d | |
# | |
# Based on git-autofixup (https://github.com/chrisarcand), | |
# who based it on work from @mislav | |
import argparse | |
import re | |
import os | |
import subprocess | |
import sys | |
regex_parts = r"^\-\-\-\ (.+)$" | |
regex_line_mutations = r"^\@\@\s\-([\,\w\d]+)" | |
def git(*args): | |
pipes = subprocess.Popen(['git'] + list(args), | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE) | |
std_out, std_err = pipes.communicate() | |
if pipes.returncode != 0: | |
print(std_out) | |
print(std_err.strip()) | |
sys.exit(pipes.returncode) | |
return std_out.decode('utf-8') | |
def staged_changes(): | |
return git('diff', '--no-prefix', '--cached', '-U0') | |
def parts(staged_changes): | |
parts = list(re.finditer(regex_parts, staged_changes, re.MULTILINE)) | |
result = dict() | |
for idx, match in enumerate(parts): | |
filename = match.group(1) | |
last_char = (parts[idx + 1].span()[0] | |
if idx + 1 < len(parts) else len(staged_changes)) | |
result[filename] = staged_changes[match.span()[1]:last_char] | |
return result | |
def commit_timestamp(sha): | |
timestamp = git('show', '-s', '--format=%ct', sha) | |
return int(timestamp) | |
def original_lines(filename): | |
if filename == '/dev/null': | |
return [] | |
result = git('show', 'HEAD:%s' % filename) | |
return result.split('\n') | |
def shas(parts): | |
result = set() | |
sha_2_changes = dict() | |
for filename, part in parts.items(): | |
# If file was deleted in the staged changes... | |
if not os.path.exists(filename): | |
# TODO: maybe we should use the set of all previous shas that | |
# produced the latest version of the file | |
# for sha in git('log', '--pretty=format:%H', '--', filename): | |
# result.add(sha) | |
continue | |
lines = original_lines(filename) | |
matches = re.finditer(regex_line_mutations, part, re.MULTILINE) | |
for matchNum, match in enumerate(matches): | |
match = match.group(1) | |
parts = match.split(",") | |
start_line = int(parts[0]) | |
nr_lines_removed = (1 if len(parts) == 1 else int(parts[1])) | |
for line in range(start_line, start_line + nr_lines_removed): | |
line_idx = line - 1 | |
blame = git('blame', 'HEAD', filename, '-L', '%d,+1' % line) | |
blame = blame[1:] if blame[:1] == '^' else blame | |
sha = blame.split()[0] | |
changes = sha_2_changes.setdefault(sha, []) | |
changes.append((filename, line, lines[line_idx])) | |
result.add(sha) | |
sha_2_timestamp = {sha: commit_timestamp(sha) for sha in result} | |
sha_list = sorted(list(result), key=lambda sha: sha_2_timestamp[sha]) | |
return sha_list, sha_2_changes | |
def commit_message(sha): | |
return git('show', '-s', '--oneline', sha)[:-1] | |
def print_changed_lines(changes): | |
for change in changes: | |
prefix = ("%s:%d" % change[0:2]).ljust(30) | |
print("%s %s" % (prefix, change[2])) | |
def bordered(text): | |
lines = text.decode('ascii', 'ignore').encode('ascii').splitlines() | |
width = max(len(s) for s in lines) | |
res = ['┌' + '─' * width + '┐'] | |
for s in lines: | |
res.append('│' + (s + ' ' * width)[:width] + '│') | |
res.append('└' + '─' * width + '┘') | |
return '\n'.join(res) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser() | |
group = parser.add_mutually_exclusive_group() | |
group.add_argument( | |
"-v", | |
"--verbose", | |
action="store_true", | |
help=("Print extra information about changed lines in case " | |
"there are multiple possible target commits")) | |
group.add_argument( | |
"-f", | |
"--force", | |
action="store_true", | |
help=("Force the creation of a fixup when there are multiple " | |
"possible target commits (pick the most recent commit)")) | |
group.add_argument("-d", | |
"--dry-run", | |
action="store_true", | |
help=("Don't create any fixup commit")) | |
args = parser.parse_args() | |
shas, sha_2_changes = shas(parts(staged_changes())) | |
if len(shas) == 0: | |
print("No changed lines") | |
elif len(shas) == 1 or args.force: | |
print(commit_message(shas[-1])) | |
if not args.dry_run: | |
git('commit', '--fixup', shas[-1]) | |
else: | |
print("There are multiple possible target commits:") | |
for sha in shas: | |
if args.verbose: | |
print(bordered(commit_message(sha))) | |
print_changed_lines(sha_2_changes[sha]) | |
print("") | |
else: | |
print(commit_message(sha)) | |
sys.exit(1) |
Hi @asfaltboy, yes, I created a GitSavvy custom command (~/.config/sublime-text-3/Packages/User/User.sublime-commands)
[
{
"caption": "git: fixdown",
"command": "gs_custom",
"args": {
"output_to_panel": true,
"args": ["fixdown"],
}
}
]
Is this what you meant?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@mnieber did you end up using this in Sublime Text, with a GitSavvy custom command? If so, care to share the definition ?