Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Master depth #7

Merged
merged 1 commit into from
Jan 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ install:

# command to run tests
script:
- git --version
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yajo Is this line still required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, there was a problem with Travis' git version: it fails with git pull --depth 2 --no-edit remote ref, but works with git pull --no-edit --depth 2 remote ref. I had to add the line to see if that was the bug because local tests worked.

I guess it's good to keep that line as reference, just in case for future Travis vs localhost git version conflicts.

- flake8 . --exclude=__init__.py
- coverage run --source git_aggregator setup.py test

Expand Down
43 changes: 43 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ You can specify that you want to fetch all references from all remotes you have

fetch_all: true

Shallow repositories
--------------------

To save big amounts of bandwidth and disk space, you can use shallow clones.
These download only a restricted amount of commits depending on some criteria.
Available options are `depth`_, `shallow-since`_ and `shallow-exclude`_.

.. warning::

Available options depend on server and client Git version, be sure to use
options available for your environment.

.. _depth: https://git-scm.com/docs/git-fetch#git-fetch---depthltdepthgt
.. _shallow-since: https://git-scm.com/docs/git-fetch#git-fetch---shallow-sinceltdategt
.. _shallow-exclude: https://git-scm.com/docs/git-fetch#git-fetch---shallow-excludeltrevisiongt

You can use those in the ``defaults`` sections to apply them everywhere, or
specifying them in the corresponding ``merges`` section, for which you must use
the ``dict`` alternate construction. If you need to disable a default in
``merges``, set it to ``false``:

.. code-block:: yaml

./odoo:
defaults:
depth: 20
remotes:
odoo: https://github.com/odoo/odoo.git
ocb: https://github.com/OCA/OCB.git
acsone: https://github.com/acsone/odoo.git
merges:
-
remote: ocb
ref: "9.0"
depth: 1000
-
remote: odoo
ref: refs/pull/14859/head
target: acsone 9.0

Remember that you need to fetch at least the common ancestor of all merges for
it to succeed.

Triggers
--------

Expand Down
38 changes: 25 additions & 13 deletions git_aggregator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def get_repos(config):
directory = os.path.abspath(directory)
repo_dict = {
'cwd': directory,
'defaults': repo_data.get('defaults', dict()),
}
remote_names = set()
if 'remotes' in repo_data:
Expand All @@ -50,21 +51,32 @@ def get_repos(config):
merges = []
merge_data = repo_data.get('merges') or []
for merge in merge_data:
parts = merge.split(' ')
if len(parts) != 2:
raise ConfigException(
'%s: Merge must be formatted as '
'"remote_name ref".' % directory)

