Last active
July 10, 2020 22:49
-
-
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.
This file contains 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
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