blob: b68956a8f91f3899683ad47079fb7b76a4ca2ade [file]
# Copyright (C) 2026 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
import optparse
from typing import List, Optional
import command
import error
import git_config
class Error(error.RepoExitError):
"""Exit error when the config command fails."""
class NotSetError(error.SilentRepoExitError):
"""Exit quietly when a requested setting is not set, like `git config`."""
# Subcommand name -> number of positional arguments it takes.
_ACTIONS = {
"get": 1,
"set": 2,
"unset": 1,
"list": 0,
}
def _CanonicalizeBool(name: str, value: str) -> str:
"""Returns |value| as "true"/"false", or raises Error."""
ret = git_config.ParseBoolean(value)
if ret is None:
raise Error(f"error: {name}: '{value}' is not a boolean")
return "true" if ret else "false"
def _CanonicalizeInt(name: str, value: str) -> str:
"""Returns |value| as a plain decimal integer, or raises Error."""
ret = git_config.ParseInt(value)
if ret is None:
raise Error(f"error: {name}: '{value}' is not an integer")
return str(ret)
# --type name -> function turning a value into its canonical string form.
_TYPES = {
"bool": _CanonicalizeBool,
"int": _CanonicalizeInt,
}
class Config(command.Command, command.MirrorSafeCommand):
"""Read and write repo's per-user settings."""
COMMON = True
MULTI_MANIFEST_SUPPORT = True
helpSummary = "Read and write per-user repo settings"
helpUsage = """
%prog [--type=<type>] [-z] get <name>
%prog [--type=<type>] set <name> <value>
%prog unset <name>
%prog [-z] list
"""
helpDescription = """
The '%prog' command reads and writes repo's own per-user settings. These
are separate from git's config and from a checkout's settings; see
docs/repoconfig.md for where they live and which settings repo understands.
Settings are named <section>.<name>, e.g. sync.smartsync. 'get' prints
every value of the setting and exits 1 without output if it is not set,
like `git config`.
Values are stored as strings. With --type, 'set' validates the value and
stores it in canonical form (e.g. "on" as "true", "1k" as "1024"), and
'get' prints the canonical form, like `git config --type`.
"""
def _Options(self, p: optparse.OptionParser) -> None:
p.add_option(
"--type",
choices=sorted(_TYPES),
help="validate and canonicalize the value as this type for "
"get and set (%s)" % ", ".join(sorted(_TYPES)),
)
p.add_option(
"-z",
"--null",
action="store_true",
help="terminate output of get and list with NUL instead of "
"newline, and separate key and value with a newline",
)
def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None:
if not args:
self.Usage()
action, rest = args[0], args[1:]
if action not in _ACTIONS:
self.OptionParser.error(f"unknown action '{action}'")
if len(rest) != _ACTIONS[action]:
self.Usage()
if opt.type and action not in ("get", "set"):
self.OptionParser.error("--type only applies to get and set")
if opt.null and action not in ("get", "list"):
self.OptionParser.error("-z/--null only applies to get and list")
def Execute(self, opt: optparse.Values, args: List[str]) -> None:
config = git_config.RepoConfig.ForUser()
action, rest = args[0], args[1:]
dispatch = {
"get": self._ExecuteGet,
"set": self._ExecuteSet,
"unset": self._ExecuteUnset,
"list": self._ExecuteList,
}[action]
dispatch(opt, config, *rest)
@staticmethod
def _Print(opt: optparse.Values, text: str) -> None:
"""Prints |text| terminated by NUL with -z, else by newline."""
print(text, end="\0" if opt.null else "\n")
@staticmethod
def _Format(opt: optparse.Values, name: str, value: Optional[str]) -> str:
"""Formats a value for output, canonicalizing it per --type."""
if value is None:
# A key without `=` in the file has no value; git prints a blank.
return ""
if opt.type:
return _TYPES[opt.type](name, value)
return value
def _ExecuteGet(
self, opt: optparse.Values, config: git_config.RepoConfig, name: str
) -> None:
values = config.GetString(name, all_keys=True)
if not values:
raise NotSetError(exit_code=1)
for value in values:
self._Print(opt, self._Format(opt, name, value))
def _ExecuteSet(
self,
opt: optparse.Values,
config: git_config.RepoConfig,
name: str,
value: str,
) -> None:
if opt.type:
value = _TYPES[opt.type](name, value)
self._Write(config, name, value)
def _ExecuteUnset(
self, opt: optparse.Values, config: git_config.RepoConfig, name: str
) -> None:
self._Write(config, name, None)
def _ExecuteList(
self, opt: optparse.Values, config: git_config.RepoConfig
) -> None:
sep = "\n" if opt.null else "="
for key in sorted(config.DumpConfigDict()):
for value in config.GetString(key, all_keys=True):
self._Print(opt, key if value is None else f"{key}{sep}{value}")
@staticmethod
def _Write(
config: git_config.RepoConfig, name: str, value: Optional[str]
) -> None:
"""Writes |value| (None to delete) via `git config`, which also
validates the name."""
try:
config.SetString(name, value)
except error.GitError as e:
raise Error(str(e))