blob: bd4ca58eefe3d84c7c7fe29c4ce633e93574ab33 [file] [log] [blame]
James E. Blair4886cc12012-07-18 15:39:41 -07001# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair4076e2b2014-01-28 12:42:20 -08002# Copyright 2013-2014 OpenStack Foundation
James E. Blair4886cc12012-07-18 15:39:41 -07003#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
James E. Blairba1c8c02017-10-04 08:47:48 -070016from contextlib import contextmanager
17import logging
18import os
19import shutil
20
James E. Blair4886cc12012-07-18 15:39:41 -070021import git
James E. Blair1960d682017-04-28 15:44:14 -070022import gitdb
James E. Blairac2c3242014-01-24 13:38:51 -080023
24import zuul.model
James E. Blair4886cc12012-07-18 15:39:41 -070025
James E. Blair289f5932017-07-27 15:02:29 -070026NULL_REF = '0000000000000000000000000000000000000000'
27
James E. Blair4886cc12012-07-18 15:39:41 -070028
James E. Blair879dafb2015-07-17 14:04:49 -070029def reset_repo_to_head(repo):
30 # This lets us reset the repo even if there is a file in the root
31 # directory named 'HEAD'. Currently, GitPython does not allow us
32 # to instruct it to always include the '--' to disambiguate. This
33 # should no longer be necessary if this PR merges:
34 # https://github.com/gitpython-developers/GitPython/pull/319
35 try:
36 repo.git.reset('--hard', 'HEAD', '--')
37 except git.GitCommandError as e:
38 # git nowadays may use 1 as status to indicate there are still unstaged
39 # modifications after the reset
40 if e.status != 1:
41 raise
42
43
James E. Blairba1c8c02017-10-04 08:47:48 -070044@contextmanager
45def timeout_handler(path):
46 try:
47 yield
48 except git.exc.GitCommandError as e:
49 if e.status == -9:
50 # Timeout. The repo could be in a bad state, so delete it.
51 shutil.rmtree(path)
52 raise
53
54
James E. Blair4886cc12012-07-18 15:39:41 -070055class ZuulReference(git.Reference):
56 _common_path_default = "refs/zuul"
57 _points_to_commits_only = True
58
59
60class Repo(object):
Paul Belangeredadfed2017-10-05 16:04:27 -040061 def __init__(self, remote, local, email, username, speed_limit, speed_time,
James E. Blairba1c8c02017-10-04 08:47:48 -070062 sshkey=None, cache_path=None, logger=None, git_timeout=300):
James E. Blairda182de2017-05-26 14:24:56 -070063 if logger is None:
64 self.log = logging.getLogger("zuul.Repo")
65 else:
66 self.log = logger
Paul Belanger06ab26d2017-10-05 14:50:31 -040067 self.env = {
Paul Belangeredadfed2017-10-05 16:04:27 -040068 'GIT_HTTP_LOW_SPEED_LIMIT': speed_limit,
69 'GIT_HTTP_LOW_SPEED_TIME': speed_time,
Paul Belanger06ab26d2017-10-05 14:50:31 -040070 }
James E. Blairba1c8c02017-10-04 08:47:48 -070071 self.git_timeout = git_timeout
James E. Blair197e8202017-06-09 12:54:28 -070072 if sshkey:
Paul Belanger06ab26d2017-10-05 14:50:31 -040073 self.env['GIT_SSH_COMMAND'] = 'ssh -i %s' % (sshkey,)
74
James E. Blair4886cc12012-07-18 15:39:41 -070075 self.remote_url = remote
76 self.local_path = local
James E. Blair287c06d2013-07-24 10:39:30 -070077 self.email = email
78 self.username = username
James E. Blairf327c572017-05-24 13:58:42 -070079 self.cache_path = cache_path
James E. Blair287c06d2013-07-24 10:39:30 -070080 self._initialized = False
81 try:
82 self._ensure_cloned()
James E. Blairba1c8c02017-10-04 08:47:48 -070083 except Exception:
James E. Blair287c06d2013-07-24 10:39:30 -070084 self.log.exception("Unable to initialize repo for %s" % remote)
James E. Blair4886cc12012-07-18 15:39:41 -070085
86 def _ensure_cloned(self):
Antoine Mussobfd8f2a2014-09-23 15:31:40 +020087 repo_is_cloned = os.path.exists(os.path.join(self.local_path, '.git'))
Clark Boylan6dbbc482013-10-18 10:57:31 -070088 if self._initialized and repo_is_cloned:
James E. Blair287c06d2013-07-24 10:39:30 -070089 return
Clark Boylan6dbbc482013-10-18 10:57:31 -070090 # If the repo does not exist, clone the repo.
James E. Blairf327c572017-05-24 13:58:42 -070091 rewrite_url = False
Clark Boylan6dbbc482013-10-18 10:57:31 -070092 if not repo_is_cloned:
James E. Blair4886cc12012-07-18 15:39:41 -070093 self.log.debug("Cloning from %s to %s" % (self.remote_url,
94 self.local_path))
James E. Blairf327c572017-05-24 13:58:42 -070095 if self.cache_path:
James E. Blairba1c8c02017-10-04 08:47:48 -070096 self._git_clone(self.cache_path)
James E. Blairf327c572017-05-24 13:58:42 -070097 rewrite_url = True
98 else:
James E. Blairba1c8c02017-10-04 08:47:48 -070099 self._git_clone(self.remote_url)
Clark Boylan4ba48d92013-11-11 18:03:53 -0800100 repo = git.Repo(self.local_path)
James E. Blair197e8202017-06-09 12:54:28 -0700101 repo.git.update_environment(**self.env)
James E. Blairf327c572017-05-24 13:58:42 -0700102 # Create local branches corresponding to all the remote branches
103 if not repo_is_cloned:
104 origin = repo.remotes.origin
105 for ref in origin.refs:
106 if ref.remote_head == 'HEAD':
107 continue
108 repo.create_head(ref.remote_head, ref, force=True)
Clint Byrumdeaf1fc2017-05-10 21:28:31 -0700109 with repo.config_writer() as config_writer:
110 if self.email:
111 config_writer.set_value('user', 'email', self.email)
112 if self.username:
113 config_writer.set_value('user', 'name', self.username)
114 config_writer.write()
James E. Blairf327c572017-05-24 13:58:42 -0700115 if rewrite_url:
116 with repo.remotes.origin.config_writer as config_writer:
117 config_writer.set('url', self.remote_url)
James E. Blair287c06d2013-07-24 10:39:30 -0700118 self._initialized = True
James E. Blair4886cc12012-07-18 15:39:41 -0700119
Antoine Mussoa6529c62014-06-03 14:55:27 +0200120 def isInitialized(self):
121 return self._initialized
122
James E. Blairba1c8c02017-10-04 08:47:48 -0700123 def _git_clone(self, url):
124 mygit = git.cmd.Git(os.getcwd())
125 mygit.update_environment(**self.env)
126 with timeout_handler(self.local_path):
127 mygit.clone(git.cmd.Git.polish_url(url), self.local_path,
128 kill_after_timeout=self.git_timeout)
129
130 def _git_fetch(self, repo, remote, ref=None, **kwargs):
131 with timeout_handler(self.local_path):
132 repo.git.fetch(remote, ref, kill_after_timeout=self.git_timeout,
133 **kwargs)
134
Clark Boylan4ba48d92013-11-11 18:03:53 -0800135 def createRepoObject(self):
James E. Blair96c6bf82016-01-15 16:20:40 -0800136 self._ensure_cloned()
137 repo = git.Repo(self.local_path)
James E. Blair197e8202017-06-09 12:54:28 -0700138 repo.git.update_environment(**self.env)
Clark Boylan4ba48d92013-11-11 18:03:53 -0800139 return repo
Clark Boylanc2592322013-02-20 17:12:28 -0800140
James E. Blairb34e9262013-08-27 17:12:31 -0700141 def reset(self):
James E. Blairb34e9262013-08-27 17:12:31 -0700142 self.log.debug("Resetting repository %s" % self.local_path)
143 self.update()
James E. Blairfbdcdaa2015-07-20 15:56:37 -0700144 repo = self.createRepoObject()
Clark Boylan4ba48d92013-11-11 18:03:53 -0800145 origin = repo.remotes.origin
James E. Blairb34e9262013-08-27 17:12:31 -0700146 for ref in origin.refs:
147 if ref.remote_head == 'HEAD':
148 continue
Clark Boylan4ba48d92013-11-11 18:03:53 -0800149 repo.create_head(ref.remote_head, ref, force=True)
James E. Blairb34e9262013-08-27 17:12:31 -0700150
Arieb8a77922016-08-29 14:53:25 +0300151 # try reset to remote HEAD (usually origin/master)
152 # If it fails, pick the first reference
153 try:
154 repo.head.reference = origin.refs['HEAD']
155 except IndexError:
156 repo.head.reference = origin.refs[0]
James E. Blair879dafb2015-07-17 14:04:49 -0700157 reset_repo_to_head(repo)
Clark Boylan4ba48d92013-11-11 18:03:53 -0800158 repo.git.clean('-x', '-f', '-d')
James E. Blairb34e9262013-08-27 17:12:31 -0700159
Antoine Musso965233a2014-05-05 17:28:27 +0200160 def prune(self):
161 repo = self.createRepoObject()
162 origin = repo.remotes.origin
163 stale_refs = origin.stale_refs
164 if stale_refs:
165 self.log.debug("Pruning stale refs: %s", stale_refs)
166 git.refs.RemoteReference.delete(repo, *stale_refs)
167
James E. Blairb34e9262013-08-27 17:12:31 -0700168 def getBranchHead(self, branch):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800169 repo = self.createRepoObject()
170 branch_head = repo.heads[branch]
James E. Blairac2c3242014-01-24 13:38:51 -0800171 return branch_head.commit
172
Antoine Mussod3fe17f2014-05-05 18:19:36 +0200173 def hasBranch(self, branch):
174 repo = self.createRepoObject()
175 origin = repo.remotes.origin
176 return branch in origin.refs
177
James E. Blair2ec590b2017-05-24 13:59:17 -0700178 def getBranches(self):
James E. Blairedff2c22017-10-30 14:04:48 -0700179 # TODO(jeblair): deprecate with override-branch; replaced by
180 # getRefs().
James E. Blair2ec590b2017-05-24 13:59:17 -0700181 repo = self.createRepoObject()
182 return [x.name for x in repo.heads]
183
James E. Blairac2c3242014-01-24 13:38:51 -0800184 def getCommitFromRef(self, refname):
185 repo = self.createRepoObject()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000186 if refname not in repo.refs:
James E. Blairac2c3242014-01-24 13:38:51 -0800187 return None
188 ref = repo.refs[refname]
189 return ref.commit
James E. Blairb34e9262013-08-27 17:12:31 -0700190
James E. Blair34c7daa2017-04-28 13:31:27 -0700191 def getRefs(self):
192 repo = self.createRepoObject()
193 return repo.refs
194
James E. Blair57c51a12017-05-24 13:53:35 -0700195 def setRef(self, path, hexsha, repo=None):
196 if repo is None:
197 repo = self.createRepoObject()
198 binsha = gitdb.util.to_bin_sha(hexsha)
199 obj = git.objects.Object.new_from_sha(repo, binsha)
200 self.log.debug("Create reference %s", path)
201 git.refs.Reference.create(repo, path, obj, force=True)
202
James E. Blair1960d682017-04-28 15:44:14 -0700203 def setRefs(self, refs):
204 repo = self.createRepoObject()
205 current_refs = {}
206 for ref in repo.refs:
207 current_refs[ref.path] = ref
208 unseen = set(current_refs.keys())
209 for path, hexsha in refs.items():
James E. Blair57c51a12017-05-24 13:53:35 -0700210 self.setRef(path, hexsha, repo)
James E. Blair1960d682017-04-28 15:44:14 -0700211 unseen.discard(path)
212 for path in unseen:
James E. Blair289f5932017-07-27 15:02:29 -0700213 self.deleteRef(path, repo)
214
215 def deleteRef(self, path, repo=None):
216 if repo is None:
217 repo = self.createRepoObject()
218 self.log.debug("Delete reference %s", path)
219 git.refs.SymbolicReference.delete(repo, path)
James E. Blair1960d682017-04-28 15:44:14 -0700220
Clark Boylanc2592322013-02-20 17:12:28 -0800221 def checkout(self, ref):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800222 repo = self.createRepoObject()
Clark Boylanc2592322013-02-20 17:12:28 -0800223 self.log.debug("Checking out %s" % ref)
James E. Blairf49b5252017-09-08 16:04:52 -0700224 # Perform a hard reset before checking out so that we clean up
225 # anything that might be left over from a merge.
James E. Blair879dafb2015-07-17 14:04:49 -0700226 reset_repo_to_head(repo)
James E. Blairf49b5252017-09-08 16:04:52 -0700227 repo.git.checkout(ref)
James E. Blairf5f1a612015-06-24 09:34:03 -0700228 return repo.head.commit
James E. Blairc6294a52012-08-17 10:19:48 -0700229
James E. Blairf327c572017-05-24 13:58:42 -0700230 def checkoutLocalBranch(self, branch):
James E. Blairf49b5252017-09-08 16:04:52 -0700231 # TODO(jeblair): retire in favor of checkout
James E. Blairf327c572017-05-24 13:58:42 -0700232 repo = self.createRepoObject()
James E. Blaire5921fd2017-05-26 14:29:13 -0700233 # Perform a hard reset before checking out so that we clean up
234 # anything that might be left over from a merge.
235 reset_repo_to_head(repo)
236 repo.heads[branch].checkout()
James E. Blairf327c572017-05-24 13:58:42 -0700237
James E. Blair4886cc12012-07-18 15:39:41 -0700238 def cherryPick(self, ref):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800239 repo = self.createRepoObject()
James E. Blair4886cc12012-07-18 15:39:41 -0700240 self.log.debug("Cherry-picking %s" % ref)
Clark Boylan14b55372012-11-01 11:57:50 -0700241 self.fetch(ref)
Clark Boylan4ba48d92013-11-11 18:03:53 -0800242 repo.git.cherry_pick("FETCH_HEAD")
James E. Blairac2c3242014-01-24 13:38:51 -0800243 return repo.head.commit
James E. Blair4886cc12012-07-18 15:39:41 -0700244
James E. Blair19deff22013-08-25 13:17:35 -0700245 def merge(self, ref, strategy=None):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800246 repo = self.createRepoObject()
James E. Blair19deff22013-08-25 13:17:35 -0700247 args = []
248 if strategy:
249 args += ['-s', strategy]
250 args.append('FETCH_HEAD')
Clark Boylan14b55372012-11-01 11:57:50 -0700251 self.fetch(ref)
James E. Blair19deff22013-08-25 13:17:35 -0700252 self.log.debug("Merging %s with args %s" % (ref, args))
Clark Boylan4ba48d92013-11-11 18:03:53 -0800253 repo.git.merge(*args)
James E. Blairac2c3242014-01-24 13:38:51 -0800254 return repo.head.commit
James E. Blair4886cc12012-07-18 15:39:41 -0700255
Clark Boylan14b55372012-11-01 11:57:50 -0700256 def fetch(self, ref):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800257 repo = self.createRepoObject()
James E. Blairba1c8c02017-10-04 08:47:48 -0700258 # NOTE: The following is currently not applicable, but if we
259 # switch back to fetch methods from GitPython, we need to
260 # consider it:
261 # The git.remote.fetch method may read in git progress info and
262 # interpret it improperly causing an AssertionError. Because the
263 # data was fetched properly subsequent fetches don't seem to fail.
264 # So try again if an AssertionError is caught.
265 self._git_fetch(repo, 'origin', ref)
Clark Boylan14b55372012-11-01 11:57:50 -0700266
James E. Blair247cab72017-07-20 16:52:36 -0700267 def fetchFrom(self, repository, ref):
Antoine Musso45dd2cb2014-01-29 17:17:43 +0100268 repo = self.createRepoObject()
James E. Blairba1c8c02017-10-04 08:47:48 -0700269 self._git_fetch(repo, repository, ref)
Antoine Musso45dd2cb2014-01-29 17:17:43 +0100270
Clark Boylanc2592322013-02-20 17:12:28 -0800271 def createZuulRef(self, ref, commit='HEAD'):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800272 repo = self.createRepoObject()
Spencer Krum438ec532015-08-06 13:56:46 -0700273 self.log.debug("CreateZuulRef %s at %s on %s" % (ref, commit, repo))
Clark Boylan4ba48d92013-11-11 18:03:53 -0800274 ref = ZuulReference.create(repo, ref, commit)
Clark Boylanc2592322013-02-20 17:12:28 -0800275 return ref.commit
James E. Blair4886cc12012-07-18 15:39:41 -0700276
James E. Blairdaabed22012-08-15 15:38:57 -0700277 def push(self, local, remote):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800278 repo = self.createRepoObject()
Antoine Mussof0506fa2014-06-03 15:03:38 +0200279 self.log.debug("Pushing %s:%s to %s" % (local, remote,
280 self.remote_url))
Clark Boylan4ba48d92013-11-11 18:03:53 -0800281 repo.remotes.origin.push('%s:%s' % (local, remote))
James E. Blairdaabed22012-08-15 15:38:57 -0700282
Antoine Mussod71e2972013-01-17 13:40:10 +0100283 def update(self):
Clark Boylan4ba48d92013-11-11 18:03:53 -0800284 repo = self.createRepoObject()
Antoine Mussod71e2972013-01-17 13:40:10 +0100285 self.log.debug("Updating repository %s" % self.local_path)
James E. Blair84c4f942016-07-29 10:38:29 -0700286 if repo.git.version_info[:2] < (1, 9):
287 # Before 1.9, 'git fetch --tags' did not include the
288 # behavior covered by 'git --fetch', so we run both
289 # commands in that case. Starting with 1.9, 'git fetch
290 # --tags' is all that is necessary. See
291 # https://github.com/git/git/blob/master/Documentation/RelNotes/1.9.0.txt#L18-L20
James E. Blairba1c8c02017-10-04 08:47:48 -0700292 self._git_fetch(repo, 'origin')
293 self._git_fetch(repo, 'origin', tags=True)
Antoine Mussod71e2972013-01-17 13:40:10 +0100294
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000295 def getFiles(self, files, dirs=[], branch=None, commit=None):
James E. Blair14abdf42015-12-09 16:11:53 -0800296 ret = {}
297 repo = self.createRepoObject()
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700298 if branch:
James E. Blair14abdf42015-12-09 16:11:53 -0800299 tree = repo.heads[branch].commit.tree
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700300 else:
301 tree = repo.commit(commit).tree
302 for fn in files:
James E. Blair14abdf42015-12-09 16:11:53 -0800303 if fn in tree:
Clint Byrumf322fe22017-05-10 20:53:12 -0700304 ret[fn] = tree[fn].data_stream.read().decode('utf8')
James E. Blair14abdf42015-12-09 16:11:53 -0800305 else:
306 ret[fn] = None
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000307 if dirs:
308 for dn in dirs:
309 if dn not in tree:
310 continue
311 for blob in tree[dn].traverse():
312 if blob.path.endswith(".yaml"):
313 ret[blob.path] = blob.data_stream.read().decode(
314 'utf-8')
James E. Blair14abdf42015-12-09 16:11:53 -0800315 return ret
316
Fabien Boucher194a2bf2017-12-02 18:17:58 +0100317 def getFilesChanges(self, branch, tosha=None):
318 repo = self.createRepoObject()
319 files = set()
320 head = repo.heads[branch].commit
321 files.update(set(head.stats.files.keys()))
322 if tosha:
323 for cmt in head.iter_parents():
324 if cmt.hexsha == tosha:
325 break
326 files.update(set(cmt.stats.files.keys()))
327 return list(files)
328
James E. Blairf327c572017-05-24 13:58:42 -0700329 def deleteRemote(self, remote):
330 repo = self.createRepoObject()
331 repo.delete_remote(repo.remotes[remote])
332
James E. Blair4886cc12012-07-18 15:39:41 -0700333
334class Merger(object):
James E. Blairf327c572017-05-24 13:58:42 -0700335 def __init__(self, working_root, connections, email, username,
Paul Belangeredadfed2017-10-05 16:04:27 -0400336 speed_limit, speed_time, cache_root=None, logger=None):
James E. Blairda182de2017-05-26 14:24:56 -0700337 self.logger = logger
338 if logger is None:
339 self.log = logging.getLogger("zuul.Merger")
340 else:
341 self.log = logger
James E. Blair4886cc12012-07-18 15:39:41 -0700342 self.repos = {}
343 self.working_root = working_root
344 if not os.path.exists(working_root):
345 os.makedirs(working_root)
Joshua Heskethca5d0ca2017-02-21 13:49:22 -0500346 self.connections = connections
Paul Belangerb67aba12013-05-13 19:22:14 -0400347 self.email = email
348 self.username = username
Paul Belangeredadfed2017-10-05 16:04:27 -0400349 self.speed_limit = speed_limit
350 self.speed_time = speed_time
James E. Blairf327c572017-05-24 13:58:42 -0700351 self.cache_root = cache_root
James E. Blairad615012012-11-30 16:14:21 -0800352
James E. Blair197e8202017-06-09 12:54:28 -0700353 def _addProject(self, hostname, project_name, url, sshkey):
James E. Blairac2c3242014-01-24 13:38:51 -0800354 repo = None
James E. Blair2a535672017-04-27 12:03:15 -0700355 key = '/'.join([hostname, project_name])
James E. Blair4886cc12012-07-18 15:39:41 -0700356 try:
James E. Blair2a535672017-04-27 12:03:15 -0700357 path = os.path.join(self.working_root, hostname, project_name)
James E. Blairf327c572017-05-24 13:58:42 -0700358 if self.cache_root:
359 cache_path = os.path.join(self.cache_root, hostname,
360 project_name)
361 else:
362 cache_path = None
Paul Belangeredadfed2017-10-05 16:04:27 -0400363 repo = Repo(
364 url, path, self.email, self.username, self.speed_limit,
365 self.speed_time, sshkey, cache_path, self.logger)
Paul Belangerb67aba12013-05-13 19:22:14 -0400366
James E. Blair2a535672017-04-27 12:03:15 -0700367 self.repos[key] = repo
James E. Blairac2c3242014-01-24 13:38:51 -0800368 except Exception:
James E. Blair2a535672017-04-27 12:03:15 -0700369 self.log.exception("Unable to add project %s/%s" %
370 (hostname, project_name))
James E. Blairac2c3242014-01-24 13:38:51 -0800371 return repo
James E. Blair4886cc12012-07-18 15:39:41 -0700372
James E. Blair2a535672017-04-27 12:03:15 -0700373 def getRepo(self, connection_name, project_name):
374 source = self.connections.getSource(connection_name)
375 project = source.getProject(project_name)
376 hostname = project.canonical_hostname
377 url = source.getGitUrl(project)
378 key = '/'.join([hostname, project_name])
379 if key in self.repos:
380 return self.repos[key]
James E. Blair197e8202017-06-09 12:54:28 -0700381 sshkey = self.connections.connections.get(connection_name).\
382 connection_config.get('sshkey')
James E. Blairac2c3242014-01-24 13:38:51 -0800383 if not url:
James E. Blair2a535672017-04-27 12:03:15 -0700384 raise Exception("Unable to set up repo for project %s/%s"
385 " without a url" %
386 (connection_name, project_name,))
James E. Blair197e8202017-06-09 12:54:28 -0700387 return self._addProject(hostname, project_name, url, sshkey)
Clark Boylanc2592322013-02-20 17:12:28 -0800388
James E. Blair2a535672017-04-27 12:03:15 -0700389 def updateRepo(self, connection_name, project_name):
James E. Blair2a535672017-04-27 12:03:15 -0700390 repo = self.getRepo(connection_name, project_name)
Antoine Mussofeba9672013-01-17 13:44:59 +0100391 try:
James E. Blair2a535672017-04-27 12:03:15 -0700392 self.log.info("Updating local repository %s/%s",
393 connection_name, project_name)
James E. Blaird8d55c72017-02-14 09:05:49 -0800394 repo.reset()
Clark Boylan4c6566b2014-03-10 11:02:01 -0700395 except Exception:
James E. Blair2a535672017-04-27 12:03:15 -0700396 self.log.exception("Unable to update %s/%s",
397 connection_name, project_name)
Antoine Mussofeba9672013-01-17 13:44:59 +0100398
James E. Blair2a535672017-04-27 12:03:15 -0700399 def checkoutBranch(self, connection_name, project_name, branch):
James E. Blairf327c572017-05-24 13:58:42 -0700400 self.log.info("Checking out %s/%s branch %s",
401 connection_name, project_name, branch)
James E. Blair2a535672017-04-27 12:03:15 -0700402 repo = self.getRepo(connection_name, project_name)
James E. Blairedff2c22017-10-30 14:04:48 -0700403 repo.checkout(branch)
James E. Blairc73c73a2017-01-20 15:15:15 -0800404
James E. Blair34c7daa2017-04-28 13:31:27 -0700405 def _saveRepoState(self, connection_name, project_name, repo,
James E. Blair66c60682017-05-31 12:48:01 -0400406 repo_state, recent):
James E. Blair34c7daa2017-04-28 13:31:27 -0700407 projects = repo_state.setdefault(connection_name, {})
408 project = projects.setdefault(project_name, {})
James E. Blair34c7daa2017-04-28 13:31:27 -0700409 for ref in repo.getRefs():
James E. Blair66c60682017-05-31 12:48:01 -0400410 if ref.path.startswith('refs/zuul/'):
James E. Blair34c7daa2017-04-28 13:31:27 -0700411 continue
James E. Blair66c60682017-05-31 12:48:01 -0400412 if ref.path.startswith('refs/remotes/'):
James E. Blair1960d682017-04-28 15:44:14 -0700413 continue
James E. Blair66c60682017-05-31 12:48:01 -0400414 if ref.path.startswith('refs/heads/'):
415 branch = ref.path[len('refs/heads/'):]
416 key = (connection_name, project_name, branch)
417 if key not in recent:
418 recent[key] = ref.object
James E. Blair34c7daa2017-04-28 13:31:27 -0700419 project[ref.path] = ref.object.hexsha
420
James E. Blair289f5932017-07-27 15:02:29 -0700421 def _alterRepoState(self, connection_name, project_name,
422 repo_state, path, hexsha):
423 projects = repo_state.setdefault(connection_name, {})
424 project = projects.setdefault(project_name, {})
425 if hexsha == NULL_REF:
426 if path in project:
427 del project[path]
428 else:
429 project[path] = hexsha
430
James E. Blair1960d682017-04-28 15:44:14 -0700431 def _restoreRepoState(self, connection_name, project_name, repo,
432 repo_state):
433 projects = repo_state.get(connection_name, {})
434 project = projects.get(project_name, {})
435 if not project:
436 # We don't have a state for this project.
437 return
438 self.log.debug("Restore repo state for project %s/%s",
439 connection_name, project_name)
440 repo.setRefs(project)
441
James E. Blairac2c3242014-01-24 13:38:51 -0800442 def _mergeChange(self, item, ref):
James E. Blair2a535672017-04-27 12:03:15 -0700443 repo = self.getRepo(item['connection'], item['project'])
Clark Boylanc2592322013-02-20 17:12:28 -0800444 try:
445 repo.checkout(ref)
James E. Blair4076e2b2014-01-28 12:42:20 -0800446 except Exception:
Clark Boylanc2592322013-02-20 17:12:28 -0800447 self.log.exception("Unable to checkout %s" % ref)
James E. Blair4076e2b2014-01-28 12:42:20 -0800448 return None
Clark Boylanc2592322013-02-20 17:12:28 -0800449
450 try:
James E. Blairac2c3242014-01-24 13:38:51 -0800451 mode = item['merge_mode']
452 if mode == zuul.model.MERGER_MERGE:
James E. Blair247cab72017-07-20 16:52:36 -0700453 commit = repo.merge(item['ref'])
James E. Blairac2c3242014-01-24 13:38:51 -0800454 elif mode == zuul.model.MERGER_MERGE_RESOLVE:
James E. Blair247cab72017-07-20 16:52:36 -0700455 commit = repo.merge(item['ref'], 'resolve')
James E. Blairac2c3242014-01-24 13:38:51 -0800456 elif mode == zuul.model.MERGER_CHERRY_PICK:
James E. Blair247cab72017-07-20 16:52:36 -0700457 commit = repo.cherryPick(item['ref'])
James E. Blair19deff22013-08-25 13:17:35 -0700458 else:
459 raise Exception("Unsupported merge mode: %s" % mode)
Antoine Musso5f53e602014-02-04 14:16:18 +0100460 except git.GitCommandError:
461 # Log git exceptions at debug level because they are
Clark Boylanc2592322013-02-20 17:12:28 -0800462 # usually benign merge conflicts
James E. Blairac2c3242014-01-24 13:38:51 -0800463 self.log.debug("Unable to merge %s" % item, exc_info=True)
James E. Blair4076e2b2014-01-28 12:42:20 -0800464 return None
Antoine Musso5f53e602014-02-04 14:16:18 +0100465 except Exception:
466 self.log.exception("Exception while merging a change:")
467 return None
Clark Boylanc2592322013-02-20 17:12:28 -0800468
Clark Boylanc2592322013-02-20 17:12:28 -0800469 return commit
James E. Blair4886cc12012-07-18 15:39:41 -0700470
James E. Blair34c7daa2017-04-28 13:31:27 -0700471 def _mergeItem(self, item, recent, repo_state):
James E. Blair247cab72017-07-20 16:52:36 -0700472 self.log.debug("Processing ref %s for project %s/%s / %s uuid %s" %
473 (item['ref'], item['connection'],
474 item['project'], item['branch'],
475 item['buildset_uuid']))
James E. Blair2a535672017-04-27 12:03:15 -0700476 repo = self.getRepo(item['connection'], item['project'])
477 key = (item['connection'], item['project'], item['branch'])
Joshua Heskethca5d0ca2017-02-21 13:49:22 -0500478
James E. Blairac2c3242014-01-24 13:38:51 -0800479 # We need to merge the change
480 # Get the most recent commit for this project-branch
481 base = recent.get(key)
482 if not base:
483 # There is none, so use the branch tip
James E. Blairb34e9262013-08-27 17:12:31 -0700484 # we need to reset here in order to call getBranchHead
James E. Blairac2c3242014-01-24 13:38:51 -0800485 self.log.debug("No base commit found for %s" % (key,))
James E. Blairb34e9262013-08-27 17:12:31 -0700486 try:
487 repo.reset()
James E. Blairac2c3242014-01-24 13:38:51 -0800488 except Exception:
James E. Blairb34e9262013-08-27 17:12:31 -0700489 self.log.exception("Unable to reset repo %s" % repo)
James E. Blairac2c3242014-01-24 13:38:51 -0800490 return None
James E. Blair1960d682017-04-28 15:44:14 -0700491 self._restoreRepoState(item['connection'], item['project'], repo,
492 repo_state)
493
James E. Blairac2c3242014-01-24 13:38:51 -0800494 base = repo.getBranchHead(item['branch'])
James E. Blair34c7daa2017-04-28 13:31:27 -0700495 # Save the repo state so that later mergers can repeat
496 # this process.
497 self._saveRepoState(item['connection'], item['project'], repo,
James E. Blair66c60682017-05-31 12:48:01 -0400498 repo_state, recent)
James E. Blairac2c3242014-01-24 13:38:51 -0800499 else:
500 self.log.debug("Found base commit %s for %s" % (base, key,))
501 # Merge the change
James E. Blair197e8202017-06-09 12:54:28 -0700502 commit = self._mergeChange(item, base)
503 if not commit:
504 return None
505 # Store this commit as the most recent for this project-branch
506 recent[key] = commit
507 # Set the Zuul ref for this item to point to the most recent
508 # commits of each project-branch
509 for key, mrc in recent.items():
510 connection, project, branch = key
511 zuul_ref = None
512 try:
513 repo = self.getRepo(connection, project)
James E. Blair247cab72017-07-20 16:52:36 -0700514 zuul_ref = branch + '/' + item['buildset_uuid']
James E. Blair197e8202017-06-09 12:54:28 -0700515 if not repo.getCommitFromRef(zuul_ref):
516 repo.createZuulRef(zuul_ref, mrc)
517 except Exception:
518 self.log.exception("Unable to set zuul ref %s for "
519 "item %s" % (zuul_ref, item))
James E. Blairac2c3242014-01-24 13:38:51 -0800520 return None
James E. Blair197e8202017-06-09 12:54:28 -0700521 return commit
James E. Blairac2c3242014-01-24 13:38:51 -0800522
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000523 def mergeChanges(self, items, files=None, dirs=None, repo_state=None):
James E. Blair34c7daa2017-04-28 13:31:27 -0700524 # connection+project+branch -> commit
James E. Blairac2c3242014-01-24 13:38:51 -0800525 recent = {}
526 commit = None
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700527 read_files = []
James E. Blair34c7daa2017-04-28 13:31:27 -0700528 # connection -> project -> ref -> commit
529 if repo_state is None:
530 repo_state = {}
James E. Blairac2c3242014-01-24 13:38:51 -0800531 for item in items:
James E. Blair289f5932017-07-27 15:02:29 -0700532 self.log.debug("Merging for change %s,%s" %
533 (item["number"], item["patchset"]))
James E. Blair34c7daa2017-04-28 13:31:27 -0700534 commit = self._mergeItem(item, recent, repo_state)
James E. Blairac2c3242014-01-24 13:38:51 -0800535 if not commit:
536 return None
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000537 if files or dirs:
James E. Blair2a535672017-04-27 12:03:15 -0700538 repo = self.getRepo(item['connection'], item['project'])
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000539 repo_files = repo.getFiles(files, dirs, commit=commit)
James E. Blair2a535672017-04-27 12:03:15 -0700540 read_files.append(dict(
541 connection=item['connection'],
542 project=item['project'],
543 branch=item['branch'],
544 files=repo_files))
James E. Blair57c51a12017-05-24 13:53:35 -0700545 ret_recent = {}
546 for k, v in recent.items():
547 ret_recent[k] = v.hexsha
548 return commit.hexsha, read_files, repo_state, ret_recent
James E. Blair14abdf42015-12-09 16:11:53 -0800549
James E. Blair289f5932017-07-27 15:02:29 -0700550 def setRepoState(self, items, repo_state):
551 # Sets the repo state for the items
552 seen = set()
553 for item in items:
554 repo = self.getRepo(item['connection'], item['project'])
555 key = (item['connection'], item['project'], item['branch'])
556
557 if key in seen:
558 continue
559
560 repo.reset()
561 self._restoreRepoState(item['connection'], item['project'], repo,
562 repo_state)
563
564 def getRepoState(self, items):
565 # Gets the repo state for items. Generally this will be
566 # called in any non-change pipeline. We will return the repo
567 # state for each item, but manipulated with any information in
568 # the item (eg, if it creates a ref, that will be in the repo
569 # state regardless of the actual state).
570 seen = set()
571 recent = {}
572 repo_state = {}
573 for item in items:
574 repo = self.getRepo(item['connection'], item['project'])
575 key = (item['connection'], item['project'], item['branch'])
576 if key not in seen:
577 try:
578 repo.reset()
579 except Exception:
580 self.log.exception("Unable to reset repo %s" % repo)
581 return (False, {})
582
583 self._saveRepoState(item['connection'], item['project'], repo,
584 repo_state, recent)
585
586 if item.get('newrev'):
587 # This is a ref update rather than a branch tip, so make sure
588 # our returned state includes this change.
589 self._alterRepoState(item['connection'], item['project'],
590 repo_state, item['ref'], item['newrev'])
591 return (True, repo_state)
592
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000593 def getFiles(self, connection_name, project_name, branch, files, dirs=[]):
James E. Blair2a535672017-04-27 12:03:15 -0700594 repo = self.getRepo(connection_name, project_name)
Tristan Cacqueray829e6172017-06-13 06:49:36 +0000595 return repo.getFiles(files, dirs, branch=branch)
Fabien Boucher194a2bf2017-12-02 18:17:58 +0100596
597 def getFilesChanges(self, connection_name, project_name, branch,
598 tosha=None):
599 repo = self.getRepo(connection_name, project_name)
600 return repo.getFilesChanges(branch, tosha)