Compare commits

..

4 Commits

Author SHA1 Message Date
0ec2029833 superproject: Move enrollment to opt-out when enabled globally
Our internal experiments was a success so far and we are enrolling 100%
users now.  Instead of asking every two weeks, simply consider a lack of
unexpired choice as accepting the system default.

With this change the user would still be able to override the system
default with --no-use-superproject, or to permanently set the choice in
user's profile with git config --global repo.superprojectchoice.

Bug: [google internal] b/190688390
Change-Id: Idc77a9cbf88a169d90304169e91f0d722dc4ac8b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/317975
Tested-by: Xin Li <delphij@google.com>
Reviewed-by: Raman Tenneti <rtenneti@google.com>
2021-09-20 07:21:22 +00:00
d8e8ae8990 superproject: Log branch and remote url with every log message.
Saved superproject's remote URL in _remote_url data and used it
in the _Fecth function.

Tested:
$ ./run_tests
$ flake8 git_superproject.py
$ repo_dev init --use-superproject -u https://android.googlesource.com/platform/manifest
$ repo_dev sync

   Verified the all log messages have the following format.
   repo superproject branch: <branch> url: <url> warning: <message>

Bug: [google internal] b/200072098
Change-Id: Iac6af7c99225479fd50bc6909396b22e0ce5f76b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/318177
Reviewed-by: Xin Li <delphij@google.com>
Tested-by: Raman Tenneti <rtenneti@google.com>
2021-09-16 15:46:19 +00:00
6448a4f2af sync: Log repo sync state events as 'data' events.
git_trace2_event_log.py:
+ Added LogDataConfigEvents method to log 'data' events.
  Sync's current_sync_state and previous_sync_state are logged
  as 'data' events in the current log.

  It logs are key/value in the |config| argument. Each key is
  prefixed with |prefix| argument.

  The following are sample events that are logged during repo sync.

   {"event":"data",
   "sid":"repo-20210914T181545Z-P000330c0/repo-20210914T181545Z-P000330c0",
   "thread":"MainThread",
   "time":"2021-09-14T18:16:19.935846Z",
   "key":"previous_sync_state/repo.syncstate.main.synctime",
   "value":"2021-09-14T17:27:11.573717Z"}

   {"event":"data",
   "sid":"repo-20210914T181545Z-P000330c0/repo-20210914T181545Z-P000330c0",
   "thread":"MainThread",
   "time":"2021-09-14T18:16:19.955546Z",
   "key":"current_sync_state/repo.syncstate.main.synctime",
   "value":"2021-09-14T18:16:19.935979Z"}

tests/test_git_trace2_event_log.py:
+ Added unit tests

sync.py:
+ Changed logging calls to LogDataConfigEvents.

Tested:
$ ./run_tests

Tested it by running the following command multiple times.
$ repo_dev sync -j 20
  repo sync has finished successfully

  Verified config data is looged in trace2 event logs.

Bug: [google internal] b/199758376
Change-Id: I75fd830e90c1811ec28510538c99a2632b104e85
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/317823
Reviewed-by: Josh Steadmon <steadmon@google.com>
Reviewed-by: Xin Li <delphij@google.com>
Tested-by: Raman Tenneti <rtenneti@google.com>
2021-09-14 21:36:12 +00:00
1328c35a4d superproject: Provide accurate feedback for user choice
Currently the code would give a message that would appear like the user
have enrolled the experiment regardless of the actual choice. For users
who choose to not enroll in the experiment, we should give them
instructions to override (enable) superproject once instead of how to
disable it, which is what the code already behave.

Bug: [google internal] b/199167992
Change-Id: Iba3314cb510aedf024375a26baa8bc1d5e2846cf
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/317382
Tested-by: Xin Li <delphij@google.com>
Reviewed-by: Raman Tenneti <rtenneti@google.com>
2021-09-08 20:35:42 +00:00
5 changed files with 90 additions and 58 deletions

View File

