Skip to content

Commit 0e569fd

Browse files
committed
Avoid importing invalid and duplicate patch names
This aims to be a comprehensive repair of issues with `stg import` allowing invalid and duplicate patch names. The new Patches.make_name() method creates a unique and valid patch name based on its knowledge of existing patch names. Since StGit patch names are used as Git refs, make_name() also attempts to enforce Git's rules for ref naming. Generated patch names are checked with `git check-ref-format` before being returned. Repairs #64 Signed-off-by: Peter Grayson <[email protected]>
1 parent 6e94acc commit 0e569fd

File tree

7 files changed

+230
-44
lines changed

7 files changed

+230
-44
lines changed

stgit/commands/imprt.py

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from stgit.lib.transaction import StackTransaction, TransactionHalted
1919
from stgit.out import out
2020
from stgit.run import Run
21-
from stgit.utils import make_patch_name
2221

2322
__copyright__ = """
2423
Copyright (C) 2005, Catalin Marinas <[email protected]>
@@ -149,17 +148,6 @@
149148
directory = DirectoryHasRepository()
150149

151150

152-
def __strip_patch_name(name):
153-
stripped = re.sub('^[0-9]+-(.*)$', r'\g<1>', name)
154-
stripped = re.sub(r'^(.*)\.(diff|patch)$', r'\g<1>', stripped)
155-
return stripped
156-
157-
158-
def __replace_slashes_with_dashes(name):
159-
stripped = name.replace('/', '-')
160-
return stripped
161-
162-
163151
def __create_patch(
164152
filename, message, author_name, author_email, author_date, diff, options
165153
):
@@ -168,29 +156,31 @@ def __create_patch(
168156

169157
if options.name:
170158
name = options.name
171-
if not stack.patches.is_name_valid(name):
172-
raise CmdException('Invalid patch name: %s' % name)
173159
elif filename:
174160
name = os.path.basename(filename)
175161
else:
176162
name = ''
177-
if options.stripname:
178-
name = __strip_patch_name(name)
179163

180-
if not name:
181-
if options.ignore or options.replace:
164+
if options.stripname:
165+
# Removing leading numbers and trailing extension
166+
name = re.sub(
167+
r'''^
168+
(?:[0-9]+-)? # Optional leading patch number
169+
(.*?) # Patch name group (non-greedy)
170+
(?:\.(?:diff|patch))? # Optional .diff or .patch extension
171+
$
172+
''',
173+
r'\g<1>',
174+
name,
175+
flags=re.VERBOSE,
176+
)
182177

183-
def unacceptable_name(name):
184-
return False
178+
need_unique = not (options.ignore or options.replace)
185179

186-
else:
187-
unacceptable_name = stack.patches.exists
188-
name = make_patch_name(message, unacceptable_name)
180+
if name:
181+
name = stack.patches.make_name(name, unique=need_unique, lower=False)
189182
else:
190-
# fix possible invalid characters in the patch name
191-
name = re.sub(r'[^\w.]+', '-', name).strip('-')
192-
193-
assert stack.patches.is_name_valid(name)
183+
name = stack.patches.make_name(message, unique=need_unique, lower=True)
194184

195185
if options.ignore and name in stack.patchorder.applied:
196186
out.info('Ignoring already applied patch "%s"' % name)
@@ -266,28 +256,30 @@ def __get_handle_and_name(filename):
266256
return (open(filename, 'rb'), filename)
267257

268258

269-
def __import_file(filename, options, patch=None):
259+
def __import_file(filename, options):
270260
"""Import a patch from a file or standard input"""
271-
pname = None
272261
if filename:
273-
(f, pname) = __get_handle_and_name(filename)
262+
f, filename = __get_handle_and_name(filename)
274263
else:
275264
f = os.fdopen(sys.__stdin__.fileno(), 'rb')
276265

277-
if patch:
278-
pname = patch
279-
elif not pname:
280-
pname = filename
281-
282-
(message, author_name, author_email, author_date, diff) = parse_patch(
283-
f.read(), contains_diff=True
284-
)
266+
patch_data = f.read()
285267

286268
if filename:
287269
f.close()
288270

271+
message, author_name, author_email, author_date, diff = parse_patch(
272+
patch_data, contains_diff=True
273+
)
274+
289275
__create_patch(
290-
pname, message, author_name, author_email, author_date, diff, options
276+
filename,
277+
message,
278+
author_name,
279+
author_email,
280+
author_date,
281+
diff,
282+
options,
291283
)
292284

293285

@@ -327,9 +319,8 @@ def __import_series(filename, options):
327319
else:
328320
options.strip = 1
329321
patchfile = os.path.join(patchdir, patch)
330-
patch = __replace_slashes_with_dashes(patch)
331322

332-
__import_file(patchfile, options, patch)
323+
__import_file(patchfile, options)
333324

334325
if filename:
335326
f.close()

stgit/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
('stgit.autoimerge', ['no']),
3232
('stgit.fetchcmd', ['git fetch']),
3333
('stgit.keepoptimized', ['no']),
34+
('stgit.namelength', ['30']),
3435
('stgit.pager', ['less']),
3536
('stgit.pick.expose-format', ['format:%B%n(imported from commit %H)']),
3637
('stgit.pull-policy', ['pull']),

stgit/lib/stack.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""A Python class hierarchy wrapping the StGit on-disk metadata."""
2+
import re
23

34
from stgit.config import config
45
from stgit.exception import StackException
@@ -167,6 +168,81 @@ def new(self, name, commit, msg):
167168
self._patches[name] = p
168169
return p
169170

171+
def make_name(self, raw, unique=True, lower=True):
172+
"""Make a unique and valid patch name from provided raw name.
173+
174+
The raw name may come from a filename, commit message, or email subject line.
175+
176+
The generated patch name will meet the rules of `git check-ref-format` along
177+
with some additional StGit patch name rules.
178+
179+
"""
180+
default_name = 'patch'
181+
182+
for line in raw.split('\n'):
183+
if line:
184+
break
185+
186+
if not line:
187+
line = default_name
188+
189+
if lower:
190+
line = line.lower()
191+
192+
parts = []
193+
for part in line.split('/'):
194+
# fmt: off
195+
part = re.sub(r'\.lock$', '', part) # Disallowed in Git refs
196+
part = re.sub(r'^\.+|\.+$', '', part) # Cannot start or end with '.'
197+
part = re.sub(r'\.+', '.', part) # No consecutive '.'
198+
part = re.sub(r'[^\w.]+', '-', part) # Non-word and whitespace to dashes
199+
part = re.sub(r'-+', '-', part) # Squash consecutive dashes
200+
part = re.sub(r'^-+|-+$', '', part) # Remove leading and trailing dashes
201+
# fmt: on
202+
if part:
203+
parts.append(part)
204+
205+
long_name = '/'.join(parts)
206+
207+
# TODO: slashes could be allowed in the future.
208+
long_name = long_name.replace('/', '-')
209+
210+
if not long_name:
211+
long_name = default_name
212+
213+
assert self.is_name_valid(long_name)
214+
215+
name_len = config.getint('stgit.namelength')
216+
217+
words = long_name.split('-')
218+
short_name = words[0]
219+
for word in words[1:]:
220+
new_name = '%s-%s' % (short_name, word)
221+
if name_len <= 0 or len(new_name) <= name_len:
222+
short_name = new_name
223+
else:
224+
break
225+
assert self.is_name_valid(short_name)
226+
227+
if not unique:
228+
return short_name
229+
230+
unique_name = short_name
231+
while self.exists(unique_name):
232+
m = re.match(r'(.*?)(-)?(\d+)$', unique_name)
233+
if m:
234+
base, sep, n_str = m.groups()
235+
n = int(n_str) + 1
236+
if sep:
237+
unique_name = '%s%s%d' % (base, sep, n)
238+
else:
239+
unique_name = '%s%d' % (base, n)
240+
else:
241+
unique_name = '%s-1' % unique_name
242+
243+
assert self.is_name_valid(unique_name)
244+
return unique_name
245+
170246

