mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-11-13 07:47:23 +00:00
Per-user settings such as sync.smartsync live in ~/.repoconfig/config, but reading or changing them required knowing the file location and running `git config --file ~/.repoconfig/config ...`. Add `repo config` as a thin wrapper around RepoConfig.ForUser(), following the subcommand style of modern `git config`: * `repo config get <name>` prints the value(s), exiting 1 if unset. * `repo config set <name> <value>` sets it. * `repo config unset <name>` removes it; a missing key is a no-op. * `repo config list` shows all settings. Values are stored as strings; `--type=bool|int` validates and canonicalizes them on set and get, like `git config --type`. `-z` makes get and list NUL-terminated for scripting. Setting names are validated by `git config` itself when writing. Factor the value parsing out of GitConfig.GetBoolean()/GetInt() into ParseBoolean()/ParseInt() so the command and the readers agree, and accept on/off/1/0 as booleans like git does. Only the per-user file is handled; per-checkout settings in .repo/manifests.git/config are out of scope. Like other subcommands, it runs from inside a repo checkout. Update docs/repoconfig.md to use the new command in its examples and add zsh completion and the man page. Bug: 569927750 Test: ./run_tests Change-Id: I940d0318549fe9d79f6f9173139f0b96a706f1bf Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/645727 Reviewed-by: Mike Frysinger <vapier@google.com> Tested-by: Insaf Latypov <saferif@google.com> Commit-Queue: Insaf Latypov <saferif@google.com>
186 lines
6.1 KiB
Python
186 lines
6.1 KiB
Python
# 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))
|