Remove unneeded import
[stgit] / stgit / utils.py
1 """Common utility functions
2 """
3
4 import errno, optparse, os, os.path, re, sys
5 from stgit.exception import *
6 from stgit.config import config
7 from stgit.out import *
8
9 __copyright__ = """
10 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
11
12 This program is free software; you can redistribute it and/or modify
13 it under the terms of the GNU General Public License version 2 as
14 published by the Free Software Foundation.
15
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
20
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 """
25
26 def mkdir_file(filename, mode):
27 """Opens filename with the given mode, creating the directory it's
28 in if it doesn't already exist."""
29 create_dirs(os.path.dirname(filename))
30 return file(filename, mode)
31
32 def read_strings(filename):
33 """Reads the lines from a file
34 """
35 f = file(filename, 'r')
36 lines = [line.strip() for line in f.readlines()]
37 f.close()
38 return lines
39
40 def read_string(filename, multiline = False):
41 """Reads the first line from a file
42 """
43 f = file(filename, 'r')
44 if multiline:
45 result = f.read()
46 else:
47 result = f.readline().strip()
48 f.close()
49 return result
50
51 def write_strings(filename, lines):
52 """Write 'lines' sequence to file
53 """
54 f = file(filename, 'w+')
55 f.writelines([line + '\n' for line in lines])
56 f.close()
57
58 def write_string(filename, line, multiline = False):
59 """Writes 'line' to file and truncates it
60 """
61 f = mkdir_file(filename, 'w+')
62 if multiline:
63 f.write(line)
64 else:
65 print >> f, line
66 f.close()
67
68 def append_strings(filename, lines):
69 """Appends 'lines' sequence to file
70 """
71 f = mkdir_file(filename, 'a+')
72 for line in lines:
73 print >> f, line
74 f.close()
75
76 def append_string(filename, line):
77 """Appends 'line' to file
78 """
79 f = mkdir_file(filename, 'a+')
80 print >> f, line
81 f.close()
82
83 def insert_string(filename, line):
84 """Inserts 'line' at the beginning of the file
85 """
86 f = mkdir_file(filename, 'r+')
87 lines = f.readlines()
88 f.seek(0); f.truncate()
89 print >> f, line
90 f.writelines(lines)
91 f.close()
92
93 def create_empty_file(name):
94 """Creates an empty file
95 """
96 mkdir_file(name, 'w+').close()
97
98 def list_files_and_dirs(path):
99 """Return the sets of filenames and directory names in a
100 directory."""
101 files, dirs = [], []
102 for fd in os.listdir(path):
103 full_fd = os.path.join(path, fd)
104 if os.path.isfile(full_fd):
105 files.append(fd)
106 elif os.path.isdir(full_fd):
107 dirs.append(fd)
108 return files, dirs
109
110 def walk_tree(basedir):
111 """Starting in the given directory, iterate through all its
112 subdirectories. For each subdirectory, yield the name of the
113 subdirectory (relative to the base directory), the list of
114 filenames in the subdirectory, and the list of directory names in
115 the subdirectory."""
116 subdirs = ['']
117 while subdirs:
118 subdir = subdirs.pop()
119 files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
120 for d in dirs:
121 subdirs.append(os.path.join(subdir, d))
122 yield subdir, files, dirs
123
124 def strip_prefix(prefix, string):
125 """Return string, without the prefix. Blow up if string doesn't
126 start with prefix."""
127 assert string.startswith(prefix)
128 return string[len(prefix):]
129
130 def strip_suffix(suffix, string):
131 """Return string, without the suffix. Blow up if string doesn't
132 end with suffix."""
133 assert string.endswith(suffix)
134 return string[:-len(suffix)]
135
136 def remove_file_and_dirs(basedir, file):
137 """Remove join(basedir, file), and then remove the directory it
138 was in if empty, and try the same with its parent, until we find a
139 nonempty directory or reach basedir."""
140 os.remove(os.path.join(basedir, file))
141 try:
142 os.removedirs(os.path.join(basedir, os.path.dirname(file)))
143 except OSError:
144 # file's parent dir may not be empty after removal
145 pass
146
147 def create_dirs(directory):
148 """Create the given directory, if the path doesn't already exist."""
149 if directory and not os.path.isdir(directory):
150 create_dirs(os.path.dirname(directory))
151 try:
152 os.mkdir(directory)
153 except OSError, e:
154 if e.errno != errno.EEXIST:
155 raise e
156
157 def rename(basedir, file1, file2):
158 """Rename join(basedir, file1) to join(basedir, file2), not
159 leaving any empty directories behind and creating any directories
160 necessary."""
161 full_file2 = os.path.join(basedir, file2)
162 create_dirs(os.path.dirname(full_file2))
163 os.rename(os.path.join(basedir, file1), full_file2)
164 try:
165 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
166 except OSError:
167 # file1's parent dir may not be empty after move
168 pass
169
170 class EditorException(StgException):
171 pass
172
173 def call_editor(filename):
174 """Run the editor on the specified filename."""
175
176 # the editor
177 editor = config.get('stgit.editor')
178 if not editor:
179 editor = os.environ.get('EDITOR', 'vi')
180 editor += ' %s' % filename
181
182 out.start('Invoking the editor: "%s"' % editor)
183 err = os.system(editor)
184 if err:
185 raise EditorException, 'editor failed, exit code: %d' % err
186 out.done()
187
188 def edit_string(s, filename):
189 f = file(filename, 'w')
190 f.write(s)
191 f.close()
192 call_editor(filename)
193 f = file(filename)
194 s = f.read()
195 f.close()
196 os.remove(filename)
197 return s
198
199 def patch_name_from_msg(msg):
200 """Return a string to be used as a patch name. This is generated
201 from the top line of the string passed as argument."""
202 if not msg:
203 return None
204
205 name_len = config.get('stgit.namelength')
206 if not name_len:
207 name_len = 30
208
209 subject_line = msg.split('\n', 1)[0].lstrip().lower()
210 return re.sub('[\W]+', '-', subject_line).strip('-')[:name_len]
211
212 def make_patch_name(msg, unacceptable, default_name = 'patch'):
213 """Return a patch name generated from the given commit message,
214 guaranteed to make unacceptable(name) be false. If the commit
215 message is empty, base the name on default_name instead."""
216 patchname = patch_name_from_msg(msg)
217 if not patchname:
218 patchname = default_name
219 if unacceptable(patchname):
220 suffix = 0
221 while unacceptable('%s-%d' % (patchname, suffix)):
222 suffix += 1
223 patchname = '%s-%d' % (patchname, suffix)
224 return patchname
225
226 # any and all functions are builtin in Python 2.5 and higher, but not
227 # in 2.4.
228 if not 'any' in dir(__builtins__):
229 def any(bools):
230 for b in bools:
231 if b:
232 return True
233 return False
234 if not 'all' in dir(__builtins__):
235 def all(bools):
236 for b in bools:
237 if not b:
238 return False
239 return True
240
241 def make_sign_options():
242 def callback(option, opt_str, value, parser, sign_str):
243 if parser.values.sign_str not in [None, sign_str]:
244 raise optparse.OptionValueError(
245 '--ack and --sign were both specified')
246 parser.values.sign_str = sign_str
247 return [optparse.make_option('--sign', action = 'callback',
248 callback = callback, dest = 'sign_str',
249 callback_args = ('Signed-off-by',),
250 help = 'add Signed-off-by line'),
251 optparse.make_option('--ack', action = 'callback',
252 callback = callback, dest = 'sign_str',
253 callback_args = ('Acked-by',),
254 help = 'add Acked-by line')]
255
256 def add_sign_line(desc, sign_str, name, email):
257 if not sign_str:
258 return desc
259 sign_str = '%s: %s <%s>' % (sign_str, name, email)
260 if sign_str in desc:
261 return desc
262 desc = desc.rstrip()
263 if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']):
264 desc = desc + '\n'
265 return '%s\n%s\n' % (desc, sign_str)
266
267 def make_message_options():
268 def no_dup(parser):
269 if parser.values.message != None:
270 raise optparse.OptionValueError(
271 'Cannot give more than one --message or --file')
272 def no_combine(parser):
273 if (parser.values.message != None
274 and parser.values.save_template != None):
275 raise optparse.OptionValueError(
276 'Cannot give both --message/--file and --save-template')
277 def msg_callback(option, opt_str, value, parser):
278 no_dup(parser)
279 parser.values.message = value
280 no_combine(parser)
281 def file_callback(option, opt_str, value, parser):
282 no_dup(parser)
283 if value == '-':
284 parser.values.message = sys.stdin.read()
285 else:
286 f = file(value)
287 parser.values.message = f.read()
288 f.close()
289 no_combine(parser)
290 def templ_callback(option, opt_str, value, parser):
291 if value == '-':
292 def w(s):
293 sys.stdout.write(s)
294 else:
295 def w(s):
296 f = file(value, 'w+')
297 f.write(s)
298 f.close()
299 parser.values.save_template = w
300 no_combine(parser)
301 m = optparse.make_option
302 return [m('-m', '--message', action = 'callback', callback = msg_callback,
303 dest = 'message', type = 'string',
304 help = 'use MESSAGE instead of invoking the editor'),
305 m('-f', '--file', action = 'callback', callback = file_callback,
306 dest = 'message', type = 'string', metavar = 'FILE',
307 help = 'use FILE instead of invoking the editor'),
308 m('--save-template', action = 'callback', callback = templ_callback,
309 metavar = 'FILE', dest = 'save_template', type = 'string',
310 help = 'save the message template to FILE and exit')]
311
312 def make_diff_opts_option():
313 def diff_opts_callback(option, opt_str, value, parser):
314 if value:
315 parser.values.diff_flags.extend(value.split())
316 else:
317 parser.values.diff_flags = []
318 return [optparse.make_option(
319 '-O', '--diff-opts', dest = 'diff_flags',
320 default = (config.get('stgit.diff-opts') or '').split(),
321 action = 'callback', callback = diff_opts_callback,
322 type = 'string', metavar = 'OPTIONS',
323 help = 'extra options to pass to "git diff"')]
324
325 def parse_name_email(address):
326 """Return a tuple consisting of the name and email parsed from a
327 standard 'name <email>' or 'email (name)' string."""
328 address = re.sub(r'[\\"]', r'\\\g<0>', address)
329 str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
330 if not str_list:
331 str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
332 if not str_list:
333 return None
334 return (str_list[0][1], str_list[0][0])
335 return str_list[0]
336
337 def parse_name_email_date(address):
338 """Return a tuple consisting of the name, email and date parsed
339 from a 'name <email> date' string."""
340 address = re.sub(r'[\\"]', r'\\\g<0>', address)
341 str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address)
342 if not str_list:
343 return None
344 return str_list[0]
345
346 def make_person_options(person, short):
347 """Sets options.<person> to a function that modifies a Person
348 according to the commandline options."""
349 def short_callback(option, opt_str, value, parser, field):
350 f = getattr(parser.values, person)
351 setattr(parser.values, person,
352 lambda p: getattr(f(p), 'set_' + field)(value))
353 def full_callback(option, opt_str, value, parser):
354 ne = parse_name_email(value)
355 if not ne:
356 raise optparse.OptionValueError(
357 'Bad %s specification: %r' % (opt_str, value))
358 name, email = ne
359 short_callback(option, opt_str, name, parser, 'name')
360 short_callback(option, opt_str, email, parser, 'email')
361 return ([optparse.make_option(
362 '--%s' % person, metavar = '"NAME <EMAIL>"', type = 'string',
363 action = 'callback', callback = full_callback, dest = person,
364 default = lambda p: p, help = 'set the %s details' % person)]
365 + [optparse.make_option(
366 '--%s%s' % (short, f), metavar = f.upper(), type = 'string',
367 action = 'callback', callback = short_callback, dest = person,
368 callback_args = (f,), help = 'set the %s %s' % (person, f))
369 for f in ['name', 'email', 'date']])
370
371 def make_author_committer_options():
372 return (make_person_options('author', 'auth')
373 + make_person_options('committer', 'comm'))
374
375 # Exit codes.
376 STGIT_SUCCESS = 0 # everything's OK
377 STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error
378 STGIT_COMMAND_ERROR = 2 # seems to be a command that failed
379 STGIT_CONFLICT = 3 # merge conflict, otherwise OK
380 STGIT_BUG_ERROR = 4 # a bug in StGit
381
382 def add_dict(d1, d2):
383 """Return a new dict with the contents of both d1 and d2. In case of
384 conflicting mappings, d2 takes precedence."""
385 d = dict(d1)
386 d.update(d2)
387 return d