Refactor message printing
[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_string(filename, multiline = False):
111 """Reads the first line from a file
112 """
113 f = file(filename, 'r')
114 if multiline:
115 result = f.read()
116 else:
117 result = f.readline().strip()
118 f.close()
119 return result
120
121 def write_string(filename, line, multiline = False):
122 """Writes 'line' to file and truncates it
123 """
124 f = mkdir_file(filename, 'w+')
125 if multiline:
126 f.write(line)
127 else:
128 print >> f, line
129 f.close()
130
131 def append_strings(filename, lines):
132 """Appends 'lines' sequence to file
133 """
134 f = mkdir_file(filename, 'a+')
135 for line in lines:
136 print >> f, line
137 f.close()
138
139 def append_string(filename, line):
140 """Appends 'line' to file
141 """
142 f = mkdir_file(filename, 'a+')
143 print >> f, line
144 f.close()
145
146 def insert_string(filename, line):
147 """Inserts 'line' at the beginning of the file
148 """
149 f = mkdir_file(filename, 'r+')
150 lines = f.readlines()
151 f.seek(0); f.truncate()
152 print >> f, line
153 f.writelines(lines)
154 f.close()
155
156 def create_empty_file(name):
157 """Creates an empty file
158 """
159 mkdir_file(name, 'w+').close()
160
161 def list_files_and_dirs(path):
162 """Return the sets of filenames and directory names in a
163 directory."""
164 files, dirs = [], []
165 for fd in os.listdir(path):
166 full_fd = os.path.join(path, fd)
167 if os.path.isfile(full_fd):
168 files.append(fd)
169 elif os.path.isdir(full_fd):
170 dirs.append(fd)
171 return files, dirs
172
173 def walk_tree(basedir):
174 """Starting in the given directory, iterate through all its
175 subdirectories. For each subdirectory, yield the name of the
176 subdirectory (relative to the base directory), the list of
177 filenames in the subdirectory, and the list of directory names in
178 the subdirectory."""
179 subdirs = ['']
180 while subdirs:
181 subdir = subdirs.pop()
182 files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
183 for d in dirs:
184 subdirs.append(os.path.join(subdir, d))
185 yield subdir, files, dirs
186
187 def strip_prefix(prefix, string):
188 """Return string, without the prefix. Blow up if string doesn't
189 start with prefix."""
190 assert string.startswith(prefix)
191 return string[len(prefix):]
192
193 def strip_suffix(suffix, string):
194 """Return string, without the suffix. Blow up if string doesn't
195 end with suffix."""
196 assert string.endswith(suffix)
197 return string[:-len(suffix)]
198
199 def remove_file_and_dirs(basedir, file):
200 """Remove join(basedir, file), and then remove the directory it
201 was in if empty, and try the same with its parent, until we find a
202 nonempty directory or reach basedir."""
203 os.remove(os.path.join(basedir, file))
204 try:
205 os.removedirs(os.path.join(basedir, os.path.dirname(file)))
206 except OSError:
207 # file's parent dir may not be empty after removal
208 pass
209
210 def create_dirs(directory):
211 """Create the given directory, if the path doesn't already exist."""
212 if directory and not os.path.isdir(directory):
213 create_dirs(os.path.dirname(directory))
214 try:
215 os.mkdir(directory)
216 except OSError, e:
217 if e.errno != errno.EEXIST:
218 raise e
219
220 def rename(basedir, file1, file2):
221 """Rename join(basedir, file1) to join(basedir, file2), not
222 leaving any empty directories behind and creating any directories
223 necessary."""
224 full_file2 = os.path.join(basedir, file2)
225 create_dirs(os.path.dirname(full_file2))
226 os.rename(os.path.join(basedir, file1), full_file2)
227 try:
228 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
229 except OSError:
230 # file1's parent dir may not be empty after move
231 pass
232
233 class EditorException(Exception):
234 pass
235
236 def call_editor(filename):
237 """Run the editor on the specified filename."""
238
239 # the editor
240 editor = config.get('stgit.editor')
241 if editor:
242 pass
243 elif 'EDITOR' in os.environ:
244 editor = os.environ['EDITOR']
245 else:
246 editor = 'vi'
247 editor += ' %s' % filename
248
249 out.start('Invoking the editor: "%s"' % editor)
250 err = os.system(editor)
251 if err:
252 raise EditorException, 'editor failed, exit code: %d' % err
253 out.done()
254
255 def patch_name_from_msg(msg):
256 """Return a string to be used as a patch name. This is generated
257 from the top line of the string passed as argument, and is at most
258 30 characters long."""
259 if not msg:
260 return None
261
262 subject_line = msg.split('\n', 1)[0].lstrip().lower()
263 return re.sub('[\W]+', '-', subject_line).strip('-')[:30]
264
265 def make_patch_name(msg, unacceptable, default_name = 'patch'):
266 """Return a patch name generated from the given commit message,
267 guaranteed to make unacceptable(name) be false. If the commit
268 message is empty, base the name on default_name instead."""
269 patchname = patch_name_from_msg(msg)
270 if not patchname:
271 patchname = default_name
272 if unacceptable(patchname):
273 suffix = 0
274 while unacceptable('%s-%d' % (patchname, suffix)):
275 suffix += 1
276 patchname = '%s-%d' % (patchname, suffix)
277 return patchname