chiark / gitweb /
857c0f0986c05c7dc49624087ba579e809091385
[stgit] / stgit / utils.py
1 """Common utility functions
2 """
3
4 import errno, optparse, os, os.path, re, sys
5 from stgit.config import config
6 from stgit.out import *
7
8 __copyright__ = """
9 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
10
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License version 2 as
13 published by the Free Software Foundation.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 """
24
25 def mkdir_file(filename, mode):
26     """Opens filename with the given mode, creating the directory it's
27     in if it doesn't already exist."""
28     create_dirs(os.path.dirname(filename))
29     return file(filename, mode)
30
31 def read_strings(filename):
32     """Reads the lines from a file
33     """
34     f = file(filename, 'r')
35     lines = [line.strip() for line in f.readlines()]
36     f.close()
37     return lines
38
39 def read_string(filename, multiline = False):
40     """Reads the first line from a file
41     """
42     f = file(filename, 'r')
43     if multiline:
44         result = f.read()
45     else:
46         result = f.readline().strip()
47     f.close()
48     return result
49
50 def write_strings(filename, lines):
51     """Write 'lines' sequence to file
52     """
53     f = file(filename, 'w+')
54     f.writelines([line + '\n' for line in lines])
55     f.close()
56
57 def write_string(filename, line, multiline = False):
58     """Writes 'line' to file and truncates it
59     """
60     f = mkdir_file(filename, 'w+')
61     if multiline:
62         f.write(line)
63     else:
64         print >> f, line
65     f.close()
66
67 def append_strings(filename, lines):
68     """Appends 'lines' sequence to file
69     """
70     f = mkdir_file(filename, 'a+')
71     for line in lines:
72         print >> f, line
73     f.close()
74
75 def append_string(filename, line):
76     """Appends 'line' to file
77     """
78     f = mkdir_file(filename, 'a+')
79     print >> f, line
80     f.close()
81
82 def insert_string(filename, line):
83     """Inserts 'line' at the beginning of the file
84     """
85     f = mkdir_file(filename, 'r+')
86     lines = f.readlines()
87     f.seek(0); f.truncate()
88     print >> f, line
89     f.writelines(lines)
90     f.close()
91
92 def create_empty_file(name):
93     """Creates an empty file
94     """
95     mkdir_file(name, 'w+').close()
96
97 def list_files_and_dirs(path):
98     """Return the sets of filenames and directory names in a
99     directory."""
100     files, dirs = [], []
101     for fd in os.listdir(path):
102         full_fd = os.path.join(path, fd)
103         if os.path.isfile(full_fd):
104             files.append(fd)
105         elif os.path.isdir(full_fd):
106             dirs.append(fd)
107     return files, dirs
108
109 def walk_tree(basedir):
110     """Starting in the given directory, iterate through all its
111     subdirectories. For each subdirectory, yield the name of the
112     subdirectory (relative to the base directory), the list of
113     filenames in the subdirectory, and the list of directory names in
114     the subdirectory."""
115     subdirs = ['']
116     while subdirs:
117         subdir = subdirs.pop()
118         files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
119         for d in dirs:
120             subdirs.append(os.path.join(subdir, d))
121         yield subdir, files, dirs
122
123 def strip_prefix(prefix, string):
124     """Return string, without the prefix. Blow up if string doesn't
125     start with prefix."""
126     assert string.startswith(prefix)
127     return string[len(prefix):]
128
129 def strip_suffix(suffix, string):
130     """Return string, without the suffix. Blow up if string doesn't
131     end with suffix."""
132     assert string.endswith(suffix)
133     return string[:-len(suffix)]
134
135 def remove_file_and_dirs(basedir, file):
136     """Remove join(basedir, file), and then remove the directory it
137     was in if empty, and try the same with its parent, until we find a
138     nonempty directory or reach basedir."""
139     os.remove(os.path.join(basedir, file))
140     try:
141         os.removedirs(os.path.join(basedir, os.path.dirname(file)))
142     except OSError:
143         # file's parent dir may not be empty after removal
144         pass
145
146 def create_dirs(directory):
147     """Create the given directory, if the path doesn't already exist."""
148     if directory and not os.path.isdir(directory):
149         create_dirs(os.path.dirname(directory))
150         try:
151             os.mkdir(directory)
152         except OSError, e:
153             if e.errno != errno.EEXIST:
154                 raise e
155
156 def rename(basedir, file1, file2):
157     """Rename join(basedir, file1) to join(basedir, file2), not
158     leaving any empty directories behind and creating any directories
159     necessary."""
160     full_file2 = os.path.join(basedir, file2)
161     create_dirs(os.path.dirname(full_file2))
162     os.rename(os.path.join(basedir, file1), full_file2)
163     try:
164         os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
165     except OSError:
166         # file1's parent dir may not be empty after move
167         pass
168
169 class EditorException(Exception):
170     pass
171
172 def call_editor(filename):
173     """Run the editor on the specified filename."""
174
175     # the editor
176     editor = config.get('stgit.editor')
177     if editor:
178         pass
179     elif 'EDITOR' in os.environ:
180         editor = os.environ['EDITOR']
181     else:
182         editor = 'vi'
183     editor += ' %s' % filename
184
185     out.start('Invoking the editor: "%s"' % editor)
186     err = os.system(editor)
187     if err:
188         raise EditorException, 'editor failed, exit code: %d' % err
189     out.done()
190
191 def patch_name_from_msg(msg):
192     """Return a string to be used as a patch name. This is generated
193     from the top line of the string passed as argument."""
194     if not msg:
195         return None
196
197     name_len = config.get('stgit.namelength')
198     if not name_len:
199         name_len = 30
200
201     subject_line = msg.split('\n', 1)[0].lstrip().lower()
202     return re.sub('[\W]+', '-', subject_line).strip('-')[:name_len]
203
204 def make_patch_name(msg, unacceptable, default_name = 'patch'):
205     """Return a patch name generated from the given commit message,
206     guaranteed to make unacceptable(name) be false. If the commit
207     message is empty, base the name on default_name instead."""
208     patchname = patch_name_from_msg(msg)
209     if not patchname:
210         patchname = default_name
211     if unacceptable(patchname):
212         suffix = 0
213         while unacceptable('%s-%d' % (patchname, suffix)):
214             suffix += 1
215         patchname = '%s-%d' % (patchname, suffix)
216     return patchname
217
218 # any and all functions are builtin in Python 2.5 and higher, but not
219 # in 2.4.
220 if not 'any' in dir(__builtins__):
221     def any(bools):
222         for b in bools:
223             if b:
224                 return True
225         return False
226 if not 'all' in dir(__builtins__):
227     def all(bools):
228         for b in bools:
229             if not b:
230                 return False
231         return True
232
233 def make_sign_options():
234     def callback(option, opt_str, value, parser, sign_str):
235         if parser.values.sign_str not in [None, sign_str]:
236             raise optparse.OptionValueError(
237                 '--ack and --sign were both specified')
238         parser.values.sign_str = sign_str
239     return [optparse.make_option('--sign', action = 'callback',
240                                  callback = callback, dest = 'sign_str',
241                                  callback_args = ('Signed-off-by',),
242                                  help = 'add Signed-off-by line'),
243             optparse.make_option('--ack', action = 'callback',
244                                  callback = callback, dest = 'sign_str',
245                                  callback_args = ('Acked-by',),
246                                  help = 'add Acked-by line')]
247
248 def add_sign_line(desc, sign_str, name, email):
249     if not sign_str:
250         return desc
251     sign_str = '%s: %s <%s>' % (sign_str, name, email)
252     if sign_str in desc:
253         return desc
254     desc = desc.rstrip()
255     if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']):
256         desc = desc + '\n'
257     return '%s\n%s\n' % (desc, sign_str)