2 # Copyright 2014 The Chromium Authors
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Script that attempts to push to a special git repository to verify that git
7 credentials are configured correctly. It also verifies that gclient solution is
8 configured to use git checkout.
10 It will be added as gclient hook shortly before Chromium switches to git and
11 removed after the switch.
13 When running as hook in *.corp.google.com network it will also report status
14 of the push attempt to the server (on appengine), so that chrome-infra team can
15 collect information about misconfigured Git accounts.
18 from __future__ import print_function
41 # Absolute path to src/ directory.
42 REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44 # Absolute path to a file with gclient solutions.
45 GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient')
47 # Incremented whenever some changes to scrip logic are made. Change in version
48 # will cause the check to be rerun on next gclient runhooks invocation.
51 # Do not attempt to upload a report after this date.
52 UPLOAD_DISABLE_TS = datetime.datetime(2014, 10, 1)
54 # URL to POST json with results to.
56 'https://chromium-git-access.appspot.com/'
57 'git_access/api/v1/reports/access_check')
59 # Repository to push test commits to.
60 TEST_REPO_URL = 'https://chromium.googlesource.com/a/playground/access_test'
62 # Git-compatible gclient solution.
63 GOOD_GCLIENT_SOLUTION = {
67 'url': 'https://chromium.googlesource.com/chromium/src.git',
70 # Possible chunks of git push response in case .netrc is misconfigured.
72 '(prohibited by Gerrit)',
73 'does not match your user account',
74 'Git repository not found',
75 'Invalid user name or password',
76 'Please make sure you have the correct access rights',
79 # Git executable to call.
80 GIT_EXE = 'git.bat' if sys.platform == 'win32' else 'git'
84 """True when running under buildbot."""
85 return os.environ.get('CHROME_HEADLESS') == '1'
88 def is_in_google_corp():
89 """True when running in google corp network."""
91 return socket.getfqdn().endswith('.corp.google.com')
93 logging.exception('Failed to get FQDN')
98 """True if git checkout is used."""
99 return os.path.exists(os.path.join(REPO_ROOT, '.git', 'objects'))
103 """True if svn checkout is used."""
104 return os.path.exists(os.path.join(REPO_ROOT, '.svn'))
107 def read_git_config(prop):
108 """Reads git config property of src.git repo.
110 Returns empty string in case of errors.
113 proc = subprocess.Popen(
114 [GIT_EXE, 'config', prop], stdout=subprocess.PIPE, cwd=REPO_ROOT)
115 out, _ = proc.communicate()
116 return out.strip().decode('utf-8')
117 except OSError as exc:
118 if exc.errno != errno.ENOENT:
119 logging.exception('Unexpected error when calling git')
123 def read_netrc_user(netrc_obj, host):
124 """Reads 'user' field of a host entry in netrc.
126 Returns empty string if netrc is missing, or host is not there.
130 entry = netrc_obj.authenticators(host)
136 def get_git_version():
137 """Returns version of git or None if git is not available."""
139 proc = subprocess.Popen([GIT_EXE, '--version'], stdout=subprocess.PIPE)
140 out, _ = proc.communicate()
141 return out.strip() if proc.returncode == 0 else ''
142 except OSError as exc:
143 if exc.errno != errno.ENOENT:
144 logging.exception('Unexpected error when calling git')
148 def read_gclient_solution():
149 """Read information about 'src' gclient solution from .gclient file.
152 (url, deps_file, managed)
154 (None, None, None) if no such solution.
158 execfile(GCLIENT_CONFIG, env, env)
159 for sol in (env.get('solutions') or []):
160 if sol.get('name') == 'src':
161 return sol.get('url'), sol.get('deps_file'), sol.get('managed')
162 return None, None, None
164 logging.exception('Failed to read .gclient solution')
165 return None, None, None
168 def read_git_insteadof(host):
169 """Reads relevant insteadOf config entries."""
171 proc = subprocess.Popen([GIT_EXE, 'config', '-l'], stdout=subprocess.PIPE)
172 out, _ = proc.communicate()
174 for line in out.strip().split('\n'):
176 if 'insteadof=' in line and host in line:
178 return '\n'.join(lines)
179 except OSError as exc:
180 if exc.errno != errno.ENOENT:
181 logging.exception('Unexpected error when calling git')
185 def scan_configuration():
186 """Scans local environment for git related configuration values."""
188 is_git = is_using_git()
190 # On Windows HOME should be set.
191 if 'HOME' in os.environ:
192 netrc_path = os.path.join(
194 '_netrc' if sys.platform.startswith('win') else '.netrc')
199 is_using_netrc = netrc_path and os.path.exists(netrc_path)
205 netrc_obj = netrc.netrc(netrc_path)
207 logging.exception('Failed to read netrc from %s', netrc_path)
210 # Read gclient 'src' solution.
211 gclient_url, gclient_deps, gclient_managed = read_gclient_solution()
214 'checker_version': CHECKER_VERSION,
216 'is_home_set': 'HOME' in os.environ,
217 'is_using_netrc': is_using_netrc,
218 'netrc_file_mode': os.stat(netrc_path).st_mode if is_using_netrc else 0,
219 'git_version': get_git_version(),
220 'platform': sys.platform,
221 'username': getpass.getuser(),
222 'git_user_email': read_git_config('user.email') if is_git else '',
223 'git_user_name': read_git_config('user.name') if is_git else '',
224 'git_insteadof': read_git_insteadof('chromium.googlesource.com'),
225 'chromium_netrc_email':
226 read_netrc_user(netrc_obj, 'chromium.googlesource.com'),
227 'chrome_internal_netrc_email':
228 read_netrc_user(netrc_obj, 'chrome-internal.googlesource.com'),
229 'gclient_deps': gclient_deps,
230 'gclient_managed': gclient_managed,
231 'gclient_url': gclient_url,
235 def last_configuration_path():
236 """Path to store last checked configuration."""
238 return os.path.join(REPO_ROOT, '.git', 'check_git_push_access_conf.json')
240 return os.path.join(REPO_ROOT, '.svn', 'check_git_push_access_conf.json')
242 return os.path.join(REPO_ROOT, '.check_git_push_access_conf.json')
245 def read_last_configuration():
246 """Reads last checked configuration if it exists."""
248 with open(last_configuration_path(), 'r') as f:
250 except (IOError, ValueError):
254 def write_last_configuration(conf):
255 """Writes last checked configuration to a file."""
257 with open(last_configuration_path(), 'w') as f:
258 json.dump(conf, f, indent=2, sort_keys=True)
260 logging.exception('Failed to write JSON to %s', path)
263 @contextlib.contextmanager
264 def temp_directory():
265 """Creates a temp directory, then nukes it."""
266 tmp = tempfile.mkdtemp()
272 except (OSError, IOError):
273 logging.exception('Failed to remove temp directory %s', tmp)
276 class Runner(object):
277 """Runs a bunch of commands in some directory, collects logs from them."""
279 def __init__(self, cwd, verbose):
281 self.verbose = verbose
285 self.append_to_log('> ' + ' '.join(cmd))
288 proc = subprocess.Popen(
290 stdout=subprocess.PIPE,
291 stderr=subprocess.STDOUT,
293 out, _ = proc.communicate()
295 retcode = proc.returncode
296 except OSError as exc:
299 out += '\n(exit code: %d)' % retcode
300 self.append_to_log(out)
303 def append_to_log(self, text):
305 self.log.append(text)
307 logging.warning(text)
310 def check_git_config(conf, report_url, verbose):
311 """Attempts to push to a git repository, reports results to a server.
313 Returns True if the check finished without incidents (push itself may
314 have failed) and should NOT be retried on next invocation of the hook.
316 # Don't even try to push if netrc is not configured.
317 if not conf['chromium_netrc_email']:
318 return upload_report(
326 # Ref to push to, each user has its own ref.
327 ref = 'refs/push-test/%s' % conf['chromium_netrc_email']
331 started = time.time()
333 logging.warning('Checking push access to the git repository...')
334 with temp_directory() as tmp:
335 # Prepare a simple commit on a new timeline.
336 runner = Runner(tmp, verbose)
337 runner.run([GIT_EXE, 'init', '.'])
338 if conf['git_user_name']:
339 runner.run([GIT_EXE, 'config', 'user.name', conf['git_user_name']])
340 if conf['git_user_email']:
341 runner.run([GIT_EXE, 'config', 'user.email', conf['git_user_email']])
342 with open(os.path.join(tmp, 'timestamp'), 'w') as f:
343 f.write(str(int(time.time() * 1000)))
344 runner.run([GIT_EXE, 'add', 'timestamp'])
345 runner.run([GIT_EXE, 'commit', '-m', 'Push test.'])
346 # Try to push multiple times if it fails due to issues other than ACLs.
350 logging.info('Pushing to %s %s', TEST_REPO_URL, ref)
352 [GIT_EXE, 'push', TEST_REPO_URL, 'HEAD:%s' % ref, '-f'])
356 if any(x in runner.log[-1] for x in BAD_ACL_ERRORS):
360 logging.exception('Unexpected exception when pushing')
364 logging.warning('Git push works!')
367 'Git push doesn\'t work, which is fine if you are not a committer.')
369 uploaded = upload_report(
373 push_works=push_works,
374 push_log='\n'.join(runner.log),
375 push_duration_ms=int((time.time() - started) * 1000))
376 return uploaded and not flake
379 def check_gclient_config(conf):
380 """Shows warning if gclient solution is not properly configured for git."""
381 # Ignore configs that do not have 'src' solution at all.
382 if not conf['gclient_url']:
386 'deps_file': conf['gclient_deps'] or 'DEPS',
387 'managed': conf['gclient_managed'] or False,
388 'url': conf['gclient_url'],
390 # After depot_tools r291592 both DEPS and .DEPS.git are valid.
391 good = GOOD_GCLIENT_SOLUTION.copy()
392 good['deps_file'] = current['deps_file']
395 # Show big warning if url or deps_file is wrong.
396 if current['url'] != good['url'] or current['deps_file'] != good['deps_file']:
398 print('Your gclient solution is not set to use supported git workflow!')
400 print('Your \'src\' solution (in %s):' % GCLIENT_CONFIG)
401 print(pprint.pformat(current, indent=2))
403 print('Correct \'src\' solution to use git:')
404 print(pprint.pformat(good, indent=2))
406 print('Please update your .gclient file ASAP.')
408 # Show smaller (additional) warning about managed workflow.
409 if current['managed']:
411 print('You are using managed gclient mode with git, which was deprecated '
413 print('https://groups.google.com/a/chromium.org/'
414 'forum/#!topic/chromium-dev/n9N5N3JL2_U')
416 print('It is strongly advised to switch to unmanaged mode. For more '
417 'information about managed mode and reasons for its deprecation see:')
419 'http://www.chromium.org/developers/how-tos/get-the-code/gclient-managed-mode'
422 print('There\'s also a large suite of tools to assist managing git '
423 'checkouts.\nSee \'man depot_tools\' (or read '
424 'depot_tools/man/html/depot_tools.html).')
429 conf, report_url, verbose, push_works, push_log, push_duration_ms):
430 """Posts report to the server, returns True if server accepted it.
432 Uploads the report only if script is running in Google corp network. Otherwise
433 just prints the report.
437 push_works=push_works,
439 push_duration_ms=push_duration_ms)
441 as_bytes = json.dumps({'access_check': report}, indent=2, sort_keys=True)
443 print('Status of git push attempt:')
446 # Do not upload it outside of corp or if server side is already disabled.
447 if not is_in_google_corp() or datetime.datetime.now() > UPLOAD_DISABLE_TS:
450 'You can send the above report to chrome-git-migration@google.com '
451 'if you need help to set up you committer git account.')
454 req = urllib2.Request(
457 headers={'Content-Type': 'application/json; charset=utf-8'})
461 while not success and attempt < 10:
465 'Attempting to upload the report to %s...',
466 urlparse.urlparse(report_url).netloc)
467 resp = urllib2.urlopen(req, timeout=5)
470 report_id = json.load(resp)['report_id']
471 except (ValueError, TypeError, KeyError):
473 logging.warning('Report uploaded: %s', report_id)
475 except (urllib2.URLError, socket.error, ssl.SSLError) as exc:
476 logging.warning('Failed to upload the report: %s', exc)
481 parser = optparse.OptionParser(description=sys.modules[__name__].__doc__)
485 help='Set when invoked from gclient hook')
488 default=MOTHERSHIP_URL,
489 help='URL to submit the report to')
494 options, args = parser.parse_args()
496 parser.error('Unknown argument %s' % args)
498 format='%(message)s',
499 level=logging.INFO if options.verbose else logging.WARN)
501 # When invoked not as a hook, always run the check.
502 if not options.running_as_hook:
503 config = scan_configuration()
504 check_gclient_config(config)
505 check_git_config(config, options.report_url, True)
508 # Always do nothing on bots.
512 # Read current config, verify gclient solution looks correct.
513 config = scan_configuration()
514 check_gclient_config(config)
516 # Do not attempt to push from non-google owned machines.
517 if not is_in_google_corp():
518 logging.info('Skipping git push check: non *.corp.google.com machine.')
521 # Skip git push check if current configuration was already checked.
522 if config == read_last_configuration():
523 logging.info('Check already performed, skipping.')
526 # Run the check. Mark configuration as checked only on success. Ignore any
527 # exceptions or errors. This check must not break gclient runhooks.
529 ok = check_git_config(config, options.report_url, False)
531 write_last_configuration(config)
533 logging.warning('Check failed and will be retried on the next run')
535 logging.exception('Unexpected exception when performing git access check')
539 if __name__ == '__main__':
540 sys.exit(main(sys.argv[1:]))