Skip to content

Commit c41c1a1

Browse files
committed
Config: move to cffi
This halves the amount of code we have to take into account for dealing with the config. There is a slight change in the API. Config.get_multivar() returns an iterator instead of a list, which lets us reuse code from the general iterator and is closer to libgit2's API.
1 parent 3cc129d commit c41c1a1

File tree

11 files changed

+339
-653
lines changed

11 files changed

+339
-653
lines changed

pygit2/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .settings import Settings
3939
from .credentials import *
4040
from .remote import Remote, get_credentials
41+
from .config import Config
4142
from .errors import check_error
4243
from .ffi import ffi, C, to_str
4344

pygit2/config.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2010-2014 The pygit2 contributors
4+
#
5+
# This file is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License, version 2,
7+
# as published by the Free Software Foundation.
8+
#
9+
# In addition to the permissions in the GNU General Public License,
10+
# the authors give you unlimited permission to link the compiled
11+
# version of this file into combinations with other programs,
12+
# and to distribute those combinations without any restriction
13+
# coming from the use of this file. (The General Public License
14+
# restrictions do apply in other respects; for example, they cover
15+
# modification of the file, and distribution when not linked into
16+
# a combined executable.)
17+
#
18+
# This file is distributed in the hope that it will be useful, but
19+
# WITHOUT ANY WARRANTY; without even the implied warranty of
20+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21+
# General Public License for more details.
22+
#
23+
# You should have received a copy of the GNU General Public License
24+
# along with this program; see the file COPYING. If not, write to
25+
# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
26+
# Boston, MA 02110-1301, USA.
27+
28+
# Import from the future
29+
from __future__ import absolute_import, unicode_literals
30+
31+
from _pygit2 import Oid
32+
33+
from .ffi import ffi, C, to_str, is_string
34+
from .errors import check_error, GitError
35+
from .refspec import Refspec
36+
37+
def assert_string(v, desc):
38+
if not is_string(v):
39+
raise TypeError("%s must be a string" % desc)
40+
41+
class ConfigIterator(object):
42+
43+
def __init__(self, config, ptr):
44+
self._iter = ptr
45+
self._config = config
46+
47+
def __del__(self):
48+
C.git_config_iterator_free(self._iter)
49+
50+
def __iter__(self):
51+
return self
52+
53+
def _next_entry(self):
54+
centry = ffi.new('git_config_entry **')
55+
err = C.git_config_next(centry, self._iter)
56+
check_error(err)
57+
58+
return centry[0]
59+
60+
def next(self):
61+
return self.__next__()
62+
63+
def __next__(self):
64+
entry = self._next_entry()
65+
name = ffi.string(entry.name).decode('utf-8')
66+
value = ffi.string(entry.value).decode('utf-8')
67+
68+
return name, value
69+
70+
class ConfigMultivarIterator(ConfigIterator):
71+
def __next__(self):
72+
entry = self._next_entry()
73+
74+
return ffi.string(entry.value).decode('utf-8')
75+
76+
class Config(object):
77+
"""Git configuration management"""
78+
79+
def __init__(self, path=None):
80+
cconfig = ffi.new('git_config **')
81+
82+
if not path:
83+
err = C.git_config_new(cconfig)
84+
else:
85+
assert_string(path, "path")
86+
err = C.git_config_open_ondisk(cconfig, to_str(path))
87+
88+
check_error(err, True)
89+
self._config = cconfig[0]
90+
91+
@classmethod
92+
def from_c(cls, repo, ptr):
93+
config = cls.__new__(cls)
94+
config._repo = repo
95+
config._config = ptr
96+
97+
return config
98+
99+
def __del__(self):
100+
C.git_config_free(self._config)
101+
102+
def _get(self, key):
103+
assert_string(key, "key")
104+
105+
cstr = ffi.new('char **')
106+
err = C.git_config_get_string(cstr, self._config, to_str(key))
107+
108+
return err, cstr
109+
110+
def _get_string(self, key):
111+
err, cstr = self._get(key)
112+
113+
if err == C.GIT_ENOTFOUND:
114+
raise KeyError(key)
115+
116+
check_error(err)
117+
return cstr[0]
118+
119+
def __contains__(self, key):
120+
err, cstr = self._get(key)
121+
122+
if err == C.GIT_ENOTFOUND:
123+
return False
124+
125+
check_error(err)
126+
127+
return True
128+
129+
def __getitem__(self, key):
130+
val = self._get_string(key)
131+
132+
return ffi.string(val).decode()
133+
134+
def __setitem__(self, key, value):
135+
assert_string(key, "key")
136+
137+
err = 0
138+
if isinstance(value, bool):
139+
err = C.git_config_set_bool(self._config, to_str(key), value)
140+
elif isinstance(value, int):
141+
err = C.git_config_set_int64(self._config, to_str(key), value)
142+
else:
143+
err = C.git_config_set_string(self._config, to_str(key), to_str(value))
144+
145+
check_error(err)
146+
147+
def __delitem__(self, key):
148+
assert_string(key, "key")
149+
150+
err = C.git_config_delete_entry(self._config, to_str(key))
151+
check_error(err)
152+
153+
def __iter__(self):
154+
citer = ffi.new('git_config_iterator **')
155+
err = C.git_config_iterator_new(citer, self._config)
156+
check_error(err)
157+
158+
return ConfigIterator(self, citer[0])
159+
160+
def get_multivar(self, name, regex=None):
161+
"""get_multivar(name[, regex]) -> [str, ...]
162+
163+
Get each value of a multivar ''name'' as a list. The optional ''regex''
164+
parameter is expected to be a regular expression to filter the variables
165+
we're interested in."""
166+
167+
assert_string(name, "name")
168+
169+
citer = ffi.new('git_config_iterator **')
170+
err = C.git_config_multivar_iterator_new(citer, self._config, to_str(name), to_str(regex))
171+
check_error(err)
172+
173+
return ConfigMultivarIterator(self, citer[0])
174+
175+
def set_multivar(self, name, regex, value):
176+
"""set_multivar(name, regex, value)
177+
178+
Set a multivar ''name'' to ''value''. ''regexp'' is a regular expression
179+
to indicate which values to replace"""
180+
181+
assert_string(name, "name")
182+
assert_string(regex, "regex")
183+
assert_string(value, "value")
184+
185+
err = C.git_config_set_multivar(self._config, to_str(name), to_str(regex), to_str(value))
186+
check_error(err)
187+
188+
def get_bool(self, key):
189+
"""get_bool(key) -> Bool
190+
191+
Look up *key* and parse its value as a boolean as per the git-config rules
192+
193+
Truthy values are: 'true', 1, 'on' or 'yes'. Falsy values are: 'false',
194+
0, 'off' and 'no'"""
195+
196+
val = self._get_string(key)
197+
res = ffi.new('int *')
198+
err = C.git_config_parse_bool(res, val)
199+
check_error(err)
200+
201+
return res[0] != 0
202+
203+
def get_int(self, key):
204+
"""get_int(key) -> int
205+
206+
Look up *key* and parse its value as an integer as per the git-config rules.
207+
208+
A value can have a suffix 'k', 'm' or 'g' which stand for 'kilo', 'mega' and
209+
'giga' respectively"""
210+
211+
val = self._get_string(key)
212+
res = ffi.new('int64_t *')
213+
err = C.git_config_parse_int64(res, val)
214+
check_error(err)
215+
216+
return res[0]
217+
218+
def add_file(self, path, level=0, force=0):
219+
"""add_file(path, level=0, force=0)
220+
221+
Add a config file instance to an existing config."""
222+
223+
err = C.git_config_add_file_ondisk(self._config, to_str(path), level, force)
224+
check_error(err)
225+
226+
#
227+
# Static methods to get specialized version of the config
228+
#
229+
230+
@staticmethod
231+
def _from_found_config(fn):
232+
buf = ffi.new('char []', C.GIT_PATH_MAX)
233+
err = fn(buf, C.GIT_PATH_MAX)
234+
check_error(err, True)
235+
return Config(ffi.string(buf).decode())
236+
237+
@staticmethod
238+
def get_system_config():
239+
"""get_system_config() -> Config
240+
241+
Return an object representing the system configuration file."""
242+
243+
return Config._from_found_config(C.git_config_find_system)
244+
245+
@staticmethod
246+
def get_global_config():
247+
"""get_global_config() -> Config
248+
249+
Return an object representing the global configuration file."""
250+
251+
return Config._from_found_config(C.git_config_find_global)
252+
253+
@staticmethod
254+
def get_xdg_config():
255+
"""get_xdg_config() -> Config
256+
257+
Return an object representing the global configuration file."""
258+
259+
return Config._from_found_config(C.git_config_find_xdg)

