feat: allow custom refs for patch import & export (#41306)

* feat: allow custom refs for patch import & export

feat: add Patch-Dir metainfo, a sibling to Patch-Filename

* chore: copyediting

* refactor: minor copyediting
This commit is contained in:
Charles Kerr 2024-02-12 10:05:53 -06:00 committed by GitHub
parent 5f785f213e
commit 6a616ab70c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 42 deletions

View file

@ -13,6 +13,9 @@ import re
import subprocess import subprocess
import sys import sys
from .patches import PATCH_FILENAME_PREFIX, is_patch_location_line
UPSTREAM_HEAD='refs/patches/upstream-head'
def is_repo_root(path): def is_repo_root(path):
path_exists = os.path.exists(path) path_exists = os.path.exists(path)
@ -75,14 +78,10 @@ def am(repo, patch_data, threeway=False, directory=None, exclude=None,
proc.returncode)) proc.returncode))
def import_patches(repo, **kwargs): def import_patches(repo, ref=UPSTREAM_HEAD, **kwargs):
"""same as am(), but we save the upstream HEAD so we can refer to it when we """same as am(), but we save the upstream HEAD so we can refer to it when we
later export patches""" later export patches"""
update_ref( update_ref(repo=repo, ref=ref, newvalue='HEAD')
repo=repo,
ref='refs/patches/upstream-head',
newvalue='HEAD'
)
am(repo=repo, **kwargs) am(repo=repo, **kwargs)
@ -92,32 +91,18 @@ def update_ref(repo, ref, newvalue):
return subprocess.check_call(args) return subprocess.check_call(args)
def get_upstream_head(repo): def get_commit_for_ref(repo, ref):
args = [ args = ['git', '-C', repo, 'rev-parse', '--verify', ref]
'git',
'-C',
repo,
'rev-parse',
'--verify',
'refs/patches/upstream-head',
]
return subprocess.check_output(args).decode('utf-8').strip() return subprocess.check_output(args).decode('utf-8').strip()
def get_commit_count(repo, commit_range): def get_commit_count(repo, commit_range):
args = [ args = ['git', '-C', repo, 'rev-list', '--count', commit_range]
'git',
'-C',
repo,
'rev-list',
'--count',
commit_range
]
return int(subprocess.check_output(args).decode('utf-8').strip()) return int(subprocess.check_output(args).decode('utf-8').strip())
def guess_base_commit(repo): def guess_base_commit(repo, ref):
"""Guess which commit the patches might be based on""" """Guess which commit the patches might be based on"""
try: try:
upstream_head = get_upstream_head(repo) upstream_head = get_commit_for_ref(repo, ref)
num_commits = get_commit_count(repo, upstream_head + '..') num_commits = get_commit_count(repo, upstream_head + '..')
return [upstream_head, num_commits] return [upstream_head, num_commits]
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
@ -149,7 +134,6 @@ def format_patch(repo, since):
'format-patch', 'format-patch',
'--keep-subject', '--keep-subject',
'--no-stat', '--no-stat',
'--notes',
'--stdout', '--stdout',
# Per RFC 3676 the signature is separated from the body by a line with # Per RFC 3676 the signature is separated from the body by a line with
@ -204,8 +188,8 @@ def get_file_name(patch):
"""Return the name of the file to which the patch should be written""" """Return the name of the file to which the patch should be written"""
file_name = None file_name = None
for line in patch: for line in patch:
if line.startswith('Patch-Filename: '): if line.startswith(PATCH_FILENAME_PREFIX):
file_name = line[len('Patch-Filename: '):] file_name = line[len(PATCH_FILENAME_PREFIX):]
break break
# If no patch-filename header, munge the subject. # If no patch-filename header, munge the subject.
if not file_name: if not file_name:
@ -218,19 +202,18 @@ def get_file_name(patch):
def join_patch(patch): def join_patch(patch):
"""Joins and formats patch contents""" """Joins and formats patch contents"""
return ''.join(remove_patch_filename(patch)).rstrip('\n') + '\n' return ''.join(remove_patch_location(patch)).rstrip('\n') + '\n'
def remove_patch_filename(patch): def remove_patch_location(patch):
"""Strip out the Patch-Filename trailer from a patch's message body""" """Strip out the patch location lines from a patch's message body"""
force_keep_next_line = False force_keep_next_line = False
n = len(patch)
for i, l in enumerate(patch): for i, l in enumerate(patch):
is_patchfilename = l.startswith('Patch-Filename: ') skip_line = is_patch_location_line(l)
next_is_patchfilename = i < len(patch) - 1 and patch[i + 1].startswith( skip_next = i < n - 1 and is_patch_location_line(patch[i + 1])
'Patch-Filename: '
)
if not force_keep_next_line and ( if not force_keep_next_line and (
is_patchfilename or (next_is_patchfilename and len(l.rstrip()) == 0) skip_line or (skip_next and len(l.rstrip()) == 0)
): ):
pass # drop this line pass # drop this line
else: else:
@ -238,20 +221,24 @@ def remove_patch_filename(patch):
force_keep_next_line = l.startswith('Subject: ') force_keep_next_line = l.startswith('Subject: ')
def export_patches(repo, out_dir, patch_range=None, dry_run=False, grep=None): def export_patches(repo, out_dir,
patch_range=None, ref=UPSTREAM_HEAD,
dry_run=False, grep=None):
if not os.path.exists(repo): if not os.path.exists(repo):
sys.stderr.write( sys.stderr.write(
"Skipping patches in {} because it does not exist.\n".format(repo) "Skipping patches in {} because it does not exist.\n".format(repo)
) )
return return
if patch_range is None: if patch_range is None:
patch_range, num_patches = guess_base_commit(repo) patch_range, num_patches = guess_base_commit(repo, ref)
sys.stderr.write("Exporting {} patches in {} since {}\n".format( sys.stderr.write("Exporting {} patches in {} since {}\n".format(
num_patches, repo, patch_range[0:7])) num_patches, repo, patch_range[0:7]))
patch_data = format_patch(repo, patch_range) patch_data = format_patch(repo, patch_range)
patches = split_patches(patch_data) patches = split_patches(patch_data)
if grep: if grep:
olen = len(patches)
patches = filter_patches(patches, grep) patches = filter_patches(patches, grep)
sys.stderr.write("Exporting {} of {} patches\n".format(len(patches), olen))
try: try:
os.mkdir(out_dir) os.mkdir(out_dir)

