~jelmer/dulwich/lp-pqm

« back to all changes in this revision

Viewing changes to dulwich/config.py

  • Committer: Jelmer Vernooij
  • Date: 2012-02-01 22:13:51 UTC
  • mfrom: (413.11.554)
  • Revision ID: jelmer@samba.org-20120201221351-b3n2p9zttzh62dwu
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# config.py - Reading and writing Git config files
 
2
# Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org>
 
3
#
 
4
# This program is free software; you can redistribute it and/or
 
5
# modify it under the terms of the GNU General Public License
 
6
# as published by the Free Software Foundation; version 2
 
7
# of the License or (at your option) a later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
17
# MA  02110-1301, USA.
 
18
 
 
19
"""Reading and writing Git configuration files.
 
20
 
 
21
TODO:
 
22
 * preserve formatting when updating configuration files
 
23
 * treat subsection names as case-insensitive for [branch.foo] style
 
24
   subsections
 
25
"""
 
26
 
 
27
import errno
 
28
import os
 
29
import re
 
30
 
 
31
from dulwich.file import GitFile
 
32
 
 
33
 
 
34
class Config(object):
 
35
    """A Git configuration."""
 
36
 
 
37
    def get(self, section, name):
 
38
        """Retrieve the contents of a configuration setting.
 
39
        
 
40
        :param section: Tuple with section name and optional subsection namee
 
41
        :param subsection: Subsection name
 
42
        :return: Contents of the setting
 
43
        :raise KeyError: if the value is not set
 
44
        """
 
45
        raise NotImplementedError(self.get)
 
46
 
 
47
    def get_boolean(self, section, name, default=None):
 
48
        """Retrieve a configuration setting as boolean.
 
49
 
 
50
        :param section: Tuple with section name and optional subsection namee
 
51
        :param name: Name of the setting, including section and possible
 
52
            subsection.
 
53
        :return: Contents of the setting
 
54
        :raise KeyError: if the value is not set
 
55
        """
 
56
        try:
 
57
            value = self.get(section, name)
 
58
        except KeyError:
 
59
            return default
 
60
        if value.lower() == "true":
 
61
            return True
 
62
        elif value.lower() == "false":
 
63
            return False
 
64
        raise ValueError("not a valid boolean string: %r" % value)
 
65
 
 
66
    def set(self, section, name, value):
 
67
        """Set a configuration value.
 
68
        
 
69
        :param name: Name of the configuration value, including section
 
70
            and optional subsection
 
71
        :param: Value of the setting
 
72
        """
 
73
        raise NotImplementedError(self.set)
 
74
 
 
75
 
 
76
class ConfigDict(Config):
 
77
    """Git configuration stored in a dictionary."""
 
78
 
 
79
    def __init__(self, values=None):
 
80
        """Create a new ConfigDict."""
 
81
        if values is None:
 
82
            values = {}
 
83
        self._values = values
 
84
 
 
85
    def __repr__(self):
 
86
        return "%s(%r)" % (self.__class__.__name__, self._values)
 
87
 
 
88
    def __eq__(self, other):
 
89
        return (
 
90
            isinstance(other, self.__class__) and
 
91
            other._values == self._values)
 
92
 
 
93
    @classmethod
 
94
    def _parse_setting(cls, name):
 
95
        parts = name.split(".")
 
96
        if len(parts) == 3:
 
97
            return (parts[0], parts[1], parts[2])
 
98
        else:
 
99
            return (parts[0], None, parts[1])
 
100
 
 
101
    def get(self, section, name):
 
102
        if isinstance(section, basestring):
 
103
            section = (section, )
 
104
        if len(section) > 1:
 
105
            try:
 
106
                return self._values[section][name]
 
107
            except KeyError:
 
108
                pass
 
109
        return self._values[(section[0],)][name]
 
110
 
 
111
    def set(self, section, name, value):
 
112
        if isinstance(section, basestring):
 
113
            section = (section, )
 
114
        self._values.setdefault(section, {})[name] = value
 
115
 
 
116
 
 
117
def _format_string(value):
 
