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_strings(filename
):
111 """Reads the lines from a file
113 f
= file(filename
, 'r')
114 lines
= [line
.strip() for line
in f
.readlines()]
118 def read_string(filename
, multiline
= False):
119 """Reads the first line from a file
121 f
= file(filename
, 'r')
125 result
= f
.readline().strip()
129 def write_strings(filename
, lines
):
130 """Write 'lines' sequence to file
132 f
= file(filename
, 'w+')
133 f
.writelines([line
+ '\n' for line
in lines
])
136 def write_string(filename
, line
, multiline
= False):
137 """Writes 'line' to file and truncates it
139 f
= mkdir_file(filename
, 'w+')
146 def append_strings(filename
, lines
):
147 """Appends 'lines' sequence to file
149 f
= mkdir_file(filename
, 'a+')
154 def append_string(filename
, line
):
155 """Appends 'line' to file
157 f
= mkdir_file(filename
, 'a+')
161 def insert_string(filename
, line
):
162 """Inserts 'line' at the beginning of the file
164 f
= mkdir_file(filename
, 'r+')
165 lines
= f
.readlines()
166 f
.seek(0); f
.truncate()
171 def create_empty_file(name
):
172 """Creates an empty file
174 mkdir_file(name
, 'w+').close()
176 def list_files_and_dirs(path
):
177 """Return the sets of filenames and directory names in a
180 for fd
in os
.listdir(path
):
181 full_fd
= os
.path
.join(path
, fd
)
182 if os
.path
.isfile(full_fd
):
184 elif os
.path
.isdir(full_fd
):
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
196 subdir
= subdirs
.pop()
197 files
, dirs
= list_files_and_dirs(os
.path
.join(basedir
, subdir
))
199 subdirs
.append(os
.path
.join(subdir
, d
))
200 yield subdir
, files
, dirs
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
):]
208 def strip_suffix(suffix
, string
):
209 """Return string, without the suffix. Blow up if string doesn't
211 assert string
.endswith(suffix
)
212 return string
[:-len(suffix
)]
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))
220 os
.removedirs(os
.path
.join(basedir
, os
.path
.dirname(file)))
222 # file's parent dir may not be empty after removal
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
))
232 if e
.errno
!= errno
.EEXIST
:
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
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
)
243 os
.removedirs(os
.path
.join(basedir
, os
.path
.dirname(file1
)))
245 # file1's parent dir may not be empty after move
248 class EditorException(Exception):
251 def call_editor(filename
):
252 """Run the editor on the specified filename."""
255 editor
= config
.get('stgit.editor')
258 elif 'EDITOR' in os
.environ
:
259 editor
= os
.environ
['EDITOR']
262 editor
+= ' %s' % filename
264 out
.start('Invoking the editor: "%s"' % editor
)
265 err
= os
.system(editor
)
267 raise EditorException
, 'editor failed, exit code: %d' % err
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."""
277 subject_line
= msg
.split('\n', 1)[0].lstrip().lower()
278 return re
.sub('[\W]+', '-', subject_line
).strip('-')[:30]
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
)
286 patchname
= default_name
287 if unacceptable(patchname
):
289 while unacceptable('%s-%d' % (patchname
, suffix
)):
291 patchname
= '%s-%d' % (patchname
, suffix
)