View file

@ -3,19 +3,27 @@
import codecs import codecs
import os import os
PATCH_DIR_PREFIX = "Patch-Dir: "
PATCH_FILENAME_PREFIX = "Patch-Filename: "
PATCH_LINE_PREFIXES = (PATCH_DIR_PREFIX, PATCH_FILENAME_PREFIX)
def is_patch_location_line(line):
return line.startswith(PATCH_LINE_PREFIXES)
def read_patch(patch_dir, patch_filename): def read_patch(patch_dir, patch_filename):
"""Read a patch from |patch_dir/filename| and amend the commit message with """Read a patch from |patch_dir/filename| and amend the commit message with
metadata about the patch file it came from.""" metadata about the patch file it came from."""
ret = [] ret = []
added_filename_line = False added_patch_location = False
patch_path = os.path.join(patch_dir, patch_filename) patch_path = os.path.join(patch_dir, patch_filename)
with codecs.open(patch_path, encoding='utf-8') as f: with codecs.open(patch_path, encoding='utf-8') as f:
for l in f.readlines(): for l in f.readlines():
line_has_correct_start = l.startswith('diff -') or l.startswith('---') line_has_correct_start = l.startswith('diff -') or l.startswith('---')
if not added_filename_line and line_has_correct_start: if not added_patch_location and line_has_correct_start:
ret.append('Patch-Filename: {}\n'.format(patch_filename)) ret.append('{}{}\n'.format(PATCH_DIR_PREFIX, patch_dir))
added_filename_line = True ret.append('{}{}\n'.format(PATCH_FILENAME_PREFIX, patch_filename))
added_patch_location = True
ret.append(l) ret.append(l)
return ''.join(ret) return ''.join(ret)