Repair `stg log` with patches from subdir
[stgit.git] / stgit / utils.py
blobb75dd60bfe089bec49bb8aa99b516e136b09b14a
1 """Common utility functions"""
3 import errno
4 import os
5 import re
6 import tempfile
7 from io import open
9 from stgit.compat import environ_get
10 from stgit.config import config
11 from stgit.exception import StgException
12 from stgit.out import out
13 from stgit.run import Run
15 __copyright__ = """
16 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
18 This program is free software; you can redistribute it and/or modify
19 it under the terms of the GNU General Public License version 2 as
20 published by the Free Software Foundation.
22 This program is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
27 You should have received a copy of the GNU General Public License
28 along with this program; if not, see http://www.gnu.org/licenses/.
29 """
32 def mkdir_file(filename, mode, encoding='utf-8'):
33 """Opens filename with the given mode, creating the directory it's
34 in if it doesn't already exist."""
35 create_dirs(os.path.dirname(filename))
36 return open(filename, mode, encoding=encoding)
39 def read_strings(filename, encoding='utf-8'):
40 """Reads the lines from a file"""
41 with open(filename, encoding=encoding) as f:
42 return [line.strip() for line in f.readlines()]
45 def read_string(filename, encoding='utf-8'):
46 """Reads the first line from a file"""
47 with open(filename, encoding=encoding) as f:
48 return f.readline().strip()
51 def write_strings(filename, lines, encoding='utf-8'):
52 """Write 'lines' sequence to file"""
53 with open(filename, 'w+', encoding=encoding) as f:
54 for line in lines:
55 print(line, file=f)
58 def write_string(filename, line, multiline=False, encoding='utf-8'):
59 """Writes 'line' to file and truncates it"""
60 with mkdir_file(filename, 'w+', encoding) as f:
61 print(line, end='' if multiline else '\n', file=f)
64 def create_empty_file(name):
65 """Creates an empty file"""
66 mkdir_file(name, 'w+').close()
69 def strip_prefix(prefix, string):
70 """Return string, without the prefix. Blow up if string doesn't
71 start with prefix."""
72 assert string.startswith(prefix)
73 return string[len(prefix) :]
76 def create_dirs(directory):
77 """Create the given directory, if the path doesn't already exist."""
78 if directory and not os.path.isdir(directory):
79 create_dirs(os.path.dirname(directory))
80 try:
81 os.mkdir(directory)
82 except OSError as e:
83 if e.errno != errno.EEXIST:
84 raise e
87 def rename(basedir, file1, file2):
88 """Rename join(basedir, file1) to join(basedir, file2), not
89 leaving any empty directories behind and creating any directories
90 necessary."""
91 full_file2 = os.path.join(basedir, file2)
92 create_dirs(os.path.dirname(full_file2))
93 os.rename(os.path.join(basedir, file1), full_file2)
94 try:
95 os.removedirs(os.path.join(basedir, os.path.dirname(file1)))
96 except OSError:
97 # file1's parent dir may not be empty after move
98 pass
101 class EditorException(StgException):
102 pass
105 def get_editor():
106 for editor in [
107 environ_get('GIT_EDITOR'),
108 config.get('stgit.editor'), # legacy
109 config.get('core.editor'),
110 environ_get('VISUAL'),
111 environ_get('EDITOR'),
112 'vi',
114 if editor:
115 return editor
118 def call_editor(filename):
119 """Run the editor on the specified filename."""
120 cmd = '%s %s' % (get_editor(), filename)
121 out.start('Invoking the editor: "%s"' % cmd)
122 err = os.system(cmd)
123 if err:
124 raise EditorException('editor failed, exit code: %d' % err)
125 out.done()
128 def get_hook(repository, hook_name, extra_env={}):
129 hook_path = os.path.join(repository.directory, 'hooks', hook_name)
130 if not (os.path.isfile(hook_path) and os.access(hook_path, os.X_OK)):
131 return None
133 default_iw = repository.default_iw
134 prefix_dir = os.path.relpath(os.getcwd(), default_iw.cwd)
135 if prefix_dir == os.curdir:
136 prefix = ''
137 else:
138 prefix = os.path.join(prefix_dir, '')
139 extra_env = add_dict(extra_env, {'GIT_PREFIX': prefix})
141 def hook(*parameters):
142 argv = [hook_path]
143 argv.extend(parameters)
145 # On Windows, run the hook using "bash" explicitly
146 if os.name != 'posix':
147 argv.insert(0, 'bash')
149 default_iw.run(argv, extra_env).run()
151 hook.__name__ = str(hook_name)
152 return hook
155 def run_hook_on_bytes(hook, byte_data, *args):
156 temp = tempfile.NamedTemporaryFile('wb', prefix='stgit-hook', delete=False)
157 try:
158 with temp:
159 temp.write(byte_data)
160 hook(temp.name)
161 with open(temp.name, 'rb') as data_file:
162 return data_file.read()
163 finally:
164 os.unlink(temp.name)
167 def edit_string(s, filename, encoding='utf-8'):
168 with open(filename, 'w', encoding=encoding) as f:
169 f.write(s)
170 call_editor(filename)
171 with open(filename, encoding=encoding) as f:
172 s = f.read()
173 os.remove(filename)
174 return s
177 def edit_bytes(s, filename):
178 with open(filename, 'wb') as f:
179 f.write(s)
180 call_editor(filename)
181 with open(filename, 'rb') as f:
182 s = f.read()
183 os.remove(filename)
184 return s
187 def find_patch_name(patchname, unacceptable):
188 """Find a patch name which is acceptable."""
189 if unacceptable(patchname):
190 suffix = 0
191 while unacceptable('%s-%d' % (patchname, suffix)):
192 suffix += 1
193 patchname = '%s-%d' % (patchname, suffix)
194 return patchname
197 def patch_name_from_msg(msg):
198 """Return a string to be used as a patch name. This is generated
199 from the top line of the string passed as argument."""
200 if not msg:
201 return None
203 name_len = config.getint('stgit.namelength')
204 if not name_len:
205 name_len = 30
207 subject_line = msg.split('\n', 1)[0].lstrip().lower()
208 words = re.sub(r'(?u)[\W]+', ' ', subject_line).split()
210 # use loop to avoid truncating the last name
211 name = words and words[0] or 'unknown'
212 for word in words[1:]:
213 new = name + '-' + word
214 if len(new) > name_len:
215 break
216 name = new
218 return name
221 def make_patch_name(msg, unacceptable, default_name='patch'):
222 """Return a patch name generated from the given commit message,
223 guaranteed to make unacceptable(name) be false. If the commit
224 message is empty, base the name on default_name instead."""
225 patchname = patch_name_from_msg(msg)
226 if not patchname:
227 patchname = default_name
228 return find_patch_name(patchname, unacceptable)
231 def add_trailer(message, trailer, name, email):
232 trailer_line = '%s: %s <%s>' % (trailer, name, email)
233 return (
234 Run('git', 'interpret-trailers', '--trailer', trailer_line)
235 .raw_input(message)
236 .raw_output()
240 def parse_name_email(address):
241 """Return a tuple consisting of the name and email parsed from a
242 standard 'name <email>' or 'email (name)' string."""
243 address = re.sub(r'[\\"]', r'\\\g<0>', address)
244 str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
245 if not str_list:
246 str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
247 if not str_list:
248 return None
249 return (str_list[0][1], str_list[0][0])
250 return str_list[0]
253 # Exit codes.
254 STGIT_SUCCESS = 0 # everything's OK
255 STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error
256 STGIT_COMMAND_ERROR = 2 # seems to be a command that failed
257 STGIT_CONFLICT = 3 # merge conflict, otherwise OK
258 STGIT_BUG_ERROR = 4 # a bug in StGit
261 def add_dict(d1, d2):
262 """Return a new dict with the contents of both d1 and d2. In case of
263 conflicting mappings, d2 takes precedence."""
264 d = dict(d1)
265 d.update(d2)
266 return d