gclient_scm.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  1. # Copyright (c) 2010 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Gclient-specific SCM-specific operations."""
  5. import logging
  6. import os
  7. import posixpath
  8. import re
  9. import sys
  10. import time
  11. import gclient_utils
  12. import scm
  13. import subprocess2
  14. class DiffFilterer(object):
  15. """Simple class which tracks which file is being diffed and
  16. replaces instances of its file name in the original and
  17. working copy lines of the svn/git diff output."""
  18. index_string = "Index: "
  19. original_prefix = "--- "
  20. working_prefix = "+++ "
  21. def __init__(self, relpath):
  22. # Note that we always use '/' as the path separator to be
  23. # consistent with svn's cygwin-style output on Windows
  24. self._relpath = relpath.replace("\\", "/")
  25. self._current_file = ""
  26. self._replacement_file = ""
  27. def SetCurrentFile(self, current_file):
  28. self._current_file = current_file
  29. # Note that we always use '/' as the path separator to be
  30. # consistent with svn's cygwin-style output on Windows
  31. self._replacement_file = posixpath.join(self._relpath, current_file)
  32. def _Replace(self, line):
  33. return line.replace(self._current_file, self._replacement_file)
  34. def Filter(self, line):
  35. if (line.startswith(self.index_string)):
  36. self.SetCurrentFile(line[len(self.index_string):])
  37. line = self._Replace(line)
  38. else:
  39. if (line.startswith(self.original_prefix) or
  40. line.startswith(self.working_prefix)):
  41. line = self._Replace(line)
  42. print(line)
  43. def ask_for_data(prompt):
  44. try:
  45. return raw_input(prompt)
  46. except KeyboardInterrupt:
  47. # Hide the exception.
  48. sys.exit(1)
  49. ### SCM abstraction layer
  50. # Factory Method for SCM wrapper creation
  51. def GetScmName(url):
  52. if url:
  53. url, _ = gclient_utils.SplitUrlRevision(url)
  54. if (url.startswith('git://') or url.startswith('ssh://') or
  55. url.endswith('.git')):
  56. return 'git'
  57. elif (url.startswith('http://') or url.startswith('https://') or
  58. url.startswith('svn://') or url.startswith('svn+ssh://')):
  59. return 'svn'
  60. return None
  61. def CreateSCM(url, root_dir=None, relpath=None):
  62. SCM_MAP = {
  63. 'svn' : SVNWrapper,
  64. 'git' : GitWrapper,
  65. }
  66. scm_name = GetScmName(url)
  67. if not scm_name in SCM_MAP:
  68. raise gclient_utils.Error('No SCM found for url %s' % url)
  69. return SCM_MAP[scm_name](url, root_dir, relpath)
  70. # SCMWrapper base class
  71. class SCMWrapper(object):
  72. """Add necessary glue between all the supported SCM.
  73. This is the abstraction layer to bind to different SCM.
  74. """
  75. def __init__(self, url=None, root_dir=None, relpath=None):
  76. self.url = url
  77. self._root_dir = root_dir
  78. if self._root_dir:
  79. self._root_dir = self._root_dir.replace('/', os.sep)
  80. self.relpath = relpath
  81. if self.relpath:
  82. self.relpath = self.relpath.replace('/', os.sep)
  83. if self.relpath and self._root_dir:
  84. self.checkout_path = os.path.join(self._root_dir, self.relpath)
  85. def RunCommand(self, command, options, args, file_list=None):
  86. # file_list will have all files that are modified appended to it.
  87. if file_list is None:
  88. file_list = []
  89. commands = ['cleanup', 'update', 'updatesingle', 'revert',
  90. 'revinfo', 'status', 'diff', 'pack', 'runhooks']
  91. if not command in commands:
  92. raise gclient_utils.Error('Unknown command %s' % command)
  93. if not command in dir(self):
  94. raise gclient_utils.Error('Command %s not implemented in %s wrapper' % (
  95. command, self.__class__.__name__))
  96. return getattr(self, command)(options, args, file_list)
  97. class GitWrapper(SCMWrapper):
  98. """Wrapper for Git"""
  99. def GetRevisionDate(self, revision):
  100. """Returns the given revision's date in ISO-8601 format (which contains the
  101. time zone)."""
  102. # TODO(floitsch): get the time-stamp of the given revision and not just the
  103. # time-stamp of the currently checked out revision.
  104. return self._Capture(['log', '-n', '1', '--format=%ai'])
  105. @staticmethod
  106. def cleanup(options, args, file_list):
  107. """'Cleanup' the repo.
  108. There's no real git equivalent for the svn cleanup command, do a no-op.
  109. """
  110. def diff(self, options, args, file_list):
  111. merge_base = self._Capture(['merge-base', 'HEAD', 'origin'])
  112. self._Run(['diff', merge_base], options)
  113. def pack(self, options, args, file_list):
  114. """Generates a patch file which can be applied to the root of the
  115. repository.
  116. The patch file is generated from a diff of the merge base of HEAD and
  117. its upstream branch.
  118. """
  119. merge_base = self._Capture(['merge-base', 'HEAD', 'origin'])
  120. gclient_utils.CheckCallAndFilter(
  121. ['git', 'diff', merge_base],
  122. cwd=self.checkout_path,
  123. filter_fn=DiffFilterer(self.relpath).Filter)
  124. def update(self, options, args, file_list):
  125. """Runs git to update or transparently checkout the working copy.
  126. All updated files will be appended to file_list.
  127. Raises:
  128. Error: if can't get URL for relative path.
  129. """
  130. if args:
  131. raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
  132. self._CheckMinVersion("1.6.6")
  133. default_rev = "refs/heads/master"
  134. url, deps_revision = gclient_utils.SplitUrlRevision(self.url)
  135. rev_str = ""
  136. revision = deps_revision
  137. if options.revision:
  138. # Override the revision number.
  139. revision = str(options.revision)
  140. if not revision:
  141. revision = default_rev
  142. if gclient_utils.IsDateRevision(revision):
  143. # Date-revisions only work on git-repositories if the reflog hasn't
  144. # expired yet. Use rev-list to get the corresponding revision.
  145. # git rev-list -n 1 --before='time-stamp' branchname
  146. if options.transitive:
  147. print('Warning: --transitive only works for SVN repositories.')
  148. revision = default_rev
  149. rev_str = ' at %s' % revision
  150. files = []
  151. printed_path = False
  152. verbose = []
  153. if options.verbose:
  154. print('\n_____ %s%s' % (self.relpath, rev_str))
  155. verbose = ['--verbose']
  156. printed_path = True
  157. if revision.startswith('refs/heads/'):
  158. rev_type = "branch"
  159. elif revision.startswith('origin/'):
  160. # For compatability with old naming, translate 'origin' to 'refs/heads'
  161. revision = revision.replace('origin/', 'refs/heads/')
  162. rev_type = "branch"
  163. else:
  164. # hash is also a tag, only make a distinction at checkout
  165. rev_type = "hash"
  166. if not os.path.exists(self.checkout_path):
  167. self._Clone(revision, url, options)
  168. files = self._Capture(['ls-files']).splitlines()
  169. file_list.extend([os.path.join(self.checkout_path, f) for f in files])
  170. if not verbose:
  171. # Make the output a little prettier. It's nice to have some whitespace
  172. # between projects when cloning.
  173. print('')
  174. return
  175. if not os.path.exists(os.path.join(self.checkout_path, '.git')):
  176. raise gclient_utils.Error('\n____ %s%s\n'
  177. '\tPath is not a git repo. No .git dir.\n'
  178. '\tTo resolve:\n'
  179. '\t\trm -rf %s\n'
  180. '\tAnd run gclient sync again\n'
  181. % (self.relpath, rev_str, self.relpath))
  182. # See if the url has changed (the unittests use git://foo for the url, let
  183. # that through).
  184. current_url = self._Capture(['config', 'remote.origin.url'])
  185. # TODO(maruel): Delete url != 'git://foo' since it's just to make the
  186. # unit test pass. (and update the comment above)
  187. if current_url != url and url != 'git://foo':
  188. print('_____ switching %s to a new upstream' % self.relpath)
  189. # Make sure it's clean
  190. self._CheckClean(rev_str)
  191. # Switch over to the new upstream
  192. self._Run(['remote', 'set-url', 'origin', url], options)
  193. quiet = []
  194. if not options.verbose:
  195. quiet = ['--quiet']
  196. self._Run(['fetch', 'origin', '--prune'] + quiet, options)
  197. self._Run(['reset', '--hard', 'origin/master'] + quiet, options)
  198. files = self._Capture(['ls-files']).splitlines()
  199. file_list.extend([os.path.join(self.checkout_path, f) for f in files])
  200. return
  201. cur_branch = self._GetCurrentBranch()
  202. # Cases:
  203. # 0) HEAD is detached. Probably from our initial clone.
  204. # - make sure HEAD is contained by a named ref, then update.
  205. # Cases 1-4. HEAD is a branch.
  206. # 1) current branch is not tracking a remote branch (could be git-svn)
  207. # - try to rebase onto the new hash or branch
  208. # 2) current branch is tracking a remote branch with local committed
  209. # changes, but the DEPS file switched to point to a hash
  210. # - rebase those changes on top of the hash
  211. # 3) current branch is tracking a remote branch w/or w/out changes,
  212. # no switch
  213. # - see if we can FF, if not, prompt the user for rebase, merge, or stop
  214. # 4) current branch is tracking a remote branch, switches to a different
  215. # remote branch
  216. # - exit
  217. # GetUpstreamBranch returns something like 'refs/remotes/origin/master' for
  218. # a tracking branch
  219. # or 'master' if not a tracking branch (it's based on a specific rev/hash)
  220. # or it returns None if it couldn't find an upstream
  221. if cur_branch is None:
  222. upstream_branch = None
  223. current_type = "detached"
  224. logging.debug("Detached HEAD")
  225. else:
  226. upstream_branch = scm.GIT.GetUpstreamBranch(self.checkout_path)
  227. if not upstream_branch or not upstream_branch.startswith('refs/remotes'):
  228. current_type = "hash"
  229. logging.debug("Current branch is not tracking an upstream (remote)"
  230. " branch.")
  231. elif upstream_branch.startswith('refs/remotes'):
  232. current_type = "branch"
  233. else:
  234. raise gclient_utils.Error('Invalid Upstream: %s' % upstream_branch)
  235. # Update the remotes first so we have all the refs.
  236. backoff_time = 5
  237. for _ in range(10):
  238. try:
  239. remote_output = scm.GIT.Capture(
  240. ['remote'] + verbose + ['update'],
  241. cwd=self.checkout_path)
  242. break
  243. except gclient_utils.CheckCallError, e:
  244. # Hackish but at that point, git is known to work so just checking for
  245. # 502 in stderr should be fine.
  246. if '502' in e.stderr:
  247. print(str(e))
  248. print('Sleeping %.1f seconds and retrying...' % backoff_time)
  249. time.sleep(backoff_time)
  250. backoff_time *= 1.3
  251. continue
  252. raise
  253. if verbose:
  254. print(remote_output.strip())
  255. # This is a big hammer, debatable if it should even be here...
  256. if options.force or options.reset:
  257. self._Run(['reset', '--hard', 'HEAD'], options)
  258. if current_type == 'detached':
  259. # case 0
  260. self._CheckClean(rev_str)
  261. self._CheckDetachedHead(rev_str, options)
  262. self._Capture(['checkout', '--quiet', '%s^0' % revision])
  263. if not printed_path:
  264. print('\n_____ %s%s' % (self.relpath, rev_str))
  265. elif current_type == 'hash':
  266. # case 1
  267. if scm.GIT.IsGitSvn(self.checkout_path) and upstream_branch is not None:
  268. # Our git-svn branch (upstream_branch) is our upstream
  269. self._AttemptRebase(upstream_branch, files, options,
  270. newbase=revision, printed_path=printed_path)
  271. printed_path = True
  272. else:
  273. # Can't find a merge-base since we don't know our upstream. That makes
  274. # this command VERY likely to produce a rebase failure. For now we
  275. # assume origin is our upstream since that's what the old behavior was.
  276. upstream_branch = 'origin'
  277. if options.revision or deps_revision:
  278. upstream_branch = revision
  279. self._AttemptRebase(upstream_branch, files, options,
  280. printed_path=printed_path)
  281. printed_path = True
  282. elif rev_type == 'hash':
  283. # case 2
  284. self._AttemptRebase(upstream_branch, files, options,
  285. newbase=revision, printed_path=printed_path)
  286. printed_path = True
  287. elif revision.replace('heads', 'remotes/origin') != upstream_branch:
  288. # case 4
  289. new_base = revision.replace('heads', 'remotes/origin')
  290. if not printed_path:
  291. print('\n_____ %s%s' % (self.relpath, rev_str))
  292. switch_error = ("Switching upstream branch from %s to %s\n"
  293. % (upstream_branch, new_base) +
  294. "Please merge or rebase manually:\n" +
  295. "cd %s; git rebase %s\n" % (self.checkout_path, new_base) +
  296. "OR git checkout -b <some new branch> %s" % new_base)
  297. raise gclient_utils.Error(switch_error)
  298. else:
  299. # case 3 - the default case
  300. files = self._Capture(['diff', upstream_branch, '--name-only']).split()
  301. if verbose:
  302. print('Trying fast-forward merge to branch : %s' % upstream_branch)
  303. try:
  304. merge_output = scm.GIT.Capture(['merge', '--ff-only', upstream_branch],
  305. cwd=self.checkout_path)
  306. except gclient_utils.CheckCallError, e:
  307. if re.match('fatal: Not possible to fast-forward, aborting.', e.stderr):
  308. if not printed_path:
  309. print('\n_____ %s%s' % (self.relpath, rev_str))
  310. printed_path = True
  311. while True:
  312. try:
  313. # TODO(maruel): That can't work with --jobs.
  314. action = ask_for_data(
  315. 'Cannot fast-forward merge, attempt to rebase? '
  316. '(y)es / (q)uit / (s)kip : ')
  317. except ValueError:
  318. raise gclient_utils.Error('Invalid Character')
  319. if re.match(r'yes|y', action, re.I):
  320. self._AttemptRebase(upstream_branch, files, options,
  321. printed_path=printed_path)
  322. printed_path = True
  323. break
  324. elif re.match(r'quit|q', action, re.I):
  325. raise gclient_utils.Error("Can't fast-forward, please merge or "
  326. "rebase manually.\n"
  327. "cd %s && git " % self.checkout_path
  328. + "rebase %s" % upstream_branch)
  329. elif re.match(r'skip|s', action, re.I):
  330. print('Skipping %s' % self.relpath)
  331. return
  332. else:
  333. print('Input not recognized')
  334. elif re.match("error: Your local changes to '.*' would be "
  335. "overwritten by merge. Aborting.\nPlease, commit your "
  336. "changes or stash them before you can merge.\n",
  337. e.stderr):
  338. if not printed_path:
  339. print('\n_____ %s%s' % (self.relpath, rev_str))
  340. printed_path = True
  341. raise gclient_utils.Error(e.stderr)
  342. else:
  343. # Some other problem happened with the merge
  344. logging.error("Error during fast-forward merge in %s!" % self.relpath)
  345. print(e.stderr)
  346. raise
  347. else:
  348. # Fast-forward merge was successful
  349. if not re.match('Already up-to-date.', merge_output) or verbose:
  350. if not printed_path:
  351. print('\n_____ %s%s' % (self.relpath, rev_str))
  352. printed_path = True
  353. print(merge_output.strip())
  354. if not verbose:
  355. # Make the output a little prettier. It's nice to have some
  356. # whitespace between projects when syncing.
  357. print('')
  358. file_list.extend([os.path.join(self.checkout_path, f) for f in files])
  359. # If the rebase generated a conflict, abort and ask user to fix
  360. if self._IsRebasing():
  361. raise gclient_utils.Error('\n____ %s%s\n'
  362. '\nConflict while rebasing this branch.\n'
  363. 'Fix the conflict and run gclient again.\n'
  364. 'See man git-rebase for details.\n'
  365. % (self.relpath, rev_str))
  366. if verbose:
  367. print('Checked out revision %s' % self.revinfo(options, (), None))
  368. def revert(self, options, args, file_list):
  369. """Reverts local modifications.
  370. All reverted files will be appended to file_list.
  371. """
  372. if not os.path.isdir(self.checkout_path):
  373. # revert won't work if the directory doesn't exist. It needs to
  374. # checkout instead.
  375. print('\n_____ %s is missing, synching instead' % self.relpath)
  376. # Don't reuse the args.
  377. return self.update(options, [], file_list)
  378. default_rev = "refs/heads/master"
  379. _, deps_revision = gclient_utils.SplitUrlRevision(self.url)
  380. if not deps_revision:
  381. deps_revision = default_rev
  382. if deps_revision.startswith('refs/heads/'):
  383. deps_revision = deps_revision.replace('refs/heads/', 'origin/')
  384. files = self._Capture(['diff', deps_revision, '--name-only']).split()
  385. self._Run(['reset', '--hard', deps_revision], options)
  386. file_list.extend([os.path.join(self.checkout_path, f) for f in files])
  387. def revinfo(self, options, args, file_list):
  388. """Returns revision"""
  389. return self._Capture(['rev-parse', 'HEAD'])
  390. def runhooks(self, options, args, file_list):
  391. self.status(options, args, file_list)
  392. def status(self, options, args, file_list):
  393. """Display status information."""
  394. if not os.path.isdir(self.checkout_path):
  395. print(('\n________ couldn\'t run status in %s:\n'
  396. 'The directory does not exist.') % self.checkout_path)
  397. else:
  398. merge_base = self._Capture(['merge-base', 'HEAD', 'origin'])
  399. self._Run(['diff', '--name-status', merge_base], options)
  400. files = self._Capture(['diff', '--name-only', merge_base]).split()
  401. file_list.extend([os.path.join(self.checkout_path, f) for f in files])
  402. def FullUrlForRelativeUrl(self, url):
  403. # Strip from last '/'
  404. # Equivalent to unix basename
  405. base_url = self.url
  406. return base_url[:base_url.rfind('/')] + url
  407. def _Clone(self, revision, url, options):
  408. """Clone a git repository from the given URL.
  409. Once we've cloned the repo, we checkout a working branch if the specified
  410. revision is a branch head. If it is a tag or a specific commit, then we
  411. leave HEAD detached as it makes future updates simpler -- in this case the
  412. user should first create a new branch or switch to an existing branch before
  413. making changes in the repo."""
  414. if not options.verbose:
  415. # git clone doesn't seem to insert a newline properly before printing
  416. # to stdout
  417. print('')
  418. clone_cmd = ['clone']
  419. if revision.startswith('refs/heads/'):
  420. clone_cmd.extend(['-b', revision.replace('refs/heads/', '')])
  421. detach_head = False
  422. else:
  423. clone_cmd.append('--no-checkout')
  424. detach_head = True
  425. if options.verbose:
  426. clone_cmd.append('--verbose')
  427. clone_cmd.extend([url, self.checkout_path])
  428. for _ in range(3):
  429. try:
  430. self._Run(clone_cmd, options, cwd=self._root_dir)
  431. break
  432. except (gclient_utils.Error, subprocess2.CalledProcessError), e:
  433. # TODO(maruel): Hackish, should be fixed by moving _Run() to
  434. # CheckCall().
  435. # Too bad we don't have access to the actual output.
  436. # We should check for "transfer closed with NNN bytes remaining to
  437. # read". In the meantime, just make sure .git exists.
  438. if (e.args[0] == 'git command clone returned 128' and
  439. os.path.exists(os.path.join(self.checkout_path, '.git'))):
  440. print(str(e))
  441. print('Retrying...')
  442. continue
  443. raise e
  444. if detach_head:
  445. # Squelch git's very verbose detached HEAD warning and use our own
  446. self._Capture(['checkout', '--quiet', '%s^0' % revision])
  447. print(
  448. ('Checked out %s to a detached HEAD. Before making any commits\n'
  449. 'in this repo, you should use \'git checkout <branch>\' to switch to\n'
  450. 'an existing branch or use \'git checkout origin -b <branch>\' to\n'
  451. 'create a new branch for your work.') % revision)
  452. def _AttemptRebase(self, upstream, files, options, newbase=None,
  453. branch=None, printed_path=False):
  454. """Attempt to rebase onto either upstream or, if specified, newbase."""
  455. files.extend(self._Capture(['diff', upstream, '--name-only']).split())
  456. revision = upstream
  457. if newbase:
  458. revision = newbase
  459. if not printed_path:
  460. print('\n_____ %s : Attempting rebase onto %s...' % (
  461. self.relpath, revision))
  462. printed_path = True
  463. else:
  464. print('Attempting rebase onto %s...' % revision)
  465. # Build the rebase command here using the args
  466. # git rebase [options] [--onto <newbase>] <upstream> [<branch>]
  467. rebase_cmd = ['rebase']
  468. if options.verbose:
  469. rebase_cmd.append('--verbose')
  470. if newbase:
  471. rebase_cmd.extend(['--onto', newbase])
  472. rebase_cmd.append(upstream)
  473. if branch:
  474. rebase_cmd.append(branch)
  475. try:
  476. rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
  477. except gclient_utils.CheckCallError, e:
  478. if (re.match(r'cannot rebase: you have unstaged changes', e.stderr) or
  479. re.match(r'cannot rebase: your index contains uncommitted changes',
  480. e.stderr)):
  481. while True:
  482. rebase_action = ask_for_data(
  483. 'Cannot rebase because of unstaged changes.\n'
  484. '\'git reset --hard HEAD\' ?\n'
  485. 'WARNING: destroys any uncommitted work in your current branch!'
  486. ' (y)es / (q)uit / (s)how : ')
  487. if re.match(r'yes|y', rebase_action, re.I):
  488. self._Run(['reset', '--hard', 'HEAD'], options)
  489. # Should this be recursive?
  490. rebase_output = scm.GIT.Capture(rebase_cmd, cwd=self.checkout_path)
  491. break
  492. elif re.match(r'quit|q', rebase_action, re.I):
  493. raise gclient_utils.Error("Please merge or rebase manually\n"
  494. "cd %s && git " % self.checkout_path
  495. + "%s" % ' '.join(rebase_cmd))
  496. elif re.match(r'show|s', rebase_action, re.I):
  497. print('\n%s' % e.stderr.strip())
  498. continue
  499. else:
  500. gclient_utils.Error("Input not recognized")
  501. continue
  502. elif re.search(r'^CONFLICT', e.stdout, re.M):
  503. raise gclient_utils.Error("Conflict while rebasing this branch.\n"
  504. "Fix the conflict and run gclient again.\n"
  505. "See 'man git-rebase' for details.\n")
  506. else:
  507. print(e.stdout.strip())
  508. print('Rebase produced error output:\n%s' % e.stderr.strip())
  509. raise gclient_utils.Error("Unrecognized error, please merge or rebase "
  510. "manually.\ncd %s && git " %
  511. self.checkout_path
  512. + "%s" % ' '.join(rebase_cmd))
  513. print(rebase_output.strip())
  514. if not options.verbose:
  515. # Make the output a little prettier. It's nice to have some
  516. # whitespace between projects when syncing.
  517. print('')
  518. @staticmethod
  519. def _CheckMinVersion(min_version):
  520. (ok, current_version) = scm.GIT.AssertVersion(min_version)
  521. if not ok:
  522. raise gclient_utils.Error('git version %s < minimum required %s' %
  523. (current_version, min_version))
  524. def _IsRebasing(self):
  525. # Check for any of REBASE-i/REBASE-m/REBASE/AM. Unfortunately git doesn't
  526. # have a plumbing command to determine whether a rebase is in progress, so
  527. # for now emualate (more-or-less) git-rebase.sh / git-completion.bash
  528. g = os.path.join(self.checkout_path, '.git')
  529. return (
  530. os.path.isdir(os.path.join(g, "rebase-merge")) or
  531. os.path.isdir(os.path.join(g, "rebase-apply")))
  532. def _CheckClean(self, rev_str):
  533. # Make sure the tree is clean; see git-rebase.sh for reference
  534. try:
  535. scm.GIT.Capture(['update-index', '--ignore-submodules', '--refresh'],
  536. cwd=self.checkout_path)
  537. except gclient_utils.CheckCallError:
  538. raise gclient_utils.Error('\n____ %s%s\n'
  539. '\tYou have unstaged changes.\n'
  540. '\tPlease commit, stash, or reset.\n'
  541. % (self.relpath, rev_str))
  542. try:
  543. scm.GIT.Capture(['diff-index', '--cached', '--name-status', '-r',
  544. '--ignore-submodules', 'HEAD', '--'],
  545. cwd=self.checkout_path)
  546. except gclient_utils.CheckCallError:
  547. raise gclient_utils.Error('\n____ %s%s\n'
  548. '\tYour index contains uncommitted changes\n'
  549. '\tPlease commit, stash, or reset.\n'
  550. % (self.relpath, rev_str))
  551. def _CheckDetachedHead(self, rev_str, options):
  552. # HEAD is detached. Make sure it is safe to move away from (i.e., it is
  553. # reference by a commit). If not, error out -- most likely a rebase is
  554. # in progress, try to detect so we can give a better error.
  555. try:
  556. scm.GIT.Capture(['name-rev', '--no-undefined', 'HEAD'],
  557. cwd=self.checkout_path)
  558. except gclient_utils.CheckCallError:
  559. # Commit is not contained by any rev. See if the user is rebasing:
  560. if self._IsRebasing():
  561. # Punt to the user
  562. raise gclient_utils.Error('\n____ %s%s\n'
  563. '\tAlready in a conflict, i.e. (no branch).\n'
  564. '\tFix the conflict and run gclient again.\n'
  565. '\tOr to abort run:\n\t\tgit-rebase --abort\n'
  566. '\tSee man git-rebase for details.\n'
  567. % (self.relpath, rev_str))
  568. # Let's just save off the commit so we can proceed.
  569. name = ('saved-by-gclient-' +
  570. self._Capture(['rev-parse', '--short', 'HEAD']))
  571. self._Capture(['branch', name])
  572. print('\n_____ found an unreferenced commit and saved it as \'%s\'' %
  573. name)
  574. def _GetCurrentBranch(self):
  575. # Returns name of current branch or None for detached HEAD
  576. branch = self._Capture(['rev-parse', '--abbrev-ref=strict', 'HEAD'])
  577. if branch == 'HEAD':
  578. return None
  579. return branch
  580. def _Capture(self, args):
  581. return gclient_utils.CheckCall(
  582. ['git'] + args, cwd=self.checkout_path, print_error=False)[0].strip()
  583. def _Run(self, args, options, **kwargs):
  584. kwargs.setdefault('cwd', self.checkout_path)
  585. gclient_utils.CheckCallAndFilterAndHeader(['git'] + args,
  586. always=options.verbose, **kwargs)
  587. class SVNWrapper(SCMWrapper):
  588. """ Wrapper for SVN """
  589. def GetRevisionDate(self, revision):
  590. """Returns the given revision's date in ISO-8601 format (which contains the
  591. time zone)."""
  592. date = scm.SVN.Capture(['propget', '--revprop', 'svn:date', '-r', revision,
  593. os.path.join(self.checkout_path, '.')])
  594. return date.strip()
  595. def cleanup(self, options, args, file_list):
  596. """Cleanup working copy."""
  597. self._Run(['cleanup'] + args, options)
  598. def diff(self, options, args, file_list):
  599. # NOTE: This function does not currently modify file_list.
  600. if not os.path.isdir(self.checkout_path):
  601. raise gclient_utils.Error('Directory %s is not present.' %
  602. self.checkout_path)
  603. self._Run(['diff'] + args, options)
  604. def pack(self, options, args, file_list):
  605. """Generates a patch file which can be applied to the root of the
  606. repository."""
  607. if not os.path.isdir(self.checkout_path):
  608. raise gclient_utils.Error('Directory %s is not present.' %
  609. self.checkout_path)
  610. gclient_utils.CheckCallAndFilter(
  611. ['svn', 'diff', '-x', '--ignore-eol-style'] + args,
  612. cwd=self.checkout_path,
  613. print_stdout=False,
  614. filter_fn=DiffFilterer(self.relpath).Filter)
  615. def update(self, options, args, file_list):
  616. """Runs svn to update or transparently checkout the working copy.
  617. All updated files will be appended to file_list.
  618. Raises:
  619. Error: if can't get URL for relative path.
  620. """
  621. # Only update if git or hg is not controlling the directory.
  622. git_path = os.path.join(self.checkout_path, '.git')
  623. if os.path.exists(git_path):
  624. print('________ found .git directory; skipping %s' % self.relpath)
  625. return
  626. hg_path = os.path.join(self.checkout_path, '.hg')
  627. if os.path.exists(hg_path):
  628. print('________ found .hg directory; skipping %s' % self.relpath)
  629. return
  630. if args:
  631. raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
  632. # revision is the revision to match. It is None if no revision is specified,
  633. # i.e. the 'deps ain't pinned'.
  634. url, revision = gclient_utils.SplitUrlRevision(self.url)
  635. # Keep the original unpinned url for reference in case the repo is switched.
  636. base_url = url
  637. if options.revision:
  638. # Override the revision number.
  639. revision = str(options.revision)
  640. if revision:
  641. forced_revision = True
  642. # Reconstruct the url.
  643. url = '%s@%s' % (url, revision)
  644. rev_str = ' at %s' % revision
  645. else:
  646. forced_revision = False
  647. rev_str = ''
  648. if not os.path.exists(self.checkout_path):
  649. # We need to checkout.
  650. command = ['checkout', url, self.checkout_path]
  651. command = self._AddAdditionalUpdateFlags(command, options, revision)
  652. self._RunAndGetFileList(command, options, file_list, self._root_dir)
  653. return
  654. # Get the existing scm url and the revision number of the current checkout.
  655. try:
  656. from_info = scm.SVN.CaptureInfo(os.path.join(self.checkout_path, '.'))
  657. except (gclient_utils.Error, subprocess2.CalledProcessError):
  658. raise gclient_utils.Error(
  659. ('Can\'t update/checkout %s if an unversioned directory is present. '
  660. 'Delete the directory and try again.') % self.checkout_path)
  661. # Look for locked directories.
  662. dir_info = scm.SVN.CaptureStatus(os.path.join(self.checkout_path, '.'))
  663. if [True for d in dir_info
  664. if d[0][2] == 'L' and d[1] == self.checkout_path]:
  665. # The current directory is locked, clean it up.
  666. self._Run(['cleanup'], options)
  667. # Retrieve the current HEAD version because svn is slow at null updates.
  668. if options.manually_grab_svn_rev and not revision:
  669. from_info_live = scm.SVN.CaptureInfo(from_info['URL'])
  670. revision = str(from_info_live['Revision'])
  671. rev_str = ' at %s' % revision
  672. if from_info['URL'] != base_url:
  673. # The repository url changed, need to switch.
  674. try:
  675. to_info = scm.SVN.CaptureInfo(url)
  676. except (gclient_utils.Error, subprocess2.CalledProcessError):
  677. # The url is invalid or the server is not accessible, it's safer to bail
  678. # out right now.
  679. raise gclient_utils.Error('This url is unreachable: %s' % url)
  680. can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
  681. and (from_info['UUID'] == to_info['UUID']))
  682. if can_switch:
  683. print('\n_____ relocating %s to a new checkout' % self.relpath)
  684. # We have different roots, so check if we can switch --relocate.
  685. # Subversion only permits this if the repository UUIDs match.
  686. # Perform the switch --relocate, then rewrite the from_url
  687. # to reflect where we "are now." (This is the same way that
  688. # Subversion itself handles the metadata when switch --relocate
  689. # is used.) This makes the checks below for whether we
  690. # can update to a revision or have to switch to a different
  691. # branch work as expected.
  692. # TODO(maruel): TEST ME !
  693. command = ['switch', '--relocate',
  694. from_info['Repository Root'],
  695. to_info['Repository Root'],
  696. self.relpath]
  697. self._Run(command, options, cwd=self._root_dir)
  698. from_info['URL'] = from_info['URL'].replace(
  699. from_info['Repository Root'],
  700. to_info['Repository Root'])
  701. else:
  702. if not options.force and not options.reset:
  703. # Look for local modifications but ignore unversioned files.
  704. for status in scm.SVN.CaptureStatus(self.checkout_path):
  705. if status[0] != '?':
  706. raise gclient_utils.Error(
  707. ('Can\'t switch the checkout to %s; UUID don\'t match and '
  708. 'there is local changes in %s. Delete the directory and '
  709. 'try again.') % (url, self.checkout_path))
  710. # Ok delete it.
  711. print('\n_____ switching %s to a new checkout' % self.relpath)
  712. gclient_utils.RemoveDirectory(self.checkout_path)
  713. # We need to checkout.
  714. command = ['checkout', url, self.checkout_path]
  715. command = self._AddAdditionalUpdateFlags(command, options, revision)
  716. self._RunAndGetFileList(command, options, file_list, self._root_dir)
  717. return
  718. # If the provided url has a revision number that matches the revision
  719. # number of the existing directory, then we don't need to bother updating.
  720. if not options.force and str(from_info['Revision']) == revision:
  721. if options.verbose or not forced_revision:
  722. print('\n_____ %s%s' % (self.relpath, rev_str))
  723. return
  724. command = ['update', self.checkout_path]
  725. command = self._AddAdditionalUpdateFlags(command, options, revision)
  726. self._RunAndGetFileList(command, options, file_list, self._root_dir)
  727. def updatesingle(self, options, args, file_list):
  728. filename = args.pop()
  729. if scm.SVN.AssertVersion("1.5")[0]:
  730. if not os.path.exists(os.path.join(self.checkout_path, '.svn')):
  731. # Create an empty checkout and then update the one file we want. Future
  732. # operations will only apply to the one file we checked out.
  733. command = ["checkout", "--depth", "empty", self.url, self.checkout_path]
  734. self._Run(command, options, cwd=self._root_dir)
  735. if os.path.exists(os.path.join(self.checkout_path, filename)):
  736. os.remove(os.path.join(self.checkout_path, filename))
  737. command = ["update", filename]
  738. self._RunAndGetFileList(command, options, file_list)
  739. # After the initial checkout, we can use update as if it were any other
  740. # dep.
  741. self.update(options, args, file_list)
  742. else:
  743. # If the installed version of SVN doesn't support --depth, fallback to
  744. # just exporting the file. This has the downside that revision
  745. # information is not stored next to the file, so we will have to
  746. # re-export the file every time we sync.
  747. if not os.path.exists(self.checkout_path):
  748. os.makedirs(self.checkout_path)
  749. command = ["export", os.path.join(self.url, filename),
  750. os.path.join(self.checkout_path, filename)]
  751. command = self._AddAdditionalUpdateFlags(command, options,
  752. options.revision)
  753. self._Run(command, options, cwd=self._root_dir)
  754. def revert(self, options, args, file_list):
  755. """Reverts local modifications. Subversion specific.
  756. All reverted files will be appended to file_list, even if Subversion
  757. doesn't know about them.
  758. """
  759. if not os.path.isdir(self.checkout_path):
  760. # svn revert won't work if the directory doesn't exist. It needs to
  761. # checkout instead.
  762. print('\n_____ %s is missing, synching instead' % self.relpath)
  763. # Don't reuse the args.
  764. return self.update(options, [], file_list)
  765. def printcb(file_status):
  766. file_list.append(file_status[1])
  767. if logging.getLogger().isEnabledFor(logging.INFO):
  768. logging.info('%s%s' % (file_status[0], file_status[1]))
  769. else:
  770. print(os.path.join(self.checkout_path, file_status[1]))
  771. scm.SVN.Revert(self.checkout_path, callback=printcb)
  772. try:
  773. # svn revert is so broken we don't even use it. Using
  774. # "svn up --revision BASE" achieve the same effect.
  775. # file_list will contain duplicates.
  776. self._RunAndGetFileList(['update', '--revision', 'BASE'], options,
  777. file_list)
  778. except OSError, e:
  779. # Maybe the directory disapeared meanwhile. Do not throw an exception.
  780. logging.error('Failed to update:\n%s' % str(e))
  781. def revinfo(self, options, args, file_list):
  782. """Display revision"""
  783. try:
  784. return scm.SVN.CaptureRevision(self.checkout_path)
  785. except (gclient_utils.Error, subprocess2.CalledProcessError):
  786. return None
  787. def runhooks(self, options, args, file_list):
  788. self.status(options, args, file_list)
  789. def status(self, options, args, file_list):
  790. """Display status information."""
  791. command = ['status'] + args
  792. if not os.path.isdir(self.checkout_path):
  793. # svn status won't work if the directory doesn't exist.
  794. print(('\n________ couldn\'t run \'%s\' in \'%s\':\n'
  795. 'The directory does not exist.') %
  796. (' '.join(command), self.checkout_path))
  797. # There's no file list to retrieve.
  798. else:
  799. self._RunAndGetFileList(command, options, file_list)
  800. def FullUrlForRelativeUrl(self, url):
  801. # Find the forth '/' and strip from there. A bit hackish.
  802. return '/'.join(self.url.split('/')[:4]) + url
  803. def _Run(self, args, options, **kwargs):
  804. """Runs a commands that goes to stdout."""
  805. kwargs.setdefault('cwd', self.checkout_path)
  806. gclient_utils.CheckCallAndFilterAndHeader(['svn'] + args,
  807. always=options.verbose, **kwargs)
  808. def _RunAndGetFileList(self, args, options, file_list, cwd=None):
  809. """Runs a commands that goes to stdout and grabs the file listed."""
  810. cwd = cwd or self.checkout_path
  811. scm.SVN.RunAndGetFileList(
  812. options.verbose,
  813. args + ['--ignore-externals'],
  814. cwd=cwd,
  815. file_list=file_list)
  816. @staticmethod
  817. def _AddAdditionalUpdateFlags(command, options, revision):
  818. """Add additional flags to command depending on what options are set.
  819. command should be a list of strings that represents an svn command.
  820. This method returns a new list to be used as a command."""
  821. new_command = command[:]
  822. if revision:
  823. new_command.extend(['--revision', str(revision).strip()])
  824. # --force was added to 'svn update' in svn 1.5.
  825. if ((options.force or options.manually_grab_svn_rev) and
  826. scm.SVN.AssertVersion("1.5")[0]):
  827. new_command.append('--force')
  828. return new_command