chiark / gitweb /
Revert 'Changed rebasing safety check to look for reachability of
[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