diff options
-rw-r--r-- | git/config.py | 105 | ||||
-rw-r--r-- | git/test/fixtures/git_config_with_comments | 183 | ||||
-rw-r--r-- | git/test/test_config.py | 22 |
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") |