33# License AGPLv3 (http://www.gnu.org/licenses/agpl-3.0-standalone.html)
44# Parts of the code comes from ANYBOX
55# https://github.com/anybox/anybox.recipe.odoo
6+
67from __future__ import unicode_literals
8+
79import os
810import logging
911import re
1012import subprocess
13+ import collections
1114
1215import requests
1316
@@ -131,24 +134,38 @@ def init_git_version(cls, v_str):
131134 "report this" % v_str )
132135 return version
133136
134- def query_remote_ref (self , remote , ref ):
135- """Query remote repo about given ref.
136- :return: ``('tag', sha)`` if ref is a tag in remote
137+ def query_remote (self , remote , refs = None ):
138+ """Query remote repo optionaly about given refs
139+
140+ :return: iterator of
141+ ``('tag', sha)`` if ref is a tag in remote
137142 ``('branch', sha)`` if ref is branch (aka "head") in remote
138143 ``(None, ref)`` if ref does not exist in remote. This happens
139144 notably if ref if a commit sha (they can't be queried)
140145 """
141- out = self .log_call (['git' , 'ls-remote' , remote , ref ],
146+ cmd = ['git' , 'ls-remote' , remote ]
147+ if refs is not None :
148+ if isinstance (refs , str ):
149+ refs = [refs ]
150+ cmd += refs
151+ out = self .log_call (cmd ,
142152 cwd = self .cwd ,
143153 callwith = subprocess .check_output ).strip ()
154+ if len (out ) == 0 :
155+ for ref in refs :
156+ yield None , ref , ref
157+ return
144158 for sha , fullref in (l .split () for l in out .splitlines ()):
145- if fullref == 'refs/heads/' + ref :
146- return 'branch' , sha
147- elif fullref == 'refs/tags/' + ref :
148- return 'tag' , sha
149- elif fullref == ref and ref == 'HEAD' :
150- return 'HEAD' , sha
151- return None , ref
159+ if fullref .startswith ('refs/heads/' ):
160+ yield 'branch' , fullref , sha
161+ elif fullref .startswith ('refs/tags/' ):
162+ yield 'tag' , fullref , sha
163+ elif fullref == 'HEAD' :
164+ yield 'HEAD' , fullref , sha
165+ else :
166+ raise GitAggregatorException (
167+ "Unrecognized type for value from ls-remote: %r"
168+ % (fullref , ))
152169
153170 def log_call (self , cmd , callwith = subprocess .check_call ,
154171 log_level = logging .DEBUG , ** kw ):
@@ -162,9 +179,10 @@ def log_call(self, cmd, callwith=subprocess.check_call,
162179 return ret
163180
164181 def aggregate (self ):
165- """ Aggregate all merges into the target branch
166- If the target_dir doesn't exist, create an empty git repo otherwise
167- clean it, add all remotes , and merge all merges.
182+ """Aggregate all merges into the target branch
183+
184+ If the target_dir doesn't exist, create an empty git repo
185+ otherwise clean it, add all remotes, and merge all merges.
168186 """
169187 logger .info ('Start aggregation of %s' , self .cwd )
170188 target_dir = self .cwd
@@ -173,18 +191,26 @@ def aggregate(self):
173191 if is_new :
174192 self .init_repository (target_dir )
175193
176- self ._switch_to_branch (self .target ['branch' ])
177194 for r in self .remotes :
178195 self ._set_remote (** r )
179- self .fetch ()
196+ fetch_heads = self .fetch ()
197+ logger .debug ("fetch_heads: %r" , fetch_heads )
180198 merges = self .merges
181- if not is_new :
199+ origin = merges [0 ]
200+ origin_sha1 = fetch_heads [(origin ["remote" ],
201+ origin ["ref" ])]
202+ merges = merges [1 :]
203+ if is_new :
204+ self ._switch_to_branch (
205+ self .target ['branch' ],
206+ origin_sha1 )
207+ else :
182208 # reset to the first merge
183- origin = merges [0 ]
184- merges = merges [1 :]
185- self ._reset_to (origin ["remote" ], origin ["ref" ])
209+ self ._reset_to (origin_sha1 )
186210 for merge in merges :
187- self ._merge (merge )
211+ logger .info ("Merge %s, %s" , merge ["remote" ], merge ["ref" ])
212+ self ._merge (fetch_heads [(merge ["remote" ],
213+ merge ["ref" ])])
188214 self ._execute_shell_command_after ()
189215 logger .info ('End aggregation of %s' , self .cwd )
190216
@@ -193,19 +219,80 @@ def init_repository(self, target_dir):
193219 self .log_call (['git' , 'init' , target_dir ])
194220
195221 def fetch (self ):
222+ """Fetch all given (remote, ref) and associate their SHA
223+
224+ Will query and fetch all (remote, ref) in current git repository,
225+ it'll take care to resolve each tuple in the local commit's SHA1
226+
227+ It returns a dict structure associating each (remote, ref) to their
228+ SHA in local repository.
229+ """
230+ merges_requested = [(m ["remote" ], m ["ref" ])
231+ for m in self .merges ]
196232 basecmd = ("git" , "fetch" )
197233 logger .info ("Fetching required remotes" )
198- for merge in self .merges :
199- cmd = basecmd + self ._fetch_options (merge ) + (merge ["remote" ],)
200- if merge ["remote" ] not in self .fetch_all :
201- cmd += (merge ["ref" ],)
234+ fetch_heads = {}
235+ ls_remote_refs = collections .defaultdict (list ) # to ls-query
236+ while merges_requested :
237+ remote , ref = merges_requested [0 ]
238+ merges_requested = merges_requested [1 :]
239+ cmd = (
240+ basecmd +
241+ self ._fetch_options ({"remote" : remote , "ref" : ref }) +
242+ (remote ,))
243+ if remote not in self .fetch_all :
244+ cmd += (ref , )
245+ else :
246+ ls_remote_refs [remote ].append (ref )
202247 self .log_call (cmd , cwd = self .cwd )
248+ with open (os .path .join (self .cwd , ".git" , "FETCH_HEAD" ), "r" ) as f :
249+ for line in f :
250+ fetch_head , for_merge , _ = line .split ("\t " )
251+ if for_merge == "not-for-merge" :
252+ continue
253+ break
254+ fetch_heads [(remote , ref )] = fetch_head
255+ if self .fetch_all :
256+ if self .fetch_all is True :
257+ remotes = self .remotes
258+ else :
259+ remotes = [r for r in self .remotes
260+ if r ["name" ] in self .fetch_all ]
261+ for remote in remotes :
262+ refs = self .query_remote (
263+ remote ["url" ],
264+ ls_remote_refs [remote ["name" ]])
265+ for _ , ref , sha in refs :
266+ if (remote ["name" ], ref ) in merges_requested :
267+ merges_requested .remove ((remote ["name" ], ref ))
268+ fetch_heads [(remote ["name" ], ref )] = sha
269+ if len (merges_requested ):
270+ # Last case: our ref is a sha and remote git repository does
271+ # not support querying commit directly by SHA. In this case
272+ # we need just to check if ref is actually SHA, and if we have
273+ # this SHA locally.
274+ for remote , ref in merges_requested :
275+ if not re .search ("[0-9a-f]{4,}" , ref ):
276+ raise ValueError ("Could not resolv ref %r on remote %r"
277+ % (ref , remote ))
278+ valid_local_shas = self .log_call (
279+ ['git' , 'rev-parse' , '-v' ] + [sha
280+ for _r , sha in merges_requested ],
281+ cwd = self .cwd , callwith = subprocess .check_output
282+ ).strip ().splitlines ()
283+ for remote , sha in merges_requested :
284+ if sha not in valid_local_shas :
285+ raise ValueError (
286+ "Could not find SHA ref %r after fetch on remote %r"
287+ % (ref , remote ))
288+ fetch_heads [(remote ["name" ], sha )] = sha
289+ return fetch_heads
203290
204291 def push (self ):
205292 remote = self .target ['remote' ]
206293 branch = self .target ['branch' ]
207294 logger .info ("Push %s to %s" , branch , remote )
208- self .log_call (['git' , 'push' , '-f' , remote , branch ], cwd = self .cwd )
295+ self .log_call (['git' , 'push' , '-f' , remote , "HEAD:%s" % branch ], cwd = self .cwd )
209296
210297 def _check_status (self ):
211298 """Check repo status and except if dirty."""
@@ -227,42 +314,43 @@ def _fetch_options(self, merge):
227314 cmd += ("--%s" % option , str (value ))
228315 return cmd
229316
230- def _reset_to (self , remote , ref ):
317+ def _reset_to (self , ref ):
231318 if not self .force :
232319 self ._check_status ()
233- logger .info ('Reset branch to %s %s' , remote , ref )
234- rtype , sha = self .query_remote_ref (remote , ref )
235- if rtype is None and not ishex (ref ):
236- raise GitAggregatorException (
237- 'Could not reset %s to %s. No commit found for %s '
238- % (remote , ref , ref ))
239- cmd = ['git' , 'reset' , '--hard' , sha ]
320+ logger .info ('Reset branch to %s' , ref )
321+ cmd = ['git' , 'reset' , '--hard' , ref ]
240322 if logger .getEffectiveLevel () != logging .DEBUG :
241323 cmd .insert (2 , '--quiet' )
242324 self .log_call (cmd , cwd = self .cwd )
243325 self .log_call (['git' , 'clean' , '-ffd' ], cwd = self .cwd )
244326
245- def _switch_to_branch (self , branch_name ):
327+ def _switch_to_branch (self , branch_name , ref = None ):
246328 # check if the branch already exists
247329 logger .info ("Switch to branch %s" , branch_name )
248- self .log_call (['git' , 'checkout' , '-B' , branch_name ], cwd = self .cwd )
330+ cmd = ['git' , 'checkout' , '-B' , branch_name ]
331+ if ref is not None :
332+ sha1 = self .log_call (
333+ ['git' , 'rev-parse' , ref ],
334+ callwith = subprocess .check_output ,
335+ cwd = self .cwd ).strip ()
336+ cmd .append (sha1 )
337+ self .log_call (cmd , cwd = self .cwd )
249338
250339 def _execute_shell_command_after (self ):
251340 logger .info ('Execute shell after commands' )
252341 for cmd in self .shell_command_after :
253342 self .log_call (cmd , shell = True , cwd = self .cwd )
254343
255344 def _merge (self , merge ):
256- logger .info ("Pull %s, %s" , merge ["remote" ], merge ["ref" ])
257- cmd = ("git" , "pull" )
345+ cmd = ("git" , "merge" )
258346 if self .git_version >= (1 , 7 , 10 ):
259347 # --edit and --no-edit appear with Git 1.7.10
260348 # see Documentation/RelNotes/1.7.10.txt of Git
261349 # (https://git.kernel.org/cgit/git/git.git/tree)
262350 cmd += ('--no-edit' ,)
263351 if logger .getEffectiveLevel () != logging .DEBUG :
264352 cmd += ('--quiet' ,)
265- cmd += self . _fetch_options (merge ) + ( merge [ "remote" ], merge [ "ref" ] )
353+ cmd += (merge , )
266354 self .log_call (cmd , cwd = self .cwd )
267355
268356 def _get_remotes (self ):
0 commit comments