1#!/usr/bin/env python
2
3# Copyright 2017 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
9"""Submit one or more try jobs."""
10
11
12import argparse
13import json
14import os
15import re
16import subprocess
17import sys
18import tempfile
19import urllib2
20
21
22BUCKET_SKIA_PRIMARY = 'skia/skia.primary'
23BUCKET_SKIA_INTERNAL = 'skia-internal/skia.internal'
24INFRA_BOTS = os.path.join('infra', 'bots')
25TASKS_JSON = os.path.join(INFRA_BOTS, 'tasks.json')
26REPO_INTERNAL = 'https://skia.googlesource.com/internal_test.git'
27TMP_DIR = os.path.join(tempfile.gettempdir(), 'sktry')
28
29SKIA_ROOT = os.path.realpath(os.path.join(
30    os.path.dirname(os.path.abspath(__file__)), os.pardir))
31SKIA_INFRA_BOTS = os.path.join(SKIA_ROOT, INFRA_BOTS)
32sys.path.insert(0, SKIA_INFRA_BOTS)
33
34import utils
35
36
37def find_repo_root():
38  """Find the root directory of the current repository."""
39  cwd = os.getcwd()
40  while True:
41    if os.path.isdir(os.path.join(cwd, '.git')):
42      return cwd
43    next_cwd = os.path.dirname(cwd)
44    if next_cwd == cwd:
45      raise Exception('Failed to find repo root!')
46    cwd = next_cwd
47
48
49def get_jobs(repo):
50  """Obtain the list of jobs from the given repo."""
51  # Maintain a copy of the repo in the temp dir.
52  if not os.path.isdir(TMP_DIR):
53    os.mkdir(TMP_DIR)
54  with utils.chdir(TMP_DIR):
55    dirname = repo.split('/')[-1]
56    if not os.path.isdir(dirname):
57      subprocess.check_call([
58          utils.GIT, 'clone', '--mirror', repo, dirname])
59    with utils.chdir(dirname):
60      subprocess.check_call([utils.GIT, 'remote', 'update'])
61      jobs = json.loads(subprocess.check_output([
62          utils.GIT, 'show', 'master:%s' % JOBS_JSON]))
63      return (BUCKET_SKIA_INTERNAL, jobs)
64
65
66def main():
67  # Parse arguments.
68  d = 'Helper script for triggering try jobs.'
69  parser = argparse.ArgumentParser(description=d)
70  parser.add_argument('--list', action='store_true', default=False,
71                      help='Just list the jobs; do not trigger anything.')
72  parser.add_argument('--internal', action='store_true', default=False,
73                      help=('If set, include internal jobs. You must have '
74                            'permission to view internal repos.'))
75  parser.add_argument('job', nargs='?', default=None,
76                      help='Job name or regular expression to match job names.')
77  args = parser.parse_args()
78
79  # First, find the Gerrit issue number. If the change was uploaded using Depot
80  # Tools, this configuration will be present in the git config.
81  branch = subprocess.check_output(['git', 'branch', '--show-current']).rstrip()
82  if not branch:
83    print 'Not on any branch; cannot trigger try jobs.'
84    sys.exit(1)
85  branch_issue_config = 'branch.%s.gerritissue' % branch
86  try:
87    issue = subprocess.check_output([
88        'git', 'config', '--local', branch_issue_config])
89  except subprocess.CalledProcessError:
90    # Not using Depot Tools. Find the Change-Id line in the most recent commit
91    # and obtain the issue number using that.
92    print '"git cl issue" not set; searching for Change-Id footer.'
93    msg = subprocess.check_output(['git', 'log', '-n1', branch])
94    m = re.search('Change-Id: (I[a-f0-9]+)', msg)
95    if not m:
96      print ('No gerrit issue found in `git config --local %s` and no Change-Id'
97             ' found in most recent commit message.')
98      sys.exit(1)
99    url = 'https://skia-review.googlesource.com/changes/%s' % m.groups()[0]
100    resp = urllib2.urlopen(url).read()
101    issue = str(json.loads('\n'.join(resp.splitlines()[1:]))['_number'])
102    print 'Setting "git cl issue %s"' % issue
103    subprocess.check_call(['git', 'cl', 'issue', issue])
104  # Load and filter the list of jobs.
105  jobs = []
106  tasks_json = os.path.join(find_repo_root(), TASKS_JSON)
107  with open(tasks_json) as f:
108    tasks_cfg = json.load(f)
109  skia_primary_jobs = []
110  for k, v in tasks_cfg['jobs'].iteritems():
111    skia_primary_jobs.append(k)
112  skia_primary_jobs.sort()
113
114  # TODO(borenet): This assumes that the current repo is associated with the
115  # skia.primary bucket. This will work for most repos but it would be better to
116  # look up the correct bucket to use.
117  jobs.append((BUCKET_SKIA_PRIMARY, skia_primary_jobs))
118  if args.internal:
119    jobs.append(get_jobs(REPO_INTERNAL))
120  if args.job:
121    filtered_jobs = []
122    for bucket, job_list in jobs:
123      filtered = [j for j in job_list if re.search(args.job, j)]
124      if len(filtered) > 0:
125        filtered_jobs.append((bucket, filtered))
126    jobs = filtered_jobs
127
128  # Display the list of jobs.
129  if len(jobs) == 0:
130    print 'Found no jobs matching "%s"' % repr(args.job)
131    sys.exit(1)
132  count = 0
133  for bucket, job_list in jobs:
134    count += len(job_list)
135  print 'Found %d jobs:' % count
136  for bucket, job_list in jobs:
137    print '  %s:' % bucket
138    for j in job_list:
139      print '    %s' % j
140  if args.list:
141    return
142
143  if count > 1:
144    # Prompt before triggering jobs.
145    resp = raw_input('\nDo you want to trigger these jobs? (y/n or i for '
146                     'interactive): ')
147    print ''
148    if resp != 'y' and resp != 'i':
149      sys.exit(1)
150    if resp == 'i':
151      filtered_jobs = []
152      for bucket, job_list in jobs:
153        new_job_list = []
154        for j in job_list:
155          incl = raw_input(('Trigger %s? (y/n): ' % j))
156          if incl == 'y':
157            new_job_list.append(j)
158        if len(new_job_list) > 0:
159          filtered_jobs.append((bucket, new_job_list))
160      jobs = filtered_jobs
161
162  # Trigger the try jobs.
163  for bucket, job_list in jobs:
164    cmd = ['git', 'cl', 'try', '-B', bucket]
165    for j in job_list:
166      cmd.extend(['-b', j])
167    try:
168      subprocess.check_call(cmd)
169    except subprocess.CalledProcessError:
170      # Output from the command will fall through, so just exit here rather than
171      # printing a stack trace.
172      sys.exit(1)
173
174
175if __name__ == '__main__':
176  main()
177