Use raw strings for regexps with unescaped \'s
[stgit.git] / stgit / utils.py
blobff78ea890d7a3d612ff56a7074a4b46815dad7ab
1 # -*- coding: utf-8 -*-
2 """Common utility functions"""
4 from __future__ import absolute_import, division, print_function
5 import errno
6 import os
7 import re
8 import tempfile
10 from stgit.config import config
11 from stgit.exception import StgException
12 from stgit.out import out
14 __copyright__ = """
15 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
17 This program is free software; you can redistribute it and/or modify
18 it under the terms of the GNU General Public License version 2 as
19 published by the Free Software Foundation.
21 This program is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 GNU General Public License for more details.
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, see http://www.gnu.org/licenses/.
28 """
31 def mkdir_file(filename, mode):
32 """Opens filename with the given mode, creating the directory it's
33 in if it doesn't already exist."""
34 create_dirs(os.path.dirname(filename))
35 return open(filename, mode)
37 def read_strings(filename):
38 """Reads the lines from a file
39 """
40 with open(filename) as f:
41 return [line.strip() for line in f.readlines()]
43 def read_string(filename, multiline = False):
44 """Reads the first line from a file
45 """
46 with open(filename) as f:
47 if multiline:
48 return f.read()
49 else:
50 return f.readline().strip()
52 def write_strings(filename, lines):
53 """Write 'lines' sequence to file
54 """
55 with open(filename, 'w+') as f:
56 f.writelines([line + '\n' for line in lines])
58 def write_string(filename, line, multiline = False):
59 """Writes 'line' to file and truncates it
60 """
61 with mkdir_file(filename, 'w+') as f:
62 print(line,
63 end='' if multiline else '\n',
64 file=f)
66 def append_strings(filename, lines):
67 """Appends 'lines' sequence to file
68 """
69 with mkdir_file(filename, 'a+') as f:
70 for line in lines:
71 print(line, file=f)
73 def append_string(filename, line):
74 """Appends 'line' to file
75 """
76 with mkdir_file(filename, 'a+') as f:
77 print(line, file=f)
79 def insert_string(filename, line):
80 """Inserts 'line' at the beginning of the file
81 """
82 with mkdir_file(filename, 'r+') as f:
83 lines = f.readlines()
84 f.seek(0)
85 f.truncate()
86 print(line, file=f)
87 f.writelines(lines)
89 def create_empty_file(name):
90 """Creates an empty file
91 """
92 mkdir_file(name, 'w+').close()
94 def list_files_and_dirs(path):
95 """Return the sets of filenames and directory names in a
96 directory."""
97 files, dirs = [], []
98 for fd in os.listdir(path):
99 full_fd = os.path.join(path, fd)
100 if os.path.isfile(full_fd):
101 files.append(fd)
102 elif os.path.isdir(full_fd):
103 dirs.append(fd)
104 return files, dirs
106 def walk_tree(basedir):
107 """Starting in the given directory, iterate through all its
108 subdirectories. For each subdirectory, yield the name of the
109 subdirectory (relative to the base directory), the list of
110 filenames in the subdirectory, and the list of directory names in
111 the subdirectory."""
112 subdirs = ['']
113 while subdirs:
114 subdir = subdirs.pop()
115 files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
116 for d in dirs:
117 subdirs.append(os.path.join(subdir, d))
118 yield subdir, files, dirs
120 def strip_prefix(prefix, string):
121 """Return string, without the prefix. Blow up if string doesn't
122 start with prefix."""
123 assert string.startswith(prefix)
124 return string[len(prefix):]
126 def strip_suffix(suffix, string):
127 """Return string, without the suffix. Blow up if string doesn't
128 end with suffix."""
129 assert string.endswith(suffix)
130 return string[:-len(suffix)]
132 def remove_file_and_dirs(basedir, file):
133 """Remove join(basedir, file), and then remove the directory it
134 was in if empty, and try the same with its parent, until we find a
135 nonempty directory or reach basedir."""
136 os.remove(os.path.join(basedir, file))
137 try:
138 os.removedirs(os.path.join(basedir, os.path.dirname(file)))
139 except OSError:
140 # file's parent dir may not be empty after removal
141 pass
143 def create_dirs(directory):
144 """Create the given directory, if the path doesn't already exist."""
145 if directory and not os.path.isdir(directory):
146 create_dirs(os.path.dirname(directory))
147 try:
148 os.mkdir(directory)
149 except OSError as e:
150 if e.errno != errno.EEXIST:
151 raise e
153 def rename(basedir, file1, file2):
154 """Rename join(basedir, file1) to join(basedir, file2), not
155 leaving any empty directories behind and creating any directories
156 necessary."""
157 full_file2 = os.path.join(basedir, file2)
158 create_dirs(os.path.dirname(full_file2))
159 os.rename(os.path.join(basedir, file1), full_file2)
160 try:
161 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
162 except OSError:
163 # file1's parent dir may not be empty after move
164 pass
166 class EditorException(StgException):
167 pass
169 def get_editor():
170 for editor in [os.environ.get('GIT_EDITOR'),
171 config.get('stgit.editor'), # legacy
172 config.get('core.editor'),
173 os.environ.get('VISUAL'),
174 os.environ.get('EDITOR'),
175 'vi']:
176 if editor:
177 return editor
179 def call_editor(filename):
180 """Run the editor on the specified filename."""
181 cmd = '%s %s' % (get_editor(), filename)
182 out.start('Invoking the editor: "%s"' % cmd)
183 err = os.system(cmd)
184 if err:
185 raise EditorException('editor failed, exit code: %d' % err)
186 out.done()
188 def get_hook(repository, hook_name, extra_env={}):
189 hook_path = os.path.join(repository.directory, 'hooks', hook_name)
190 if not (os.path.isfile(hook_path) and os.access(hook_path, os.X_OK)):
191 return None
193 default_iw = repository.default_iw
194 prefix_dir = os.path.relpath(os.getcwd(), default_iw.cwd)
195 if prefix_dir == os.curdir:
196 prefix = ''
197 else:
198 prefix = os.path.join(prefix_dir, '')
199 extra_env = add_dict(extra_env, {'GIT_PREFIX': prefix})
201 def hook(*parameters):
202 argv = [hook_path]
203 argv.extend(parameters)
205 # On Windows, run the hook using "bash" explicitly
206 if os.name != 'posix':
207 argv.insert(0, 'bash')
209 default_iw.run(argv, extra_env).run()
211 hook.__name__ = hook_name
212 return hook
214 def run_hook_on_string(hook, s, *args):
215 if hook is not None:
216 temp = tempfile.NamedTemporaryFile('wb', delete=False)
217 try:
218 try:
219 temp.write(s)
220 finally:
221 temp.close()
223 hook(temp.name, *args)
225 output = open(temp.name, 'rb')
226 try:
227 s = output.read()
228 finally:
229 output.close()
230 finally:
231 os.unlink(temp.name)
233 return s
235 def edit_string(s, filename):
236 with open(filename, 'w') as f:
237 f.write(s)
238 call_editor(filename)
239 with open(filename) as f:
240 s = f.read()
241 os.remove(filename)
242 return s
244 def append_comment(s, comment, separator = '---'):
245 return ('%s\n\n%s\nEverything following the line with "%s" will be'
246 ' ignored\n\n%s' % (s, separator, separator, comment))
248 def strip_comment(s, separator = '---'):
249 try:
250 return s[:s.index('\n%s\n' % separator)]
251 except ValueError:
252 return s
254 def find_patch_name(patchname, unacceptable):
255 """Find a patch name which is acceptable."""
256 if unacceptable(patchname):
257 suffix = 0
258 while unacceptable('%s-%d' % (patchname, suffix)):
259 suffix += 1
260 patchname = '%s-%d' % (patchname, suffix)
261 return patchname
263 def patch_name_from_msg(msg):
264 """Return a string to be used as a patch name. This is generated
265 from the top line of the string passed as argument."""
266 if not msg:
267 return None
269 name_len = config.getint('stgit.namelength')
270 if not name_len:
271 name_len = 30
273 subject_line = msg.split('\n', 1)[0].lstrip().lower()
274 words = re.sub(r'[\W]+', ' ', subject_line).split()
276 # use loop to avoid truncating the last name
277 name = words and words[0] or 'unknown'
278 for word in words[1:]:
279 new = name + '-' + word
280 if len(new) > name_len:
281 break
282 name = new
284 return name
286 def make_patch_name(msg, unacceptable, default_name = 'patch'):
287 """Return a patch name generated from the given commit message,
288 guaranteed to make unacceptable(name) be false. If the commit
289 message is empty, base the name on default_name instead."""
290 patchname = patch_name_from_msg(msg)
291 if not patchname:
292 patchname = default_name
293 return find_patch_name(patchname, unacceptable)
296 def add_sign_line(desc, sign_str, name, email):
297 if not sign_str:
298 return desc
299 sign_str = '%s: %s <%s>' % (sign_str, name, email)
300 if sign_str in desc:
301 return desc
302 desc = desc.rstrip()
303 preamble, lastblank, lastpara = desc.rpartition('\n\n')
304 is_signoff = re.compile(r'[A-Z][a-z]*(-[A-Za-z][a-z]*)*: ').match
305 if not (lastblank and all(is_signoff(l) for l in lastpara.split('\n'))):
306 desc = desc + '\n'
307 return '%s\n%s\n' % (desc, sign_str)
309 def parse_name_email(address):
310 """Return a tuple consisting of the name and email parsed from a
311 standard 'name <email>' or 'email (name)' string."""
312 address = re.sub(r'[\\"]', r'\\\g<0>', address)
313 str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
314 if not str_list:
315 str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
316 if not str_list:
317 return None
318 return (str_list[0][1], str_list[0][0])
319 return str_list[0]
321 def parse_name_email_date(address):
322 """Return a tuple consisting of the name, email and date parsed
323 from a 'name <email> date' string."""
324 address = re.sub(r'[\\"]', r'\\\g<0>', address)
325 str_list = re.findall(r'^(.*)\s*<(.*)>\s*(.*)\s*$', address)
326 if not str_list:
327 return None
328 return str_list[0]
330 # Exit codes.
331 STGIT_SUCCESS = 0 # everything's OK
332 STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error
333 STGIT_COMMAND_ERROR = 2 # seems to be a command that failed
334 STGIT_CONFLICT = 3 # merge conflict, otherwise OK
335 STGIT_BUG_ERROR = 4 # a bug in StGit
337 def add_dict(d1, d2):
338 """Return a new dict with the contents of both d1 and d2. In case of
339 conflicting mappings, d2 takes precedence."""
340 d = dict(d1)
341 d.update(d2)
342 return d