1 """Common utility functions
4 import errno, os, os.path, re, sys
5 from stgit.config import config
8 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
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.
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.
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
24 class MessagePrinter(object):
27 def __init__(self, write, flush):
30 self.at_start_of_line = True
33 """Ensure that we're at the beginning of a line."""
34 if not self.at_start_of_line:
36 self.at_start_of_line = True
37 def single_line(self, msg, print_newline = True,
39 """Write a single line. Newline before and after are
40 separately configurable."""
43 if self.at_start_of_line:
44 self.write(' '*self.level)
48 self.at_start_of_line = True
51 self.at_start_of_line = False
52 def tagged_lines(self, tag, lines):
55 self.single_line(tag + line)
57 def write_line(self, line):
58 """Write one line of text on a lines of its own, not
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
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
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):
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)
90 """Start a long-running operation."""
91 self.__out.single_line('%s ... ' % msg, print_newline = False)
93 def done(self, extramsg = None):
94 """Finish long-running operation."""
97 msg = 'done (%s)' % extramsg
100 self.__out.single_line(msg, need_newline = False)
102 out = MessagePrinter()
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)
110 def read_string(filename, multiline = False):
111 """Reads the first line from a file
113 f = file(filename, 'r')
117 result = f.readline().strip()
121 def write_string(filename, line, multiline = False):
122 """Writes 'line' to file and truncates it
124 f = mkdir_file(filename, 'w+')
131 def append_strings(filename, lines):
132 """Appends 'lines' sequence to file
134 f = mkdir_file(filename, 'a+')
139 def append_string(filename, line):
140 """Appends 'line' to file
142 f = mkdir_file(filename, 'a+')
146 def insert_string(filename, line):
147 """Inserts 'line' at the beginning of the file
149 f = mkdir_file(filename, 'r+')
150 lines = f.readlines()
151 f.seek(0); f.truncate()
156 def create_empty_file(name):
157 """Creates an empty file
159 mkdir_file(name, 'w+').close()
161 def list_files_and_dirs(path):
162 """Return the sets of filenames and directory names in a
165 for fd in os.listdir(path):
166 full_fd = os.path.join(path, fd)
167 if os.path.isfile(full_fd):
169 elif os.path.isdir(full_fd):
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
181 subdir = subdirs.pop()
182 files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
184 subdirs.append(os.path.join(subdir, d))
185 yield subdir, files, dirs
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):]
193 def strip_suffix(suffix, string):
194 """Return string, without the suffix. Blow up if string doesn't
196 assert string.endswith(suffix)
197 return string[:-len(suffix)]
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))
205 os.removedirs(os.path.join(basedir, os.path.dirname(file)))
207 # file's parent dir may not be empty after removal
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))
217 if e.errno != errno.EEXIST:
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
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)
228 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
230 # file1's parent dir may not be empty after move
233 class EditorException(Exception):
236 def call_editor(filename):
237 """Run the editor on the specified filename."""
240 editor = config.get('stgit.editor')
243 elif 'EDITOR' in os.environ:
244 editor = os.environ['EDITOR']
247 editor += ' %s' % filename
249 out.start('Invoking the editor: "%s"' % editor)
250 err = os.system(editor)
252 raise EditorException, 'editor failed, exit code: %d' % err
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."""
262 subject_line = msg.split('\n', 1)[0].lstrip().lower()
263 return re.sub('[\W]+', '-', subject_line).strip('-')[:30]
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)
271 patchname = default_name
272 if unacceptable(patchname):
274 while unacceptable('%s-%d' % (patchname, suffix)):
276 patchname = '%s-%d' % (patchname, suffix)