chiark / gitweb /
2983ea8b0bb42ad2ba574d4b8f5e036e4b642dcb
[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 strip_leading(prefix, s):
383     """Strip leading prefix from a string. Blow up if the prefix isn't
384     there."""
385     assert s.startswith(prefix)
386     return s[len(prefix):]
387
388 def add_dict(d1, d2):
389     """Return a new dict with the contents of both d1 and d2. In case of
390     conflicting mappings, d2 takes precedence."""
391     d = dict(d1)
392     d.update(d2)
393     return d