1 # -*- coding: utf-8 -*-
2 """Function/variables common to all the commands"""
3 from __future__
import (absolute_import
, division
, print_function
,
11 from stgit
import stack
, git
, templates
12 from stgit
.compat
import text
, decode_utf8_with_latin1
13 from stgit
.config
import config
14 from stgit
.exception
import StgException
15 from stgit
.lib
import git
as libgit
16 from stgit
.lib
import log
17 from stgit
.lib
import stack
as libstack
18 from stgit
.out
import out
19 from stgit
.run
import Run
, RunException
20 from stgit
.utils
import (EditorException
,
24 parse_name_email_date
,
29 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
31 This program is free software; you can redistribute it and/or modify
32 it under the terms of the GNU General Public License version 2 as
33 published by the Free Software Foundation.
35 This program is distributed in the hope that it will be useful,
36 but WITHOUT ANY WARRANTY; without even the implied warranty of
37 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38 GNU General Public License for more details.
40 You should have received a copy of the GNU General Public License
41 along with this program; if not, see http://www.gnu.org/licenses/.
45 # Command exception class
46 class CmdException(StgException
):
51 """Parse a revision specification into its branch:patch parts.
54 branch
, patch
= rev
.split(':', 1)
59 return (branch
, patch
)
61 def git_id(crt_series
, rev
):
64 # TODO: remove this function once all the occurrences were converted
66 repository
= libstack
.Repository
.default()
67 return git_commit(rev
, repository
, crt_series
.get_name()).sha1
69 def get_public_ref(branch_name
):
70 """Return the public ref of the branch."""
71 public_ref
= config
.get('branch.%s.public' % branch_name
)
73 public_ref
= 'refs/heads/%s.public' % branch_name
76 def git_commit(name
, repository
, branch_name
= None):
77 """Return the a Commit object if 'name' is a patch name or Git commit.
78 The patch names allowed are in the form '<branch>:<patch>' and can
79 be followed by standard symbols used by git rev-parse. If <patch>
80 is '{base}', it represents the bottom of the stack. If <patch> is
81 {public}, it represents the public branch corresponding to the stack as
82 described in the 'publish' command.
84 # Try a [branch:]patch name first
85 branch
, patch
= parse_rev(name
)
87 branch
= branch_name
or repository
.current_branch_name
90 if patch
.startswith('{base}'):
91 base_id
= repository
.get_stack(branch
).base
.sha1
92 return repository
.rev_parse(base_id
+
93 strip_prefix('{base}', patch
))
94 elif patch
.startswith('{public}'):
95 public_ref
= get_public_ref(branch
)
96 return repository
.rev_parse(public_ref
+
97 strip_prefix('{public}', patch
),
98 discard_stderr
= True)
100 # Other combination of branch and patch
102 return repository
.rev_parse('patches/%s/%s' % (branch
, patch
),
103 discard_stderr
= True)
104 except libgit
.RepositoryException
:
109 return repository
.rev_parse(name
, discard_stderr
= True)
110 except libgit
.RepositoryException
:
111 raise CmdException('%s: Unknown patch or revision name' % name
)
113 def color_diff_flags():
114 """Return the git flags for coloured diff output if the configuration and
116 stdout_is_tty
= (sys
.stdout
.isatty() and 'true') or 'false'
117 if config
.get_colorbool('color.diff', stdout_is_tty
) == 'true':
122 def check_local_changes():
123 if git
.local_changes():
124 raise CmdException('local changes in the tree. Use "refresh" or'
127 def check_head_top_equal(crt_series
):
128 if not crt_series
.head_top_equal():
129 raise CmdException('HEAD and top are not the same. This can happen'
130 ' if you modify a branch with git. "stg repair'
131 ' --help" explains more about what to do next.')
133 def check_conflicts():
134 if git
.get_conflicts():
135 raise CmdException('Unsolved conflicts. Please fix the conflicts'
136 ' then use "git add --update <files>" or revert the'
137 ' changes with "reset --hard".')
139 def print_crt_patch(crt_series
, branch
= None):
141 patch
= crt_series
.get_current()
143 patch
= stack
.Series(branch
).get_current()
146 out
.info('Now at patch "%s"' % patch
)
148 out
.info('No patches applied')
151 def push_patches(crt_series
, patches
, check_merged
= False):
152 """Push multiple patches onto the stack. This function is shared
153 between the push and pull commands
155 forwarded
= crt_series
.forward_patches(patches
)
157 out
.info('Fast-forwarded patches "%s" - "%s"'
158 % (patches
[0], patches
[forwarded
- 1]))
160 out
.info('Fast-forwarded patch "%s"' % patches
[0])
162 names
= patches
[forwarded
:]
164 # check for patches merged upstream
165 if names
and check_merged
:
166 out
.start('Checking for patches merged upstream')
168 merged
= crt_series
.merged_patches(names
)
170 out
.done('%d found' % len(merged
))
175 out
.start('Pushing patch "%s"' % p
)
178 crt_series
.push_empty_patch(p
)
179 out
.done('merged upstream')
181 modified
= crt_series
.push_patch(p
)
183 if crt_series
.empty_patch(p
):
184 out
.done('empty patch')
190 def pop_patches(crt_series
, patches
, keep
= False):
191 """Pop the patches in the list from the stack. It is assumed that
192 the patches are listed in the stack reverse order.
194 if len(patches
) == 0:
195 out
.info('Nothing to push/pop')
198 if len(patches
) == 1:
199 out
.start('Popping patch "%s"' % p
)
201 out
.start('Popping patches "%s" - "%s"' % (patches
[0], p
))
202 crt_series
.pop_patch(p
, keep
)
205 def get_patch_from_list(part_name
, patch_list
):
206 candidates
= [full
for full
in patch_list
if part_name
in full
]
207 if len(candidates
) >= 2:
208 out
.info('Possible patches:\n %s' % '\n '.join(candidates
))
209 raise CmdException('Ambiguous patch name "%s"' % part_name
)
210 elif len(candidates
) == 1:
215 def parse_patches(patch_args
, patch_list
, boundary
= 0, ordered
= False):
216 """Parse patch_args list for patch names in patch_list and return
217 a list. The names can be individual patches and/or in the
218 patch1..patch2 format.
220 # in case it receives a tuple
221 patch_list
= list(patch_list
)
224 for name
in patch_args
:
225 pair
= name
.split('..')
227 if p
and p
not in patch_list
:
228 raise CmdException('Unknown patch name: %s' % p
)
234 # patch range [p1]..[p2]
237 first
= patch_list
.index(pair
[0])
242 last
= patch_list
.index(pair
[1]) + 1
246 # only cross the boundary if explicitly asked
248 boundary
= len(patch_list
)
258 last
= len(patch_list
)
261 pl
= patch_list
[first
:last
]
263 pl
= patch_list
[(last
- 1):(first
+ 1)]
266 raise CmdException('Malformed patch name: %s' % name
)
270 raise CmdException('Duplicate patch name: %s' % p
)
275 patches
= [p
for p
in patch_list
if p
in patches
]
279 def name_email(address
):
280 p
= email
.utils
.parseaddr(address
)
284 raise CmdException('Incorrect "name <email>"/"email (name)" string: %s'
287 def name_email_date(address
):
288 p
= parse_name_email_date(address
)
292 raise CmdException('Incorrect "name <email> date" string: %s' % address
)
294 def address_or_alias(addr_pair
):
295 """Return a name-email tuple the e-mail address is valid or look up
296 the aliases in the config files.
300 # it's an e-mail address
302 alias
= config
.get('mail.alias.' + addr
)
305 return name_email(alias
)
306 raise CmdException('unknown e-mail alias: %s' % addr
)
308 def prepare_rebase(crt_series
):
310 applied
= crt_series
.get_applied()
312 out
.start('Popping all applied patches')
313 crt_series
.pop_patch(applied
[0])
317 def rebase(crt_series
, target
):
319 tree_id
= git_id(crt_series
, target
)
321 # it might be that we use a custom rebase command with its own
325 out
.start('Rebasing to "%s"' % target
)
327 out
.start('Rebasing to the default target')
328 git
.rebase(tree_id
= tree_id
)
331 def post_rebase(crt_series
, applied
, nopush
, merged
):
332 # memorize that we rebased to here
333 crt_series
._set
_field
('orig-base', git
.get_head())
334 # push the patches back
336 push_patches(crt_series
, applied
, merged
)
339 # Patch description/e-mail/diff parsing
341 def __end_descr(line
):
342 return re
.match(r
'---\s*$', line
) or re
.match('diff -', line
) or \
343 re
.match('Index: ', line
) or re
.match('--- \w', line
)
345 def __split_descr_diff(string
):
346 """Return the description and the diff from the given string
351 for line
in string
.split('\n'):
353 if not __end_descr(line
):
360 return (descr
.rstrip(), diff
)
362 def __parse_description(descr
):
363 """Parse the patch description and return the new description and
364 author information (if any).
367 authname
= authemail
= authdate
= None
369 descr_lines
= [line
.rstrip() for line
in descr
.split('\n')]
371 raise CmdException("Empty patch description")
374 end
= len(descr_lines
)
377 # Parse the patch header
378 for pos
in range(0, end
):
379 if not descr_lines
[pos
]:
381 # check for a "From|Author:" line
382 if re
.match(r
'\s*(?:from|author):\s+', descr_lines
[pos
], re
.I
):
383 auth
= re
.findall(r
'^.*?:\s+(.*)$', descr_lines
[pos
])[0]
384 authname
, authemail
= name_email(auth
)
387 # check for a "Date:" line
388 if re
.match(r
'\s*date:\s+', descr_lines
[pos
], re
.I
):
389 authdate
= re
.findall(r
'^.*?:\s+(.*)$', descr_lines
[pos
])[0]
395 subject
= descr_lines
[pos
][descr_strip
:]
396 if re
.match(r
'commit [\da-f]{40}$', subject
):
397 # 'git show' output, look for the real subject
404 body
= '\n' + '\n'.join(l
[descr_strip
:] for l
in descr_lines
[lasthdr
:])
406 return (subject
+ body
, authname
, authemail
, authdate
)
409 """Parse the message object and return (description, authname,
410 authemail, authdate, diff)
413 if sys
.version_info
[0] <= 2:
414 # Python 2's decode_header() fails to decode encoded words if they are
415 # quoted. This does not match the behavior of Python3 or `git mailinfo`.
416 # For example, Python2 does not handle this header correctly:
418 # From: "=?UTF-8?q?Christian=20K=C3=B6nig?=" <name@example.com>
420 # By replacing the encoded words regex in the email.header module, we can
421 # bless Python2 with the same behavior as Python3.
422 email
.header
.ecre
= re
.compile(
423 (r
'=\? (?P<charset>[^?]*?)'
424 r
' \? (?P<encoding>[QqBb])'
425 r
' \? (?P<encoded>.*?)'
426 r
' \?='), re
.VERBOSE | re
.MULTILINE
)
428 def __decode_header(header
):
429 """Decode a qp-encoded e-mail header as per rfc2047"""
431 decoded_words
= email
.header
.decode_header(header
)
432 return text(email
.header
.make_header(decoded_words
))
433 except Exception as ex
:
434 raise CmdException('header decoding error: %s' % str(ex
))
438 authname
, authemail
= name_email(__decode_header(msg
['from']))
440 authname
= authemail
= None
442 # '\n\t' can be found on multi-line headers
443 descr
= __decode_header(msg
['subject'])
444 descr
= re
.sub('\n[ \t]*', ' ', descr
)
445 authdate
= msg
['date']
447 # remove the '[*PATCH*]' expression in the subject
449 descr
= re
.findall(r
'^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
452 raise CmdException('Subject: line not found')
454 # the rest of the message
456 for part
in msg
.walk():
457 if part
.get_content_type() in ['text/plain',
458 'application/octet-stream']:
459 payload
= part
.get_payload(decode
=True)
460 charset
= part
.get_content_charset('utf-8')
461 if codecs
.lookup(charset
).name
== 'utf-8':
462 msg_text
+= decode_utf8_with_latin1(payload
)
464 msg_text
+= payload
.decode(charset
)
466 rem_descr
, diff
= __split_descr_diff(msg_text
)
468 descr
+= '\n\n' + rem_descr
470 # parse the description for author information
471 descr
, descr_authname
, descr_authemail
, descr_authdate
= \
472 __parse_description(descr
)
474 authname
= descr_authname
476 authemail
= descr_authemail
478 authdate
= descr_authdate
480 return (descr
, authname
, authemail
, authdate
, diff
)
482 def parse_patch(text
, contains_diff
):
483 """Parse the input text and return (description, authname,
484 authemail, authdate, diff)
487 (text
, diff
) = __split_descr_diff(text
)
490 (descr
, authname
, authemail
, authdate
) = __parse_description(text
)
492 # we don't yet have an agreed place for the creation date.
494 return (descr
, authname
, authemail
, authdate
, diff
)
496 def readonly_constant_property(f
):
497 """Decorator that converts a function that computes a value to an
498 attribute that returns the value. The value is computed only once,
499 the first time it is accessed."""
501 n
= '__' + f
.__name
__
502 if not hasattr(self
, n
):
503 setattr(self
, n
, f(self
))
504 return getattr(self
, n
)
505 return property(new_f
)
507 def run_commit_msg_hook(repo
, cd
, editor_is_used
=True):
508 """Run the commit-msg hook (if any) on a commit.
510 @param cd: The L{CommitData<stgit.lib.git.CommitData>} to run the
513 Return the new L{CommitData<stgit.lib.git.CommitData>}."""
515 if not editor_is_used
:
516 env
['GIT_EDITOR'] = ':'
517 commit_msg_hook
= get_hook(repo
, 'commit-msg', env
)
520 new_msg
= run_hook_on_string(commit_msg_hook
, cd
.message
)
521 except RunException
as exc
:
522 raise EditorException(str(exc
))
524 return cd
.set_message(new_msg
)
526 def update_commit_data(cd
, options
):
527 """Return a new CommitData object updated according to the command line
529 # Set the commit message from commandline.
530 if options
.message
is not None:
531 cd
= cd
.set_message(options
.message
)
533 # Modify author data.
534 cd
= cd
.set_author(options
.author(cd
.author
))
536 # Add Signed-off-by: or similar.
537 if options
.sign_str
is not None:
538 sign_str
= options
.sign_str
540 sign_str
= config
.get("stgit.autosign")
541 if sign_str
is not None:
543 add_sign_line(cd
.message
, sign_str
,
544 cd
.committer
.name
, cd
.committer
.email
))
546 # Let user edit the commit message manually, unless
547 # --save-template or --message was specified.
548 if not getattr(options
, 'save_template', None) and options
.message
is None:
549 tmpl
= templates
.get_template('patchdescr.tmpl')
551 cd
= cd
.set_message(cd
.message
+ tmpl
)
552 cd
= cd
.set_message(edit_string(cd
.message
, '.stgit-new.txt'))
556 class DirectoryException(StgException
):
559 class _Directory(object):
560 def __init__(self
, needs_current_series
= True, log
= True):
561 self
.needs_current_series
= needs_current_series
563 @readonly_constant_property
566 return Run('git', 'rev-parse', '--git-dir'
567 ).discard_stderr().output_one_line()
569 raise DirectoryException('No git repository found')
570 @readonly_constant_property
571 def __topdir_path(self
):
573 lines
= Run('git', 'rev-parse', '--show-cdup'
574 ).discard_stderr().output_lines()
577 elif len(lines
) == 1:
580 raise RunException('Too much output')
582 raise DirectoryException('No git repository found')
583 @readonly_constant_property
584 def is_inside_git_dir(self
):
585 return { 'true': True, 'false': False
586 }[Run('git', 'rev-parse', '--is-inside-git-dir'
588 @readonly_constant_property
589 def is_inside_worktree(self
):
590 return { 'true': True, 'false': False
591 }[Run('git', 'rev-parse', '--is-inside-work-tree'
593 def cd_to_topdir(self
):
594 os
.chdir(self
.__topdir
_path
)
595 def write_log(self
, msg
):
597 log
.compat_log_entry(msg
)
599 class DirectoryAnywhere(_Directory
):
603 class DirectoryHasRepository(_Directory
):
605 self
.git_dir
# might throw an exception
606 log
.compat_log_external_mods()
608 class DirectoryInWorktree(DirectoryHasRepository
):
610 DirectoryHasRepository
.setup(self
)
611 if not self
.is_inside_worktree
:
612 raise DirectoryException('Not inside a git worktree')
614 class DirectoryGotoToplevel(DirectoryInWorktree
):
616 DirectoryInWorktree
.setup(self
)
619 class DirectoryHasRepositoryLib(_Directory
):
620 """For commands that use the new infrastructure in stgit.lib.*."""
622 self
.needs_current_series
= False
623 self
.log
= False # stgit.lib.transaction handles logging
625 # This will throw an exception if we don't have a repository.
626 self
.repository
= libstack
.Repository
.default()