1from __future__ import unicode_literals
2
3import json
4import logging
5import os
6import shutil
7import sys
8import tempfile
9
10
11CONFIG_FILE = '.reviewboardrc'
12
13tempfiles = []
14tempdirs = []
15builtin = {}
16
17
18def is_exe_in_path(name):
19    """Checks whether an executable is in the user's search path.
20
21    This expects a name without any system-specific executable extension.
22    It will append the proper extension as necessary. For example,
23    use "myapp" and not "myapp.exe".
24
25    This will return True if the app is in the path, or False otherwise.
26
27    Taken from djblets.util.filesystem to avoid an extra dependency
28    """
29    if sys.platform == 'win32' and not name.endswith('.exe'):
30        name += '.exe'
31
32    for dir in os.environ['PATH'].split(os.pathsep):
33        if os.path.exists(os.path.join(dir, name)):
34            return True
35
36    return False
37
38
39def cleanup_tempfiles():
40    for tmpfile in tempfiles:
41        try:
42            os.unlink(tmpfile)
43        except OSError:
44            pass
45
46    for tmpdir in tempdirs:
47        shutil.rmtree(tmpdir, ignore_errors=True)
48
49
50def _load_python_file(filename, config):
51    with open(filename) as f:
52        exec(compile(f.read(), filename, 'exec'), config)
53        return config
54
55
56def make_tempfile(content=None, prefix='rbtools.', suffix=None, filename=None):
57    """Create a temporary file and return the path.
58
59    If not manually removed, then the resulting temp file will be removed when
60    RBTools exits (or if :py:func:`cleanup_tempfiles` is called).
61
62    This can be given an explicit name for a temporary file, in which case
63    the file will be created inside of a temporary directory (created with
64    :py:func:`make_tempdir`. In this case, the parent directory will only
65    be deleted when :py:func:`cleanup_tempfiles` is called.
66
67    Args:
68        content (bytes, optional):
69            The content for the text file.
70
71        prefix (bool, optional):
72            The prefix for the temp filename. This defaults to ``rbtools.``.
73
74        suffix (bool, optional):
75            The suffix for the temp filename.
76
77        filename (unicode, optional):
78            An explicit name of the file. If provided, this will override
79            ``suffix`` and ``prefix``.
80
81    Returns:
82        unicode:
83        The temp file path.
84    """
85    if filename is not None:
86        tmpdir = make_tempdir()
87        tmpfile = os.path.join(tmpdir, filename)
88
89        with open(tmpfile, 'wb') as fp:
90            if content:
91                fp.write(content)
92    else:
93        with tempfile.NamedTemporaryFile(prefix=prefix,
94                                         suffix=suffix or '',
95                                         delete=False) as fp:
96            tmpfile = fp.name
97
98            if content:
99                fp.write(content)
100
101    tempfiles.append(tmpfile)
102
103    return tmpfile
104
105
106def make_tempdir(parent=None):
107    """Create a temporary directory and return the path.
108
109    The path is stored in an array for later cleanup.
110
111    Args:
112        parent (unicode, optional):
113            An optional parent directory to create the path in.
114
115    Returns:
116        unicode:
117        The name of the new temporary directory.
118    """
119    tmpdir = tempfile.mkdtemp(prefix='rbtools.',
120                              dir=parent)
121    tempdirs.append(tmpdir)
122
123    return tmpdir
124
125
126def make_empty_files(files):
127    """Creates each file in the given list and any intermediate directories."""
128    for f in files:
129        path = os.path.dirname(f)
130
131        if path and not os.path.exists(path):
132            try:
133                os.makedirs(path)
134            except OSError as e:
135                logging.error('Unable to create directory %s: %s', path, e)
136                continue
137
138        try:
139            with open(f, 'w'):
140                # Set the file access and modified times to the current time.
141                os.utime(f, None)
142        except IOError as e:
143            logging.error('Unable to create empty file %s: %s', f, e)
144
145
146def walk_parents(path):
147    """Walks up the tree to the root directory."""
148    while os.path.splitdrive(path)[1] != os.sep:
149        yield path
150        path = os.path.dirname(path)
151
152
153def get_home_path():
154    """Retrieve the homepath."""
155    if 'HOME' in os.environ:
156        return os.environ['HOME']
157    elif 'APPDATA' in os.environ:
158        return os.environ['APPDATA']
159    else:
160        return ''
161
162
163def get_config_paths():
164    """Return the paths to each :file:`.reviewboardrc` influencing the cwd.
165
166    A list of paths to :file:`.reviewboardrc` files will be returned, where
167    each subsequent list entry should have lower precedence than the previous.
168    i.e. configuration found in files further up the list will take precedence.
169
170    Configuration in the paths set in :envvar:`$RBTOOLS_CONFIG_PATH` will take
171    precedence over files found in the current working directory or its
172    parents.
173    """
174    config_paths = []
175
176    # Apply config files from $RBTOOLS_CONFIG_PATH first, ...
177    for path in os.environ.get('RBTOOLS_CONFIG_PATH', '').split(os.pathsep):
178        # Filter out empty paths, this also takes care of if
179        # $RBTOOLS_CONFIG_PATH is unset or empty.
180        if not path:
181            continue
182
183        filename = os.path.realpath(os.path.join(path, CONFIG_FILE))
184
185        if os.path.exists(filename) and filename not in config_paths:
186            config_paths.append(filename)
187
188    # ... then config files from the current or parent directories.
189    for path in walk_parents(os.getcwd()):
190        filename = os.path.realpath(os.path.join(path, CONFIG_FILE))
191
192        if os.path.exists(filename) and filename not in config_paths:
193            config_paths.append(filename)
194
195    # Finally, the user's own config file.
196    home_config_path = os.path.realpath(os.path.join(get_home_path(),
197                                                     CONFIG_FILE))
198
199    if (os.path.exists(home_config_path) and
200        home_config_path not in config_paths):
201        config_paths.append(home_config_path)
202
203    return config_paths
204
205
206def parse_config_file(filename):
207    """Parse a .reviewboardrc file.
208
209    Returns a dictionary containing the configuration from the file.
210
211    The ``filename`` argument should contain a full path to a
212    .reviewboardrc file.
213    """
214    config = {
215        'TREES': {},
216        'ALIASES': {},
217    }
218
219    try:
220        config = _load_python_file(filename, config)
221    except SyntaxError as e:
222        raise Exception('Syntax error in config file: %s\n'
223                        'Line %i offset %i\n'
224                        % (filename, e.lineno, e.offset))
225
226    return dict((k, config[k])
227                for k in set(config.keys()) - set(builtin.keys()))
228
229
230def load_config():
231    """Load configuration from .reviewboardrc files.
232
233    This will read all of the .reviewboardrc files influencing the
234    cwd and return a dictionary containing the configuration.
235    """
236    nested_config = {
237        'ALIASES': {},
238        'COLOR': {
239            'INFO': None,
240            'DEBUG': None,
241            'WARNING': 'yellow',
242            'ERROR': 'red',
243            'CRITICAL': 'red'
244        },
245        'TREES': {},
246    }
247    config = {}
248
249    for filename in reversed(get_config_paths()):
250        parsed_config = parse_config_file(filename)
251
252        for key in nested_config:
253            nested_config[key].update(parsed_config.pop(key, {}))
254
255        config.update(parsed_config)
256
257    config.update(nested_config)
258
259    return config
260
261
262# This extracts a dictionary of the built-in globals in order to have a clean
263# dictionary of settings, consisting of only what has been specified in the
264# config file.
265exec('True', builtin)
266