Skip to content

Commit 03f7a19

Browse files
vaabTom Blauwendraat
authored andcommitted
new: support annotated tags as merge source (fixes #27, fixes #28)
Avoids also contacting twice (``fetch`` and then ``pull``) remote before merging. Signed-off-by: Valentin Lab <[email protected]>
1 parent 1fa89fe commit 03f7a19

File tree

2 files changed

+183
-43
lines changed

2 files changed

+183
-43
lines changed

git_aggregator/repo.py

Lines changed: 129 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
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+
67
from __future__ import unicode_literals
8+
79
import os
810
import logging
911
import re
1012
import subprocess
13+
import collections
1114

1215
import 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 if os.path.exists(self.cwd) else None,
143153
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, ))
152169

153170
def log_call(self, cmd, callwith=subprocess.check_call,
154171
log_level=logging.DEBUG, **kw):
@@ -166,9 +183,10 @@ def log_call(self, cmd, callwith=subprocess.check_call,
166183
return ret
167184

168185
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.
172190
"""
173191
logger.info('Start aggregation of %s', self.cwd)
174192
target_dir = self.cwd
@@ -177,18 +195,26 @@ def aggregate(self):
177195
if is_new:
178196
cloned = self.init_repository(target_dir)
179197

180-
self._switch_to_branch(self.target['branch'])
181198
for r in self.remotes:
182199
self._set_remote(**r)
183-
self.fetch()
200+
fetch_heads = self.fetch()
201+
logger.debug("fetch_heads: %r", fetch_heads)
184202
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:
186212
# 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)
190214
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"])])
192218
self._execute_shell_command_after()
193219
logger.info('End aggregation of %s', self.cwd)
194220

@@ -239,13 +265,74 @@ def init_repository(self, target_dir):
239265
return True
240266

241267
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]
242278
basecmd = ("git", "fetch")
243279
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)
248293
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
249336

250337
def push(self):
251338
remote = self.target['remote']
@@ -255,7 +342,7 @@ def push(self):
255342
"Cannot push %s, no target remote configured" % branch
256343
)
257344
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)
259346

260347
def _check_status(self):
261348
"""Check repo status and except if dirty."""
@@ -277,42 +364,44 @@ def _fetch_options(self, merge):
277364
cmd += ("--%s" % option, str(value))
278365
return cmd
279366

280-
def _reset_to(self, remote, ref):
367+
def _reset_to(self, ref):
281368
if not self.force:
282369
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]
290372
if logger.getEffectiveLevel() != logging.DEBUG:
291373
cmd.insert(2, '--quiet')
292374
self.log_call(cmd, cwd=self.cwd)
293375
self.log_call(['git', 'clean', '-ffd'], cwd=self.cwd)
294376

295-
def _switch_to_branch(self, branch_name):
377+
def _switch_to_branch(self, branch_name, ref=None):
296378
# check if the branch already exists
297379
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)
299388

300389
def _execute_shell_command_after(self):
301390
logger.info('Execute shell after commands')
302391
for cmd in self.shell_command_after:
303392
self.log_call(cmd, shell=True, cwd=self.cwd)
304393

305394
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")
308397
if self.git_version >= (1, 7, 10):
309398
# --edit and --no-edit appear with Git 1.7.10
310399
# see Documentation/RelNotes/1.7.10.txt of Git
311400
# (https://git.kernel.org/cgit/git/git.git/tree)
312401
cmd += ('--no-edit',)
313402
if logger.getEffectiveLevel() != logging.DEBUG:
314403
cmd += ('--quiet',)
315-
cmd += self._fetch_options(merge) + (merge["remote"], merge["ref"])
404+
cmd += (merge,)
316405
self.log_call(cmd, cwd=self.cwd)
317406

318407
def _get_remotes(self):

tests/test_repo.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ def setUp(self):
7373
commit 1 -> fork after -> remote 2
7474
tag1
7575
commit 2
76-
*remote2 (clone remote 1)
76+
annotated tag2
77+
* remote2 (clone remote 1)
7778
commit 1
7879
commit 3
7980
branch b2
@@ -94,6 +95,7 @@ def setUp(self):
9495
subprocess.check_call(['git', 'tag', 'tag1'], cwd=self.remote1)
9596
self.commit_2_sha = git_write_commit(
9697
self.remote1, 'tracked', "last", msg="last commit")
98+
subprocess.check_call(['git', 'tag', '-am', 'foo', 'tag2'], cwd=self.remote1)
9799
self.commit_3_sha = git_write_commit(
98100
self.remote2, 'tracked2', "remote2", msg="new commit")
99101
subprocess.check_call(['git', 'checkout', '-b', 'b2'],
@@ -121,6 +123,24 @@ def test_minimal(self):
121123
last_rev = git_get_last_rev(self.cwd)
122124
self.assertEqual(last_rev, self.commit_1_sha)
123125

126+
def test_annotated_tag(self):
127+
remotes = [{
128+
'name': 'r1',
129+
'url': self.url_remote1
130+
}]
131+
merges = [{
132+
'remote': 'r1',
133+
'ref': 'tag2'
134+
}]
135+
target = {
136+
'remote': 'r1',
137+
'branch': 'agg1'
138+
}
139+
repo = Repo(self.cwd, remotes, merges, target)
140+
repo.aggregate()
141+
last_rev = git_get_last_rev(self.cwd)
142+
self.assertEqual(last_rev, self.commit_2_sha)
143+
124144
def test_simple_merge(self):
125145
remotes = [{
126146
'name': 'r1',
@@ -146,8 +166,39 @@ def test_simple_merge(self):
146166
self.assertEqual(last_rev, self.commit_3_sha)
147167
# push
148168
repo.push()
149-
rtype, sha = repo.query_remote_ref('r1', 'agg')
150-
self.assertEqual(rtype, 'branch')
169+
rtype, ref, sha = list(repo.query_remote('r1', 'agg'))[0]
170+
self.assertEquals(rtype, 'branch')
171+
self.assertTrue(sha)
172+
173+
def test_simple_merge_2(self):
174+
## Launched from an existing git repository
175+
remotes = [{
176+
'name': 'r1',
177+
'url': self.url_remote1
178+
}, {
179+
'name': 'r2',
180+
'url': self.url_remote2
181+
}]
182+
merges = [{
183+
'remote': 'r1',
184+
'ref': 'tag1'
185+
}, {
186+
'remote': 'r2',
187+
'ref': self.commit_3_sha
188+
}]
189+
target = {
190+
'remote': 'r1',
191+
'branch': 'agg'
192+
}
193+
subprocess.call(['git', 'init', self.cwd])
194+
repo = Repo(self.cwd, remotes, merges, target, fetch_all=True)
195+
repo.aggregate()
196+
last_rev = git_get_last_rev(self.cwd)
197+
self.assertEqual(last_rev, self.commit_3_sha)
198+
# push
199+
repo.push()
200+
rtype, ref, sha = list(repo.query_remote('r1', 'agg'))[0]
201+
self.assertEquals(rtype, 'branch')
151202
self.assertTrue(sha)
152203

153204
def test_push_missing_remote(self):

0 commit comments

Comments
 (0)