Skip to content

Instantly share code, notes, and snippets.

@darrnshn
Last active September 8, 2017 06:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save darrnshn/5b36692391313793994c3c8e650cd45a to your computer and use it in GitHub Desktop.
Save darrnshn/5b36692391313793994c3c8e650cd45a to your computer and use it in GitHub Desktop.
Script to automatically upload diffs of generated code as Gists. Put this somewhere and set the USERNAME, ACCESS_TOKEN and WORKING_DIR variables correctly.
#!/usr/bin/env python
"""
Usage:
On the branch that you want to submit a CL for, run:
./gist_diff.py <directory/containing/generated/files>
This script uploads a diff of generated files (called build artifacts here)
to a Gist on GitHub. This is useful to show reviewers the changes you made
to generated code, since they are not visible on rietveld.
To use this script, you'll need to modify some configuration below.
The script does the following:
1. Build current branch.
2. Copy build artifacts to a temporary folder.
3. Build upstream branch.
4. Find build artifacts that differ.
5. Upload original build artifacts as a new Gist on GitHub.
6. Upload new build artifacts as a revision of the original Gist.
7. Switch back to original branch.
Example Usage:
gist_diff ~/chromium/src/out/Default/gen/blink/core
"""
from collections import namedtuple
import requests
import json
from subprocess import check_output
from glob import glob
import os
import shutil
import argparse
# Your Github username
USERNAME = ''
# Your access token which has permissions to create gists.
# To get an access token:
# 1. Login to Github and go to https://github.com/settings/tokens
# 2. Click "Generate new token"
# 3. Give it a descriptive name
# 4. Tick the "gist" scope
# 5. Click "Generate token"
# 6. Copy the token (looks like a hash) into the variable below
ACCESS_TOKEN = ''
# The directory from which all the commands in this script are run.
# Normally it's the absolute path to your chromium src directory.
WORKING_DIR = '~/chromium/src'
# The name of the build (the folder name in the out/ directory, e.g. Debug, Release)
BUILD_NAME = 'Default'
# The build target containing generated files.
# The default is a shortcut that only runs the generator scripts,
# which is much faster than a full build.
BUILD_TARGET = 'third_party/WebKit/Source/core:all_generators'
#BUILD_TARGET = 'blink_tests'
# The command to build the generated files.
CMD_BUILD = ['ninja', '-C', 'out/' + BUILD_NAME, '-j1000', BUILD_TARGET]
# The command to clean the generated files.
CMD_CLEAN = ['ninja', '-C', 'out/' + BUILD_NAME, '-t', 'clean', BUILD_TARGET]
# A temporary directory for copying the generated files to.
# Make sure it's only used by this script.
TMP_DIR = '/tmp/diffs'
# Command to get the current branch name.
CMD_GET_BRANCH = ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
# Command to checkout a branch
CMD_CHECKOUT = ['git', 'checkout']
def auth_user(username, access_token):
print '[Authenticating user...]'
r = requests.get('https://api.github.com/user', auth=(username, access_token))
assert r.status_code == 200
result = r.json()
print '=> Authenticated successfully as {} ({})'.format(result['login'], result['name'])
return result
def put_gist(files):
data = {
'description': '',
'public': True,
'files': {fname.split('/')[-1]:{'content': content} for fname, content in files.items()}
}
r = requests.post('https://api.github.com/gists', auth=(USERNAME, ACCESS_TOKEN),
data=json.dumps(data))
assert r.status_code == 201, 'Error: {}'.format(r.text)
result = r.json()
print '=> Created new gist at {}.'.format(result['url'])
return result
def patch_gist(gist_id, files):
data = {
'description': '',
'files': {fname.split('/')[-1]:{'content': content} for fname, content in files.items()}
}
r = requests.patch('https://api.github.com/gists/{}'.format(gist_id),
auth=(USERNAME, ACCESS_TOKEN),
data=json.dumps(data))
assert r.status_code == 200, 'Error: {}'.format(r.text)
result = r.json()
print '=> Updated gist at {}.'.format(result['url'])
return result
def run_cmd(description, cmd):
print '[{}] {}'.format(description, ' '.join(cmd))
with open(os.devnull, 'w') as devnull:
output = check_output(cmd, cwd=os.path.expanduser(WORKING_DIR), stderr=devnull)
return output
def copy_dir_to_tmp(path):
# Ensure the tmp dir is clean
print '[Cleaning "{}"...]'.format(TMP_DIR)
try:
shutil.rmtree(TMP_DIR)
except OSError:
pass
# Copy all the files to tmp
print '[Copying "{}" to "{}"...]'.format(path, TMP_DIR)
shutil.copytree(path, TMP_DIR)
def has_file_changed(a_path, b_path):
with open(a_path) as a_file, open(b_path) as b_file:
for a_line, b_line in zip(a_file, b_file):
if a_line != b_line:
return True
return False
def get_paths_in_tree(path, recursive):
if recursive:
for root, _, fnames in os.walk(path):
for fname in fnames:
yield os.path.relpath(os.path.join(root, fname), path)
else:
for fname in os.listdir(path):
if os.path.isfile(os.path.join(path, fname)):
yield fname
def read_file(path):
with open(path, 'r') as f:
return f.read()
def is_valid(path):
ext = os.path.splitext(path)[1]
return ext in ['.h', '.cpp']
def main():
parser = argparse.ArgumentParser(description='Diff generated files and upload them to GitHub')
parser.add_argument('directory', type=str)
parser.add_argument('--no-recursive', action='store_true', default=False)
parser.add_argument('--dry-run', action='store_true', default=False)
args = parser.parse_args()
# Authenticate the user to check credentials
if not args.dry_run:
auth_user(USERNAME, ACCESS_TOKEN)
# Get the path to artifacts directory
artifacts_dir = args.directory
# Build the current branch
working_branch = run_cmd('Getting current branch...', CMD_GET_BRANCH).strip()
print '=> Working branch:', working_branch
run_cmd('Cleaning...', CMD_CLEAN)
run_cmd('Build artifacts...', CMD_BUILD)
# Copy build artifacts to tmp
copy_dir_to_tmp(artifacts_dir)
# Move upstream and rebuild artifacts
try:
run_cmd('Moving to upstream...', CMD_CHECKOUT + ['@{u}'])
except:
print 'Could not move to upstream branch. Are you on the right branch?'
return
run_cmd('Cleaning...', CMD_CLEAN)
run_cmd('Rebuilding artifacts on upstream...', CMD_BUILD)
# Compare old artifacts in tmp and new artifacts in build to find which ones have changed
old_paths = set(get_paths_in_tree(artifacts_dir, not args.no_recursive))
new_paths = set(get_paths_in_tree(TMP_DIR, not args.no_recursive))
changed_paths = old_paths.union(new_paths)
for path in old_paths.intersection(new_paths):
if not has_file_changed(os.path.join(TMP_DIR, path),
os.path.join(artifacts_dir, path)) or not is_valid(path):
changed_paths.remove(path)
if len(changed_paths) == 0:
print 'No changes'
# Move back to original branch
run_cmd('Moving back to original branch...', CMD_CHECKOUT + [working_branch])
return
if args.dry_run:
print 'Not creating gist for dry run ({} files changed)'.format(len(changed_paths))
# Move back to original branch
run_cmd('Moving back to original branch...', CMD_CHECKOUT + [working_branch])
else:
# Create the gist for the original file
print '[Creating new gist with {} files...]'.format(len(changed_paths))
old_files = {path:read_file(os.path.join(artifacts_dir, path)) for path in changed_paths if path in old_paths}
old_output = put_gist(old_files)
# Patch the gist with the new artifacts to create a diff
print '[Updating gist...]'.format(len(changed_paths))
new_files = {path:read_file(os.path.join(TMP_DIR, path)) for path in changed_paths if path in new_paths}
new_output = patch_gist(old_output['id'], new_files)
# Move back to original branch
run_cmd('Moving back to original branch...', CMD_CHECKOUT + [working_branch])
print
print '-- Success! --'
print 'Gist: {}'.format(new_output['html_url'])
print 'Diff: {}'.format(new_output['html_url'] + '/revisions')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment