Page MenuHomeSoftware Heritage

No OneTemporary

diff --git a/dulwich/config.py b/dulwich/config.py
index f43850a7..96f9dbb3 100644
--- a/dulwich/config.py
+++ b/dulwich/config.py
@@ -1,622 +1,622 @@
# config.py - Reading and writing Git config files
# Copyright (C) 2011-2013 Jelmer Vernooij <jelmer@jelmer.uk>
#
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
# General Public License as public by the Free Software Foundation; version 2.0
# or (at your option) any later version. You can redistribute it and/or
# modify it under the terms of either of these two licenses.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the licenses; if not, see
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
# License, Version 2.0.
#
"""Reading and writing Git configuration files.
TODO:
* preserve formatting when updating configuration files
* treat subsection names as case-insensitive for [branch.foo] style
subsections
"""
import os
import sys
from typing import BinaryIO, Tuple, Optional
from collections import (
OrderedDict,
)
try:
from collections.abc import (
Iterable,
MutableMapping,
)
except ImportError: # python < 3.7
from collections import (
Iterable,
MutableMapping,
)
from dulwich.file import GitFile
SENTINAL = object()
def lower_key(key):
if isinstance(key, (bytes, str)):
return key.lower()
if isinstance(key, Iterable):
return type(key)(map(lower_key, key))
return key
class CaseInsensitiveDict(OrderedDict):
@classmethod
def make(cls, dict_in=None):
if isinstance(dict_in, cls):
return dict_in
out = cls()
if dict_in is None:
return out
if not isinstance(dict_in, MutableMapping):
raise TypeError
for key, value in dict_in.items():
out[key] = value
return out
def __setitem__(self, key, value, **kwargs):
key = lower_key(key)
super(CaseInsensitiveDict, self).__setitem__(key, value, **kwargs)
def __getitem__(self, item):
key = lower_key(item)
return super(CaseInsensitiveDict, self).__getitem__(key)
def get(self, key, default=SENTINAL):
try:
return self[key]
except KeyError:
pass
if default is SENTINAL:
return type(self)()
return default
def setdefault(self, key, default=SENTINAL):
try:
return self[key]
except KeyError:
self[key] = self.get(key, default)
return self[key]
class Config(object):
"""A Git configuration."""
def get(self, section, name):
"""Retrieve the contents of a configuration setting.
Args:
section: Tuple with section name and optional subsection namee
subsection: Subsection name
Returns:
Contents of the setting
Raises:
KeyError: if the value is not set
"""
raise NotImplementedError(self.get)
def get_boolean(self, section, name, default=None):
"""Retrieve a configuration setting as boolean.
Args:
section: Tuple with section name and optional subsection name
name: Name of the setting, including section and possible
subsection.
Returns:
Contents of the setting
Raises:
KeyError: if the value is not set
"""
try:
value = self.get(section, name)
except KeyError:
return default
if value.lower() == b"true":
return True
elif value.lower() == b"false":
return False
raise ValueError("not a valid boolean string: %r" % value)
def set(self, section, name, value):
"""Set a configuration value.
Args:
section: Tuple with section name and optional subsection namee
name: Name of the configuration value, including section
and optional subsection
value: value of the setting
"""
raise NotImplementedError(self.set)
def iteritems(self, section):
"""Iterate over the configuration pairs for a specific section.
Args:
section: Tuple with section name and optional subsection namee
Returns:
Iterator over (name, value) pairs
"""
raise NotImplementedError(self.iteritems)
def itersections(self):
"""Iterate over the sections.
Returns: Iterator over section tuples
"""
raise NotImplementedError(self.itersections)
def has_section(self, name):
"""Check if a specified section exists.
Args:
name: Name of section to check for
Returns:
boolean indicating whether the section exists
"""
return name in self.itersections()
class ConfigDict(Config, MutableMapping):
"""Git configuration stored in a dictionary."""
def __init__(self, values=None, encoding=None):
"""Create a new ConfigDict."""
if encoding is None:
encoding = sys.getdefaultencoding()
self.encoding = encoding
self._values = CaseInsensitiveDict.make(values)
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self._values)
def __eq__(self, other):
return isinstance(other, self.__class__) and other._values == self._values
def __getitem__(self, key):
return self._values.__getitem__(key)
def __setitem__(self, key, value):
return self._values.__setitem__(key, value)
def __delitem__(self, key):
return self._values.__delitem__(key)
def __iter__(self):
return self._values.__iter__()
def __len__(self):
return self._values.__len__()
@classmethod
def _parse_setting(cls, name):
parts = name.split(".")
if len(parts) == 3:
return (parts[0], parts[1], parts[2])
else:
return (parts[0], None, parts[1])
def _check_section_and_name(self, section, name):
if not isinstance(section, tuple):
section = (section,)
section = tuple(
[
subsection.encode(self.encoding)
if not isinstance(subsection, bytes)
else subsection
for subsection in section
]
)
if not isinstance(name, bytes):
name = name.encode(self.encoding)
return section, name
def get(self, section, name):
section, name = self._check_section_and_name(section, name)
if len(section) > 1:
try:
return self._values[section][name]
except KeyError:
pass
return self._values[(section[0],)][name]
def set(self, section, name, value):
section, name = self._check_section_and_name(section, name)
if type(value) not in (bool, bytes):
value = value.encode(self.encoding)
self._values.setdefault(section)[name] = value
def iteritems(self, section):
return self._values.get(section).items()
def itersections(self):
return self._values.keys()
def _format_string(value):
if (
value.startswith(b" ")
or value.startswith(b"\t")
or value.endswith(b" ")
or b"#" in value
or value.endswith(b"\t")
):
return b'"' + _escape_value(value) + b'"'
else:
return _escape_value(value)
_ESCAPE_TABLE = {
ord(b"\\"): ord(b"\\"),
ord(b'"'): ord(b'"'),
ord(b"n"): ord(b"\n"),
ord(b"t"): ord(b"\t"),
ord(b"b"): ord(b"\b"),
}
_COMMENT_CHARS = [ord(b"#"), ord(b";")]
_WHITESPACE_CHARS = [ord(b"\t"), ord(b" ")]
def _parse_string(value):
value = bytearray(value.strip())
ret = bytearray()
whitespace = bytearray()
in_quotes = False
i = 0
while i < len(value):
c = value[i]
if c == ord(b"\\"):
i += 1
try:
v = _ESCAPE_TABLE[value[i]]
except IndexError:
raise ValueError(
"escape character in %r at %d before end of string" % (value, i)
)
except KeyError:
raise ValueError(
"escape character followed by unknown character "
"%s at %d in %r" % (value[i], i, value)
)
if whitespace:
ret.extend(whitespace)
whitespace = bytearray()
ret.append(v)
elif c == ord(b'"'):
in_quotes = not in_quotes
elif c in _COMMENT_CHARS and not in_quotes:
# the rest of the line is a comment
break
elif c in _WHITESPACE_CHARS:
whitespace.append(c)
else:
if whitespace:
ret.extend(whitespace)
whitespace = bytearray()
ret.append(c)
i += 1
if in_quotes:
raise ValueError("missing end quote")
return bytes(ret)
def _escape_value(value):
"""Escape a value."""
value = value.replace(b"\\", b"\\\\")
value = value.replace(b"\n", b"\\n")
value = value.replace(b"\t", b"\\t")
value = value.replace(b'"', b'\\"')
return value
def _check_variable_name(name):
for i in range(len(name)):
c = name[i : i + 1]
if not c.isalnum() and c != b"-":
return False
return True
def _check_section_name(name):
for i in range(len(name)):
c = name[i : i + 1]
if not c.isalnum() and c not in (b"-", b"."):
return False
return True
def _strip_comments(line):
comment_bytes = {ord(b"#"), ord(b";")}
quote = ord(b'"')
string_open = False
# Normalize line to bytearray for simple 2/3 compatibility
for i, character in enumerate(bytearray(line)):
# Comment characters outside balanced quotes denote comment start
if character == quote:
string_open = not string_open
elif not string_open and character in comment_bytes:
return line[:i]
return line
class ConfigFile(ConfigDict):
"""A Git configuration file, like .git/config or ~/.gitconfig."""
def __init__(self, values=None, encoding=None):
super(ConfigFile, self).__init__(values=values, encoding=encoding)
self.path = None
- @classmethod
+ @classmethod # noqa: C901
def from_file(cls, f: BinaryIO) -> "ConfigFile": # noqa: C901
"""Read configuration from a file-like object."""
ret = cls()
section = None # type: Optional[Tuple[bytes, ...]]
setting = None
continuation = None
for lineno, line in enumerate(f.readlines()):
if lineno == 0 and line.startswith(b'\xef\xbb\xbf'):
line = line[3:]
line = line.lstrip()
if setting is None:
# Parse section header ("[bla]")
if len(line) > 0 and line[:1] == b"[":
line = _strip_comments(line).rstrip()
try:
last = line.index(b"]")
except ValueError:
raise ValueError("expected trailing ]")
pts = line[1:last].split(b" ", 1)
line = line[last + 1 :]
if len(pts) == 2:
if pts[1][:1] != b'"' or pts[1][-1:] != b'"':
raise ValueError("Invalid subsection %r" % pts[1])
else:
pts[1] = pts[1][1:-1]
if not _check_section_name(pts[0]):
raise ValueError("invalid section name %r" % pts[0])
section = (pts[0], pts[1])
else:
if not _check_section_name(pts[0]):
raise ValueError("invalid section name %r" % pts[0])
pts = pts[0].split(b".", 1)
if len(pts) == 2:
section = (pts[0], pts[1])
else:
section = (pts[0],)
ret._values.setdefault(section)
if _strip_comments(line).strip() == b"":
continue
if section is None:
raise ValueError("setting %r without section" % line)
try:
setting, value = line.split(b"=", 1)
except ValueError:
setting = line
value = b"true"
setting = setting.strip()
if not _check_variable_name(setting):
raise ValueError("invalid variable name %r" % setting)
if value.endswith(b"\\\n"):
continuation = value[:-2]
else:
continuation = None
value = _parse_string(value)
ret._values[section][setting] = value
setting = None
else: # continuation line
if line.endswith(b"\\\n"):
continuation += line[:-2]
else:
continuation += line
value = _parse_string(continuation)
ret._values[section][setting] = value
continuation = None
setting = None
return ret
@classmethod
def from_path(cls, path) -> "ConfigFile":
"""Read configuration from a file on disk."""
with GitFile(path, "rb") as f:
ret = cls.from_file(f)
ret.path = path
return ret
def write_to_path(self, path=None) -> None:
"""Write configuration to a file on disk."""
if path is None:
path = self.path
with GitFile(path, "wb") as f:
self.write_to_file(f)
def write_to_file(self, f: BinaryIO) -> None:
"""Write configuration to a file-like object."""
for section, values in self._values.items():
try:
section_name, subsection_name = section
except ValueError:
(section_name,) = section
subsection_name = None
if subsection_name is None:
f.write(b"[" + section_name + b"]\n")
else:
f.write(b"[" + section_name + b' "' + subsection_name + b'"]\n')
for key, value in values.items():
if value is True:
value = b"true"
elif value is False:
value = b"false"
else:
value = _format_string(value)
f.write(b"\t" + key + b" = " + value + b"\n")
def get_xdg_config_home_path(*path_segments):
xdg_config_home = os.environ.get(
"XDG_CONFIG_HOME",
os.path.expanduser("~/.config/"),
)
return os.path.join(xdg_config_home, *path_segments)
def _find_git_in_win_path():
for exe in ("git.exe", "git.cmd"):
for path in os.environ.get("PATH", "").split(";"):
if os.path.exists(os.path.join(path, exe)):
# exe path is .../Git/bin/git.exe or .../Git/cmd/git.exe
git_dir, _bin_dir = os.path.split(path)
yield git_dir
break
def _find_git_in_win_reg():
import platform
import winreg
if platform.machine() == "AMD64":
subkey = (
"SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\"
"CurrentVersion\\Uninstall\\Git_is1"
)
else:
subkey = (
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
"Uninstall\\Git_is1"
)
for key in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
try:
with winreg.OpenKey(key, subkey) as k:
val, typ = winreg.QueryValueEx(k, "InstallLocation")
if typ == winreg.REG_SZ:
yield val
except OSError:
pass
# There is no set standard for system config dirs on windows. We try the
# following:
# - %PROGRAMDATA%/Git/config - (deprecated) Windows config dir per CGit docs
# - %PROGRAMFILES%/Git/etc/gitconfig - Git for Windows (msysgit) config dir
# Used if CGit installation (Git/bin/git.exe) is found in PATH in the
# system registry
def get_win_system_paths():
if "PROGRAMDATA" in os.environ:
yield os.path.join(os.environ["PROGRAMDATA"], "Git", "config")
for git_dir in _find_git_in_win_path():
yield os.path.join(git_dir, "etc", "gitconfig")
for git_dir in _find_git_in_win_reg():
yield os.path.join(git_dir, "etc", "gitconfig")
class StackedConfig(Config):
"""Configuration which reads from multiple config files.."""
def __init__(self, backends, writable=None):
self.backends = backends
self.writable = writable
def __repr__(self):
return "<%s for %r>" % (self.__class__.__name__, self.backends)
@classmethod
def default(cls):
return cls(cls.default_backends())
@classmethod
def default_backends(cls):
"""Retrieve the default configuration.
See git-config(1) for details on the files searched.
"""
paths = []
paths.append(os.path.expanduser("~/.gitconfig"))
paths.append(get_xdg_config_home_path("git", "config"))
if "GIT_CONFIG_NOSYSTEM" not in os.environ:
paths.append("/etc/gitconfig")
if sys.platform == "win32":
paths.extend(get_win_system_paths())
backends = []
for path in paths:
try:
cf = ConfigFile.from_path(path)
except FileNotFoundError:
continue
backends.append(cf)
return backends
def get(self, section, name):
if not isinstance(section, tuple):
section = (section,)
for backend in self.backends:
try:
return backend.get(section, name)
except KeyError:
pass
raise KeyError(name)
def set(self, section, name, value):
if self.writable is None:
raise NotImplementedError(self.set)
return self.writable.set(section, name, value)
def parse_submodules(config):
"""Parse a gitmodules GitConfig file, returning submodules.
Args:
config: A `ConfigFile`
Returns:
list of tuples (submodule path, url, name),
where name is quoted part of the section's name.
"""
for section in config.keys():
section_kind, section_name = section
if section_kind == b"submodule":
sm_path = config.get(section, b"path")
sm_url = config.get(section, b"url")
yield (sm_path, sm_url, section_name)

File Metadata

Mime Type
text/x-diff
Expires
Fri, Jul 4, 3:21 PM (4 d, 18 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3304555

Event Timeline