@ -98,8 +98,11 @@ class Superproject(object):
_SUPERPROJECT_MANIFEST_NAME) _SUPERPROJECT_MANIFEST_NAME)
git_name = '' git_name = ''
if self._manifest.superproject: if self._manifest.superproject:
remote_name = self._manifest.superproject['remote'].name remote = self._manifest.superproject['remote']
git_name = hashlib.md5(remote_name.encode('utf8')).hexdigest() + '-' git_name = hashlib.md5(remote.name.encode('utf8')).hexdigest() + '-'
self._remote_url = remote.url
else:
self._remote_url = None
self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
self._work_git = os.path.join(self._superproject_path, self._work_git_name) self._work_git = os.path.join(self._superproject_path, self._work_git_name)
@ -130,13 +133,17 @@ class Superproject(object):
print(message, file=sys.stderr) print(message, file=sys.stderr)
self._git_event_log.ErrorEvent(message, f'{message}') self._git_event_log.ErrorEvent(message, f'{message}')
def _LogMessagePrefix(self):
"""Returns the prefix string to be logged in each log message"""
return f'repo superproject branch: {self._branch} url: {self._remote_url}'
def _LogError(self, message): def _LogError(self, message):
"""Logs error message to stderr and _git_event_log.""" """Logs error message to stderr and _git_event_log."""
self._LogMessage(f'repo superproject error: {message}') self._LogMessage(f'{self._LogMessagePrefix()} error: {message}')
def _LogWarning(self, message): def _LogWarning(self, message):
"""Logs warning message to stderr and _git_event_log.""" """Logs warning message to stderr and _git_event_log."""
self._LogMessage(f'repo superproject warning: {message}') self._LogMessage(f'{self._LogMessagePrefix()} warning: {message}')
def _Init(self): def _Init(self):
"""Sets up a local Git repository to get a copy of a superproject. """Sets up a local Git repository to get a copy of a superproject.
@ -162,11 +169,8 @@ class Superproject(object):
return False return False
return True return True
def _Fetch(self, url): def _Fetch(self):
"""Fetches a local copy of a superproject for the manifest based on url. """Fetches a local copy of a superproject for the manifest based on |_remote_url|.
Args:
url: superproject's url.
Returns: Returns:
True if fetch is successful, or False. True if fetch is successful, or False.
@ -177,7 +181,8 @@ class Superproject(object):
if not git_require((2, 28, 0)): if not git_require((2, 28, 0)):
self._LogWarning('superproject requires a git version 2.28 or later') self._LogWarning('superproject requires a git version 2.28 or later')
return False return False
cmd = ['fetch', url, '--depth', '1', '--force', '--no-tags', '--filter', 'blob:none'] cmd = ['fetch', self._remote_url, '--depth', '1', '--force', '--no-tags',
'--filter', 'blob:none']
if self._branch: if self._branch:
cmd += [self._branch + ':' + self._branch] cmd += [self._branch + ':' + self._branch]
p = GitCommand(None, p = GitCommand(None,
@ -234,15 +239,14 @@ class Superproject(object):
print('NOTICE: --use-superproject is in beta; report any issues to the ' print('NOTICE: --use-superproject is in beta; report any issues to the '
'address described in `repo version`', file=sys.stderr) 'address described in `repo version`', file=sys.stderr)
should_exit = True should_exit = True
url = self._manifest.superproject['remote'].url if not self._remote_url:
if not url:
self._LogWarning(f'superproject URL is not defined in manifest: ' self._LogWarning(f'superproject URL is not defined in manifest: '
f'{self._manifest.manifestFile}') f'{self._manifest.manifestFile}')
return SyncResult(False, should_exit) return SyncResult(False, should_exit)
if not self._Init(): if not self._Init():
return SyncResult(False, should_exit) return SyncResult(False, should_exit)
if not self._Fetch(url): if not self._Fetch():
return SyncResult(False, should_exit) return SyncResult(False, should_exit)
if not self._quiet: if not self._quiet:
print('%s: Initial setup for superproject completed.' % self._work_git) print('%s: Initial setup for superproject completed.' % self._work_git)
@ -260,7 +264,7 @@ class Superproject(object):
data = self._LsTree() data = self._LsTree()
if not data: if not data:
self._LogWarning(f'warning: git ls-tree failed to return data for manifest: ' self._LogWarning(f'git ls-tree failed to return data for manifest: '
f'{self._manifest.manifestFile}') f'{self._manifest.manifestFile}')
return CommitIdsResult(None, True) return CommitIdsResult(None, True)
@ -366,12 +370,17 @@ def _UseSuperprojectFromConfiguration():
user_value = user_cfg.GetBoolean('repo.superprojectChoice') user_value = user_cfg.GetBoolean('repo.superprojectChoice')
if user_value is not None: if user_value is not None:
user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire') user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire')
if user_expiration is not None and (user_expiration <= 0 or user_expiration >= time_now): if user_expiration is None or user_expiration <= 0 or user_expiration >= time_now:
# TODO(b/190688390) - Remove prompt when we are comfortable with the new # TODO(b/190688390) - Remove prompt when we are comfortable with the new
# default value. # default value.
if user_value:
print(('You are currently enrolled in Git submodules experiment ' print(('You are currently enrolled in Git submodules experiment '
'(go/android-submodules-quickstart). Use --no-use-superproject ' '(go/android-submodules-quickstart). Use --no-use-superproject '
'to override.\n'), file=sys.stderr) 'to override.\n'), file=sys.stderr)
else:
print(('You are not currently enrolled in Git submodules experiment '
'(go/android-submodules-quickstart). Use --use-superproject '
'to override.\n'), file=sys.stderr)
return user_value return user_value
# We don't have an unexpired choice, ask for one. # We don't have an unexpired choice, ask for one.
@ -379,43 +388,17 @@ def _UseSuperprojectFromConfiguration():
system_value = system_cfg.GetBoolean('repo.superprojectChoice') system_value = system_cfg.GetBoolean('repo.superprojectChoice')
if system_value: if system_value:
# The system configuration is proposing that we should enable the # The system configuration is proposing that we should enable the
# use of superproject. Present this to user for confirmation if we # use of superproject. Treat the user as enrolled for two weeks.
# are on a TTY, or, when we are not on a TTY, accept the system
# default for this time only.
# #
# TODO(b/190688390) - Remove prompt when we are comfortable with the new # TODO(b/190688390) - Remove prompt when we are comfortable with the new
# default value. # default value.
prompt = ('Repo can now use Git submodules (go/android-submodules-quickstart) ' userchoice = True
'instead of manifests to represent the state of the Android '
'superproject, which results in faster syncs and better atomicity.\n\n')
if sys.stdout.isatty():
prompt += 'Would you like to opt in for two weeks (y/N)? '
response = input(prompt).lower()
time_choiceexpire = time_now + (86400 * 14) time_choiceexpire = time_now + (86400 * 14)
if response in ('y', 'yes'):
userchoice = True
elif response in ('a', 'always'):
userchoice = True
time_choiceexpire = 0
elif response == 'never':
userchoice = False
time_choiceexpire = 0
elif response in ('n', 'no'):
userchoice = False
else:
# Unrecognized user response, assume the intention was no, but
# only for 2 hours instead of 2 weeks to balance between not
# being overly pushy while still retain the opportunity to
# enroll.
userchoice = False
time_choiceexpire = time_now + 7200
user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire)) user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire))
user_cfg.SetBoolean('repo.superprojectChoice', userchoice) user_cfg.SetBoolean('repo.superprojectChoice', userchoice)
print('You are automatically enrolled in Git submodules experiment '
return userchoice '(go/android-submodules-quickstart) for another two weeks.\n',
else: file=sys.stderr)
print('Accepting once since we are not on a TTY', file=sys.stderr)
return True return True
# For all other cases, we would not use superproject by default. # For all other cases, we would not use superproject by default.