118
    if (value.startswith(" ") or
 
119
        value.startswith("\t") or
 
120
        value.endswith(" ") or
 
121
        value.endswith("\t")):
 
122
        return '"%s"' % _escape_value(value)
 
123
    return _escape_value(value)
 
124
 
 
125
 
 
126
def _parse_string(value):
 
127
    value = value.strip()
 
128
    ret = []
 
129
    block = []
 
130
    in_quotes  = False
 
131
    for c in value:
 
132
        if c == "\"":
 
133
            in_quotes = (not in_quotes)
 
134
            ret.append(_unescape_value("".join(block)))
 
135
            block = []
 
136
        elif c in ("#", ";") and not in_quotes:
 
137
            # the rest of the line is a comment
 
138
            break
 
139
        else:
 
140
            block.append(c)
 
141
 
 
142
    if in_quotes:
 
143
        raise ValueError("value starts with quote but lacks end quote")
 
144
 
 
145
    ret.append(_unescape_value("".join(block)).rstrip())
 
146
 
 
147
    return "".join(ret)
 
148
 
 
149
 
 
150
def _unescape_value(value):
 
151
    """Unescape a value."""
 
152
    def unescape(c):
 
153
        return {
 
154
            "\\\\": "\\",
 
155
            "\\\"": "\"",
 
156
            "\\n": "\n",
 
157
            "\\t": "\t",
 
158
            "\\b": "\b",
 
159
            }[c.group(0)]
 
160
    return re.sub(r"(\\.)", unescape, value)
 
161
 
 
162
 
 
163
def _escape_value(value):
 
164
    """Escape a value."""
 
165
    return value.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"")
 
166
 
 
167
 
 
168
def _check_variable_name(name):
 
169
    for c in name:
 
170
        if not c.isalnum() and c != '-':
 
171
            return False
 
172
    return True
 
173
 
 
174
 
 
175
def _check_section_name(name):
 
176
    for c in name:
 
177
        if not c.isalnum() and c not in ('-', '.'):
 
178
            return False
 
179
    return True
 
180
 
 
181
 
 
182
def _strip_comments(line):
 
183
    line = line.split("#")[0]
 
184
    line = line.split(";")[0]
 
185
    return line
 
186
 
 
187
 
 
188
class ConfigFile(ConfigDict):
 
189
    """A Git configuration file, like .git/config or ~/.gitconfig.
 
190
    """
 
191
 
 
192
    @classmethod
 
193
    def from_file(cls, f):
 
194
        """Read configuration from a file-like object."""
 
195
        ret = cls()
 
196
        section = None
 
197
        setting = None
 
198
        for lineno, line in enumerate(f.readlines()):
 
199
            line = line.lstrip()
 
200
            if setting is None:
 
201
                if len(line) > 0 and line[0] == "[":
 
202
                    line = _strip_comments(line).rstrip()
 
203
                    last = line.index("]")
 
204
                    if last == -1:
 
205
                        raise ValueError("expected trailing ]")
 
206
                    pts = line[1:last].split(" ", 1)
 
207
                    line = line[last+1:]
 
208
                    pts[0] = pts[0].lower()
 
209
                    if len(pts) == 2:
 
210
                        if pts[1][0] != "\"" or pts[1][-1] != "\"":
 
211
                            raise ValueError(
 
212
                                "Invalid subsection " + pts[1])
 
213
                        else:
 
214
                            pts[1] = pts[1][1:-1]
 
215
                        if not _check_section_name(pts[0]):
 
216
                            raise ValueError("invalid section name %s" %
 
217
                                             pts[0])
 
218
                        section = (pts[0], pts[1])
 
219
                    else:
 
220
                        if not _check_section_name(pts[0]):
 
221
                            raise ValueError("invalid section name %s" %
 
222
                                    pts[0])
 
223
                        pts = pts[0].split(".", 1)
 
224
                        if len(pts) == 2:
 
225
                            section = (pts[0], pts[1])
 
226
                        else:
 
227
                            section = (pts[0], )
 
228
                    ret._values[section] = {}
 
229
                if _strip_comments(line).strip() == "":
 
