chiark / gitweb /
ad9b1f115679f889e1533b9eaf6364aa35a63afb
[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