View File

@ -167,6 +167,22 @@ class EventLog(object):
repo_config = {k: v for k, v in config.items() if k.startswith('repo.')} repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
self.LogConfigEvents(repo_config, 'def_param') self.LogConfigEvents(repo_config, 'def_param')
def LogDataConfigEvents(self, config, prefix):
"""Append a 'data' event for each config key/value in |config| to the current log.
For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
and the "value" of the "key" field is valueX.
Args:
config: Configuration dictionary.
prefix: Prefix for each key that is logged.
"""
for key, value in config.items():
event = self._CreateEventDict('data')
event['key'] = f'{prefix}/{key}'
event['value'] = value
self._log.append(event)
def ErrorEvent(self, msg, fmt): def ErrorEvent(self, msg, fmt):
"""Append a 'error' event to the current log.""" """Append a 'error' event to the current log."""
error_event = self._CreateEventDict('error') error_event = self._CreateEventDict('error')

View File

@ -1092,12 +1092,12 @@ later is required to fix a server side protocol bug.
sys.exit(1) sys.exit(1)
# Log the previous sync analysis state from the config. # Log the previous sync analysis state from the config.
self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
'previous_sync_state') 'previous_sync_state')
# Update and log with the new sync analysis state. # Update and log with the new sync analysis state.
mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data) mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data)
self.git_event_log.LogConfigEvents(mp.config.GetSyncAnalysisStateData(), self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
'current_sync_state') 'current_sync_state')
if not opt.quiet: if not opt.quiet:

View File

@ -12,7 +12,7 @@
intm = 10m intm = 10m
intg = 10g intg = 10g
[repo "syncstate.main"] [repo "syncstate.main"]
synctime = 2021-08-13T18:37:43.928600Z synctime = 2021-09-14T17:23:43.537338Z
version = 1 version = 1
[repo "syncstate.sys"] [repo "syncstate.sys"]
argv = ['/usr/bin/pytest-3'] argv = ['/usr/bin/pytest-3']

View File

@ -234,6 +234,39 @@ class EventLogTestCase(unittest.TestCase):
self.assertEqual(len(self._log_data), 1) self.assertEqual(len(self._log_data), 1)
self.verifyCommonKeys(self._log_data[0], expected_event_name='version') self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
def test_data_event_config(self):
"""Test 'data' event data outputs all config keys.
Expected event log:
<version event>
<data event>
<data event>
"""
config = {
'git.foo': 'bar',
'repo.partialclone': 'false',
'repo.syncstate.superproject.hassuperprojecttag': 'true',
}
prefix_value = 'prefix'
self._event_log_module.LogDataConfigEvents(config, prefix_value)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 4)
data_events = self._log_data[1:]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
for event in data_events:
self.verifyCommonKeys(event, expected_event_name='data')
# Check for 'data' event specific fields.
self.assertIn('key', event)
self.assertIn('value', event)
key = event['key'].removeprefix(f'{prefix_value}/')
value = event['value']
self.assertTrue(key in config and value == config[key])
def test_error_event(self): def test_error_event(self):
"""Test and validate 'error' event data is valid. """Test and validate 'error' event data is valid.