171247
class Stack(Branch):
172248
"""Represents a StGit stack.

stgit/utils.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,6 @@ def patch_name_from_msg(msg):
144144
return None
145145

146146
name_len = config.getint('stgit.namelength')
147-
if not name_len:
148-
name_len = 30
149147

150148
subject_line = msg.split('\n', 1)[0].lstrip().lower()
151149
words = re.sub(r'(?u)[\W]+', ' ', subject_line).split()

t/t1800-import.sh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ test_expect_success \
3131
stg delete ..
3232
'
3333

34+
test_expect_success \
35+
'Attempt import same patch twice' \
36+
'
37+
stg import "$TEST_DIRECTORY"/t1800/git-diff &&
38+
stg pop &&
39+
stg import "$TEST_DIRECTORY"/t1800/git-diff &&
40+
test "$(echo $(stg series --noprefix))" = "git-diff-1 git-diff" &&
41+
stg delete ..
42+
'
43+
3444
test_expect_success \
3545
'Apply a patch and edit message' \
3646
'
@@ -227,7 +237,7 @@ test_expect_success \
227237
'
228238
cat some.patch |
229239
stg import --ignore --author "Some Author <[email protected]>" &&
230-
stg top | grep -e "unknown" &&
240+
stg top | grep -e "patch" &&
231241
stg show | grep -e "Author: Some Author <[email protected]>" &&
232242
stg delete --top
233243
'

t/t1801-import-email.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ test_expect_success \
105105
stg delete ..
106106
'
107107

108+
test_expect_success \
109+
'Import patches from mbox with duplicate subjects' \
110+
'
111+
stg import -M "$TEST_DIRECTORY"/t1801/email-mbox-same-subject &&
112+
test "$(echo $(stg series --noprefix --applied))" = "my-patch my-patch-1 my-patch-2" &&
113+
stg delete ..
114+
'
115+
108116
test_expect_success \
109117
'Apply several patches from an mbox file from stdin' \
110118
'

t/t1801/email-mbox-same-subject

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
From nobody Sat Nov 11 12:45:27 2006
2+
From: Inge =?utf-8?q?Str=C3=B6m?= <[email protected]>
3+
Subject: My Patch
4+
To: Upstream <[email protected]>
5+
Date: Sat, 11 Nov 2006 12:45:27 +0100
6+
Message-ID: <20061111114527.31778.12942.stgit@localhost>
7+
User-Agent: StGIT/0.11
8+
MIME-Version: 1.0
9+
Content-Type: text/plain; charset="utf-8"
10+
Content-Transfer-Encoding: 8bit
11+
Status: RO
12+
Content-Length: 304
13+
Lines: 19
14+
15+
Signed-off-by: Inge Ström <[email protected]>
16+
---
17+
18+
foo.txt | 2 +-
19+
1 files changed, 1 insertions(+), 1 deletions(-)
20+
21+
diff --git a/foo.txt b/foo.txt
22+
index ad01662..91527b1 100644
23+
--- a/foo.txt
24+
+++ b/foo.txt
25+
@@ -7,7 +7,7 @@ dobidum
26+
dobodam
27+
dobodim
28+
dobodum
29+
-dibedam
30+
+dibedad
31+
dibedim
32+
dibedum
33+
dibidam
34+
35+
From nobody Sat Nov 11 12:45:27 2006
36+
From: Inge =?utf-8?q?Str=C3=B6m?= <[email protected]>
37+
Subject: My Patch
38+
To: Upstream <[email protected]>
39+
Date: Sat, 11 Nov 2006 12:45:27 +0100
40+
Message-ID: <20061111114527.31778.92851.stgit@localhost>
41+
In-Reply-To: <20061111114527.31778.12942.stgit@localhost>
42+
References: <20061111114527.31778.12942.stgit@localhost>
43+
User-Agent: StGIT/0.11
44+
MIME-Version: 1.0
45+
Content-Type: text/plain; charset="utf-8"
46+
Content-Transfer-Encoding: 8bit
47+
Status: RO
48+
Content-Length: 296
49+
Lines: 18
50+
51+
Signed-off-by: Inge Ström <[email protected]>
52+
---
53+
54+
foo.txt | 1 +
55+
1 files changed, 1 insertions(+), 0 deletions(-)
56+
57+
diff --git a/foo.txt b/foo.txt
58+
index 91527b1..79922d7 100644
59+
--- a/foo.txt
60+
+++ b/foo.txt
61+
@@ -18,6 +18,7 @@ dibodim
62+
dibodum
63+
dabedam
64+
dabedim
65+
+dibedam
66+
dabedum
67+
dabidam
68+
dabidim
69+
70+
From nobody Sat Nov 11 12:45:27 2006
71+
From: Inge =?utf-8?q?Str=C3=B6m?= <[email protected]>
72+
Subject: My Patch
73+
To: Upstream <[email protected]>
74+
Date: Sat, 11 Nov 2006 12:45:27 +0100
75+
Message-ID: <20061111114527.31778.45876.stgit@localhost>
76+
In-Reply-To: <20061111114527.31778.12942.stgit@localhost>
77+
References: <20061111114527.31778.12942.stgit@localhost>
78+
User-Agent: StGIT/0.11
79+
MIME-Version: 1.0
80+
Content-Type: text/plain; charset="utf-8"
81+
Content-Transfer-Encoding: 8bit
82+
Status: RO
83+
Content-Length: 278
84+
Lines: 16
85+
86+
Signed-off-by: Inge Ström <[email protected]>
87+
---
88+
89+
foo.txt | 1 -
90+
1 files changed, 0 insertions(+), 1 deletions(-)
91+
92+
diff --git a/foo.txt b/foo.txt
93+
index 79922d7..6f978b4 100644
94+
--- a/foo.txt
95+
+++ b/foo.txt
96+
@@ -24,5 +24,4 @@ dabidam
97+
dabidim
98+
dabidum
99+
dabodam
100+
-dabodim
101+
dabodum
102+

0 commit comments

Comments
 (0)