stg import now extracts Message-ID header
[stgit.git] / stgit / utils.py
blob095f48c2b2eed5ec8f09e1030120299507960c05
1 """Common utility functions"""
3 import os
4 import re
5 import shutil
6 import sys
7 import tempfile
8 from io import open
10 from stgit.compat import environ_get
11 from stgit.config import config
12 from stgit.exception import StgException
13 from stgit.out import out
14 from stgit.run import Run
16 __copyright__ = """
17 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
19 This program is free software; you can redistribute it and/or modify
20 it under the terms of the GNU General Public License version 2 as
21 published by the Free Software Foundation.
23 This program is distributed in the hope that it will be useful,
24 but WITHOUT ANY WARRANTY; without even the implied warranty of
25 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 GNU General Public License for more details.
28 You should have received a copy of the GNU General Public License
29 along with this program; if not, see http://www.gnu.org/licenses/.
30 """
33 def strip_prefix(prefix, string):
34 """Return string, without the specified prefix.
36 The string must start with the prefix.
38 """
39 assert string.startswith(prefix)
40 return string[len(prefix) :]
43 class EditorException(StgException):
44 pass
47 def get_editor():
48 for editor in [
49 environ_get('GIT_EDITOR'),
50 config.get('stgit.editor'), # legacy
51 config.get('core.editor'),
52 environ_get('VISUAL'),
53 environ_get('EDITOR'),
54 'vi',
56 if editor:
57 return editor
60 def call_editor(filename):
61 """Run the editor on the specified filename."""
62 cmd = '%s %s' % (get_editor(), filename)
63 out.start('Invoking the editor: "%s"' % cmd)
64 err = os.system(cmd)
65 if err:
66 raise EditorException('editor failed, exit code: %d' % err)
67 out.done()
70 def get_hooks_path(repository):
71 hooks_path = config.get('core.hookspath')
72 if hooks_path is None:
73 return os.path.join(repository.directory, 'hooks')
74 elif os.path.isabs(hooks_path):
75 return hooks_path
76 else:
77 return os.path.join(repository.default_worktree.directory, hooks_path)
80 def get_hook(repository, hook_name, extra_env=None):
81 if not extra_env:
82 extra_env = {}
83 hook_path = os.path.join(get_hooks_path(repository), hook_name)
84 if not (os.path.isfile(hook_path) and os.access(hook_path, os.X_OK)):
85 return None
87 prefix_dir = os.path.relpath(os.getcwd(), repository.default_worktree.directory)
88 if prefix_dir == os.curdir:
89 prefix = ''
90 else:
91 prefix = os.path.join(prefix_dir, '')
92 extra_env = add_dict(extra_env, {'GIT_PREFIX': prefix})
94 def hook(*parameters):
95 if sys.platform == 'win32':
96 # On Windows, run the hook using "bash" explicitly.
97 # Try to locate bash.exe in user's PATH, but avoid the WSL
98 # shim/bootstrapper %SYSTEMROOT%/System32/bash.exe
99 systemroot = os.environ.get('SYSTEMROOT')
100 if systemroot:
101 system32 = os.path.normcase(os.path.join(systemroot, 'system32'))
102 path = os.pathsep.join(
104 for p in os.environ.get('PATH', '').split(os.pathsep)
105 if os.path.normcase(p) != system32
107 else:
108 path = None
110 # Find bash with user's path (sans System32).
111 bash_exe = shutil.which('bash.exe', path=path)
112 if not bash_exe:
113 # Next try finding the bash.exe that came with Git for Windows.
114 git_exe = shutil.which('git.exe', path=path)
115 if not git_exe:
116 raise StgException('Failed to locate either bash.exe or git.exe')
117 bash_exe = os.path.join(
118 os.path.dirname(os.path.dirname(git_exe)),
119 'bin',
120 'bash.exe',
123 argv = [bash_exe, hook_path]
124 else:
125 argv = [hook_path]
127 argv.extend(parameters)
129 repository.default_iw.run(argv, extra_env).run()
131 hook.__name__ = hook_name
132 return hook
135 def run_hook_on_bytes(hook, byte_data, *args):
136 temp = tempfile.NamedTemporaryFile('wb', prefix='stgit-hook', delete=False)
137 try:
138 with temp:
139 temp.write(byte_data)
140 hook(temp.name)
141 with open(temp.name, 'rb') as data_file:
142 return data_file.read()
143 finally:
144 os.unlink(temp.name)
147 def edit_string(s, filename, encoding='utf-8'):
148 with open(filename, 'w', encoding=encoding) as f:
149 f.write(s)
150 call_editor(filename)
151 with open(filename, encoding=encoding) as f:
152 s = f.read()
153 os.remove(filename)
154 return s
157 def edit_bytes(s, filename):
158 with open(filename, 'wb') as f:
159 f.write(s)
160 call_editor(filename)
161 with open(filename, 'rb') as f:
162 s = f.read()
163 os.remove(filename)
164 return s
167 def add_trailers(message, trailers, name, email):
168 trailer_args = []
169 for trailer, user_value in trailers:
170 if user_value is None:
171 trailer_args.extend(['--trailer', '%s: %s <%s>' % (trailer, name, email)])
172 else:
173 trailer_args.extend(['--trailer', '%s: %s' % (trailer, user_value)])
174 return (
175 Run('git', 'interpret-trailers', *trailer_args).raw_input(message).raw_output()
179 def parse_name_email(address):
180 """Parse an email address string.
182 Returns a tuple consisting of the name and email parsed from a
183 standard 'name <email>' or 'email (name)' string.
186 address = re.sub(r'[\\"]', r'\\\g<0>', address)
187 str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address)
188 if not str_list:
189 str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address)
190 if not str_list:
191 return None
192 return (str_list[0][1], str_list[0][0])
193 return str_list[0]
196 # Exit codes.
197 STGIT_SUCCESS = 0 # everything's OK
198 STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error
199 STGIT_COMMAND_ERROR = 2 # seems to be a command that failed
200 STGIT_CONFLICT = 3 # merge conflict, otherwise OK
201 STGIT_BUG_ERROR = 4 # a bug in StGit
204 def add_dict(d1, d2):
205 """Return a new dict with the contents of both d1 and d2.
207 In case of conflicting mappings, d2 takes precedence.
210 d = dict(d1)
211 d.update(d2)
212 return d