Skip to content

Commit 099a4cc

Browse files
committed
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 8631b0e commit 099a4cc

File tree

2 files changed

+180
-41
lines changed

2 files changed

+180
-41
lines changed

git_aggregator/repo.py

Lines changed: 127 additions & 39 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,
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):

tests/test_repo.py

Lines changed: 53 additions & 2 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,7 +166,38 @@ 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')
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]
150201
self.assertEquals(rtype, 'branch')
151202
self.assertTrue(sha)
152203

0 commit comments

Comments
 (0)