remote_name, ref = merge.split(' ')
if remote_name not in remote_names:
try:
# Assume parts is a str
parts = merge.split(' ')
if len(parts) != 2:
raise ConfigException(
'%s: Merge must be formatted as '
'"remote_name ref".' % directory)
merge = {
"remote": parts[0],
"ref": parts[1],
}
except AttributeError:
# Parts is a dict
try:
merge["remote"] = str(merge["remote"])
merge["ref"] = str(merge["ref"])
except KeyError:
raise ConfigException(
'%s: Merge lacks mandatory '
'`remote` or `ref` keys.' % directory)
# Check remote is available
if merge["remote"] not in remote_names:
raise ConfigException(
'%s: Merge remote %s not defined in remotes.' %
(directory, remote_name))
merges.append({
'remote': remote_name,
'ref': ref,
})
(directory, merge["remote"]))
merges.append(merge)
repo_dict['merges'] = merges
if not merges:
raise ConfigException(
Expand Down
36 changes: 25 additions & 11 deletions git_aggregator/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .exception import GitAggregatorException
from ._compat import console_to_str

FETCH_DEFAULTS = ("depth", "shallow-since", "shallow-exclude")
logger = logging.getLogger(__name__)


Expand All @@ -34,7 +35,7 @@ class Repo(object):
_git_version = None

def __init__(self, cwd, remotes, merges, target,
shell_command_after=None, fetch_all=False):
shell_command_after=None, fetch_all=False, defaults=None):
"""Initialize a git repository aggregator

:param cwd: path to the directory where to initialize the repository
Expand All @@ -43,12 +44,14 @@ def __init__(self, cwd, remotes, merges, target,
:param: merges list of merge to apply to build the aggregated
repository. A merge is a dict {'remote': '', 'ref': ''}
:param target:
:param shell_command_after: an optional list of shell command to
execute after the aggregation
:param fetch_all:
Can be an iterable (recommended: ``frozenset``) that yields names
of remotes where all refs should be fetched, or ``True`` to do it
for every configured remote.
:param shell_command_after: an optional list of shell command to
execute after the aggregation
:param defaults:
Collection of default parameters to be passed to git.
"""
self.cwd = cwd
self.remotes = remotes
Expand All @@ -59,6 +62,7 @@ def __init__(self, cwd, remotes, merges, target,
self.merges = merges
self.target = target
self.shell_command_after = shell_command_after or []
self.defaults = defaults or dict()

@property
def git_version(self):
Expand Down Expand Up @@ -174,9 +178,9 @@ def aggregate(self):
# reset to the first merge
origin = merges[0]
merges = merges[1:]
self._reset_to(**origin)
self._reset_to(origin["remote"], origin["ref"])
for merge in merges:
self._merge(**merge)
self._merge(merge)
self._execute_shell_command_after()
logger.info('End aggregation of %s', self.cwd)

Expand All @@ -188,7 +192,7 @@ def fetch(self):
basecmd = ("git", "fetch")
logger.info("Fetching required remotes")
for merge in self.merges:
cmd = basecmd + (merge["remote"],)
cmd = basecmd + self._fetch_options(merge) + (merge["remote"],)
if merge["remote"] not in self.fetch_all:
cmd += (merge["ref"],)
self.log_call(cmd, cwd=self.cwd)
Expand All @@ -201,6 +205,15 @@ def push(self):
os.chdir(self.cwd)
self.log_call(['git', 'push', '-f', remote, branch])

def _fetch_options(self, merge):
"""Get the fetch options from the given merge dict."""
cmd = tuple()
for option in FETCH_DEFAULTS:
value = merge.get(option, self.defaults.get(option))
if value:
cmd += ("--%s" % option, str(value))
return cmd

def _reset_to(self, remote, ref):
logger.info('Reset branch to %s %s', remote, ref)
rtype, sha = self.query_remote_ref(remote, ref)
Expand All @@ -223,16 +236,17 @@ def _execute_shell_command_after(self):
for cmd in self.shell_command_after:
self.log_call(cmd, shell=True)

def _merge(self, remote, ref):
logger.info("Pull %s, %s", remote, ref)
cmd = ['git', 'pull', remote, ref]
def _merge(self, merge):
logger.info("Pull %s, %s", merge["remote"], merge["ref"])
cmd = ("git", "pull")
if self.git_version >= (1, 7, 10):
# --edit and --no-edit appear with Git 1.7.10
# see Documentation/RelNotes/1.7.10.txt of Git
# (https://git.kernel.org/cgit/git/git.git/tree)
cmd.insert(2, '--no-edit')
cmd += ('--no-edit',)
if logger.getEffectiveLevel() != logging.DEBUG:
cmd.insert(2, '--quiet')
cmd += ('--quiet',)
cmd += self._fetch_options(merge) + (merge["remote"], merge["ref"])
self.log_call(cmd)

def _get_remotes(self):
Expand Down
62 changes: 62 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def test_load(self):
repos[0],
{'cwd': '/product_attribute',
'fetch_all': False,
'defaults': {},
'merges': [{'ref': '8.0', 'remote': 'oca'},
{'ref': 'refs/pull/105/head', 'remote': 'oca'},
{'ref': 'refs/pull/106/head', 'remote': 'oca'}],
Expand All @@ -57,6 +58,52 @@ def test_load(self):
'url':
'git+ssh://git@github.com/acsone/product-attribute.git'}])

