Add --sign and --ack options to "stg import"
[stgit] / stgit / utils.py
1 """Common utility functions
2 """
3
4 import errno, optparse, os, os.path, re, sys
5 from stgit.config import config
6 from stgit.out import *
7
8 __copyright__ = """
9 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
10
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License version 2 as
13 published by the Free Software Foundation.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 """
24
25 def mkdir_file(filename, mode):
26 """Opens filename with the given mode, creating the directory it's
27 in if it doesn't already exist."""
28 create_dirs(os.path.dirname(filename))
29 return file(filename, mode)
30
31 def read_strings(filename):
32 """Reads the lines from a file
33 """
34 f = file(filename, 'r')
35 lines = [line.strip() for line in f.readlines()]
36 f.close()
37 return lines
38
39 def read_string(filename, multiline = False):
40 """Reads the first line from a file
41 """
42 f = file(filename, 'r')
43 if multiline:
44 result = f.read()
45 else:
46 result = f.readline().strip()
47 f.close()
48 return result
49
50 def write_strings(filename, lines):
51 """Write 'lines' sequence to file
52 """
53 f = file(filename, 'w+')
54 f.writelines([line + '\n' for line in lines])
55 f.close()
56
57 def write_string(filename, line, multiline = False):
58 """Writes 'line' to file and truncates it
59 """
60 f = mkdir_file(filename, 'w+')
61 if multiline:
62 f.write(line)
63 else:
64 print >> f, line
65 f.close()
66
67 def append_strings(filename, lines):
68 """Appends 'lines' sequence to file
69 """
70 f = mkdir_file(filename, 'a+')
71 for line in lines:
72 print >> f, line
73 f.close()
74
75 def append_string(filename, line):
76 """Appends 'line' to file
77 """
78 f = mkdir_file(filename, 'a+')
79 print >> f, line
80 f.close()
81
82 def insert_string(filename, line):
83 """Inserts 'line' at the beginning of the file
84 """
85 f = mkdir_file(filename, 'r+')
86 lines = f.readlines()
87 f.seek(0); f.truncate()
88 print >> f, line
89 f.writelines(lines)
90 f.close()
91
92 def create_empty_file(name):
93 """Creates an empty file
94 """
95 mkdir_file(name, 'w+').close()
96
97 def list_files_and_dirs(path):
98 """Return the sets of filenames and directory names in a
99 directory."""
100 files, dirs = [], []
101 for fd in os.listdir(path):
102 full_fd = os.path.join(path, fd)
103 if os.path.isfile(full_fd):
104 files.append(fd)
105 elif os.path.isdir(full_fd):
106 dirs.append(fd)
107 return files, dirs
108
109 def walk_tree(basedir):
110 """Starting in the given directory, iterate through all its
111 subdirectories. For each subdirectory, yield the name of the
112 subdirectory (relative to the base directory), the list of
113 filenames in the subdirectory, and the list of directory names in
114 the subdirectory."""
115 subdirs = ['']
116 while subdirs:
117 subdir = subdirs.pop()
118 files, dirs = list_files_and_dirs(os.path.join(basedir, subdir))
119 for d in dirs:
120 subdirs.append(os.path.join(subdir, d))
121 yield subdir, files, dirs
122
123 def strip_prefix(prefix, string):
124 """Return string, without the prefix. Blow up if string doesn't
125 start with prefix."""
126 assert string.startswith(prefix)
127 return string[len(prefix):]
128
129 def strip_suffix(suffix, string):
130 """Return string, without the suffix. Blow up if string doesn't
131 end with suffix."""
132 assert string.endswith(suffix)
133 return string[:-len(suffix)]
134
135 def remove_file_and_dirs(basedir, file):
136 """Remove join(basedir, file), and then remove the directory it
137 was in if empty, and try the same with its parent, until we find a
138 nonempty directory or reach basedir."""
139 os.remove(os.path.join(basedir, file))
140 try:
141 os.removedirs(os.path.join(basedir, os.path.dirname(file)))
142 except OSError:
143 # file's parent dir may not be empty after removal
144 pass
145
146 def create_dirs(directory):
147 """Create the given directory, if the path doesn't already exist."""
148 if directory and not os.path.isdir(directory):
149 create_dirs(os.path.dirname(directory))
150 try:
151 os.mkdir(directory)
152 except OSError, e:
153 if e.errno != errno.EEXIST:
154 raise e
155
156 def rename(basedir, file1, file2):
157 """Rename join(basedir, file1) to join(basedir, file2), not
158 leaving any empty directories behind and creating any directories
159 necessary."""
160 full_file2 = os.path.join(basedir, file2)
161 create_dirs(os.path.dirname(full_file2))
162 os.rename(os.path.join(basedir, file1), full_file2)
163 try:
164 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
165 except OSError:
166 # file1's parent dir may not be empty after move
167 pass
168
169 class EditorException(Exception):
170 pass
171
172 def call_editor(filename):
173 """Run the editor on the specified filename."""
174
175 # the editor
176 editor = config.get('stgit.editor')
177 if editor:
178 pass
179 elif 'EDITOR' in os.environ:
180 editor = os.environ['EDITOR']
181 else:
182 editor = 'vi'
183 editor += ' %s' % filename
184
185 out.start('Invoking the editor: "%s"' % editor)
186 err = os.system(editor)
187 if err:
188 raise EditorException, 'editor failed, exit code: %d' % err
189 out.done()
190
191 def patch_name_from_msg(msg):
192 """Return a string to be used as a patch name. This is generated
193 from the top line of the string passed as argument."""
194 if not msg:
195 return None
196
197 name_len = config.get('stgit.namelength')
198 if not name_len:
199 name_len = 30
200
201 subject_line = msg.split('\n', 1)[0].lstrip().lower()
202 return re.sub('[\W]+', '-', subject_line).strip('-')[:name_len]
203
204 def make_patch_name(msg, unacceptable, default_name = 'patch'):
205 """Return a patch name generated from the given commit message,
206 guaranteed to make unacceptable(name) be false. If the commit
207 message is empty, base the name on default_name instead."""
208 patchname = patch_name_from_msg(msg)
209 if not patchname:
210 patchname = default_name
211 if unacceptable(patchname):
212 suffix = 0
213 while unacceptable('%s-%d' % (patchname, suffix)):
214 suffix += 1
215 patchname = '%s-%d' % (patchname, suffix)
216 return patchname
217
218 # any and all functions are builtin in Python 2.5 and higher, but not
219 # in 2.4.
220 if not 'any' in dir(__builtins__):
221 def any(bools):
222 for b in bools:
223 if b:
224 return True
225 return False
226 if not 'all' in dir(__builtins__):
227 def all(bools):
228 for b in bools:
229 if not b:
230 return False
231 return True
232
233 def make_sign_options():
234 def callback(option, opt_str, value, parser, sign_str):
235 if parser.values.sign_str not in [None, sign_str]:
236 raise optparse.OptionValueError(
237 '--ack and --sign were both specified')
238 parser.values.sign_str = sign_str
239 return [optparse.make_option('--sign', action = 'callback',
240 callback = callback, dest = 'sign_str',
241 callback_args = ('Signed-off-by',),
242 help = 'add Signed-off-by line'),
243 optparse.make_option('--ack', action = 'callback',
244 callback = callback, dest = 'sign_str',
245 callback_args = ('Acked-by',),
246 help = 'add Acked-by line')]
247
248 def add_sign_line(desc, sign_str, name, email):
249 if not sign_str:
250 return desc
251 sign_str = '%s: %s <%s>' % (sign_str, name, email)
252 if sign_str in desc:
253 return desc
254 desc = desc.rstrip()
255 if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']):
256 desc = desc + '\n'
257 return '%s\n%s\n' % (desc, sign_str)