@ -6,15 +6,15 @@
from __future__ import annotations
import abc
import contextlib
import os
import pathlib
import platform
import re
import threading
import typing
from collections import defaultdict
from typing import Collection , Iterable , Literal, Dict
from typing import Collection , Iterable , Iterator, Literal, Dict
from typing import Optional , Sequence , Mapping
import gclient_utils
@ -48,8 +48,8 @@ def determine_scm(root):
return ' diff '
GitConfigScope = Literal [ ' system ' , ' local' , ' worktree ' ]
GitScopeOrder : list [ GitConfigScope ] = [ ' system ' , ' local' , ' worktree ' ]
GitConfigScope = Literal [ ' system ' , ' global' , ' local' , ' worktree ' ]
GitScopeOrder : list [ GitConfigScope ] = [ ' system ' , ' global' , ' local' , ' worktree ' ]
GitFlatConfigData = Mapping [ str , Sequence [ str ] ]
@ -85,7 +85,7 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
"""
@abc.abstractmethod
def set_config_multi ( self , key : str , value : str , * , append : bool ,
def set_config_multi ( self , key : str , value : str , * ,
value_pattern : Optional [ str ] , scope : GitConfigScope ) :
""" When invoked, this should replace all existing values of `key` with
` value ` in the git scope ` scope ` in this state ' s underlying data.
@ -103,7 +103,10 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
""" When invoked, remove a singlar value from `key` in this state ' s underlying data.
If missing_ok is False and ` key ` is not present in the given scope , this
must raise GitConfigUnsetMissingValue with ` key ` .
must raise GitConfigUnsetMissingValue with ` key ` and ` scope ` .
If ` key ` is multi - valued in this scope , this must raise
GitConfigUnsetMultipleValues with ` key ` and ` scope ` .
"""
@abc.abstractmethod
@ -115,7 +118,7 @@ class GitConfigStateBase(metaclass=abc.ABCMeta):
be removed .
If missing_ok is False and ` key ` is not present in the given scope , this
must raise GitConfigUnsetMissingValue with ` key ` .
must raise GitConfigUnsetMissingValue with ` key ` and ` scope ` .
TODO : Make value_pattern an re . Pattern . This wasn ' t done at the time of
this refactor to keep the refactor small .
@ -130,6 +133,26 @@ class GitConfigUnsetMissingValue(ValueError):
)
class GitConfigUnsetMultipleValues ( ValueError ) :
def __init__ ( self , key : str , scope : str ) - > None :
super ( ) . __init__ (
f ' Cannot unset multi-value key { key !r} in scope { scope !r} with modify_all=False. '
)
class GitConfigUneditableScope ( ValueError ) :
def __init__ ( self , scope : str ) - > None :
super ( ) . __init__ ( f ' Cannot edit git config in scope { scope !r} . ' )
class GitConfigUnknownScope ( ValueError ) :
def __init__ ( self , scope : str ) - > None :
super ( ) . __init__ ( f ' Unknown git config scope { scope !r} . ' )
class CachedGitConfigState ( object ) :
""" This represents the observable git configuration state for a given
repository ( whose top - level path is ` root ` ) .
@ -223,7 +246,8 @@ class CachedGitConfigState(object):
key : The specific config key to affect .
value : The value to set . If this is None , ` key ` will be unset .
append : If True and ` value ` is not None , this will append
the value instead of replacing an existing one .
the value instead of replacing an existing one . Must not be
specified with value_pattern .
missing_ok : If ` value ` is None ( i . e . this is an unset operation ) ,
ignore retcode = 5 from ` git config ` ( meaning that the value is
not present ) . If ` value ` is not None , then this option has no
@ -231,15 +255,20 @@ class CachedGitConfigState(object):
GitConfigUnsetMissingValue .
modify_all : If True , this will change a set operation to
` - - replace - all ` , and will change an unset operation to
` - - unset - all ` .
scope : By default this is the local scope , but could be ` system ` ,
` global ` , or ` worktree ` , depending on which config scope you
want to affect .
` - - unset - all ` . Must not be specified with value_pattern .
scope : By default this is the ` local ` scope , but could be ` global `
or ` worktree ` , depending on which config scope you want to affect .
Note that the ` system ` scope cannot be modified .
value_pattern : For use with ` modify_all = True ` , allows
further filtering of the set or unset operation based on
the currently configured value . Ignored for
` modify_all = False ` .
"""
if scope not in GitScopeOrder :
raise GitConfigUnknownScope ( scope )
if scope == ' system ' :
raise GitConfigUneditableScope ( scope )
if value is None :
if modify_all :
self . _impl . unset_config_multi ( key ,
@ -249,13 +278,27 @@ class CachedGitConfigState(object):
else :
self . _impl . unset_config ( key , scope = scope , missing_ok = missing_ok )
else :
if modify_all :
if value_pattern :
if not modify_all :
raise ValueError (
' SetConfig with (value_pattern) and (not modify_all) is invalid. '
)
if append :
raise ValueError (
' SetConfig with (value_pattern) and (append) is invalid. '
)
self . _impl . set_config_multi ( key ,
value ,
append = append ,
value_pattern = value_pattern ,
scope = scope )
else :
if modify_all :
self . _impl . set_config_multi ( key ,
value ,
value_pattern = None ,
scope = scope )
self . _impl . set_config ( key , value , append = append , scope = scope )
# Once the underlying storage has set the value, we clear our cache so
@ -297,13 +340,11 @@ class GitConfigStateReal(GitConfigStateBase):
args . append ( ' --add ' )
GIT . Capture ( args , cwd = self . root )
def set_config_multi ( self , key : str , value : str , * , append : bool ,
def set_config_multi ( self , key : str , value : str , * ,
value_pattern : Optional [ str ] , scope : GitConfigScope ) :
args = [ ' config ' , f ' -- { scope } ' , ' --replace-all ' , key , value ]
if value_pattern is not None :
args . append ( value_pattern )
if append :
args . append ( ' --add ' )
GIT . Capture ( args , cwd = self . root )
def unset_config ( self , key : str , * , scope : GitConfigScope ,
@ -315,6 +356,8 @@ class GitConfigStateReal(GitConfigStateBase):
accepted_retcodes = accepted_retcodes )
except subprocess2 . CalledProcessError as cpe :
if cpe . returncode == 5 :
if b ' multiple values ' in cpe . stderr :
raise GitConfigUnsetMultipleValues ( key , scope )
raise GitConfigUnsetMissingValue ( key , scope )
raise
@ -335,98 +378,151 @@ class GitConfigStateReal(GitConfigStateBase):
class GitConfigStateTest ( GitConfigStateBase ) :
""" A fake implementation of GitConfigStateBase for testing. """
""" A fake implementation of GitConfigStateBase for testing.
To properly initialize this , see tests / scm_mock . py .
"""
def __init__ ( self ,
initial_state : Optional [ Dict [ GitConfigScope ,
GitFlatConfigData ] ] = None ) :
self . state : Dict [ GitConfigScope , Dict [ str , list [ str ] ] ] = { }
if initial_state is not None :
# We want to copy initial_state to make it mutable inside our class.
for scope , data in initial_state . items ( ) :
self . state [ scope ] = { k : list ( v ) for k , v in data . items ( ) }
global_state_lock : threading . Lock ,
global_state : dict [ str , list [ str ] ] ,
* ,
system_state : Optional [ GitFlatConfigData ] = None ,
local_state : Optional [ GitFlatConfigData ] = None ,
worktree_state : Optional [ GitFlatConfigData ] = None ) :
""" Initializes a new (local, worktree) config state, with a reference to
a single global ` global ` state and an optional immutable ` system ` state .
The caller must supply a single shared Lock , plus a mutable reference to
the global - state dictionary .
Optionally , the caller may supply an initial local / worktree
configuration state .
This implementation will hold global_state_lock during all read / write
operations on the ' global ' scope .
"""
self . system_state : GitFlatConfigData = system_state or { }
self . global_state_lock = global_state_lock
self . global_state = global_state
self . worktree_state : dict [ str , list [ str ] ] = { }
if worktree_state is not None :
self . worktree_state = {
k : list ( v )
for k , v in worktree_state . items ( )
}
self . local_state : dict [ str , list [ str ] ] = { }
if local_state is not None :
self . local_state = { k : list ( v ) for k , v in local_state . items ( ) }
super ( ) . __init__ ( )
def _get_scope ( self , scope : GitConfigScope ) - > Dict [ str , list [ str ] ] :
ret = self . state . get ( scope , None )
if ret is None :
ret = { }
self . state [ scope ] = ret
return ret
@contextlib.contextmanager
def _editable_scope (
self , scope : GitConfigScope ) - > Iterator [ dict [ str , list [ str ] ] ] :
if scope == ' system ' :
# This is also checked in CachedGitConfigState.SetConfig, but double
# check here.
raise GitConfigUneditableScope ( scope )
if scope == ' global ' :
with self . global_state_lock :
yield self . global_state
elif scope == ' local ' :
yield self . local_state
elif scope == ' worktree ' :
yield self . worktree_state
else :
# This is also checked in CachedGitConfigState.SetConfig, but double
# check here.
raise GitConfigUnknownScope ( scope )
def load_config ( self ) - > GitFlatConfigData :
ret = { }
ret = { k : list ( v ) for k , v in self . system_state . items ( ) }
for scope in GitScopeOrder :
for key , value in self . _get_scope ( scope ) . items ( ) :
curvals = ret . get ( key , None )
if curvals is None :
curvals = [ ]
ret [ key ] = curvals
curvals . extend ( value )
if scope == ' system ' :
continue
with self . _editable_scope ( scope ) as cfg :
for key , value in cfg . items ( ) :
curvals = ret . get ( key , None )
if curvals is None :
curvals = [ ]
ret [ key ] = curvals
curvals . extend ( value )
return ret
def set_config ( self , key : str , value : str , * , append : bool ,
scope : GitConfigScope ) :
cfg = self . _get_scope ( scope )
cur = cfg . get ( key )
if cur is None or len ( cur ) == 1 :
if append :
cfg [ key ] = ( cur or [ ] ) + [ value ]
else :
cfg [ key ] = [ value ]
return
raise ValueError ( f ' GitConfigStateTest: Cannot set key { key } '
f ' - current value { cur !r} is multiple. ' )
with self . _editable_scope ( scope ) as cfg :
cur = cfg . get ( key )
if cur is None or len ( cur ) == 1 :
if append :
cfg [ key ] = ( cur or [ ] ) + [ value ]
else :
cfg [ key ] = [ value ]
return
raise ValueError ( f ' GitConfigStateTest: Cannot set key { key } '
f ' - current value { cur !r} is multiple. ' )
def set_config_multi ( self , key : str , value : str , * , append : bool ,
def set_config_multi ( self , key : str , value : str , * ,
value_pattern : Optional [ str ] , scope : GitConfigScope ) :
cfg = self . _get_scope ( scope )
cur = cfg . get ( key )
if value_pattern is None or cur is None :
if append :
cfg [ key ] = ( cur or [ ] ) + [ value ]
else :
with self . _editable_scope ( scope ) as cfg :
cur = cfg . get ( key )
if value_pattern is None or cur is None :
cfg [ key ] = [ value ]
return
return
pat = re . compile ( value_pattern )
newval = [ v for v in cur if pat . match ( v ) ]
newval . append ( value )
cfg [ key ] = newval
# We want to insert `value` in place of the first pattern match - if
# multiple values match, they will all be removed.
pat = re . compile ( value_pattern )
newval = [ ]
added = False
for val in cur :
if pat . match ( val ) :
if not added :
newval . append ( value )
added = True
else :
newval . append ( val )
if not added :
newval . append ( value )
cfg [ key ] = newval
def unset_config ( self , key : str , * , scope : GitConfigScope ,
missing_ok : bool ) :
cfg = self . _get_scope ( scope )
cur = cfg . get ( key )
if cur is None :
if missing_ok :
with self . _editable_scope ( scope ) as cfg :
cur = cfg . get ( key )
if cur is None :
if missing_ok :
return
raise GitConfigUnsetMissingValue ( key , scope )
if len ( cur ) == 1 :
del cfg [ key ]
return
raise GitConfigUnsetMissingValue ( key , scope )
if len ( cur ) == 1 :
del cfg [ key ]
return
raise ValueError ( f ' GitConfigStateTest: Cannot unset key { key } '
f ' - current value { cur !r} is multiple. ' )
raise GitConfigUnsetMultipleValues ( key , scope )
def unset_config_multi ( self , key : str , * , value_pattern : Optional [ str ] ,
scope : GitConfigScope , missing_ok : bool ) :
cfg = self . _get_scope ( scope )
cur = cfg . get ( key )
if cur is None :
if not missing_ok :
raise GitConfigUnsetMissingValue ( key , scope )
return
with self . _editable_scope ( scope ) as cfg :
cur = cfg . get ( key )
if cur is None :
if not missing_ok :
raise GitConfigUnsetMissingValue ( key , scope )
return
if value_pattern is None :
del cfg [ key ]
return
if value_pattern is None :
del cfg [ key ]
return
if cur is None :
del cfg [ key ]
return
if cur is None :
del cfg [ key ]
return
pat = re . compile ( value_pattern )
cfg [ key ] = [ v for v in cur if not pat . match ( v ) ]
pat = re . compile ( value_pattern )
cfg [ key ] = [ v for v in cur if not pat . match ( v ) ]
class GIT ( object ) :
@ -608,17 +704,19 @@ class GIT(object):
key : The specific config key to affect .
value : The value to set . If this is None , ` key ` will be unset .
append : If True and ` value ` is not None , this will append
the value instead of replacing an existing one .
the value instead of replacing an existing one . Must not be
specified with value_pattern .
missing_ok : If ` value ` is None ( i . e . this is an unset operation ) ,
ignore retcode = 5 from ` git config ` ( meaning that the value is
not present ) . If ` value ` is not None , then this option has no
effect .
effect . If this is false and the key is missing , this will raise
GitConfigUnsetMissingValue .
modify_all : If True , this will change a set operation to
` - - replace - all ` , and will change an unset operation to
` - - unset - all ` .
scope : By default this is the local scope , but could be ` system ` ,
` global ` , or ` worktree ` , depending on which config scope you
want to affect .
` - - unset - all ` . Must not be specified with value_pattern .
scope : By default this is the ` local ` scope , but could be ` global `
or ` worktree ` , depending on which config scope you want to affect .
Note that the ` system ` scope cannot be modified .
value_pattern : For use with ` modify_all = True ` , allows
further filtering of the set or unset operation based on
the currently configured value . Ignored for
@ -989,3 +1087,6 @@ class DIFF(object):
dirnames [ : ] = [ d for d in dirnames if should_recurse ( dirpath , d ) ]
return [ os . path . relpath ( p , cwd ) for p in paths ]
# vim: sts=4:ts=4:sw=4:tw=80:et: