Skip to content

Instantly share code, notes, and snippets.

@camertron
Last active July 10, 2020 22:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save camertron/ea9aa69777d357faadbc50bea809b60e to your computer and use it in GitHub Desktop.
Save camertron/ea9aa69777d357faadbc50bea809b60e to your computer and use it in GitHub Desktop.
Quick-and-dirty script to move lines from one source file to a new source file, preserving git authorship along the way. The new file will be made up of a number of commits, each one attributed to the original author and at the original date and time.
import argparse
from datetime import (datetime, timedelta)
import os
import re
import subprocess
class Commit(object):
def __init__(self, commit_id, fields):
self.commit_id = commit_id
self.author = fields['author']
self.author_email = fields['author-mail'][1:-1]
self.timezone = fields['author-tz']
self.timestamp = datetime.utcfromtimestamp(int(fields['author-time'])) + timedelta(hours=int(self.timezone) / 100)
self.line_numbers = []
def __repr__(self):
return str(self.__dict__)
def __eq__(self, other):
return self.commit_id == other.commit_id
def __ne__(self, other):
return self.commit_id != other.commit_id
def __hash__(self):
return hash(self.commit_id)
parser = argparse.ArgumentParser(
description='Extract git-tracked source code into a new file, '\
'preserving authors along the way.'
)
parser.add_argument(
'file',
type=str,
help='The file to extract lines from, with a line range '\
'specified in Github format, eg. /foo/file.txt#L2-L14'
)
parser.add_argument(
'outfile',
type=str,
help='The new file to create'
)
def git(cmd):
result = subprocess.run(['git', *cmd], stdout=subprocess.PIPE)
return result.stdout.decode('utf-8')
args = parser.parse_args()
file, start, end = [part for part in re.split(r'([^#]+)#L(\d+)-L(\d+)', args.file) if part]
start = int(start)
end = int(end)
with open(file, 'r') as f:
lines = f.read().split("\n")
blame = git(['blame', file, '--line-porcelain', '-L', f"{start},{end + 1}"])
commit_data = re.split(r'([a-zA-Z0-9]{40}) \d+ \d+ ?\d?', blame)[1:-1]
commits = {}
for i in range(0, len(commit_data), 2):
if i + 1 >= len(commit_data):
break
commit_id = commit_data[i]
fields = {}
for field_line in commit_data[i + 1].split("\n")[0:-1]:
field_pair = field_line.split(' ', maxsplit=1)
if len(field_pair) == 2:
fields[field_pair[0]] = field_pair[1]
if not commit_id in commits:
commits[commit_id] = Commit(commit_id, fields)
commits[commit_id].line_numbers.append(i // 2)
commits = sorted(commits.values(), key=lambda c: c.timestamp)
cur_lines = [''] * (len(commit_data) // 2)
moving_author_name = git(['config', '--get', 'user.name']).strip()
moving_author_email = git(['config', '--get', 'user.email']).strip()
moving_author_byline = f"{moving_author_name} <{moving_author_email}>"
now = datetime.now().ctime()
for commit in commits:
for line_number in commit.line_numbers:
cur_lines[line_number] = lines[start + line_number - 1]
with open(args.outfile, 'w+') as f:
f.write("\n".join(cur_lines))
git(['add', args.outfile])
original_author_byline = f'{commit.author} <{commit.author_email}>'
original_timestamp = f'{commit.timestamp} {commit.timezone}'
original_commit_msg = git(['log', '--format=%B', '-n 1', commit.commit_id])
new_commit_msg = f'***NOTE: These changes were originally committed in {commit.commit_id} '\
f'and were moved from {file} by {moving_author_byline} on {now}\n\n{original_commit_msg}'
print(f"Committing replacement for {commit.commit_id}")
git(['-c', 'core.hooksPath=/dev/null', 'commit', '--no-verify', '--author', original_author_byline, '--date', original_timestamp, '-m', new_commit_msg])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment