1 # -*- coding: utf-8 -*-
2 """Common utility functions"""
4 from __future__
import absolute_import
, division
, print_function
10 from stgit
.config
import config
11 from stgit
.exception
import StgException
12 from stgit
.out
import out
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/.
31 def mkdir_file(filename
, mode
):
32 """Opens filename with the given mode, creating the directory it's
33 in if it doesn't already exist."""
34 create_dirs(os
.path
.dirname(filename
))
35 return open(filename
, mode
)
37 def read_strings(filename
):
38 """Reads the lines from a file
40 with
open(filename
) as f
:
41 return [line
.strip() for line
in f
.readlines()]
43 def read_string(filename
, multiline
= False):
44 """Reads the first line from a file
46 with
open(filename
) as f
:
50 return f
.readline().strip()
52 def write_strings(filename
, lines
):
53 """Write 'lines' sequence to file
55 with
open(filename
, 'w+') as f
:
56 f
.writelines([line
+ '\n' for line
in lines
])
58 def write_string(filename
, line
, multiline
= False):
59 """Writes 'line' to file and truncates it
61 with
mkdir_file(filename
, 'w+') as f
:
63 end
='' if multiline
else '\n',
66 def append_strings(filename
, lines
):
67 """Appends 'lines' sequence to file
69 with
mkdir_file(filename
, 'a+') as f
:
73 def append_string(filename
, line
):
74 """Appends 'line' to file
76 with
mkdir_file(filename
, 'a+') as f
:
79 def insert_string(filename
, line
):
80 """Inserts 'line' at the beginning of the file
82 with
mkdir_file(filename
, 'r+') as f
:
89 def create_empty_file(name
):
90 """Creates an empty file
92 mkdir_file(name
, 'w+').close()
94 def list_files_and_dirs(path
):
95 """Return the sets of filenames and directory names in a
98 for fd
in os
.listdir(path
):
99 full_fd
= os
.path
.join(path
, fd
)
100 if os
.path
.isfile(full_fd
):
102 elif os
.path
.isdir(full_fd
):
106 def walk_tree(basedir
):
107 """Starting in the given directory, iterate through all its
108 subdirectories. For each subdirectory, yield the name of the
109 subdirectory (relative to the base directory), the list of
110 filenames in the subdirectory, and the list of directory names in
114 subdir
= subdirs
.pop()
115 files
, dirs
= list_files_and_dirs(os
.path
.join(basedir
, subdir
))
117 subdirs
.append(os
.path
.join(subdir
, d
))
118 yield subdir
, files
, dirs
120 def strip_prefix(prefix
, string
):
121 """Return string, without the prefix. Blow up if string doesn't
122 start with prefix."""
123 assert string
.startswith(prefix
)
124 return string
[len(prefix
):]
126 def strip_suffix(suffix
, string
):
127 """Return string, without the suffix. Blow up if string doesn't
129 assert string
.endswith(suffix
)
130 return string
[:-len(suffix
)]
132 def remove_file_and_dirs(basedir
, file):
133 """Remove join(basedir, file), and then remove the directory it
134 was in if empty, and try the same with its parent, until we find a
135 nonempty directory or reach basedir."""
136 os
.remove(os
.path
.join(basedir
, file))
138 os
.removedirs(os
.path
.join(basedir
, os
.path
.dirname(file)))
140 # file's parent dir may not be empty after removal
143 def create_dirs(directory
):
144 """Create the given directory, if the path doesn't already exist."""
145 if directory
and not os
.path
.isdir(directory
):
146 create_dirs(os
.path
.dirname(directory
))
150 if e
.errno
!= errno
.EEXIST
:
153 def rename(basedir
, file1
, file2
):
154 """Rename join(basedir, file1) to join(basedir, file2), not
155 leaving any empty directories behind and creating any directories
157 full_file2
= os
.path
.join(basedir
, file2
)
158 create_dirs(os
.path
.dirname(full_file2
))
159 os
.rename(os
.path
.join(basedir
, file1
), full_file2
)
161 os
.removedirs(os
.path
.join(basedir
, os
.path
.dirname(file1
)))
163 # file1's parent dir may not be empty after move
166 class EditorException(StgException
):
170 for editor
in [os
.environ
.get('GIT_EDITOR'),
171 config
.get('stgit.editor'), # legacy
172 config
.get('core.editor'),
173 os
.environ
.get('VISUAL'),
174 os
.environ
.get('EDITOR'),
179 def call_editor(filename
):
180 """Run the editor on the specified filename."""
181 cmd
= '%s %s' % (get_editor(), filename
)
182 out
.start('Invoking the editor: "%s"' % cmd
)
185 raise EditorException('editor failed, exit code: %d' % err
)
188 def get_hook(repository
, hook_name
, extra_env
={}):
189 hook_path
= os
.path
.join(repository
.directory
, 'hooks', hook_name
)
190 if not (os
.path
.isfile(hook_path
) and os
.access(hook_path
, os
.X_OK
)):
193 default_iw
= repository
.default_iw
194 prefix_dir
= os
.path
.relpath(os
.getcwd(), default_iw
.cwd
)
195 if prefix_dir
== os
.curdir
:
198 prefix
= os
.path
.join(prefix_dir
, '')
199 extra_env
= add_dict(extra_env
, {'GIT_PREFIX': prefix
})
201 def hook(*parameters
):
203 argv
.extend(parameters
)
205 # On Windows, run the hook using "bash" explicitly
206 if os
.name
!= 'posix':
207 argv
.insert(0, 'bash')
209 default_iw
.run(argv
, extra_env
).run()
211 hook
.__name
__ = hook_name
214 def run_hook_on_string(hook
, s
, *args
):
216 temp
= tempfile
.NamedTemporaryFile('wb', delete
=False)
223 hook(temp
.name
, *args
)
225 output
= open(temp
.name
, 'rb')
235 def edit_string(s
, filename
):
236 with
open(filename
, 'w') as f
:
238 call_editor(filename
)
239 with
open(filename
) as f
:
244 def append_comment(s
, comment
, separator
= '---'):
245 return ('%s\n\n%s\nEverything following the line with "%s" will be'
246 ' ignored\n\n%s' % (s
, separator
, separator
, comment
))
248 def strip_comment(s
, separator
= '---'):
250 return s
[:s
.index('\n%s\n' % separator
)]
254 def find_patch_name(patchname
, unacceptable
):
255 """Find a patch name which is acceptable."""
256 if unacceptable(patchname
):
258 while unacceptable('%s-%d' % (patchname
, suffix
)):
260 patchname
= '%s-%d' % (patchname
, suffix
)
263 def patch_name_from_msg(msg
):
264 """Return a string to be used as a patch name. This is generated
265 from the top line of the string passed as argument."""
269 name_len
= config
.getint('stgit.namelength')
273 subject_line
= msg
.split('\n', 1)[0].lstrip().lower()
274 words
= re
.sub(r
'[\W]+', ' ', subject_line
).split()
276 # use loop to avoid truncating the last name
277 name
= words
and words
[0] or 'unknown'
278 for word
in words
[1:]:
279 new
= name
+ '-' + word
280 if len(new
) > name_len
:
286 def make_patch_name(msg
, unacceptable
, default_name
= 'patch'):
287 """Return a patch name generated from the given commit message,
288 guaranteed to make unacceptable(name) be false. If the commit
289 message is empty, base the name on default_name instead."""
290 patchname
= patch_name_from_msg(msg
)
292 patchname
= default_name
293 return find_patch_name(patchname
, unacceptable
)
296 def add_sign_line(desc
, sign_str
, name
, email
):
299 sign_str
= '%s: %s <%s>' % (sign_str
, name
, email
)
303 preamble
, lastblank
, lastpara
= desc
.rpartition('\n\n')
304 is_signoff
= re
.compile(r
'[A-Z][a-z]*(-[A-Za-z][a-z]*)*: ').match
305 if not (lastblank
and all(is_signoff(l
) for l
in lastpara
.split('\n'))):
307 return '%s\n%s\n' % (desc
, sign_str
)
309 def parse_name_email(address
):
310 """Return a tuple consisting of the name and email parsed from a
311 standard 'name <email>' or 'email (name)' string."""
312 address
= re
.sub(r
'[\\"]', r
'\\\g<0>', address
)
313 str_list
= re
.findall(r
'^(.*)\s*<(.*)>\s*$', address
)
315 str_list
= re
.findall(r
'^(.*)\s*\((.*)\)\s*$', address
)
318 return (str_list
[0][1], str_list
[0][0])
321 def parse_name_email_date(address
):
322 """Return a tuple consisting of the name, email and date parsed
323 from a 'name <email> date' string."""
324 address
= re
.sub(r
'[\\"]', r
'\\\g<0>', address
)
325 str_list
= re
.findall(r
'^(.*)\s*<(.*)>\s*(.*)\s*$', address
)
331 STGIT_SUCCESS
= 0 # everything's OK
332 STGIT_GENERAL_ERROR
= 1 # seems to be non-command-specific error
333 STGIT_COMMAND_ERROR
= 2 # seems to be a command that failed
334 STGIT_CONFLICT
= 3 # merge conflict, otherwise OK
335 STGIT_BUG_ERROR
= 4 # a bug in StGit
337 def add_dict(d1
, d2
):
338 """Return a new dict with the contents of both d1 and d2. In case of
339 conflicting mappings, d2 takes precedence."""