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