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