Merge lp:~acsone-openerp/anybox.recipe.openerp/git-merges into lp:anybox.recipe.openerp

Proposed by Stéphane Bidoul (Acsone)
Status: Merged
Merged at revision: 533
Proposed branch: lp:~acsone-openerp/anybox.recipe.openerp/git-merges
Merge into: lp:anybox.recipe.openerp
Diff against target: 370 lines (+161/-117)
5 files modified
anybox/recipe/openerp/base.py (+4/-2)
anybox/recipe/openerp/vcs/base.py (+3/-0)
anybox/recipe/openerp/vcs/bzr.py (+3/-0)
anybox/recipe/openerp/vcs/git.py (+68/-113)
anybox/recipe/openerp/vcs/tests/test_git.py (+83/-2)
To merge this branch: bzr merge lp:~acsone-openerp/anybox.recipe.openerp/git-merges
Reviewer Review Type Date Requested Status
Georges Racinet Approve
Raphaël Valyi - http://www.akretion.com (community) Approve
Laurent Mignon (Acsone) (community) code, tested Approve
Stefan Rijnhart (Opener) Pending
Review via email: mp+222608@code.launchpad.net

Description of the change

git merges and vcs-revert support

To post a comment you must log in.
Revision history for this message
Laurent Mignon (Acsone) (lmi) wrote :

Hi,

The code is really more natural and simple with more features covered!

LGTM,

review: Approve (code, tested)
Revision history for this message
Raphaël Valyi - http://www.akretion.com (rvalyi) wrote :

LGTM as long as vcs-merge wouldn't be used to update a local clone of OCB if OCB goes for continuous git-rebase.

NOTE: a further improvement to come after this merge to support shallow clones:
https://code.launchpad.net/~akretion-team/anybox.recipe.openerp/git-shallow-clone/+merge/222980

review: Approve
Revision history for this message
Georges Racinet (gracinet) wrote :

So… this is a major refactor of the Git subsystem, together with the addition of the merge feature.

On the formal side, there is not a single existing test that had to be adapted (including the switch-branch ones).
It also leverages local idioms such that utils.check_output, or the function to issue a git commit (keeps proper insulation with other tests, works on dumb buildslaves…)

I believe the end result is indeed clearer, especially the relation with remote branches.

