summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Thiel <byronimo@gmail.com>2015-01-10 13:59:29 +0100
committerSebastian Thiel <byronimo@gmail.com>2015-01-10 13:59:29 +0100
commitb825dc74773ffa5c7a45b48d72616b222ad2023e (patch)
tree4330409bb0c4fdd02188bda740d435a2a6a0e344
parenta0cb95c5df7a559633c48f5b0f200599c4a62091 (diff)
downloadgitpython-b825dc74773ffa5c7a45b48d72616b222ad2023e.tar.gz
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
-rw-r--r--git/config.py105
-rw-r--r--git/test/fixtures/git_config_with_comments183
-rw-r--r--git/test/test_config.py22
3 files changed, 271 insertions, 39 deletions
diff --git a/git/config.py b/git/config.py
index eefab299..5828a1c1 100644
--- a/git/config.py
+++ b/git/config.py
@@ -22,7 +22,8 @@ from git.compat import (
string_types,
FileType,
defenc,
- with_metaclass
+ with_metaclass,
+ PY3
)
__all__ = ('GitConfigParser', 'SectionConstraint')
@@ -243,7 +244,21 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
cursect = None # None, or a dictionary
optname = None
lineno = 0
+ is_multi_line = False
e = None # None, or an exception
+
+ def string_decode(v):
+ if v[-1] == '\\':
+ v = v[:-1]
+ # end cut trailing escapes to prevent decode error
+
+ if PY3:
+ return v.encode(defenc).decode('unicode_escape')
+ else:
+ return v.decode('string_escape')
+ # end
+ # end
+
while True:
# we assume to read binary !
line = fp.readline().decode(defenc)
@@ -256,46 +271,60 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
# no leading whitespace
continue
- else:
- # is it a section header?
- mo = self.SECTCRE.match(line.strip())
+
+ # is it a section header?
+ mo = self.SECTCRE.match(line.strip())
+ if not is_multi_line and mo:
+ sectname = mo.group('header').strip()
+ if sectname in self._sections:
+ cursect = self._sections[sectname]
+ elif sectname == cp.DEFAULTSECT:
+ cursect = self._defaults
+ else:
+ cursect = self._dict((('__name__', sectname),))
+ self._sections[sectname] = cursect
+ self._proxies[sectname] = None
+ # So sections can't start with a continuation line
+ optname = None
+ # no section header in the file?
+ elif cursect is None:
+ raise cp.MissingSectionHeaderError(fpname, lineno, line)
+ # an option line?
+ elif not is_multi_line:
+ mo = self.OPTCRE.match(line)
if mo:
- sectname = mo.group('header').strip()
- if sectname in self._sections:
- cursect = self._sections[sectname]
- elif sectname == cp.DEFAULTSECT:
- cursect = self._defaults
- else:
- cursect = self._dict((('__name__', sectname),))
- self._sections[sectname] = cursect
- self._proxies[sectname] = None
- # So sections can't start with a continuation line
- optname = None
- # no section header in the file?
- elif cursect is None:
- raise cp.MissingSectionHeaderError(fpname, lineno, line)
- # an option line?
+ # We might just have handled the last line, which could contain a quotation we want to remove
+ optname, vi, optval = mo.group('option', 'vi', 'value')
+ if vi in ('=', ':') and ';' in optval:
+ pos = optval.find(';')
+ if pos != -1 and optval[pos - 1].isspace():
+ optval = optval[:pos]
+ optval = optval.strip()
+ if optval == '""':
+ optval = ''
+ # end handle empty string
+ optname = self.optionxform(optname.rstrip())
+ if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"':
+ is_multi_line = True
+ optval = string_decode(optval[1:])
+ # end handle multi-line
+ cursect[optname] = optval
else:
- mo = self.OPTCRE.match(line)
- if mo:
- optname, vi, optval = mo.group('option', 'vi', 'value')
- if vi in ('=', ':') and ';' in optval:
- pos = optval.find(';')
- if pos != -1 and optval[pos - 1].isspace():
- optval = optval[:pos]
- optval = optval.strip()
- if optval == '""':
- optval = ''
- optname = self.optionxform(optname.rstrip())
- cursect[optname] = optval
- else:
- if not e:
- e = cp.ParsingError(fpname)
- e.append(lineno, repr(line))
- # END
- # END ?
- # END ?
+ if not e:
+ e = cp.ParsingError(fpname)
+ e.append(lineno, repr(line))
+ print(lineno, line)
+ continue
+ else:
+ line = line.rstrip()
+ if line.endswith('"'):
+ is_multi_line = False
+ line = line[:-1]
+ # end handle quotations
+ cursect[optname] += string_decode(line)
+ # END parse section or option
# END while reading
+
# if any parsing errors occurred, raise an exception
if e:
raise e
diff --git a/git/test/fixtures/git_config_with_comments b/git/test/fixtures/git_config_with_comments
new file mode 100644
index 00000000..e9d4443d
--- /dev/null
+++ b/git/test/fixtures/git_config_with_comments
@@ -0,0 +1,183 @@
+[user]
+ name = Cody Veal
+ email = cveal05@gmail.com
+
+[github]
+ user = cjhveal
+
+[advice]
+ statusHints = false
+
+[alias]
+ # add
+ a = add
+ aa = add --all
+ ap = add --patch
+
+ aliases = !git config --list | grep 'alias\\.' | sed 's/alias\\.\\([^=]*\\)=\\(.*\\)/\\1\\\t => \\2/' | sort
+
+ # branch
+ br = branch
+ branches = branch -av
+ cp = cherry-pick
+ 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' -
+ track = checkout -t
+ nb = checkout -b
+
+ # commit
+ amend = commit --amend -C HEAD
+ c = commit
+ ca = commit --amend
+ cm = commit --message
+ msg = commit --allow-empty -m
+
+ co = checkout
+
+ # diff
+ d = diff --color-words # diff by word
+ ds = diff --staged --color-words
+ dd = diff --color-words=. # diff by char
+ dds = diff --staged --color-words=.
+ dl = diff # diff by line
+ dls = diff --staged
+
+ h = help
+
+ # log
+ authors = "!git log --pretty=format:%aN | sort | uniq -c | sort -rn"
+ lc = log ORIG_HEAD.. --stat --no-merges
+ lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative
+ lol = log --graph --decorate --pretty=oneline --abbrev-commit
+ lola = log --graph --decorate --pretty=oneline --abbrev-commit --all
+
+ # merge
+ m = merge
+ mm = merge --no-ff
+ ours = "!f() { git checkout --ours $@ && git add $@; }; f"
+ theirs = "!f() { git checkout --theirs $@ && git add $@; }; f"
+
+ # push/pull
+ l = pull
+ p = push
+ sync = !git pull && git push
+
+ # remotes
+ prune-remotes = "!for remote in `git remote`; do git remote prune $remote; done"
+ r = remote
+
+ # rebase
+ rb = rebase
+ rba = rebase --abort
+ rbc = rebase --continue
+ rbs = rebase --skip
+
+ # reset
+ rh = reset --hard
+ rhh = reset HEAD --hard
+ uncommit = reset --soft HEAD^
+ unstage = reset HEAD --
+ unpush = push -f origin HEAD^:master
+
+ # stash
+ ss = stash
+ sl = stash list
+ sp = stash pop
+ sd = stash drop
+ snapshot = !git stash save "snapshot: $(date)" && git stash apply "stash@{0}"
+
+ # status
+ s = status --short --branch
+ st = status
+
+ # submodule
+ sm = submodule
+ sma = submodule add
+ smu = submodule update --init
+ pup = !git pull && git submodule init && git submodule update
+
+ # file level ignoring
+ assume = update-index --assume-unchanged
+ unassume = update-index --no-assume-unchanged
+ assumed = "!git ls-files -v | grep ^h | cut -c 3-"
+
+
+[apply]
+ whitespace = fix
+
+[color]
+ ui = auto
+
+[color "branch"]
+ current = yellow reverse
+ local = yellow
+ remote = green
+
+[color "diff"]
+ meta = yellow
+ frag = magenta
+ old = red bold
+ new = green bold
+ whitespace = red reverse
+
+[color "status"]
+ added = green
+ changed = yellow
+ untracked = cyan
+
+[core]
+ editor = /usr/bin/vim
+ excludesfile = ~/.gitignore_global
+ attributesfile = ~/.gitattributes
+
+[diff]
+ renames = copies
+ mnemonicprefix = true
+
+[diff "zip"]
+ textconv = unzip -c -a
+
+[merge]
+ log = true
+
+[merge "railsschema"]
+ name = newer Rails schema version
+ driver = "ruby -e '\n\
+ system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n\
+ b = File.read(%(%A))\n\
+ b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n>+ .*/) do\n\
+ %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n\
+ end\n\
+ File.open(%(%A), %(w)) {|f| f.write(b)}\n\
+ exit 1 if b.include?(%(<)*%L)'"
+
+[merge "gemfilelock"]
+ name = relocks the gemfile.lock
+ driver = bundle lock
+
+[pager]
+ color = true
+
+[push]
+ default = upstream
+
+[rerere]
+ enabled = true
+
+[url "git@github.com:"]
+ insteadOf = "gh:"
+ pushInsteadOf = "github:"
+ pushInsteadOf = "git://github.com/"
+
+[url "git://github.com/"]
+ insteadOf = "github:"
+
+[url "git@gist.github.com:"]
+ insteadOf = "gst:"
+ pushInsteadOf = "gist:"
+ pushInsteadOf = "git://gist.github.com/"
+
+[url "git://gist.github.com/"]
+ insteadOf = "gist:"
+
+[url "git@heroku.com:"]
+ insteadOf = "heroku:"
diff --git a/git/test/test_config.py b/git/test/test_config.py
index 546a2fe1..9000c07f 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -3,10 +3,13 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
+# The test test_multi_line_config requires whitespace (especially tabs) to remain
+# flake8: noqa
from git.test.lib import (
TestCase,
- fixture_path
+ fixture_path,
+ assert_equal
)
from git import (
GitConfigParser
@@ -72,6 +75,23 @@ class TestBase(TestCase):
assert r_config.get(sname, oname) == val
# END for each filename
+ def test_multi_line_config(self):
+ file_obj = self._to_memcache(fixture_path("git_config_with_comments"))
+ config = GitConfigParser(file_obj, read_only=False)
+ ev = r"""ruby -e '
+ system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)
+ b = File.read(%(%A))
+ b.sub!(/^<+ .*\nActiveRecord::Schema\.define.:version => (\d+). do\n=+\nActiveRecord::Schema\.define.:version => (\d+). do\n>+ .*/) do
+ %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)
+ end
+ File.open(%(%A), %(w)) {|f| f.write(b)}
+ exit 1 if b.include?(%(<)*%L)'"""
+ assert_equal(config.get('merge "railsschema"', 'driver'), ev)
+ assert_equal(config.get('alias', 'lg'),
+ "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset'"
+ " --abbrev-commit --date=relative")
+ assert len(config.sections()) == 23
+
def test_base(self):
path_repo = fixture_path("git_config")
path_global = fixture_path("git_config_global")