| 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 |