Use reStructuredText-style docstrings
[stgit.git] / stgit / utils.py
bloba63056eca242c9f0332065d01f8b6d309474cfc4
1 """Common utility functions"""
3 import os
4 import re
5 import tempfile
6 from io import open
8 from stgit.compat import environ_get
9 from stgit.config import config
10 from stgit.exception import StgException
11 from stgit.out import out
12 from stgit.run import Run
14 __copyright__ = """
15 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
17 This program is free software; you can redistribute it and/or modify
18 it under the terms of the GNU General Public License version 2 as
19 published by the Free Software Foundation.
21 This program is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 GNU General Public License for more details.
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, see http://www.gnu.org/licenses/.
28 """
31 def strip_prefix(prefix, string):
32 """Return string, without the specified prefix.
34 The string must start with the prefix.
36 """
37 assert string.startswith(prefix)
38 return string[len(prefix) :]
41 class EditorException(StgException):
42 pass
45 def get_editor():
46 for editor in [
47 environ_get('GIT_EDITOR'),
48 config.get('stgit.editor'), # legacy
49 config.get('core.editor'),
50 environ_get('VISUAL'),
51 environ_get('EDITOR'),
52 'vi',
54 if editor:
55 return editor
58 def call_editor(filename):
59 """Run the editor on the specified filename."""
60 cmd = '%s %s' % (get_editor(), filename)
61 out.start('Invoking the editor: "%s"' % cmd)
62 err = os.system(cmd)
63 if err:
64 raise EditorException('editor failed, exit code: %d' % err)
65 out.done()
68 def get_hook(repository, hook_name, extra_env={}):
69 hook_path = os.path.join(repository.directory, 'hooks', hook_name)
70 if not (os.path.isfile(hook_path) and os.access(hook_path, os.X_OK)):
71 return None
73 default_iw = repository.default_iw
74 prefix_dir = os.path.relpath(os.getcwd(), default_iw.cwd)
75 if prefix_dir == os.curdir:
76 prefix = ''
77 else:
78 prefix = os.path.join(prefix_dir, '')
79 extra_env = add_dict(extra_env, {'GIT_PREFIX': prefix})
81 def hook(*parameters):
82 argv = [hook_path]
83 argv.extend(parameters)
85 # On Windows, run the hook using "bash" explicitly
86 if os.name != 'posix':
87 argv.insert(0, 'bash')
89 default_iw.run(argv, extra_env).run()
91 hook.__name__ = str(hook_name)
92 return hook
95 def run_hook_on_bytes(hook, byte_data, *args):
96 temp = tempfile.NamedTemporaryFile('wb', prefix='stgit-hook', delete=False)
97 try:
98 with temp:
99 temp.write(byte_data)
100 hook(temp.name)
101 with open(temp.name, 'rb') as data_file:
102 return data_file.read()
103 finally:
104 os.unlink(temp.name)
107 def edit_string(s, filename, encoding='utf-8'):
108 with open(filename, 'w', encoding=encoding) as f:
109 f.write(s)
110 call_editor(filename)
111 with open(filename, encoding=encoding) as f:
112 s = f.read()
113 os.remove(filename)
114 return s
117 def edit_bytes(s, filename):
118 with open(filename, 'wb') as f:
119 f.write(s)
120 call_editor(filename)
121 with open(filename, 'rb') as f:
122 s = f.read()
123 os.remove(filename)
124 return s
127 def find_patch_name(patchname, unacceptable):
128 """Find a patch name which is acceptable."""
129 if unacceptable(patchname):
130 suffix = 0
131 while unacceptable('%s-%d' % (patchname, suffix)):
132 suffix += 1
133 patchname = '%s-%d' % (patchname, suffix)
134 return patchname
137 def patch_name_from_msg(msg):
138 """Return a string to be used as a patch name.
140 This is generated from the first line of the specified message string.
143 if not msg:
144 return None
146 name_len = config.getint('stgit.namelength')
147 if not name_len:
148 name_len = 30
150 subject_line = msg.split('\n', 1)[0].lstrip().lower()
151 words = re.sub(r'(?u)[\W]+', ' ', subject_line).split()
153 # use loop to avoid truncating the last name
154 name = words and words[0] or 'unknown'
155 for word in words[1:]:
156 new = name + '-' + word
157 if len(new) > name_len:
158 break
159 name = new
161 return name
164 def make_patch_name(msg, unacceptable, default_name='patch'):
165 """Generate a patch name from the given commit message.
167 The generated name is guaranteed to make unacceptable(name) be false. If the commit
168 message is empty, base the name on default_name instead.
171 patchname = patch_name_from_msg(msg)
172 if not patchname:
173 patchname = default_name
174 return find_patch_name(patchname, unacceptable)
177 def add_trailer(message, trailer, name, email):
178 trailer_line = '%s: %s <%s>' % (trailer, name, email)
179 return (
180 Run('git', 'interpret-trailers', '--trailer', trailer_line)
181 .raw_input(message)
182 .raw_output()
186 def parse_name_email(address):
187 """Parse an email address string.
189 Returns a tuple consisting of the name and email parsed from a
190 standard 'name <email>' or 'email (name)' string.
193 address = re.sub(r'[\\"]', r'\\\g<0>', address)
194 str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
195 if not str_list:
196 str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
197 if not str_list:
198 return None
199 return (str_list[0][1], str_list[0][0])
200 return str_list[0]
203 # Exit codes.
204 STGIT_SUCCESS = 0 # everything's OK
205 STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error
206 STGIT_COMMAND_ERROR = 2 # seems to be a command that failed
207 STGIT_CONFLICT = 3 # merge conflict, otherwise OK
208 STGIT_BUG_ERROR = 4 # a bug in StGit
211 def add_dict(d1, d2):
212 """Return a new dict with the contents of both d1 and d2.
214 In case of conflicting mappings, d2 takes precedence.
217 d = dict(d1)
218 d.update(d2)
219 return d