Skip to content

Commit bc2d1b1

Browse files
loudonlunestephenfin
authored andcommitted
parser: Parse "Depends-on" tags in emails
Add a new function to parse "Depends-on" tags to the parser. The value may either be the message ID of a patch or cover letter email already received by Patchwork, or the web URL of a patch or series. When this tag is found, the parser will add the series (or the series the patch belongs to) as a dependency to the series it is creating. This parser feature is only active when the feature flag is enabled on the project related to the patch or cover letter. Signed-off-by: Adam Hassick <ahassick@iol.unh.edu> Acked-by: Aaron Conole <aconole@redhat.com> [stephenfin: Parse patch dependencies unconditionally] Signed-off-by: Stephen Finucane <stephen@that.guru>
1 parent e29a22e commit bc2d1b1

File tree

1 file changed

+88
-1
lines changed

1 file changed

+88
-1
lines changed

patchwork/parser.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
from fnmatch import fnmatch
1515
import logging
1616
import re
17+
from urllib.parse import urlparse, parse_qs
1718

1819
from django.contrib.auth.models import User
1920
from django.db.utils import IntegrityError
2021
from django.db import transaction
2122
from django.utils import timezone as tz_utils
23+
from django.urls import resolve, Resolver404
2224

2325
from patchwork.models import Cover
2426
from patchwork.models import CoverComment
@@ -32,7 +34,6 @@
3234
from patchwork.models import SeriesReference
3335
from patchwork.models import State
3436

35-
3637
_msgid_re = re.compile(r'<[^>]+>')
3738
_hunk_re = re.compile(r'^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@')
3839
_filename_re = re.compile(r'^(---|\+\+\+) (\S+)')
@@ -1054,6 +1055,85 @@ def parse_pull_request(content):
10541055
return None
10551056

10561057

1058+
def find_series_from_url(url):
1059+
"""
1060+
Get a series from either a series or patch URL.
1061+
"""
1062+
parse_result = urlparse(url)
1063+
1064+
# Resolve the URL path to see if this is a patch or series detail URL.
1065+
try:
1066+
result = resolve(parse_result.path)
1067+
except Resolver404:
1068+
logging.warning('Failed to resolve series or patch URL: %s', url)
1069+
return None
1070+
1071+
# TODO: Use the series detail view here.
1072+
if result.view_name == 'patch-list' and parse_result.query:
1073+
# Parse the query string.
1074+
# This can be replaced with something much friendlier once the
1075+
# series detail view is implemented.
1076+
series_query_param = parse_qs(parse_result.query)
1077+
1078+
if series_id := series_query_param.get('series'):
1079+
try:
1080+
series_id_num = int(series_id[0])
1081+
except ValueError:
1082+
logging.warning(
1083+
'Series URL with an invalid series query parameter was given: %s',
1084+
url,
1085+
)
1086+
return None
1087+
# This will return None if there are no matches.
1088+
return Series.objects.filter(id=series_id_num).first()
1089+
1090+
logging.warning(
1091+
'Series URL did not have a series query parameter: %s', url
1092+
)
1093+
return None
1094+
elif result.view_name == 'patch-detail':
1095+
msgid = Patch.decode_msgid(result.kwargs['msgid'])
1096+
if patch := Patch.objects.filter(msgid=msgid).first():
1097+
return patch.series
1098+
1099+
1100+
def find_series_from_msgid(msgid):
1101+
"""
1102+
Get a series from
1103+
"""
1104+
if patch := Patch.objects.filter(msgid=msgid).first():
1105+
return patch.series
1106+
1107+
if cover := Cover.objects.filter(msgid=msgid).first():
1108+
return cover.series
1109+
1110+
1111+
def parse_depends_on(content):
1112+
"""Parses any dependency hints in the patch or series content."""
1113+
dependencies = []
1114+
1115+
# Discover dependencies given as URLs.
1116+
for url in re.findall(
1117+
r'^Depends-on: (http[s]?:\/\/[\w\d\-.\/=&@:%?_\+()]+)\s*$',
1118+
content,
1119+
flags=re.MULTILINE | re.IGNORECASE,
1120+
):
1121+
if series := find_series_from_url(url):
1122+
dependencies.append(series)
1123+
1124+
# Discover dependencies given as message IDs.
1125+
for msgid in re.findall(
1126+
r'^Depends-on: (<[^>]+>)\s*$',
1127+
content,
1128+
flags=re.MULTILINE | re.IGNORECASE,
1129+
):
1130+
if series := find_series_from_msgid(msgid):
1131+
dependencies.append(series)
1132+
1133+
# Return list of series objects to depend on.
1134+
return dependencies
1135+
1136+
10571137
def find_state(mail):
10581138
"""Return the state with the given name or the default."""
10591139
state_name = clean_header(mail.get('X-Patchwork-State', ''))
@@ -1308,6 +1388,9 @@ def parse_mail(mail, list_id=None):
13081388
# always have a series
13091389
series.add_patch(patch, x)
13101390

1391+
# parse patch dependencies
1392+
series.add_dependencies(parse_depends_on(message))
1393+
13111394
return patch
13121395
elif x == 0: # (potential) cover letters
13131396
# if refs are empty, it's implicitly a cover letter. If not,
@@ -1375,6 +1458,10 @@ def parse_mail(mail, list_id=None):
13751458

13761459
series.add_cover_letter(cover_letter)
13771460

1461+
# cover letters are permitted to specify dependencies for the
1462+
# entire patch series; parse them
1463+
series.add_dependencies(parse_depends_on(message))
1464+
13781465
return cover_letter
13791466

13801467
# comments

0 commit comments

Comments
 (0)