127 lines
3.5 KiB
Text
127 lines
3.5 KiB
Text
|
#!/usr/bin/env python
|
||
|
|
||
|
import argparse
|
||
|
import os
|
||
|
import re
|
||
|
import subprocess
|
||
|
import sys
|
||
|
|
||
|
|
||
|
def guess_base_commit(repo):
|
||
|
"""Guess which commit the patches might be based on"""
|
||
|
args = [
|
||
|
'git',
|
||
|
'-C',
|
||
|
repo,
|
||
|
'describe',
|
||
|
'--tags',
|
||
|
]
|
||
|
return subprocess.check_output(args).split('-')[0:2]
|
||
|
|
||
|
|
||
|
def format_patch(repo, since):
|
||
|
args = [
|
||
|
'git',
|
||
|
'-C',
|
||
|
repo,
|
||
|
'format-patch',
|
||
|
'--keep-subject',
|
||
|
'--no-stat',
|
||
|
'--stdout',
|
||
|
|
||
|
# The name of the parent commit object isn't useful information in this
|
||
|
# context, so zero it out to avoid needless patch-file churn.
|
||
|
'--zero-commit',
|
||
|
|
||
|
# Some versions of git print out different numbers of characters in the
|
||
|
# 'index' line of patches, so pass --full-index to get consistent
|
||
|
# behaviour.
|
||
|
'--full-index',
|
||
|
since
|
||
|
]
|
||
|
return subprocess.check_output(args)
|
||
|
|
||
|
|
||
|
def split_patches(patch_data):
|
||
|
"""Split a concatenated series of patches into N separate patches"""
|
||
|
patches = []
|
||
|
patch_start = re.compile('^From [0-9a-f]+ ')
|
||
|
for line in patch_data.splitlines():
|
||
|
if patch_start.match(line):
|
||
|
patches.append([])
|
||
|
patches[-1].append(line)
|
||
|
return patches
|
||
|
|
||
|
|
||
|
def munge_subject_to_filename(subject):
|
||
|
"""Derive a suitable filename from a commit's subject"""
|
||
|
if subject.endswith('.patch'):
|
||
|
subject = subject[:-6]
|
||
|
return re.sub(r'[^A-Za-z0-9-]+', '_', subject).strip('_').lower() + '.patch'
|
||
|
|
||
|
|
||
|
def get_file_name(patch):
|
||
|
"""Return the name of the file to which the patch should be written"""
|
||
|
for line in patch:
|
||
|
if line.startswith('Patch-Filename: '):
|
||
|
return line[len('Patch-Filename: '):]
|
||
|
# If no patch-filename header, munge the subject.
|
||
|
for line in patch:
|
||
|
if line.startswith('Subject: '):
|
||
|
return munge_subject_to_filename(line[len('Subject: '):])
|
||
|
|
||
|
|
||
|
def remove_patch_filename(patch):
|
||
|
"""Strip out the Patch-Filename trailer from a patch's message body"""
|
||
|
force_keep_next_line = False
|
||
|
for i, l in enumerate(patch):
|
||
|
is_patchfilename = l.startswith('Patch-Filename: ')
|
||
|
next_is_patchfilename = i < len(patch) - 1 and patch[i+1].startswith('Patch-Filename: ')
|
||
|
if not force_keep_next_line and (is_patchfilename or (next_is_patchfilename and len(l.rstrip()) == 0)):
|
||
|
pass # drop this line
|
||
|
else:
|
||
|
yield l
|
||
|
force_keep_next_line = l.startswith('Subject: ')
|
||
|
|
||
|
|
||
|
def main(argv):
|
||
|
parser = argparse.ArgumentParser()
|
||
|
parser.add_argument("-o", "--output",
|
||
|
help="directory into which exported patches will be written")
|
||
|
parser.add_argument("patch_range",
|
||
|
nargs='?',
|
||
|
help="range of patches to export. Defaults to all commits since the "
|
||
|
"most recent tag or remote branch.")
|
||
|
args = parser.parse_args(argv)
|
||
|
|
||
|
repo = '.'
|
||
|
patch_range = args.patch_range
|
||
|
if patch_range is None:
|
||
|
patch_range, num_patches = guess_base_commit(repo)
|
||
|
sys.stderr.write("Exporting {} patches since {}\n".format(num_patches, patch_range))
|
||
|
patch_data = format_patch(repo, patch_range)
|
||
|
patches = split_patches(patch_data)
|
||
|
|
||
|
out_dir = args.output
|
||
|
try:
|
||
|
os.mkdir(out_dir)
|
||
|
except OSError:
|
||
|
pass
|
||
|
|
||
|
# remove old patches, so that deleted commits are correctly reflected in the
|
||
|
# patch files (as a removed file)
|
||
|
for p in os.listdir(out_dir):
|
||
|
if p.endswith('.patch'):
|
||
|
os.remove(os.path.join(out_dir, p))
|
||
|
|
||
|
with open(os.path.join(out_dir, '.patches'), 'w') as pl:
|
||
|
for patch in patches:
|
||
|
filename = get_file_name(patch)
|
||
|
with open(os.path.join(out_dir, filename), 'w') as f:
|
||
|
f.write('\n'.join(remove_patch_filename(patch)))
|
||
|
pl.write(filename + '\n')
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main(sys.argv[1:])
|