1# Software License Agreement (BSD License)
2#
3# Copyright (c) 2010, Willow Garage, Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9#
10#  * Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12#  * Redistributions in binary form must reproduce the above
13#    copyright notice, this list of conditions and the following
14#    disclaimer in the documentation and/or other materials provided
15#    with the distribution.
16#  * Neither the name of Willow Garage, Inc. nor the names of its
17#    contributors may be used to endorse or promote products derived
18#    from this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31# POSSIBILITY OF SUCH DAMAGE.
32
33"Support for any command line interface (CLI) for wstool"
34
35try:
36    from collections import OrderedDict
37except:
38    from ordereddict import OrderedDict
39import os
40import re
41from optparse import OptionParser
42from wstool.common import samefile, MultiProjectException, select_elements
43
44ONLY_OPTION_VALID_ATTRS = ['path', 'localname', 'version',
45                           'revision', 'cur_revision', 'uri', 'cur_uri', 'scmtype']
46
47
48def get_workspace(argv, shell_path, config_filename=None, varname=None):
49    """
50    If target option -t is given return value of that one. Else, if varname
51    is given and exists, considers that one, plus,
52    if config_filename is given, searches for a file named in config_filename
53    in 'shell_path' and ancestors.
54    In that case, if two solutions are found, asks the user.
55
56    :param shell_path: where to look for relevant config_filename
57    :param config_filename: optional, filename for files defining workspaces
58    :param varname: optional, env var to be used as workspace folder
59    :returns: abspath if a .rosinstall was found, error and exist else.
60    """
61    parser = OptionParser()
62    parser.add_option(
63        "-t", "--target-workspace",
64        dest="workspace", default=None,
65        help="which workspace to use",
66        action="store")
67    # suppress errors based on any other options this parser is agnostic about
68    argv2 = [x for x in argv if ((not x.startswith('-')) or
69                                 x.startswith('--target-workspace=') or
70                                 x.startswith('-t') or
71                                 x == '--target-workspace')]
72    (options, _) = parser.parse_args(argv2)
73    if options.workspace is not None:
74        if (config_filename is not None and
75            not os.path.isfile(os.path.join(options.workspace, config_filename))):
76
77            raise MultiProjectException("%s has no workspace configuration file '%s'" % (os.path.abspath(options.workspace), config_filename))
78        return os.path.abspath(options.workspace)
79
80    varname_path = None
81    if varname is not None and varname in os.environ:
82        # workspace could be relative, maybe confusing,
83        # but that's the users fault
84        varname_path = os.environ[varname]
85        if varname_path.strip() == '' or not os.path.isdir(varname_path):
86            varname_path = None
87
88    # use current dir
89    current_path = None
90    if config_filename is not None:
91        while shell_path is not None and not shell_path == os.path.dirname(shell_path):
92            if os.path.exists(os.path.join(shell_path, config_filename)):
93                current_path = shell_path
94                break
95            shell_path = os.path.dirname(shell_path)
96
97    if current_path is not None and varname_path is not None and not samefile(current_path, varname_path):
98        raise MultiProjectException("Ambiguous workspace: %s=%s, %s" % (varname, varname_path, os.path.abspath(config_filename)))
99
100    if current_path is None and varname_path is None:
101        raise MultiProjectException("Command requires a target workspace.")
102
103    if current_path is not None:
104        return current_path
105    else:
106        return varname_path
107
108
109def _uris_match(basepath, uri1, uri2):
110    """
111    True if uri2 is None or not None and same folder or equal string
112    as uri1. Relative folders resolved using basepath
113    """
114    if uri1 is None:
115        uri1 = ''
116    if uri2 is None:
117        return True
118    if ((uri1 == uri2) or
119        (basepath is not None and
120         os.path.isdir(os.path.join(basepath, uri1)) and
121         os.path.realpath(os.path.join(basepath, uri2)) == os.path.realpath(os.path.join(basepath, uri1)))):
122        return True
123    return False
124
125
126def _get_svn_version_from_uri(uri):
127    """
128    in case of SVN, we can use the final part of
129    standard uri as spec version, if it follows canonical SVN layout
130
131    :param uri: uri to extract version from
132    :returns changed_uri: str, version extracted uri
133    :returns version: str, extracted version
134    :returns: (None, None), for empty uri or when there is no regex match for version info
135    """
136    if uri is None:
137        return None, None
138    match = re.match('(.*/)((tags|branches|trunk)(/.*)*)', uri)
139    if (match is not None and
140            len(match.groups()) > 1 and
141            uri == ''.join(match.groups()[0:2])):
142        changed_uri = match.groups()[0]
143        version = match.groups()[1]
144        return changed_uri, version
145    return None, None
146
147
148def _get_status_flags(basepath, elt_dict):
149    """
150    returns a string where each char conveys status information about
151    a config element entry
152
153    :param basepath: path in which element lies
154    :param elt_dict: a dict representing one elt_dict in a table
155    :returns: str
156    """
157    if 'exists' in elt_dict and elt_dict['exists'] is False:
158        return 'x'
159    mflag = ''
160    if 'modified' in elt_dict and elt_dict['modified'] is True:
161        mflag = 'M'
162    if (('curr_uri' in elt_dict and
163         not _uris_match(basepath, elt_dict['uri'], elt_dict['curr_uri'])) or
164        ('specversion' in elt_dict and
165         elt_dict['specversion'] is not None and
166         elt_dict['actualversion'] is not None and
167         elt_dict['specversion'] != elt_dict['actualversion'])):
168        mflag += 'V'
169    if (('remote_revision' in elt_dict and
170         elt_dict['remote_revision'] != '' and
171         elt_dict['remote_revision'] is not None and
172         'actualversion' in elt_dict and
173         elt_dict['actualversion'] is not None and
174         elt_dict['remote_revision'] != elt_dict['actualversion']) or
175        (('version' not in elt_dict or
176          elt_dict['version'] is None) and
177         'default_remote_label' in elt_dict and
178         elt_dict['default_remote_label'] is not None and
179         ('curr_version' not in elt_dict or
180          elt_dict['curr_version'] != elt_dict['default_remote_label']))):
181        mflag += 'C'
182    return mflag
183
184
185def get_info_table_elements(basepath, entries, unmanaged=False):
186    """returns a list of dict with refined information from entries"""
187
188    outputs = []
189    for line in entries:
190        if not 'curr_uri' in line:
191            line['curr_uri'] = None
192        if not 'specversion' in line:
193            line['specversion'] = None
194        if not 'actualversion' in line:
195            line['actualversion'] = None
196        if not 'curr_version' in line:
197            line['curr_version'] = None
198        if not 'version' in line:
199            line['version'] = None
200        if not 'remote_revision' in line:
201            line['remote_revision'] = None
202        if not 'curr_version_label' in line:
203            line['curr_version_label'] = None
204        output_dict = {'scm': line['scm'],
205                       'uri': line['uri'],
206                       'curr_uri': None,
207                       'version': line['version'],
208                       'localname': line['localname']}
209
210        if line is None:
211            print("Bug Warning, an element is missing")
212            continue
213
214        if line['scm'] == 'git':
215            if (line['specversion'] is not None and len(line['specversion']) > 12):
216                line['specversion'] = line['specversion'][0:12]
217            if (line['actualversion'] is not None and len(line['actualversion']) > 12):
218                line['actualversion'] = line['actualversion'][0:12]
219            if (line['remote_revision'] is not None and len(line['remote_revision']) > 12):
220                line['remote_revision'] = line['remote_revision'][0:12]
221
222        if line['scm'] is not None:
223
224            if line['scm'] == 'svn':
225                (line['uri'],
226                line['version']) = _get_svn_version_from_uri(uri=line['uri'])
227                if line['curr_uri'] is not None:
228                    (line['curr_uri'],
229                     line['curr_version_label']) = _get_svn_version_from_uri(
230                         uri=line['curr_uri'])
231
232            if line['scm'] in ['git', 'svn', 'hg']:
233                line['curr_version'] = line['curr_version_label']
234
235            if line['curr_version'] is not None:
236                output_dict['version'] = line['curr_version']
237            if output_dict['version'] is not None:
238                if line['version'] != output_dict['version']:
239                    if line['version']:
240                        output_dict['version'] += "  (%s)" % line['version']
241                    else:
242                        if line['default_remote_label']:
243                            if output_dict['version'] == line['default_remote_label']:
244                                output_dict['version'] += "  (=)"
245                            else:
246                                output_dict['version'] += "  (%s)" % line['default_remote_label']
247                        else:
248                            output_dict['version'] += "  (-)"
249
250            if (line['specversion'] is not None and
251                line['specversion'] != '' and
252                line['actualversion'] != line['specversion']):
253                output_dict['matching'] = "%s (%s)" % (line['actualversion'], line['specversion'])
254            else:
255                output_dict['matching'] = line['actualversion']
256
257            common_prefixes = ["https://", "http://"]
258            if line['uri'] is not None and unmanaged is False:
259                for pre in common_prefixes:
260                    if line['uri'].startswith(pre):
261                        line['uri'] = line['uri'][len(pre):]
262                        break
263                output_dict['uri'] = line['uri']
264
265            if line['curr_uri'] is not None:
266                for pre in common_prefixes:
267                    if line['curr_uri'].startswith(pre):
268                        line['curr_uri'] = line['curr_uri'][len(pre):]
269                        break
270
271            if (not _uris_match(basepath, line['uri'], line['curr_uri'])):
272                output_dict['uri'] = "%s    (%s)" % (line[
273                                                     'curr_uri'], line['uri'])
274
275        else:
276            output_dict['matching'] = " "
277        output_dict['status'] = _get_status_flags(basepath, line)
278
279        outputs.append(output_dict)
280
281    return outputs
282
283
284def get_info_table(basepath, entries, data_only=False, reverse=False,
285                   unmanaged=False, selected_headers=None):
286    """
287    return a refined textual representation of the entries. Provides
288    column headers and processes data.
289    """
290    headers = OrderedDict([
291        ('localname', "Localname"),
292        ('status', "S"),
293        ('scm', "SCM"),
294        ('version', "Version (Spec)"),
295        ('matching', "UID  (Spec)"),
296        ('uri', "URI  (Spec) [http(s)://...]"),
297    ])
298    # table design
299    if unmanaged:
300        selected_headers = ['localname', 'scm', 'uri']
301    elif selected_headers is None:
302        selected_headers = headers.keys()
303    # validate selected_headers
304    invalid_headers = [h for h in selected_headers if h not in headers.keys()]
305    if invalid_headers:
306        raise ValueError('Invalid headers are passed: %s' % invalid_headers)
307
308    outputs = get_info_table_elements(
309        basepath=basepath,
310        entries=entries,
311        unmanaged=unmanaged)
312
313    # adjust column width
314    column_length = {}
315    for header in list(headers.keys()):
316        column_length[header] = len(headers[header])
317        for entry in outputs:
318            if entry[header] is not None:
319                column_length[header] = max(column_length[header],
320                                            len(entry[header]))
321
322    resultlines = []
323    if not data_only and len(outputs) > 0:
324        header_line = ' '
325        for i, header in enumerate(selected_headers):
326            output = headers[header]
327            if i < len(selected_headers) - 1:
328                output = output.ljust(column_length[header]) + " "
329            header_line += output
330        resultlines.append(header_line)
331        header_line = ' '
332        for i, header in enumerate(selected_headers):
333            output = '-' * len(headers[header])
334            if i < len(selected_headers) - 1:
335                output = output.ljust(column_length[header]) + " "
336            header_line += output
337        resultlines.append(header_line)
338
339    if reverse:
340        outputs = reversed(outputs)
341    for entry in outputs:
342        if entry is None:
343            print("Bug Warning, an element is missing")
344            continue
345        data_line = ' '
346        for i, header in enumerate(selected_headers):
347            output = entry[header]
348            if output is None:
349                output = ''
350            if i < len(selected_headers) - 1:
351                output = output.ljust(column_length[header]) + " "
352            data_line += output
353        resultlines.append(data_line)
354    return "\n".join(resultlines)
355
356
357def get_info_list(basepath, line, data_only=False):
358    """
359    Info for a single config entry
360    """
361
362    assert line is not None, "Bug Warning, an element is missing"
363
364    headers = {
365        'uri': "URI:",
366        'curr_uri': "Current URI:",
367        'scm': "SCM:",
368        'localname': "Localname:",
369        'path': "Path",
370        'version': "Spec-Version:",
371        'curr_version_label': "Current-Version:",
372        'status': "Status:",
373        'specversion': "Spec-Revision:",
374        'actualversion': "Current-Revision:",
375        'properties': "Other Properties:"}
376
377    # table design
378    selected_headers = ['localname', 'path', 'status',
379                        'scm', 'uri', 'curr_uri',
380                        'version', 'curr_version_label', 'specversion',
381                        'actualversion', 'properties']
382
383    line['status'] = _get_status_flags(basepath, line)
384
385    header_length = 0
386    for header in list(headers.keys()):
387        header_length = max(header_length, len(headers[header]))
388    result = ''
389    for header in selected_headers:
390        if not data_only:
391            title = "%s  " % (headers[header].ljust(header_length))
392        else:
393            title = ''
394        if header in line:
395            output = line[header]
396        if output is None:
397            output = ''
398        result += "%s%s\n" % (title, output)
399    return result
400
401
402def get_info_table_raw_csv(config, parser, properties, localnames):
403    """
404    returns raw data without decorations in comma-separated value format.
405    allows to select properties.
406    Given a config, collects all elements, and prints a line of each,
407    with selected properties in the output
408
409    :param parser: OptionParser used to throw option errors
410    :param properties: list of property ids to display
411    :param localnames: which config elements to show
412    :return: list of str, each a csv line
413    """
414    lookup_required = False
415    for attr in properties:
416        if not attr in ONLY_OPTION_VALID_ATTRS:
417            parser.error("Invalid --only option '%s', valids are %s" %
418                         (attr, ONLY_OPTION_VALID_ATTRS))
419        if attr in ['cur_revision', 'cur_uri', 'revision']:
420            lookup_required = True
421    elements = select_elements(config, localnames)
422    result=[]
423    for element in elements:
424        if lookup_required and element.is_vcs_element():
425            spec = element.get_versioned_path_spec()
426        else:
427            spec = element.get_path_spec()
428        output = []
429        for attr in properties:
430            if 'localname' == attr:
431                output.append(spec.get_local_name() or '')
432            if 'path' == attr:
433                output.append(spec.get_path() or '')
434            if 'scmtype' == attr:
435                output.append(spec.get_scmtype() or '')
436            if 'uri' == attr:
437                output.append(spec.get_uri() or '')
438            if 'version' == attr:
439                output.append(spec.get_version() or '')
440            if 'revision' == attr:
441                output.append(spec.get_revision() or '')
442            if 'cur_uri' == attr:
443                output.append(spec.get_curr_uri() or '')
444            if 'cur_revision' == attr:
445                output.append(spec.get_current_revision() or '')
446        result.append(','.join(output))
447    return result
448