230
                    continue
 
231
                if section is None:
 
232
                    raise ValueError("setting %r without section" % line)
 
233
                try:
 
234
                    setting, value = line.split("=", 1)
 
235
                except ValueError:
 
236
                    setting = line
 
237
                    value = "true"
 
238
                setting = setting.strip().lower()
 
239
                if not _check_variable_name(setting):
 
240
                    raise ValueError("invalid variable name %s" % setting)
 
241
                if value.endswith("\\\n"):
 
242
                    value = value[:-2]
 
243
                    continuation = True
 
244
                else:
 
245
                    continuation = False
 
246
                value = _parse_string(value)
 
247
                ret._values[section][setting] = value
 
248
                if not continuation:
 
249
                    setting = None
 
250
            else: # continuation line
 
251
                if line.endswith("\\\n"):
 
252
                    line = line[:-2]
 
253
                    continuation = True
 
254
                else:
 
255
                    continuation = False
 
256
                value = _parse_string(line)
 
257
                ret._values[section][setting] += value
 
258
                if not continuation:
 
259
                    setting = None
 
260
        return ret
 
261
 
 
262
    @classmethod
 
263
    def from_path(cls, path):
 
264
        """Read configuration from a file on disk."""
 
265
        f = GitFile(path, 'rb')
 
266
        try:
 
267
            ret = cls.from_file(f)
 
268
            ret.path = path
 
269
            return ret
 
270
        finally:
 
271
            f.close()
 
272
 
 
273
    def write_to_path(self, path=None):
 
274
        """Write configuration to a file on disk."""
 
275
        if path is None:
 
276
            path = self.path
 
277
        f = GitFile(path, 'wb')
 
278
        try:
 
279
            self.write_to_file(f)
 
280
        finally:
 
281
            f.close()
 
282
 
 
283
    def write_to_file(self, f):
 
284
        """Write configuration to a file-like object."""
 
285
        for section, values in self._values.iteritems():
 
286
            try:
 
287
                section_name, subsection_name = section
 
288
            except ValueError:
 
289
                (section_name, ) = section
 
290
                subsection_name = None
 
291
            if subsection_name is None:
 
292
                f.write("[%s]\n" % section_name)
 
293
            else:
 
294
                f.write("[%s \"%s\"]\n" % (section_name, subsection_name))
 
295
            for key, value in values.iteritems():
 
296
                f.write("%s = %s\n" % (key, _escape_value(value)))
 
297
 
 
298
 
 
299
class StackedConfig(Config):
 
300
    """Configuration which reads from multiple config files.."""
 
301
 
 
302
    def __init__(self, backends, writable=None):
 
303
        self.backends = backends
 
304
        self.writable = writable
 
305
 
 
306
    def __repr__(self):
 
307
        return "<%s for %r>" % (self.__class__.__name__, self.backends)
 
308
 
 
309
    @classmethod
 
310
    def default_backends(cls):
 
311
        """Retrieve the default configuration.
 
312
 
 
313
        This will look in the repository configuration (if for_path is
 
314
        specified), the users' home directory and the system
 
315
        configuration.
 
316
        """
 
317
        paths = []
 
318
        paths.append(os.path.expanduser("~/.gitconfig"))
 
319
        paths.append("/etc/gitconfig")
 
320
        backends = []
 
321
        for path in paths:
 
322
            try:
 
323
                cf = ConfigFile.from_path(path)
 
324
            except (IOError, OSError), e:
 
325
                if e.errno != errno.ENOENT:
 
326
                    raise
 
327
                else:
 
328
                    continue
 
329
            backends.append(cf)
 
330
        return backends
 
331
 
 
332
    def get(self, section, name):
 
333
        for backend in self.backends:
 
334
            try:
 
335
                return backend.get(section, name)
 
336
            except KeyError:
 
337
                pass
 
338
        raise KeyError(name)
 
339
 
 
340
    def set(self, section, name, value):
 
341
        if self.writable is None:
 
342
            raise NotImplementedError(self.set)
 
343
        return self.writable.set(section, name, value)