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
1=== modified file 'anybox/recipe/openerp/base.py'
2--- anybox/recipe/openerp/base.py 2014-06-08 21:55:32 +0000
3+++ anybox/recipe/openerp/base.py 2014-06-10 09:23:53 +0000
4@@ -494,12 +494,14 @@
5 if not split:
6 return
7 loc_type = split[0]
8- if loc_type != 'bzr':
9- raise UserError("Only merges of type 'bzr' are "
10+ if not loc_type in ('bzr', 'git'):
11+ raise UserError("Only merges of type 'bzr' and 'git' are "
12 "currently supported.")
13 options = dict(opt.split('=') for opt in split[4:])
14 if loc_type == 'bzr':
15 options['bzr-init'] = 'merge'
16+ else:
17+ options['merge'] = True
18
19 repo_url, local_dir, repo_rev = split[1:4]
20 location_spec = (repo_url, repo_rev)
21
22=== modified file 'anybox/recipe/openerp/vcs/base.py'
23--- anybox/recipe/openerp/vcs/base.py 2014-06-08 12:37:29 +0000
24+++ anybox/recipe/openerp/vcs/base.py 2014-06-10 09:23:53 +0000
25@@ -176,3 +176,6 @@
26 on the VCS type.
27 """
28 raise NotImplementedError
29+
30+ def archive(self, target_path):
31+ raise NotImplementedError
32
33=== modified file 'anybox/recipe/openerp/vcs/bzr.py'
34--- anybox/recipe/openerp/vcs/bzr.py 2014-06-08 12:37:29 +0000
35+++ anybox/recipe/openerp/vcs/bzr.py 2014-06-10 09:23:53 +0000
36@@ -255,6 +255,9 @@
37 If target_dir already exists, does a simple pull.
38 Offline-mode: no branch nor pull, but update.
39 In all cases, an attempt to update is performed before any pull
40+
41+ Special case: if the 'merge' option is True,
42+ merge revision into current branch.
43 """
44 target_dir = self.target_dir
45 offline = self.offline
46
47=== modified file 'anybox/recipe/openerp/vcs/git.py'
48--- anybox/recipe/openerp/vcs/git.py 2014-06-08 12:37:29 +0000
49+++ anybox/recipe/openerp/vcs/git.py 2014-06-10 09:23:53 +0000
50@@ -1,5 +1,4 @@
51 import os
52-import sys
53 import subprocess
54 import logging
55 import tempfile
56@@ -8,13 +7,14 @@
57 from .base import BaseRepo
58 from .base import SUBPROCESS_ENV
59 from anybox.recipe.openerp import utils
60-import re
61
62 logger = logging.getLogger(__name__)
63
64+BUILDOUT_ORIGIN = 'origin'
65+
66
67 class GitRepo(BaseRepo):
68- """Represent a Git clone tied to a reference branch."""
69+ """Represent a Git clone tied to a reference branch/commit/tag."""
70
71 vcs_control_dir = '.git'
72
73@@ -46,50 +46,69 @@
74 return bool(out.strip())
75
76 def get_update(self, revision):
77- """Ensure that target_dir is a branch of url at specified revision.
78+ """Make it so that the target directory is at the prescribed revision.
79
80- If target_dir already exists, does a simple fetch.
81- Offline-mode: no branch nor fetch, but checkout.
82+ Special case: if the 'merge' option is True,
83+ merge revision into current branch.
84 """
85+ if self.options.get('merge'):
86+ return self.merge(revision)
87+
88 target_dir = self.target_dir
89 url = self.url
90 offline = self.offline
91- rev_str = revision
92
93 with working_directory_keeper:
94- is_target_dir_exists = os.path.exists(target_dir)
95- if not is_target_dir_exists:
96- # TODO case of local url ?
97+ if not os.path.exists(target_dir):
98 if offline:
99+ # TODO case of local url ?
100 raise IOError(
101- "git repository %s does not exist; cannot clone it "
102- "from %s (offline mode)" % (target_dir, url))
103-
104- os.chdir(os.path.split(target_dir)[0])
105- logger.info("Cloning %s ...", url)
106- subprocess.check_call(['git', 'clone', url, target_dir])
107- os.chdir(target_dir)
108-
109- if is_target_dir_exists:
110+ "git repository %s does not exist; cannot clone "
111+ "it from %s (offline mode)" % (target_dir, url))
112+ logger.info("%s> git init", target_dir)
113+ subprocess.check_call(['git', 'init', target_dir])
114+ os.chdir(target_dir)
115+ logger.info("%s> git remote add %s %s",
116+ target_dir, BUILDOUT_ORIGIN, url)
117+ subprocess.check_call(['git', 'remote', 'add',
118+ BUILDOUT_ORIGIN, url])
119+
120+ if not offline:
121 # TODO what if remote repo is actually local fs ?
122- if not offline:
123- logger.info("Fetch for git repo %s (rev %s)...",
124- target_dir, rev_str)
125- subprocess.check_call(['git', 'fetch'])
126-
127- if revision and self._needToSwitchRevision(revision):
128- # switch to the expected revision
129- logger.info("Checkout %s to revision %s",
130- target_dir, revision)
131- self._switch(revision)
132-
133- if self._isATrackedBranch(revision):
134- if not offline:
135- logger.info("Pull for git repo %s (rev %s)...",
136- target_dir, rev_str)
137- subprocess.check_call(['git', 'pull'])
138+ os.chdir(target_dir)
139+ logger.info("%s> git remote set-url %s %s",
140+ target_dir, BUILDOUT_ORIGIN, url)
141+ subprocess.call(['git', 'remote', 'set-url',
142+ BUILDOUT_ORIGIN, url])
143+ logger.info("%s> git fetch %s",
144+ target_dir, BUILDOUT_ORIGIN)
145+ subprocess.check_call(['git', 'fetch', BUILDOUT_ORIGIN])
146+ # TODO: check what happens when there are local changes
147+ # TODO: what about the 'clean' option
148+ logger.info("%s> git checkout %s", target_dir, revision)
149+ subprocess.check_call(['git', 'checkout', revision])
150+ if self._is_a_branch(revision):
151+ # fast forward
152+ logger.info("%s> git merge merge %s/%s",
153+ target_dir, BUILDOUT_ORIGIN, revision)
154+ subprocess.check_call(['git', 'merge',
155+ BUILDOUT_ORIGIN + '/' + revision])
156+
157+ def merge(self, revision):
158+ """Merge revision into current branch"""
159+ with working_directory_keeper:
160+ if not self.is_versioned(self.target_dir):
161+ raise RuntimeError("Cannot merge into non existent "
162+ "or non git local directory %s" %
163+ self.target_dir)
164+ os.chdir(self.target_dir)
165+ logger.info("%s> git pull %s %s",
166+ self.target_dir, self.url, revision)
167+ subprocess.check_call(['git', 'pull', '--no-edit',
168+ self.url, revision])
169
170 def archive(self, target_path):
171+ # TODO: does this work with merge-ins?
172 revision = self.parents()[0]
173 if not os.path.exists(target_path):
174 os.makedirs(target_path)
175@@ -104,81 +123,17 @@
176 '-C', target_path])
177 os.unlink(target_tar.name)
178
179- def _isATrackedBranch(self, revision):
180- rbp = self._remote_branch_prefix
181- branches = utils.check_output(["git", "branch", "-a"])
182- branch = revision
183- return re.search(
184- "^ " + re.escape(rbp) + "\/" + re.escape(branch) + "$", branches,
185- re.M)
186-
187- def _needToSwitchRevision(self, revision):
188- """ Check if we need to checkout to an other branch
189- """
190- p = utils.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
191- rev = p.split()[0] # remove \n
192- logger.info("Current revision '%s' - Expected revision '%s'",
193- rev, revision)
194- return rev != revision
195-
196- def _switch(self, revision):
197- rbp = self._remote_branch_prefix
198- branches = utils.check_output(["git", "branch", "-a"])
199- branch = revision
200- if re.search("^(\*| ) %s$" % re.escape(branch), branches, re.M):
201- # the branch is local, normal checkout will work
202- logger.info("The branch is local; normal checkout ")
203- argv = ["checkout", branch]
204- elif re.search(
205- "^ " + re.escape(rbp) + "\/" + re.escape(branch) + "$", branches,
206- re.M):
207- # the branch is not local, normal checkout won't work here
208- logger.info("The branch is not local; checkout remote branch ")
209- argv = ["checkout", "-b", branch, "%s/%s" % (rbp, branch)]
210- else:
211- # A tag or revision was specified instead of a branch
212- logger.info("Checkout tag or revision")
213- argv = ["checkout", revision]
214- # runs the checkout with predetermined arguments
215- argv.insert(0, "git")
216- subprocess.check_call(argv)
217-
218- @property
219- def _remote_branch_prefix(self):
220- version = self._git_version
221- if version < (1, 6, 3):
222- return "origin"
223- else:
224- return "remotes/origin"
225-
226- @property
227- def _git_version(self):
228- out = utils.check_output(["git", "--version"])
229- m = re.search("git version (\d+)\.(\d+)(\.\d+)?(\.\d+)?", out)
230- if m is None:
231- logger.error("Unable to parse git version output")
232- logger.error("'git --version' output was:\n%s\n%s", out)
233- sys.exit(1)
234- version = m.groups()
235-
236- if version[3] is not None:
237- version = (
238- int(version[0]),
239- int(version[1]),
240- int(version[2][1:]),
241- int(version[3][1:])
242- )
243- elif version[2] is not None:
244- version = (
245- int(version[0]),
246- int(version[1]),
247- int(version[2][1:])
248- )
249- else:
250- version = (int(version[0]), int(version[1]))
251- if version < (1, 5):
252- logger.error(
253- "Git version %s is unsupported, please upgrade",
254- ".".join([str(v) for v in version]))
255- sys.exit(1)
256- return version
257+ def revert(self, revision):
258+ with working_directory_keeper:
259+ os.chdir(self.target_dir)
260+ subprocess.check_call(['git', 'checkout', revision])
261+ if self._is_a_branch(revision):
262+ subprocess.check_call(['git', 'reset', '--hard',
263+ BUILDOUT_ORIGIN + '/' + revision])
264+ else:
265+ subprocess.check_call(['git', 'reset', '--hard', revision])
266+
267+ def _is_a_branch(self, revision):
268+ branches = utils.check_output(["git", "branch"])
269+ branches = branches.split()
270+ return revision in branches
271
272=== modified file 'anybox/recipe/openerp/vcs/tests/test_git.py'
273--- anybox/recipe/openerp/vcs/tests/test_git.py 2014-06-08 12:37:29 +0000
274+++ anybox/recipe/openerp/vcs/tests/test_git.py 2014-06-10 09:23:53 +0000
275@@ -36,8 +36,8 @@
276
277 def create_src(self):
278 os.chdir(self.src_dir)
279- subprocess.call(['git', 'init', 'src-branch'])
280- self.src_repo = os.path.join(self.src_dir, 'src-branch')
281+ subprocess.call(['git', 'init', 'src-repo'])
282+ self.src_repo = os.path.join(self.src_dir, 'src-repo')
283 self.commit_1_sha = git_write_commit(self.src_repo, 'tracked',
284 "first", msg="initial commit")
285 self.commit_2_sha = git_write_commit(self.src_repo, 'tracked',
286@@ -229,3 +229,84 @@
287 branch("remotebranch")
288 with open(os.path.join(target_dir, 'tracked')) as f:
289 self.assertEquals(f.read().strip(), "last after remote branch")
290+
291+
292+class GitMergeTestCase(GitBaseTestCase):
293+
294+ def create_src(self):
295+ GitBaseTestCase.create_src(self)
296+ os.chdir(self.src_repo)
297+
298+ self.make_branch(self.src_repo, 'branch1')
299+ self.checkout_branch(self.src_repo, 'branch1')
300+ git_write_commit(self.src_repo, 'file_on_branch1',
301+ "file on branch 1", msg="on branch 1")
302+ self.checkout_branch(self.src_repo, 'master')
303+
304+ self.make_branch(self.src_repo, 'branch2')
305+ self.checkout_branch(self.src_repo, 'branch2')
306+ git_write_commit(self.src_repo, 'file_on_branch2',
307+ "file on branch 2", msg="on branch 2")
308+ self.checkout_branch(self.src_repo, 'master')
309+
310+ def make_branch(self, src_dir, name):
311+ """create a branch
312+ """
313+ subprocess.check_call(['git', 'branch', name], cwd=src_dir)
314+
315+ def checkout_branch(self, src_dir, name):
316+ """checkout a branch
317+ """
318+ subprocess.check_call(['git', 'checkout', name], cwd=src_dir)
319+
320+ def test_01_check_src_repo(self):
321+ """test if the creation of source repo worked as expected"""
322+ target_dir = os.path.join(self.dst_dir, "to_repo")
323+ repo = GitRepo(target_dir, self.src_repo)
324+
325+ repo('master')
326+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
327+ 'file_on_branch1 should not exist')
328+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
329+ 'file_on_branch2 should not exist')
330+
331+ repo('branch1')
332+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
333+ 'file_on_branch1 should exist')
334+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
335+ 'file_on_branch2 should not exist')
336+
337+ repo('branch2')
338+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
339+ 'file_on_branch1 should not exist')
340+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
341+ 'file_on_branch2 should exist')
342+
343+ def test_02_merge(self):
344+ target_dir = os.path.join(self.dst_dir, "to_repo")
345+ repo = GitRepo(target_dir, self.src_repo)
346+ repo('master')
347+
348+ repo.merge('branch1')
349+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
350+ 'file_on_branch1 should exist')
351+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
352+ 'file_on_branch2 should not exist')
353+ repo.merge('branch2')
354+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
355+ 'file_on_branch1 should exist')
356+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch2')),
357+ 'file_on_branch2 should exist')
358+
359+ def test_03_revert(self):
360+ target_dir = os.path.join(self.dst_dir, "to_repo")
361+ repo = GitRepo(target_dir, self.src_repo)
362+ repo('master')
363+
364+ repo.merge('branch1')
365+ self.assertTrue(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
366+ 'file_on_branch1 should exist')
367+
368+ repo.revert('master')
369+ self.assertFalse(os.path.exists(os.path.join(target_dir, 'file_on_branch1')),
370+ 'file_on_branch1 should not exist')

Subscribers

People subscribed via source and target branches