Merge lp:~acsone-openerp/anybox.recipe.openerp/git-merges into lp:anybox.recipe.openerp
- git-merges
- Merge into trunk
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 | ||||
Related bugs: |
|
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 |
Commit message
Description of the change
git merges and vcs-revert support
Laurent Mignon (Acsone) (lmi) wrote : | # |
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:/
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 !
Preview Diff
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') |
Hi,
The code is really more natural and simple with more features covered!
LGTM,