def test_load_defaults(self):
config_yaml = dedent("""
/web:
defaults:
depth: 1
remotes:
oca: https://github.com/OCA/web.git
acsone: git+ssh://git@github.com/acsone/web.git
merges:
-
remote: oca
ref: 8.0
depth: 1000
- oca refs/pull/105/head
-
remote: oca
ref: refs/pull/106/head
target: acsone aggregated_branch_name
""")
repos = config.get_repos(self._parse_config(config_yaml))
self.assertEquals(len(repos), 1)
# remotes are configured as dict therefore the order is not preserved
# when parsed
remotes = repos[0]['remotes']
repos[0]['remotes'] = []
self.assertDictEqual(
repos[0],
{'cwd': '/web',
'fetch_all': False,
'defaults': {'depth': 1},
'merges': [{'ref': '8.0', 'remote': 'oca', 'depth': 1000},
{'ref': 'refs/pull/105/head', 'remote': 'oca'},
{'ref': 'refs/pull/106/head', 'remote': 'oca'}],
'remotes': [],
'shell_command_after': [],
'target': {'branch': 'aggregated_branch_name',
'remote': 'acsone'}})
assertfn = self.assertItemsEqual if PY2 else self.assertCountEqual
assertfn(
remotes,
[{'name': 'oca',
'url': 'https://github.com/OCA/web.git'},
{'name': 'acsone',
'url':
'git+ssh://git@github.com/acsone/web.git'}])

def test_load_shell_command_after(self):
"""Shell command after are alway parser as a list
"""
Expand Down Expand Up @@ -181,6 +228,21 @@ def test_load_merges_exception(self):
ex.exception.args[0],
'/product_attribute: Merge remote oba not defined in remotes.')

config_yaml = dedent("""
/web:
remotes:
oca: https://github.com/OCA/web.git
merges:
-
depth: 1
target: oca aggregated_branch
""")
with self.assertRaises(ConfigException) as ex:
config.get_repos(self._parse_config(config_yaml))
self.assertEquals(
ex.exception.args[0],
'/web: Merge lacks mandatory `remote` or `ref` keys.')

def test_load_target_exception(self):
config_yaml = """
/product_attribute:
Expand Down
67 changes: 67 additions & 0 deletions tests/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,70 @@ def test_update_aggregate_2(self):
self.remote1, 'tracked_new', "last", msg="new file on remote1")
repo.aggregate()
self.assertFalse(os.path.isfile(os.path.join(self.cwd, 'tracked_new')))

def test_depth_1(self):
"""Ensure a simple shallow clone with 1 commit works."""
remotes = [{
'name': 'shallow',
'url': self.url_remote1
}]
merges = [{
'remote': 'shallow',
"ref": "master",
}]
target = {
'remote': 'shallow',
'branch': 'master'
}
defaults = {
"depth": 1,
}
repo = Repo(self.cwd, remotes, merges, target, defaults=defaults)
repo.aggregate()
self.assertTrue(os.path.isfile(os.path.join(self.cwd, 'tracked')))

with working_directory_keeper:
os.chdir(self.cwd)
log_shallow = subprocess.check_output(
("git", "rev-list", "shallow/master"))
# Shallow fetch: just 1 commmit
self.assertEqual(len(log_shallow.splitlines()), 1)

def test_depth(self):
"""Ensure `depth` is used correctly."""
remotes = [{
'name': 'r1',
'url': self.url_remote1
}, {
'name': 'r2',
'url': self.url_remote2
}]
merges = [{
'remote': 'r1',
"ref": "master",
}, {
"remote": "r2",
'ref': "b2",
}]
target = {
'remote': 'r1',
'branch': 'agg'
}
defaults = {
"depth": 2,
}
repo = Repo(self.cwd, remotes, merges, target, defaults=defaults)
repo.aggregate()
self.assertTrue(os.path.isfile(os.path.join(self.cwd, 'tracked')))
self.assertTrue(os.path.isfile(os.path.join(self.cwd, 'tracked2')))

with working_directory_keeper:
os.chdir(self.cwd)
log_r1 = subprocess.check_output(
("git", "rev-list", "r1/master"))
log_r2 = subprocess.check_output(
("git", "rev-list", "r2/b2"))
# Shallow fetch: just 1 commmit
self.assertEqual(len(log_r1.splitlines()), 2)
# Full fetch: all 3 commits
self.assertEqual(len(log_r2.splitlines()), 2)