1from __future__ import print_function, unicode_literals
2
3import logging
4
5import six
6
7from rbtools.api.errors import APIError
8from rbtools.commands import Command, CommandError, Option, OptionGroup
9from rbtools.utils.commands import stamp_commit_with_review_url
10from rbtools.utils.console import confirm
11from rbtools.utils.review_request import (find_review_request_by_change_id,
12                                          get_draft_or_current_value,
13                                          get_revisions,
14                                          guess_existing_review_request)
15
16
17class Stamp(Command):
18    """Add the review request URL to the commit message.
19
20    Stamps the review request URL onto the commit message of the revision
21    specified. The revisions argument behaves like it does in rbt post, where
22    it is required for some SCMs (e.g. Perforce) and unnecessary/ignored for
23    others (e.g. Git).
24
25    Normally, this command will guess the review request (based on the revision
26    number if provided, and the commit summary and description otherwise).
27    However, if a review request ID is specified by the user, it stamps the URL
28    of that review request instead of guessing.
29    """
30
31    name = 'stamp'
32    author = 'The Review Board Project'
33    description = 'Adds the review request URL to the commit message.'
34    args = '[revisions]'
35
36    option_list = [
37        OptionGroup(
38            name='Stamp Options',
39            description='Controls the behavior of a stamp, including what '
40                        'review request URL gets stamped.',
41            option_list=[
42                Option('-r', '--review-request-id',
43                       dest='rid',
44                       metavar='ID',
45                       default=None,
46                       help='Specifies the existing review request ID to '
47                            'be stamped.'),
48            ]
49        ),
50        Command.server_options,
51        Command.repository_options,
52        Command.diff_options,
53        Command.branch_options,
54        Command.perforce_options,
55    ]
56
57    def no_commit_error(self):
58        raise CommandError('No existing commit to stamp on.')
59
60    def _ask_review_request_match(self, review_request):
61        question = ('Stamp with Review Request #%s: "%s"? '
62                    % (review_request.id,
63                       get_draft_or_current_value(
64                           'summary', review_request)))
65
66        return confirm(question)
67
68    def determine_review_request(self, api_client, api_root, repository_info,
69                                 repository_name, revisions):
70        """Determine the correct review request for a commit.
71
72        A tuple (review request ID, review request absolute URL) is returned.
73        If no review request ID is found by any of the strategies,
74        (None, None) is returned.
75        """
76        # First, try to match the changeset to a review request directly.
77        if repository_info.supports_changesets:
78            review_request = find_review_request_by_change_id(
79                api_client, api_root, repository_info, repository_name,
80                revisions)
81
82            if review_request and review_request.id:
83                return review_request.id, review_request.absolute_url
84
85        # Fall back on guessing based on the description. This may return None
86        # if no suitable review request is found.
87        logging.debug('Attempting to guess review request based on '
88                      'summary and description')
89
90        try:
91            review_request = guess_existing_review_request(
92                repository_info, repository_name, api_root, api_client,
93                self.tool, revisions, guess_summary=False,
94                guess_description=False,
95                is_fuzzy_match_func=self._ask_review_request_match,
96                no_commit_error=self.no_commit_error)
97        except ValueError as e:
98            raise CommandError(six.text_type(e))
99
100        if review_request:
101            logging.debug('Found review request ID %d', review_request.id)
102            return review_request.id, review_request.absolute_url
103        else:
104            logging.debug('Could not find a matching review request')
105            return None, None
106
107    def main(self, *args):
108        """Add the review request URL to a commit message."""
109        self.cmd_args = list(args)
110
111        repository_info, self.tool = self.initialize_scm_tool(
112            client_name=self.options.repository_type)
113        server_url = self.get_server_url(repository_info, self.tool)
114        api_client, api_root = self.get_api(server_url)
115        self.setup_tool(self.tool, api_root=api_root)
116
117        if not self.tool.can_amend_commit:
118            raise NotImplementedError('rbt stamp is not supported with %s.'
119                                      % self.tool.name)
120
121        try:
122            if self.tool.has_pending_changes():
123                raise CommandError('Working directory is not clean.')
124        except NotImplementedError:
125            pass
126
127        revisions = get_revisions(self.tool, self.cmd_args)
128
129        # Use the ID from the command line options if present.
130        if self.options.rid:
131            review_request_id = self.options.rid
132
133            try:
134                review_request = api_root.get_review_request(
135                    review_request_id=review_request_id)
136            except APIError as e:
137                raise CommandError('Error getting review request %s: %s'
138                                   % (review_request_id, e))
139
140            review_request_url = review_request.absolute_url
141        else:
142            review_request_id, review_request_url = \
143                self. determine_review_request(
144                    api_client, api_root, repository_info,
145                    self.options.repository_name, revisions)
146
147        if not review_request_url:
148            raise CommandError('Could not determine the existing review '
149                               'request URL to stamp with.')
150
151        stamp_commit_with_review_url(revisions, review_request_url, self.tool)
152
153        print('Successfully stamped change with the URL:')
154        print(review_request_url)
155