1#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Update third_party/WebKit using git.
7
8Under the assumption third_party/WebKit is a clone of git.webkit.org,
9we can use git commands to make it match the version requested by DEPS.
10
11See http://code.google.com/p/chromium/wiki/UsingWebKitGit for details on
12how to use this.
13"""
14
15import logging
16import optparse
17import os
18import re
19import subprocess
20import sys
21import urllib
22
23
24def RunGit(command):
25  """Run a git subcommand, returning its output."""
26  # On Windows, use shell=True to get PATH interpretation.
27  command = ['git'] + command
28  logging.info(' '.join(command))
29  shell = (os.name == 'nt')
30  proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE)
31  out = proc.communicate()[0].strip()
32  logging.info('Returned "%s"' % out)
33  return out
34
35
36def GetOverrideShortBranchName():
37  """Returns the user-configured override branch name, if any."""
38  override_config_name = 'chromium.sync-branch'
39  return RunGit(['config', '--get', override_config_name])
40
41
42def GetGClientBranchName():
43  """Returns the name of the magic branch that lets us know that DEPS is
44  managing the update cycle."""
45  # Is there an override branch specified?
46  override_branch_name = GetOverrideShortBranchName()
47  if not override_branch_name:
48    return 'refs/heads/gclient' # No override, so return the default branch.
49
50  # Verify that the branch from config exists.
51  ref_branch = 'refs/heads/' + override_branch_name
52  current_head = RunGit(['show-ref', '--hash', ref_branch])
53  if current_head:
54    return ref_branch
55
56  # Inform the user about the problem and how to fix it.
57  print ("The specified override branch ('%s') doesn't appear to exist." %
58         override_branch_name)
59  print "Please fix your git config value '%s'." % overide_config_name
60  sys.exit(1)
61
62
63def GetWebKitRev():
64  """Extract the 'webkit_revision' variable out of DEPS."""
65  locals = {'Var': lambda _: locals["vars"][_],
66            'From': lambda *args: None}
67  execfile('DEPS', {}, locals)
68  return locals['vars']['webkit_revision']
69
70
71def GetWebKitRevFromTarball(version):
72  """Extract the 'webkit_revision' variable out of tarball DEPS."""
73  deps_url = "http://src.chromium.org/svn/releases/" + version + "/DEPS"
74  f = urllib.urlopen(deps_url)
75  s = f.read()
76  m = re.search('(?<=/Source@)\w+', s)
77  return m.group(0)
78
79
80def FindSVNRev(branch_name, target_rev):
81  """Map an SVN revision to a git hash.
82  Like 'git svn find-rev' but without the git-svn bits."""
83
84  # We iterate through the commit log looking for "git-svn-id" lines,
85  # which contain the SVN revision of that commit.  We can stop once
86  # we've found our target (or hit a revision number lower than what
87  # we're looking for, indicating not found).
88
89  target_rev = int(target_rev)
90
91  # regexp matching the "commit" line from the log.
92  commit_re = re.compile(r'^commit ([a-f\d]{40})$')
93  # regexp matching the git-svn line from the log.
94  git_svn_re = re.compile(r'^\s+git-svn-id: [^@]+@(\d+) ')
95  if not branch_name:
96    branch_name = 'origin/master'
97  cmd = ['git', 'log', '--no-color', '--first-parent', '--pretty=medium',
98         branch_name]
99  logging.info(' '.join(cmd))
100  log = subprocess.Popen(cmd, shell=(os.name == 'nt'), stdout=subprocess.PIPE)
101  # Track whether we saw a revision *later* than the one we're seeking.
102  saw_later = False
103  for line in log.stdout:
104    match = commit_re.match(line)
105    if match:
106      commit = match.group(1)
107      continue
108    match = git_svn_re.match(line)
109    if match:
110      rev = int(match.group(1))
111      if rev <= target_rev:
112        log.stdout.close()  # Break pipe.
113        if rev < target_rev:
114          if not saw_later:
115            return None  # Can't be sure whether this rev is ok.
116          print ("WARNING: r%d not found, so using next nearest earlier r%d" %
117                 (target_rev, rev))
118        return commit
119      else:
120        saw_later = True
121
122  print "Error: reached end of log without finding commit info."
123  print "Something has likely gone horribly wrong."
124  return None
125
126
127def GetRemote():
128  branch = GetOverrideShortBranchName()
129  if not branch:
130    branch = 'gclient'
131
132  remote = RunGit(['config', '--get', 'branch.' + branch + '.remote'])
133  if remote:
134    return remote
135  return 'origin'
136
137
138def UpdateGClientBranch(branch_name, webkit_rev, magic_gclient_branch):
139  """Update the magic gclient branch to point at |webkit_rev|.
140
141  Returns: true if the branch didn't need changes."""
142  target = FindSVNRev(branch_name, webkit_rev)
143  if not target:
144    print "r%s not available; fetching." % webkit_rev
145    subprocess.check_call(['git', 'fetch', GetRemote()],
146                          shell=(os.name == 'nt'))
147    target = FindSVNRev(branch_name, webkit_rev)
148  if not target:
149    print "ERROR: Couldn't map r%s to a git revision." % webkit_rev
150    sys.exit(1)
151
152  current = RunGit(['show-ref', '--hash', magic_gclient_branch])
153  if current == target:
154    return False  # No change necessary.
155
156  subprocess.check_call(['git', 'update-ref', '-m', 'gclient sync',
157                         magic_gclient_branch, target],
158                         shell=(os.name == 'nt'))
159  return True
160
161
162def UpdateCurrentCheckoutIfAppropriate(magic_gclient_branch):
163  """Reset the current gclient branch if that's what we have checked out."""
164  branch = RunGit(['symbolic-ref', '-q', 'HEAD'])
165  if branch != magic_gclient_branch:
166    print "We have now updated the 'gclient' branch, but third_party/WebKit"
167    print "has some other branch ('%s') checked out." % branch
168    print "Run 'git checkout gclient' under third_party/WebKit if you want"
169    print "to switch it to the version requested by DEPS."
170    return 1
171
172  if subprocess.call(['git', 'diff-index', '--exit-code', '--shortstat',
173                      'HEAD'], shell=(os.name == 'nt')):
174    print "Resetting tree state to new revision."
175    subprocess.check_call(['git', 'reset', '--hard'], shell=(os.name == 'nt'))
176
177
178def main():
179  parser = optparse.OptionParser()
180  parser.add_option('-v', '--verbose', action='store_true')
181  parser.add_option('-r', '--revision', help="switch to desired revision")
182  parser.add_option('-t', '--tarball', help="switch to desired tarball release")
183  parser.add_option('-b', '--branch', help="branch name that gclient generate")
184  options, args = parser.parse_args()
185  if options.verbose:
186    logging.basicConfig(level=logging.INFO)
187  if not os.path.exists('third_party/WebKit/.git'):
188    if os.path.exists('third_party/WebKit'):
189      print "ERROR: third_party/WebKit appears to not be under git control."
190    else:
191      print "ERROR: third_party/WebKit could not be found."
192      print "Did you run this script from the right directory?"
193
194    print "See http://code.google.com/p/chromium/wiki/UsingWebKitGit for"
195    print "setup instructions."
196    return 1
197
198  if options.revision:
199    webkit_rev = options.revision
200    if options.tarball:
201      print "WARNING: --revision is given, so ignore --tarball"
202  else:
203    if options.tarball:
204      webkit_rev = GetWebKitRevFromTarball(options.tarball)
205    else:
206      webkit_rev = GetWebKitRev()
207
208  print 'Desired revision: r%s.' % webkit_rev
209  os.chdir('third_party/WebKit')
210  magic_gclient_branch = GetGClientBranchName()
211  changed = UpdateGClientBranch(options.branch, webkit_rev,
212                                magic_gclient_branch)
213  if changed:
214    return UpdateCurrentCheckoutIfAppropriate(magic_gclient_branch)
215  else:
216    print "Already on correct revision."
217  return 0
218
219
220if __name__ == '__main__':
221  sys.exit(main())
222