Skip to content

Commit b825dc7

Browse files
committed
Implemented multi-line parsing of git-config to the point where a sepcific test-file is working.
This brings us much closer to what git can do, and should at least prevent errors while reading configuration files (which would break a lot of features, like handling of remotes since these rely reading configuration files). Fixes #112
1 parent a0cb95c commit b825dc7

File tree

3 files changed

+271
-39
lines changed

3 files changed

+271
-39
lines changed

git/config.py

+67-38
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
string_types,
2323
FileType,
2424
defenc,
25-
with_metaclass
25+
with_metaclass,
26+
PY3
2627
)
2728

2829
__all__ = ('GitConfigParser', 'SectionConstraint')
@@ -243,7 +244,21 @@ def _read(self, fp, fpname):
243244
cursect = None # None, or a dictionary
244245
optname = None
245246
lineno = 0
247+
is_multi_line = False
246248
e = None # None, or an exception
249+
250+
def string_decode(v):
251+
if v[-1] == '\\':
252+
v = v[:-1]
253+
# end cut trailing escapes to prevent decode error
254+
255+
if PY3:
256+
return v.encode(defenc).decode('unicode_escape')
257+
else:
258+
return v.decode('string_escape')
259+
# end
260+
# end
261+
247262
while True:
248263
# we assume to read binary !
249264
line = fp.readline().decode(defenc)
@@ -256,46 +271,60 @@ def _read(self, fp, fpname):
256271
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
257272
# no leading whitespace
258273
continue
259-
else:
260-
# is it a section header?
261-
mo = self.SECTCRE.match(line.strip())
274+
275+
# is it a section header?
276+
mo = self.SECTCRE.match(line.strip())
277+
if not is_multi_line and mo:
278+
sectname = mo.group('header').strip()
279+
if sectname in self._sections:
280+
cursect = self._sections[sectname]
281+
elif sectname == cp.DEFAULTSECT:
282+
cursect = self._defaults
283+
else:
284+
cursect = self._dict((('__name__', sectname),))
285+
self._sections[sectname] = cursect
286+
self._proxies[sectname] = None
287+
# So sections can't start with a continuation line
288+
optname = None
289+
# no section header in the file?
290+
elif cursect is None:
291+
raise cp.MissingSectionHeaderError(fpname, lineno, line)
292+
# an option line?
293+
elif not is_multi_line:
294+
mo = self.OPTCRE.match(line)
262295
if mo:
263-
sectname = mo.group('header').strip()
264-
if sectname in self._sections:
265-
cursect = self._sections[sectname]
266-
elif sectname == cp.DEFAULTSECT:
267-
cursect = self._defaults
268-
else:
269-
cursect = self._dict((('__name__', sectname),))
270-
self._sections[sectname] = cursect
271-
self._proxies[sectname] = None
272-
# So sections can't start with a continuation line
273-
optname = None
274-
# no section header in the file?
275-
elif cursect is None:
276-
raise cp.MissingSectionHeaderError(fpname, lineno, line)
277-
# an option line?
296+
# We might just have handled the last line, which could contain a quotation we want to remove
297+
optname, vi, optval = mo.group('option', 'vi', 'value')
298+
if vi in ('=', ':') and ';' in optval:
299+
pos = optval.find(';')
300+
if pos != -1 and optval[pos - 1].isspace():
301+
optval = optval[:pos]
302+
optval = optval.strip()
303+
if optval == '""':
304+
optval = ''
305+
# end handle empty string
306+
optname = self.optionxform(optname.rstrip())
307+
if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
308+
is_multi_line = True
309+
optval = string_decode(optval[1:])
310+
# end handle multi-line
311+
cursect[optname] = optval
278312
else:
279-
mo = self.OPTCRE.match(line)
280-
if mo:
281-
optname, vi, optval = mo.group('option', 'vi', 'value')
282-
if vi in ('=', ':') and ';' in optval:
283-
pos = optval.find(';')
284-
if pos != -1 and optval[pos - 1].isspace():
285-
optval = optval[:pos]
286-
optval = optval.strip()
287-
if optval == '""':
288-
optval = ''
289-
optname = self.optionxform(optname.rstrip())
290-
cursect[optname] = optval
291-
else:
292-
if not e:
293-
e = cp.ParsingError(fpname)
294-
e.append(lineno, repr(line))
295-
# END
296-
# END ?
297-
# END ?
313+
if not e:
314+
e = cp.ParsingError(fpname)
315+
e.append(lineno, repr(line))
316+
print(lineno, line)
317+
continue
318+
else:
319+
line = line.rstrip()
320+
if line.endswith('"'):
321+
is_multi_line = False
322+
line = line[:-1]
323+
# end handle quotations
324+
cursect[optname] += string_decode(line)
325+
# END parse section or option
298326
# END while reading
327+
299328
# if any parsing errors occurred, raise an exception
300329
if e:
301330
raise e
+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
[user]
2+
name = Cody Veal
3+
4+
5+
[github]
6+
user = cjhveal
7+
8+
[advice]
9+
statusHints = false
10+
11+
[alias]
12+
# add
13+
a = add
14+
aa = add --all
15+
ap = add --patch
16+
17+
aliases = !git config --list | grep 'alias\\.' | sed 's/alias\\.\\([^=]*\\)=\\(.*\\)/\\1\\\t => \\2/' | sort
18+
19+
# branch
20+
br = branch
21+
branches = branch -av
22+
cp = cherry-pick
23+
diverges = !bash -c 'diff -u <(git rev-list --first-parent "${1}") <(git rev-list --first-parent "${2:-HEAD}"g | sed -ne \"s/^ //p\" | head -1' -
24+
track = checkout -t
25+
nb = checkout -b
26+
27+
# commit
28+
amend = commit --amend -C HEAD
29+
c = commit
30+
ca = commit --amend
31+
cm = commit --message
32+
msg = commit --allow-empty -m
33+
34+
co = checkout
35+
36+
# diff
37+
d = diff --color-words # diff by word
38+
ds = diff --staged --color-words
39+
dd = diff --color-words=. # diff by char
40+
dds = diff --staged --color-words=.
41+
dl = diff # diff by line
42+
dls = diff --staged
43+
44+
h = help
45+
46+
# log
47+
authors = "!git log --pretty=format:%aN | sort | uniq -c | sort -rn"
48+
lc = log ORIG_HEAD.. --stat --no-merges
49+
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative
50+
lol = log --graph --decorate --pretty=oneline --abbrev-commit
51+
lola = log --graph --decorate --pretty=oneline --abbrev-commit --all
52+
53+
# merge
54+
m = merge
55+
mm = merge --no-ff
56+
ours = "!f() { git checkout --ours $@ && git add $@; }; f"
57+
theirs = "!f() { git checkout --theirs $@ && git add $@; }; f"
58+
59+
# push/pull
60+
l = pull
61+
p = push
62+
sync = !git pull && git push
63+
64+
# remotes
65+
prune-remotes = "!for remote in `git remote`; do git remote prune $remote; done"
66+
r = remote
67+
68+
# rebase
69+
rb = rebase
70+
rba = rebase --abort
71+
rbc = rebase --continue
72+
rbs = rebase --skip
73+
74+
# reset
75+
rh = reset --hard
76+
rhh = reset HEAD --hard
77+
uncommit = reset --soft HEAD^
78+
unstage = reset HEAD --
79+
unpush = push -f origin HEAD^:master
80+
81+
# stash
82+
ss = stash
83+
sl = stash list
84+
sp = stash pop
85+
sd = stash drop
86+
snapshot = !git stash save "snapshot: $(date)" && git stash apply "stash@{0}"
87+
88+
# status
89+
s = status --short --branch
90+
st = status
91+
92+
# submodule
93+
sm = submodule
94+
sma = submodule add
95+
smu = submodule update --init
96+
pup = !git pull && git submodule init && git submodule update
97+
98+
# file level ignoring
99+
assume = update-index --assume-unchanged
100+
unassume = update-index --no-assume-unchanged
101+
assumed = "!git ls-files -v | grep ^h | cut -c 3-"
102+
103+
104+
[apply]
105+
whitespace = fix
106+
107+
[color]
108+
ui = auto
109+
110+
[color "branch"]
111+
current = yellow reverse
112+
local = yellow
113+
remote = green
114+
115+
[color "diff"]
116+
meta = yellow
117+
frag = magenta
118+
old = red bold
119+
new = green bold
120+
whitespace = red reverse
121+
122+
[color "status"]
123+
added = green
124+
changed = yellow
125+
untracked = cyan
126+
127+
[core]
128+
editor = /usr/bin/vim
129+
excludesfile = ~/.gitignore_global
130+
attributesfile = ~/.gitattributes
131+
132+
[diff]
133+
renames = copies
134+
mnemonicprefix = true
135+
136+
[diff "zip"]
137+
textconv = unzip -c -a
138+
139+
[merge]
140+
log = true
141+
142+
[merge "railsschema"]
143+
name = newer Rails schema version
144+
driver = "ruby -e '\n\
145+
system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n\
146+
b = File.read(%(%A))\n\
147+
b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n>+ .*/) do\n\
148+
%(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n\
149+
end\n\
150+
File.open(%(%A), %(w)) {|f| f.write(b)}\n\
151+
exit 1 if b.include?(%(<)*%L)'"
152+
153+
[merge "gemfilelock"]
154+
name = relocks the gemfile.lock
155+
driver = bundle lock
156+
157+
[pager]
158+
color = true
159+
160+
[push]
161+
default = upstream
162+
163+
[rerere]
164+
enabled = true
165+
166+
167+
insteadOf = "gh:"
168+
pushInsteadOf = "github:"
169+
pushInsteadOf = "git://github.com/"
170+
171+
[url "git://github.com/"]
172+
insteadOf = "github:"
173+
174+
175+
insteadOf = "gst:"
176+
pushInsteadOf = "gist:"
177+
pushInsteadOf = "git://gist.github.com/"
178+
179+
[url "git://gist.github.com/"]
180+
insteadOf = "gist:"
181+
182+
183+
insteadOf = "heroku:"

git/test/test_config.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
#
44
# This module is part of GitPython and is released under
55
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
6+
# The test test_multi_line_config requires whitespace (especially tabs) to remain
7+
# flake8: noqa
68

79
from git.test.lib import (
810
TestCase,
9-
fixture_path
11+
fixture_path,
12+
assert_equal
1013
)
1114
from git import (
1215
GitConfigParser
@@ -72,6 +75,23 @@ def test_read_write(self):
7275
assert r_config.get(sname, oname) == val
7376
# END for each filename
7477

78+
def test_multi_line_config(self):
79+
file_obj = self._to_memcache(fixture_path("git_config_with_comments"))
80+
config = GitConfigParser(file_obj, read_only=False)
81+
ev = r"""ruby -e '
82+
system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)
83+
b = File.read(%(%A))
84+
b.sub!(/^<+ .*\nActiveRecord::Schema\.define.:version => (\d+). do\n=+\nActiveRecord::Schema\.define.:version => (\d+). do\n>+ .*/) do
85+
%(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)
86+
end
87+
File.open(%(%A), %(w)) {|f| f.write(b)}
88+
exit 1 if b.include?(%(<)*%L)'"""
89+
assert_equal(config.get('merge "railsschema"', 'driver'), ev)
90+
assert_equal(config.get('alias', 'lg'),
91+
"log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
92+
" --abbrev-commit --date=relative")
93+
assert len(config.sections()) == 23
94+
7595
def test_base(self):
7696
path_repo = fixture_path("git_config")
7797
path_global = fixture_path("git_config_global")

0 commit comments

Comments
 (0)