| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2014 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 |
| 5 import collections |
| 6 import logging |
| 7 import posixpath |
| 8 import sys |
| 9 |
| 10 from infra.libs.git2 import CalledProcessError |
| 11 from infra.libs.git2 import INVALID |
| 12 from infra.libs.git2 import config_ref |
| 13 from infra.libs.git2 import repo |
| 14 |
| 15 |
| 16 LOGGER = logging.getLogger(__name__) |
| 17 |
| 18 |
| 19 ################################################################################ |
| 20 # ConfigRef |
| 21 ################################################################################ |
| 22 |
| 23 class GsubtreedConfigRef(config_ref.ConfigRef): |
| 24 CONVERT = { |
| 25 'interval': lambda self, val: float(val), |
| 26 'subtree_synthesized_prefix': lambda self, val: str(val), |
| 27 'subtree_processed_prefix': lambda self, val: str(val), |
| 28 |
| 29 'base_url': lambda self, val: str(val) or self.repo.url, |
| 30 'enabled_refglobs': lambda self, val: map(str, list(val)), |
| 31 # normpath to avoid trailing/double-slash errors. |
| 32 'enabled_paths': lambda self, val: map(posixpath.normpath, map(str, val)), |
| 33 } |
| 34 DEFAULTS = { |
| 35 'interval': 5.0, |
| 36 |
| 37 # e.g. while processing the subtree 'b/foo' on refs/heads/master |
| 38 # refs/heads/master <- real commits |
| 39 # refs/subtree-processed/b/foo/-/heads/master <- ancestor tag of master |
| 40 # refs/subtree-synthesized/b/foo/-/heads/master <- ref with synth commits |
| 41 # For the sake of implementation simplicity, this daemon assumes the |
| 42 # googlesource.com guarantee of transactional multi-ref pushes within a |
| 43 # single repo. |
| 44 'subtree_processed_prefix': 'refs/subtree-processed', |
| 45 'subtree_synthesized_prefix': 'refs/subtree-synthesized', |
| 46 |
| 47 # The base URL is the url relative to which all mirror repos are assumed to |
| 48 # exist. For example, if you mirror the path 'bob', and base_url is |
| 49 # https://host.domain.tld/main_repo, then it would assume that the mirror |
| 50 # for the bob subtree is https://host.domain.tld/main_repo/bob. |
| 51 # |
| 52 # By default, base_url is set to the repo that gsubtreed is processing |
| 53 'base_url': None, |
| 54 'enabled_refglobs': ['refs/heads/*'], |
| 55 'enabled_paths': [], |
| 56 } |
| 57 REF = 'refs/gsubtreed-config/main' |
| 58 |
| 59 |
| 60 |
| 61 ################################################################################ |
| 62 # Core functionality |
| 63 ################################################################################ |
| 64 |
| 65 def process_path(path, origin_repo, config): |
| 66 def join(prefix, ref): |
| 67 assert ref.ref.startswith('refs/') |
| 68 ref = '/'.join((prefix, path)) + '/-/' + ref.ref[len('refs/'):] |
| 69 return origin_repo[ref] |
| 70 |
| 71 origin_push = {} |
| 72 |
| 73 base_url = config['base_url'] |
| 74 mirror_url = '[FILE-URL]' if base_url.startswith('file:') else origin_repo.url |
| 75 |
| 76 subtree_repo = repo.Repo(posixpath.join(base_url, path)) |
| 77 subtree_repo.repos_dir = origin_repo.repos_dir |
| 78 subtree_repo.reify(share_from=origin_repo) |
| 79 subtree_repo.run('fetch', stdout=sys.stdout, stderr=sys.stderr) |
| 80 subtree_repo_push = {} |
| 81 |
| 82 synthed_count = 0 |
| 83 |
| 84 for glob in config['enabled_refglobs']: |
| 85 for ref in origin_repo.refglob(glob): |
| 86 LOGGER.info('processing ref %s', ref) |
| 87 processed = join(config['subtree_processed_prefix'], ref) |
| 88 synthed = join(config['subtree_synthesized_prefix'], ref) |
| 89 |
| 90 synth_parent = synthed.commit |
| 91 cur_tree = synthed.commit.data.tree |
| 92 LOGGER.info('starting with tree %r', cur_tree) |
| 93 |
| 94 for commit in processed.to(ref): |
| 95 LOGGER.info('processing commit %s', commit) |
| 96 try: |
| 97 obj_name = '{.hsh}:{}'.format(commit, path) |
| 98 typ = origin_repo.run('cat-file', '-t', obj_name).strip() |
| 99 if typ != 'tree': |
| 100 LOGGER.warn('path %r is not a tree in commit %s', path, commit) |
| 101 continue |
| 102 dir_tree = origin_repo.run('rev-parse', obj_name).strip() |
| 103 except CalledProcessError: |
| 104 continue |
| 105 |
| 106 if dir_tree != cur_tree: |
| 107 LOGGER.info('found new tree %r', dir_tree) |
| 108 cur_tree = dir_tree |
| 109 synthed_count += 1 |
| 110 synth_parent = commit.alter( |
| 111 parents=[synth_parent.hsh] if synth_parent is not INVALID else [], |
| 112 tree=dir_tree, |
| 113 footers=collections.OrderedDict([ |
| 114 ('Cr-Mirrored-From', [mirror_url]), |
| 115 ('Cr-Mirrored-Commit', [commit.hsh]), |
| 116 ('Cr-Mirrored-Subtree', [path]), |
| 117 ]), |
| 118 ) |
| 119 origin_push[synthed] = synth_parent |
| 120 subtree_repo_push[subtree_repo[ref.ref]] = synth_parent |
| 121 |
| 122 origin_push[processed] = ref.commit |
| 123 |
| 124 success = True |
| 125 try: |
| 126 # because the hashes are deterministic based on the real history, if the |
| 127 # first push succeeds, but the second does not, it just means we'll end up |
| 128 # doing a bit of extra work on the next loop, but correctness will still be |
| 129 # ensured |
| 130 |
| 131 # TODO(iannucci): Return the pushspecs from this method, and then thread |
| 132 # the dispatches to subtree_repo. Additionally, can batch the origin_repo |
| 133 # pushes (and push them serially in batches as the subtree_repo pushes |
| 134 # complete). As long as each (sub, origin) push happens in order, it doesn't |
| 135 # matter what else gets pushed along with it. |
| 136 subtree_repo.fast_forward_push(subtree_repo_push) |
| 137 origin_repo.fast_forward_push(origin_push) |
| 138 except Exception: # pragma: no cover |
| 139 LOGGER.exception('Caught exception while pushing in process_path') |
| 140 success = False |
| 141 |
| 142 return success, synthed_count |
| 143 |
| 144 |
| 145 def inner_loop(origin_repo, config): |
| 146 """Returns (success, {path: #commits_synthesized}).""" |
| 147 |
| 148 LOGGER.debug('fetching %r', origin_repo) |
| 149 origin_repo.run('fetch', stdout=sys.stdout, stderr=sys.stderr) |
| 150 config.evaluate() |
| 151 |
| 152 success = True |
| 153 processed = {} |
| 154 for path in config['enabled_paths']: |
| 155 LOGGER.info('processing path %s', path) |
| 156 try: |
| 157 path_success, num_synthed = process_path(path, origin_repo, config) |
| 158 success = path_success and success |
| 159 processed[path] = num_synthed |
| 160 except Exception: # pragma: no cover |
| 161 LOGGER.exception('Caught in inner_loop') |
| 162 success = False |
| 163 |
| 164 return success, processed |
| OLD | NEW |