pygit2/decl.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ typedef ... git_push;
55
typedef ... git_cred;
66
typedef ... git_diff_file;
77
typedef ... git_tree;
8+
typedef ... git_config;
9+
typedef ... git_config_iterator;
810

911
#define GIT_OID_RAWSZ ...
12+
#define GIT_PATH_MAX ...
1013

1114
typedef struct git_oid {
1215
unsigned char id[20];
@@ -212,3 +215,49 @@ int git_clone(git_repository **out,
212215
const char *url,
213216
const char *local_path,
214217
const git_clone_options *options);
218+
219+
220+
typedef enum {
221+
GIT_CONFIG_LEVEL_SYSTEM = 1,
222+
GIT_CONFIG_LEVEL_XDG = 2,
223+
GIT_CONFIG_LEVEL_GLOBAL = 3,
224+
GIT_CONFIG_LEVEL_LOCAL = 4,
225+
GIT_CONFIG_LEVEL_APP = 5,
226+
GIT_CONFIG_HIGHEST_LEVEL = -1,
227+
} git_config_level_t;
228+
229+
typedef struct {
230+
const char *name;
231+
const char *value;
232+
git_config_level_t level;
233+
} git_config_entry;
234+
235+
int git_repository_config(git_config **out, git_repository *repo);
236+
void git_config_free(git_config *cfg);
237+
238+
int git_config_get_string(const char **out, const git_config *cfg, const char *name);
239+
int git_config_set_string(git_config *cfg, const char *name, const char *value);
240+
int git_config_set_bool(git_config *cfg, const char *name, int value);
241+
int git_config_set_int64(git_config *cfg, const char *name, int64_t value);
242+
243+
int git_config_parse_bool(int *out, const char *value);
244+
int git_config_parse_int64(int64_t *out, const char *value);
245+
246+
int git_config_delete_entry(git_config *cfg, const char *name);
247+
int git_config_add_file_ondisk(git_config *cfg,
248+
const char *path,
249+
git_config_level_t level,
250+
int force);
251+
252+
int git_config_iterator_new(git_config_iterator **out, const git_config *cfg);
253+
int git_config_next(git_config_entry **entry, git_config_iterator *iter);
254+
void git_config_iterator_free(git_config_iterator *iter);
255+
256+
int git_config_multivar_iterator_new(git_config_iterator **out, const git_config *cfg, const char *name, const char *regexp);
257+
int git_config_set_multivar(git_config *cfg, const char *name, const char *regexp, const char *value);
258+
259+
int git_config_new(git_config **out);
260+
int git_config_open_ondisk(git_config **out, const char *path);
261+
int git_config_find_system(char *out, size_t length);
262+
int git_config_find_global(char *out, size_t length);
263+
int git_config_find_xdg(char *out, size_t length);

pygit2/errors.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
from _pygit2 import GitError
3535

36-
def check_error(err):
36+
def check_error(err, io=False):
3737
if err >= 0:
3838
return
3939

@@ -45,7 +45,10 @@ def check_error(err):
4545
if err in [C.GIT_EEXISTS, C.GIT_EINVALIDSPEC, C.GIT_EEXISTS, C.GIT_EAMBIGUOUS]:
4646
raise ValueError(message)
4747
elif err == C.GIT_ENOTFOUND:
48-
raise KeyError(message)
48+
if io:
49+
raise IOError(message)
50+
else:
51+
raise KeyError(message)
4952
elif err == C.GIT_EINVALIDSPEC:
5053
raise ValueError(message)
5154
elif err == C.GIT_ITEROVER:

pygit2/repository.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .ffi import ffi, C, to_str
3939
from .errors import check_error
4040
from .remote import Remote
41+
from .config import Config
4142

4243
class Repository(_Repository):
4344

@@ -110,6 +111,23 @@ def remotes(self):
110111
C.git_strarray_free(names)
111112

112113

114+
#
115+
# Configuration
116+
#
117+
@property
118+
def config(self):
119+
"""The configuration file for this repository
120+
121+
If a the configuration hasn't been set yet, the default config for
122+
repository will be returned, including global and system configurations
123+
(if they are available)."""
124+
125+
cconfig = ffi.new('git_config **')
126+
err = C.git_repository_config(cconfig, self._repo)
127+
check_error(err)
128+
129+
return Config.from_c(self, cconfig[0])
130+
113131
#
114132
# References
115133
#

0 commit comments

Comments
 (0)