The only caveat is that test_git has some pep8 errors (the kind I'll fix in a second).

So, a big thank you, let's test this in the trunk !

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'anybox/recipe/openerp/base.py'
--- anybox/recipe/openerp/base.py 2014-06-08 21:55:32 +0000
+++ anybox/recipe/openerp/base.py 2014-06-10 09:23:53 +0000
@@ -494,12 +494,14 @@
494 if not split:494 if not split:
495 return495 return
496 loc_type = split[0]496 loc_type = split[0]
497 if loc_type != 'bzr':497 if not loc_type in ('bzr', 'git'):
498 raise UserError("Only merges of type 'bzr' are "498 raise UserError("Only merges of type 'bzr' and 'git' are "
499 "currently supported.")499 "currently supported.")
500 options = dict(opt.split('=') for opt in split[4:])500 options = dict(opt.split('=') for opt in split[4:])
501 if loc_type == 'bzr':501 if loc_type == 'bzr':
502 options['bzr-init'] = 'merge'502 options['bzr-init'] = 'merge'
503 else:
504 options['merge'] = True
503505
504 repo_url, local_dir, repo_rev = split[1:4]506 repo_url, local_dir, repo_rev = split[1:4]
505 location_spec = (repo_url, repo_rev)507 location_spec = (repo_url, repo_rev)
506508
=== modified file 'anybox/recipe/openerp/vcs/base.py'
--- anybox/recipe/openerp/vcs/base.py 2014-06-08 12:37:29 +0000
+++ anybox/recipe/openerp/vcs/base.py 2014-06-10 09:23:53 +0000
@@ -176,3 +176,6 @@
176 on the VCS type.176 on the VCS type.
177 """177 """
178 raise NotImplementedError178 raise NotImplementedError
179
180 def archive(self, target_path):
181 raise NotImplementedError
179182
=== modified file 'anybox/recipe/openerp/vcs/bzr.py'
--- anybox/recipe/openerp/vcs/bzr.py 2014-06-08 12:37:29 +0000
+++ anybox/recipe/openerp/vcs/bzr.py 2014-06-10 09:23:53 +0000
@@ -255,6 +255,9 @@
255 If target_dir already exists, does a simple pull.255 If target_dir already exists, does a simple pull.
256 Offline-mode: no branch nor pull, but update.256 Offline-mode: no branch nor pull, but update.
257 In all cases, an attempt to update is performed before any pull257 In all cases, an attempt to update is performed before any pull
258
259 Special case: if the 'merge' option is True,
260 merge revision into current branch.
258 """261 """
259 target_dir = self.target_dir262 target_dir = self.target_dir
260 offline = self.offline263 offline = self.offline
261264
=== modified file 'anybox/recipe/openerp/vcs/git.py'
--- anybox/recipe/openerp/vcs/git.py 2014-06-08 12:37:29 +0000
+++ anybox/recipe/openerp/vcs/git.py 2014-06-10 09:23:53 +0000
@@ -1,5 +1,4 @@
1import os1import os
2import sys
3import subprocess2import subprocess
4import logging3import logging
5import tempfile4import tempfile
@@ -8,13 +7,14 @@
8from .base import BaseRepo7from .base import BaseRepo
9from .base import SUBPROCESS_ENV8from .base import SUBPROCESS_ENV
10from anybox.recipe.openerp import utils9from anybox.recipe.openerp import utils
11import re
1210
13logger = logging.getLogger(__name__)11logger = logging.getLogger(__name__)
1412
13BUILDOUT_ORIGIN = 'origin'
14
1515
16class GitRepo(BaseRepo):16class GitRepo(BaseRepo):
17 """Represent a Git clone tied to a reference branch."""17 """Represent a Git clone tied to a reference branch/commit/tag."""
1818
19 vcs_control_dir = '.git'19 vcs_control_dir = '.git'
2020
@@ -46,50 +46,69 @@
46 return bool(out.strip())46 return bool(out.strip())
4747
48 def get_update(self, revision):48 def get_update(self, revision):
49 """Ensure that target_dir is a branch of url at specified revision.49 """Make it so that the target directory is at the prescribed revision.
5050
51 If target_dir already exists, does a simple fetch.51 Special case: if the 'merge' option is True,
52 Offline-mode: no branch nor fetch, but checkout.52 merge revision into current branch.
53 """53 """
54 if self.options.get('merge'):
55 return self.merge(revision)
56
54 target_dir = self.target_dir57 target_dir = self.target_dir
55 url = self.url58 url = self.url
56 offline = self.offline59 offline = self.offline
57 rev_str = revision
5860
59 with working_directory_keeper:61 with working_directory_keeper:
60 is_target_dir_exists = os.path.exists(target_dir)62 if not os.path.exists(target_dir):
61 if not is_target_dir_exists:
62 # TODO case of local url ?
63 if offline:63 if offline:
64 # TODO case of local url ?
64 raise IOError(65 raise IOError(
65 "git repository %s does not exist; cannot clone it "66 "git repository %s does not exist; cannot clone "
66 "from %s (offline mode)" % (target_dir, url))67 "it from %s (offline mode)" % (target_dir, url))
6768 logger.info("%s> git init", target_dir)
68 os.chdir(os.path.split(target_dir)[0])69 subprocess.check_call(['git', 'init', target_dir])
69 logger.info("Cloning %s ...", url)70 os.chdir(target_dir)
70 subprocess.check_call(['git', 'clone', url, target_dir])71 logger.info("%s> git remote add %s %s",
71 os.chdir(target_dir)72 target_dir, BUILDOUT_ORIGIN, url)
7273 subprocess.check_call(['git', 'remote', 'add',
73 if is_target_dir_exists:74 BUILDOUT_ORIGIN, url])
75
76 if not offline:
74 # TODO what if remote repo is actually local fs ?77 # TODO what if remote repo is actually local fs ?
75 if not offline:78 os.chdir(target_dir)
76 logger.info("Fetch for git repo %s (rev %s)...",79 logger.info("%s> git remote set-url %s %s",
77 target_dir, rev_str)80 target_dir, BUILDOUT_ORIGIN, url)
78 subprocess.check_call(['git', 'fetch'])81 subprocess.call(['git', 'remote', 'set-url',
7982 BUILDOUT_ORIGIN, url])
80 if revision and self._needToSwitchRevision(revision):83 logger.info("%s> git fetch %s",
81 # switch to the expected revision84 target_dir, BUILDOUT_ORIGIN)
82 logger.info("Checkout %s to revision %s",85 subprocess.check_call(['git', 'fetch', BUILDOUT_ORIGIN])
83 target_dir, revision)86 # TODO: check what happens when there are local changes
84 self._switch(revision)87 # TODO: what about the 'clean' option
8588 logger.info("%s> git checkout %s", target_dir, revision)
86 if self._isATrackedBranch(revision):89 subprocess.check_call(['git', 'checkout', revision])
87 if not offline:90 if self._is_a_branch(revision):
88 logger.info("Pull for git repo %s (rev %s)...",91 # fast forward
89 target_dir, rev_str)92 logger.info("%s> git merge merge %s/%s",
90 subprocess.check_call(['git', 'pull'])93 target_dir, BUILDOUT_ORIGIN, revision)
94 subprocess.check_call(['git', 'merge',
95 BUILDOUT_ORIGIN + '/' + revision])
96
97 def merge(self, revision):
98 """Merge revision into current branch"""
99 with working_directory_keeper:
100 if not self.is_versioned(self.target_dir):
101 raise RuntimeError("Cannot merge into non existent "
102 "or non git local directory %s" %
103 self.target_dir)
104 os.chdir(self.target_dir)
105 logger.info("%s> git pull %s %s",
106 self.target_dir, self.url, revision)
107 subprocess.check_call(['git', 'pull', '--no-edit',
108 self.url, revision])
91109
92 def archive(self, target_path):110 def archive(self, target_path):
111 # TODO: does this work with merge-ins?
93 revision = self.parents()[0]112 revision = self.parents()[0]
94 if not os.path.exists(target_path):113 if not os.path.exists(target_path):
95 os.makedirs(target_path)114 os.makedirs(target_path)
@@ -104,81 +123,17 @@
104 '-C', target_path])123 '-C', target_path])
105 os.unlink(target_tar.name)124 os.unlink(target_tar.name)
106125
107 def _isATrackedBranch(self, revision):126 def revert(self, revision):
108 rbp = self._remote_branch_prefix127 with working_directory_keeper:
109 branches = utils.check_output(["git", "branch", "-a"])128 os.chdir(self.target_dir)
110 branch = revision129 subprocess.check_call(['git', 'checkout', revision])
111 return re.search(130 if self._is_a_branch(revision):
112 "^ " + re.escape(rbp) + "\/" + re.escape(branch) + "$", branches,131 subprocess.check_call(['git', 'reset', '--hard',
113 re.M)132 BUILDOUT_ORIGIN + '/' + revision])
114133 else:
115 def _needToSwitchRevision(self, revision):134 subprocess.check_call(['git', 'reset', '--hard', revision])
116 """ Check if we need to checkout to an other branch135
117 """136 def _is_a_branch(self, revision):
118 p = utils.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])137 branches = utils.check_output(["git", "branch"])
119 rev = p.split()[0] # remove \n138 branches = branches.split()
120 logger.info("Current revision '%s' - Expected revision '%s'",139 return revision in branches
121 rev, revision)
122 return rev != revision
123
124 def _switch(self, revision):
125 rbp = self._remote_branch_prefix
126 branches = utils.check_output(["git", "branch", "-a"])
127 branch = revision
128 if re.search("^(\*| ) %s$" % re.escape(branch), branches, re.M):
129 # the branch is local, normal checkout will work
130 logger.info("The branch is local; normal checkout ")
131 argv = ["checkout", branch]
132 elif re.search(
133 "^ " + re.escape(rbp) + "\/" + re.escape(branch) + "$", branches,
134 re.M):
135 # the branch is not local, normal checkout won't work here
136 logger.info("The branch is not local; checkout remote branch ")
137 argv = ["checkout", "-b", branch, "%s/%s" % (rbp, branch)]
138 else:
139 # A tag or revision was specified instead of a branch
140 logger.info("Checkout tag or revision")
141 argv = ["checkout", revision]
142 # runs the checkout with predetermined arguments
143 argv.insert(0, "git")
144 subprocess.check_call(argv)
145
146 @property
147 def _remote_branch_prefix(self):
148 version = self._git_version
149 if version < (1, 6, 3):
150 return "origin"
151 else:
152 return "remotes/origin"
153
154 @property
155 def _git_version(self):
156 out = utils.check_output(["git", "--version"])
157 m = re.search("git version (\d+)\.(\d+)(\.\d+)?(\.\d+)?", out)
158 if m is None:
159 logger.error("Unable to parse git version output")
160 logger.error("'git --version' output was:\n%s\n%s", out)
161 sys.exit(1)
162 version = m.groups()
163
164 if version[3] is not None:
165 version = (
166 int(version[0]),
167 int(version[1]),
168 int(version[2][1:]),
169 int(version[3][1:])
170 )
171 elif version[2] is not None:
172 version = (
173 int(version[0]),
174 int(version[1]),
175 int(version[2][1:])
176 )
177 else:
178 version = (int(version[0]), int(version[1]))
179 if version < (1, 5):
180 logger.error(
181 "Git version %s is unsupported, please upgrade",
182 ".".join([str(v) for v in version]))
183 sys.exit(1)
184 return version
185140
=== modified file 'anybox/recipe/openerp/vcs/tests/test_git.py'
--- anybox/recipe/openerp/vcs/tests/test_git.py 2014-06-08 12:37:29 +0000
+++ anybox/recipe/openerp/vcs/tests/test_git.py 2014-06-10 09:23:53 +0000
@@ -36,8 +36,8 @@
3636
37 def create_src(self):37 def create_src(self):
38 os.chdir(self.src_dir)38 os.chdir(self.src_dir)
39 subprocess.call(['git', 'init', 'src-branch'])39 subprocess.call(['git', 'init', 'src-repo'])
40 self.src_repo = os.path.join(self.src_dir, 'src-branch')40 self.src_repo = os.path.join(self.src_dir, 'src-repo')
41 self.commit_1_sha = git_write_commit(self.src_repo, 'tracked',41 self.commit_1_sha = git_write_commit(self.src_repo, 'tracked',
42 "first", msg="initial commit")42 "first", msg="initial commit")
43 self.commit_2_sha = git_write_commit(self.src_repo, 'tracked',43 self.commit_2_sha = git_write_commit(self.src_repo, 'tracked',
@@ -229,3 +229,84 @@
229 branch("remotebranch")229 branch("remotebranch")
230 with open(os.path.join(target_dir, 'tracked')) as f:230 with open(os.path.join(target_dir, 'tracked')) as f:
231 self.assertEquals(f.read().strip(), "last after remote branch")231 self.assertEquals(f.read().strip(), "last after remote branch")
232
233
234class GitMergeTestCase(GitBaseTestCase):
235
236 def create_src(self):
237 GitBaseTestCase.create_src(self)
238 os.chdir(self.src_repo)
239
240 self.make_branch(self.src_repo, 'branch1')
241 self.checkout_branch(self.src_repo, 'branch1')
242 git_write_commit(self.src_repo, 'file_on_branch1',
243 "file on branch 1", msg="on branch 1")
244 self.checkout_branch(self.src_repo, 'master')
245
246 self.make_branch(self.src_repo, 'branch2')
247 self.checkout_branch(self.src_repo, 'branch2')
248 git_write_commit(self.src_repo, 'file_on_branch2',
249 "file on branch 2", msg="on branch 2")
250 self.checkout_branch(self.src_repo, 'master')
251
252 def make_branch(self, src_dir, name):
253 """create a branch
254 """
255 subprocess.check_call(['git', 'branch', name], cwd=src_dir)
256
257 def checkout_branch(self, src_dir, name):
258 """checkout a branch
259 """
260 subprocess.check_call(['git', 'checkout', name], cwd=src_dir)
261
262 def test_01_check_src_repo(self):
263 """test if the creation of source repo worked as expected"""
264 target_dir = os.path.join(self.dst_dir, "to_repo")
265 repo = GitRepo(target_dir, self.src_repo)
266
267 repo('master')
268 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
269 'file_on_branch1 should not exist')
270 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
271 'file_on_branch2 should not exist')
272
273 repo('branch1')
274 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
275 'file_on_branch1 should exist')
276 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
277 'file_on_branch2 should not exist')
278
279 repo('branch2')
280 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
281 'file_on_branch1 should not exist')
282 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
283 'file_on_branch2 should exist')
284
285 def test_02_merge(self):
286 target_dir = os.path.join(self.dst_dir, "to_repo")
287 repo = GitRepo(target_dir, self.src_repo)
288 repo('master')
289
290 repo.merge('branch1')
291 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
292 'file_on_branch1 should exist')
293 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
294 'file_on_branch2 should not exist')
295 repo.merge('branch2')
296 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
297 'file_on_branch1 should exist')
298 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
299 'file_on_branch2 should exist')
300
301 def test_03_revert(self):
302 target_dir = os.path.join(self.dst_dir, "to_repo")
303 repo = GitRepo(target_dir, self.src_repo)
304 repo('master')
305
306 repo.merge('branch1')
307 self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
308 'file_on_branch1 should exist')
309
310 repo.revert('master')
311 self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
312 'file_on_branch1 should not exist')

Subscribers

People subscribed via source and target branches