3
3
# License AGPLv3 (http://www.gnu.org/licenses/agpl-3.0-standalone.html)
4
4
# Parts of the code comes from ANYBOX
5
5
# https://github.com/anybox/anybox.recipe.odoo
6
+
6
7
from __future__ import unicode_literals
8
+
7
9
import os
8
10
import logging
9
11
import re
10
12
import subprocess
13
+ import collections
11
14
12
15
import requests
13
16
@@ -131,24 +134,38 @@ def init_git_version(cls, v_str):
131
134
"report this" % v_str )
132
135
return version
133
136
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
137
142
``('branch', sha)`` if ref is branch (aka "head") in remote
138
143
``(None, ref)`` if ref does not exist in remote. This happens
139
144
notably if ref if a commit sha (they can't be queried)
140
145
"""
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 ,
142
152
cwd = self .cwd if os .path .exists (self .cwd ) else None ,
143
153
callwith = subprocess .check_output ).strip ()
144
- for sha , fullref in (line .split () for line 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
154
+ if len (out ) == 0 :
155
+ for ref in refs :
156
+ yield None , ref , ref
157
+ return
158
+ for sha , fullref in (l .split () for l in out .splitlines ()):
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 , ))
152
169
153
170
def log_call (self , cmd , callwith = subprocess .check_call ,
154
171
log_level = logging .DEBUG , ** kw ):
@@ -166,9 +183,10 @@ def log_call(self, cmd, callwith=subprocess.check_call,
166
183
return ret
167
184
168
185
def aggregate (self ):
169
- """ Aggregate all merges into the target branch
170
- If the target_dir doesn't exist, create an empty git repo otherwise
171
- clean it, add all remotes , and merge all merges.
186
+ """Aggregate all merges into the target branch
187
+
188
+ If the target_dir doesn't exist, create an empty git repo
189
+ otherwise clean it, add all remotes, and merge all merges.
172
190
"""
173
191
logger .info ('Start aggregation of %s' , self .cwd )
174
192
target_dir = self .cwd
@@ -177,18 +195,26 @@ def aggregate(self):
177
195
if is_new :
178
196
cloned = self .init_repository (target_dir )
179
197
180
- self ._switch_to_branch (self .target ['branch' ])
181
198
for r in self .remotes :
182
199
self ._set_remote (** r )
183
- self .fetch ()
200
+ fetch_heads = self .fetch ()
201
+ logger .debug ("fetch_heads: %r" , fetch_heads )
184
202
merges = self .merges
185
- if not is_new or cloned :
203
+ origin = merges [0 ]
204
+ origin_sha1 = fetch_heads [(origin ["remote" ],
205
+ origin ["ref" ])]
206
+ merges = merges [1 :]
207
+ if is_new and not cloned :
208
+ self ._switch_to_branch (
209
+ self .target ['branch' ],
210
+ origin_sha1 )
211
+ else :
186
212
# reset to the first merge
187
- origin = merges [0 ]
188
- merges = merges [1 :]
189
- self ._reset_to (origin ["remote" ], origin ["ref" ])
213
+ self ._reset_to (origin_sha1 )
190
214
for merge in merges :
191
- self ._merge (merge )
215
+ logger .info ("Merge %s, %s" , merge ["remote" ], merge ["ref" ])
216
+ self ._merge (fetch_heads [(merge ["remote" ],
217
+ merge ["ref" ])])
192
218
self ._execute_shell_command_after ()
193
219
logger .info ('End aggregation of %s' , self .cwd )
194
220
@@ -239,13 +265,74 @@ def init_repository(self, target_dir):
239
265
return True
240
266
241
267
def fetch (self ):
268
+ """Fetch all given (remote, ref) and associate their SHA
269
+
270
+ Will query and fetch all (remote, ref) in current git repository,
271
+ it'll take care to resolve each tuple in the local commit's SHA1
272
+
273
+ It returns a dict structure associating each (remote, ref) to their
274
+ SHA in local repository.
275
+ """
276
+ merges_requested = [(m ["remote" ], m ["ref" ])
277
+ for m in self .merges ]
242
278
basecmd = ("git" , "fetch" )
243
279
logger .info ("Fetching required remotes" )
244
- for merge in self .merges :
245
- cmd = basecmd + self ._fetch_options (merge ) + (merge ["remote" ],)
246
- if merge ["remote" ] not in self .fetch_all :
247
- cmd += (merge ["ref" ],)
280
+ fetch_heads = {}
281
+ ls_remote_refs = collections .defaultdict (list ) # to ls-query
282
+ while merges_requested :
283
+ remote , ref = merges_requested [0 ]
284
+ merges_requested = merges_requested [1 :]
285
+ cmd = (
286
+ basecmd +
287
+ self ._fetch_options ({"remote" : remote , "ref" : ref }) +
288
+ (remote ,))
289
+ if remote not in self .fetch_all :
290
+ cmd += (ref , )
291
+ else :
292
+ ls_remote_refs [remote ].append (ref )
248
293
self .log_call (cmd , cwd = self .cwd )
294
+ with open (os .path .join (self .cwd , ".git" , "FETCH_HEAD" ), "r" ) as f :
295
+ for line in f :
296
+ fetch_head , for_merge , _ = line .split ("\t " )
297
+ if for_merge == "not-for-merge" :
298
+ continue
299
+ break
300
+ fetch_heads [(remote , ref )] = fetch_head
301
+ if self .fetch_all :
302
+ if self .fetch_all is True :
303
+ remotes = self .remotes
304
+ else :
305
+ remotes = [r for r in self .remotes
306
+ if r ["name" ] in self .fetch_all ]
307
+ for remote in remotes :
308
+ refs = self .query_remote (
309
+ remote ["url" ],
310
+ ls_remote_refs [remote ["name" ]])
311
+ for _ , ref , sha in refs :
312
+ if (remote ["name" ], ref ) in merges_requested :
313
+ merges_requested .remove ((remote ["name" ], ref ))
314
+ fetch_heads [(remote ["name" ], ref )] = sha
315
+ if len (merges_requested ):
316
+ # Last case: our ref is a sha and remote git repository does
317
+ # not support querying commit directly by SHA. In this case
318
+ # we need just to check if ref is actually SHA, and if we have
319
+ # this SHA locally.
320
+ for remote , ref in merges_requested :
321
+ if not re .search ("[0-9a-f]{4,}" , ref ):
322
+ raise ValueError ("Could not resolv ref %r on remote %r"
323
+ % (ref , remote ))
324
+ valid_local_shas = self .log_call (
325
+ ['git' , 'rev-parse' , '-v' ] + [sha
326
+ for _r , sha in merges_requested ],
327
+ cwd = self .cwd , callwith = subprocess .check_output
328
+ ).strip ().splitlines ()
329
+ for remote , sha in merges_requested :
330
+ if sha not in valid_local_shas :
331
+ raise ValueError (
332
+ "Could not find SHA ref %r after fetch on remote %r"
333
+ % (ref , remote ))
334
+ fetch_heads [(remote ["name" ], sha )] = sha
335
+ return fetch_heads
249
336
250
337
def push (self ):
251
338
remote = self .target ['remote' ]
@@ -255,7 +342,7 @@ def push(self):
255
342
"Cannot push %s, no target remote configured" % branch
256
343
)
257
344
logger .info ("Push %s to %s" , branch , remote )
258
- self .log_call (['git' , 'push' , '-f' , remote , branch ], cwd = self .cwd )
345
+ self .log_call (['git' , 'push' , '-f' , remote , "HEAD:%s" % branch ], cwd = self .cwd )
259
346
260
347
def _check_status (self ):
261
348
"""Check repo status and except if dirty."""
@@ -277,42 +364,44 @@ def _fetch_options(self, merge):
277
364
cmd += ("--%s" % option , str (value ))
278
365
return cmd
279
366
280
- def _reset_to (self , remote , ref ):
367
+ def _reset_to (self , ref ):
281
368
if not self .force :
282
369
self ._check_status ()
283
- logger .info ('Reset branch to %s %s' , remote , ref )
284
- rtype , sha = self .query_remote_ref (remote , ref )
285
- if rtype is None and not ishex (ref ):
286
- raise GitAggregatorException (
287
- 'Could not reset %s to %s. No commit found for %s '
288
- % (remote , ref , ref ))
289
- cmd = ['git' , 'reset' , '--hard' , sha ]
370
+ logger .info ('Reset branch to %s' , ref )
371
+ cmd = ['git' , 'reset' , '--hard' , ref ]
290
372
if logger .getEffectiveLevel () != logging .DEBUG :
291
373
cmd .insert (2 , '--quiet' )
292
374
self .log_call (cmd , cwd = self .cwd )
293
375
self .log_call (['git' , 'clean' , '-ffd' ], cwd = self .cwd )
294
376
295
- def _switch_to_branch (self , branch_name ):
377
+ def _switch_to_branch (self , branch_name , ref = None ):
296
378
# check if the branch already exists
297
379
logger .info ("Switch to branch %s" , branch_name )
298
- self .log_call (['git' , 'checkout' , '-B' , branch_name ], cwd = self .cwd )
380
+ cmd = ['git' , 'checkout' , '-B' , branch_name ]
381
+ if ref is not None :
382
+ sha1 = self .log_call (
383
+ ['git' , 'rev-parse' , ref ],
384
+ callwith = subprocess .check_output ,
385
+ cwd = self .cwd ).strip ()
386
+ cmd .append (sha1 )
387
+ self .log_call (cmd , cwd = self .cwd )
299
388
300
389
def _execute_shell_command_after (self ):
301
390
logger .info ('Execute shell after commands' )
302
391
for cmd in self .shell_command_after :
303
392
self .log_call (cmd , shell = True , cwd = self .cwd )
304
393
305
394
def _merge (self , merge ):
306
- logger .info ("Pull %s, %s" , merge ["remote" ], merge ["ref" ])
307
- cmd = ("git" , "pull" , "--ff" , "--no-rebase " )
395
+ logger .info ("Merge %s, %s" , merge ["remote" ], merge ["ref" ])
396
+ cmd = ("git" , "merge " )
308
397
if self .git_version >= (1 , 7 , 10 ):
309
398
# --edit and --no-edit appear with Git 1.7.10
310
399
# see Documentation/RelNotes/1.7.10.txt of Git
311
400
# (https://git.kernel.org/cgit/git/git.git/tree)
312
401
cmd += ('--no-edit' ,)
313
402
if logger .getEffectiveLevel () != logging .DEBUG :
314
403
cmd += ('--quiet' ,)
315
- cmd += self . _fetch_options (merge ) + ( merge [ "remote" ], merge [ "ref" ] )
404
+ cmd += (merge , )
316
405
self .log_call (cmd , cwd = self .cwd )
317
406
318
407
def _get_remotes (self ):
0 commit comments