Merge lp:~spiv/bzr-builder/merge-subdirs-479705 into lp:~james-w/bzr-builder/trunk-old

Proposed by Andrew Bennetts
Status: Work in progress
Proposed branch: lp:~spiv/bzr-builder/merge-subdirs-479705
Merge into: lp:~james-w/bzr-builder/trunk-old
Prerequisite: lp:~spiv/bzr-builder/refactor
Diff against target: 2784 lines (+1496/-600)
10 files modified
TODO (+0/-3)
__init__.py (+33/-377)
cmds.py (+492/-0)
ppa.py (+124/-0)
recipe.py (+421/-150)
setup.py (+13/-12)
tests/__init__.py (+4/-3)
tests/test_blackbox.py (+62/-16)
tests/test_ppa.py (+28/-0)
tests/test_recipe.py (+319/-39)
To merge this branch: bzr merge lp:~spiv/bzr-builder/merge-subdirs-479705
Reviewer Review Type Date Requested Status
Andrew Bennetts (community) Disapprove
James Westby Needs Fixing
Review via email: mp+14979@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

This fixes bug 479705. It allows a merge line of a builder recipe to specify a subdirectory to merge, rather than the whole named branch. The obvious application of this is to allow merging just the debian/ directory from a branch that contains more than just a debian/ directory.

Revision history for this message
James Westby (james-w) wrote :

Hi,

Thanks for working on this.

I have some concerns about the utility of it. If the branch
you are merging doesn't make any modifications outside of
./debian/ then this makes no difference, therefore it must
be for the case where it does.

Is this then to take the packaging without any modifications
to the source, so that it is a vanilla packaging?

If so, then it is just assumed that this won't be done if
some of the modifications are needed, or if the packaging
depends on some of the modifications?

That would be acceptable, but I wonder why we want to encourage
people to do things this way, instead of say, layering another
branch on top that undoes the unwanted things? Obviously the two
are not equivalent, but the latter seems to fit better with the
model to me.

As for a code review, two things mean that this is not mergeable
as is:

  * Needs a format bump.
  * Needs an update to the documentation so that people know it is possible.

Other than that the changes look fine with one obvious exception:

45 + :param subpath: XXX

Thanks,

James

review: Needs Resubmitting
Revision history for this message
Robert Collins (lifeless) wrote :

> Hi,
>
> Thanks for working on this.
>
> I have some concerns about the utility of it. If the branch
> you are merging doesn't make any modifications outside of
> ./debian/ then this makes no difference, therefore it must
> be for the case where it does.

This is false. If the branches are not related, merging a subdir can avoid very many unnecessary conflicts while still generating a single tree.

> Is this then to take the packaging without any modifications
> to the source, so that it is a vanilla packaging?

Or to merge unrelated branches.

> As for a code review, two things mean that this is not mergeable
> as is:
>
> * Needs a format bump.

I don't see why it needs this: old versions will error appropriately, new versions will work.

If old versions would misbehave then I'd agree on the need to bump the format.

Revision history for this message
James Westby (james-w) wrote :

On Tue Nov 24 00:59:18 UTC 2009 Robert Collins wrote:
> > Hi,
> >
> > Thanks for working on this.
> >
> > I have some concerns about the utility of it. If the branch
> > you are merging doesn't make any modifications outside of
> > ./debian/ then this makes no difference, therefore it must
> > be for the case where it does.
>
> This is false. If the branches are not related, merging a subdir can avoid very
> many unnecessary conflicts while still generating a single tree.

If the branches are not related there is no way in builder to merge
them at this point anyway (no way to specify a merge base). I haven't
tested to make sure that the API works the same as the UI here, so
apologies if I mispoke.

I appreciate trying to improve this situation, but I'm not sure that
this is the correct fix, at least at this time. I would like to know
more about the design though, as I feel that there is something I am
missing.

Thanks,

James

Revision history for this message
Andrew Bennetts (spiv) wrote :

> > This is false. If the branches are not related, merging a subdir can avoid
> very
> > many unnecessary conflicts while still generating a single tree.
>
> If the branches are not related there is no way in builder to merge
> them at this point anyway (no way to specify a merge base). I haven't
> tested to make sure that the API works the same as the UI here, so
> apologies if I mispoke.

You're right about that. It would be easy to catch UnrelatedBranches and if a subpath is specified try again with a base of NULL_REVISION. Or we could add a 'mergeunrelated' directive. Or further extend the syntax to allow specifying bases.

Do you have a preference for one of those options, or do you feel this is the wrong track entirely? It seems to me that however it is specified a partial merge is a fairly neat way to provide "please give me this branch, plus the debian/ dir from another branch."

I'm sure there are many cases where blindly combining a debian/ dir from an existing package with an arbitrary version of upstream won't work. I think it's likely it will
work ok for many nightly packages though: have a recipe that specifies "current trunk + packaging from $recent-packaging". I'm not certain which use-case Robert had in mind, but that's what I'm thinking of.

Revision history for this message
James Westby (james-w) wrote :

Hi,

I'm sorry the discussion on this has stalled, but I'm not sure what to do
about it.

I'd rather know the concrete use cases for the change and how this would fix it
before merging so that I can better support them in future. Help understanding that
would be greatly appreciated.

Thanks,

James

Revision history for this message
Robert Collins (lifeless) wrote :

So, in two parts:
 - Andrew is looking at making sure it works with unrelated branches.

Use cases: as we discussed, for launchpads 'build from branch' to be successful for upstreams, they need easy access to debian/ in package branches that are likely to have no history relationship: This syntax allows them to specify that that is what they want without us resorting to guessing.

Revision history for this message
James Westby (james-w) wrote :

Hi,

I think I would like to see catching UnrelatedBranches, and then if a subdir
is specified, try again with NULL_REVISION as the base. If there are then
any conflicts it should bail out as normal.

This should have it work for the simple case, which is a start.

Why specify the subdirectory separate from the URL? Could we not do an
open_containing and use the subpath returned from that?

Thanks,

James

64. By Andrew Bennetts

Make merge_unrelated_branch work using my merge-into-merger branch of bzrlib.

65. By Andrew Bennetts

Define a new 'merge-into' instruction.

66. By Andrew Bennetts

Expand docstrings, and allow specifying a target subdir.

67. By Andrew Bennetts

Bump format, add tests for parsing of merge-into, revert unused change to allow 'merge' instructions to take subpaths.

68. By Andrew Bennetts

Reduce cruft slightly.

69. By Andrew Bennetts

Rename 'merge-into' instruction to 'merge-part'.

Revision history for this message
James Westby (james-w) wrote :

Hi Andrew,

This is great now, thanks.

merge-part NAME BRANCH REVISION SUBDIR [TARGET-DIR]

I think I might prefer

merge-part NAME BRANCH SUBDIR REVISION [TARGET-DIR]

(Maybe even:

merge-part NAME BRANCH SUBDIR [REVISION [TARGET-DIR]]

or

merge-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]])

We can't do

from bzrlib.cleanup import OperationWithCleanups

unfortunately, as it is too new for the bzr that we need to work with.
We could backport newer bzr's for building older distroseries, but that's
quite a bit of work.

MergeIntoMerger only seems to be in newer bzr's too? Is there any way that
we can avoid depending on that?

108 + if target_subdir is None:
109 + target_subdir = os.path.basename(subpath)

I wouldn't expect that, I would think that it would just be "subpath".

I'm happy to do the work to merge this in, the only blocker right now
is MergeIntoMerger, so if you could comment on that then we can move
forward.

Thanks,

James

review: Needs Fixing
Revision history for this message
James Westby (james-w) wrote :

Right, sorry, I now see that MergeIntoMerger is proposed
for adding to bzr.

I think that's the right way to go as it should be in bzr,
but it does make this hard for deploying on older distroseries
in Launchpad.

Thanks,

James

Revision history for this message
Andrew Bennetts (spiv) wrote :

James Westby wrote:
> Right, sorry, I now see that MergeIntoMerger is proposed
> for adding to bzr.
>
> I think that's the right way to go as it should be in bzr,
> but it does make this hard for deploying on older distroseries
> in Launchpad.

Ah, I didn't realise that was a requirement. I can extract that out in a way
that will work with older bzrs without much trouble. Would you like that in
bzr-builder or a separate plugin?

Also, what's the oldest bzr version that bzr-builder wants to work with?

Revision history for this message
James Westby (james-w) wrote :

On Fri, 09 Jul 2010 00:49:38 -0000, Andrew Bennetts <email address hidden> wrote:
> James Westby wrote:
> > Right, sorry, I now see that MergeIntoMerger is proposed
> > for adding to bzr.
> >
> > I think that's the right way to go as it should be in bzr,
> > but it does make this hard for deploying on older distroseries
> > in Launchpad.
>
> Ah, I didn't realise that was a requirement. I can extract that out in a way
> that will work with older bzrs without much trouble. Would you like that in
> bzr-builder or a separate plugin?

Great. bzr-builder is easier.

Do you think it should still go in bzr with a "backport" inside
bzr-builder?

> Also, what's the oldest bzr version that bzr-builder wants to work with?

Well, dapper is still "supported" so 0.8.2.

Really though we're probably only going to have hardy and later,
meaning 1.3.1.

Previously bzr-builder used very simple APIs, so this wasn't really an
issue. If we keep running in to this we can review our deployment and
perhaps use a newer bzr as well as a newer bzr-builder (given that the
plugin isn't even in hardy).

Thanks,

James

Revision history for this message
Andrew Bennetts (spiv) wrote :

James Westby wrote:
> On Fri, 09 Jul 2010 00:49:38 -0000, Andrew Bennetts <email address hidden> wrote:
[...]
> Great. bzr-builder is easier.
>
> Do you think it should still go in bzr with a "backport" inside
> bzr-builder?

Yes, exactly.

> > Also, what's the oldest bzr version that bzr-builder wants to work with?
>
> Well, dapper is still "supported" so 0.8.2.

Ouch!

> Really though we're probably only going to have hardy and later,
> meaning 1.3.1.
>
> Previously bzr-builder used very simple APIs, so this wasn't really an
> issue. If we keep running in to this we can review our deployment and
> perhaps use a newer bzr as well as a newer bzr-builder (given that the
> plugin isn't even in hardy).

Considering that so many branches on Launchpad are 2a format now it
seems to me that bzr >= 2.0 is almost a requirement anyway. Please tell
me I'm right ;)

A compromise be to say "merge-part" is not available unless you
are using a new-enough bzr. (Again, probably >= 2.0 with the help of a
backport in bzr-builder.)

Revision history for this message
James Westby (james-w) wrote :

On Fri, 09 Jul 2010 05:25:53 -0000, Andrew Bennetts <email address hidden> wrote:
> Considering that so many branches on Launchpad are 2a format now it
> seems to me that bzr >= 2.0 is almost a requirement anyway. Please tell
> me I'm right ;)

That's a good point. It may be that the archive we are using to backport
bzr-builder already has a bzr in for other reasons, and so that is
getting picked up.

Checking...

It does not, so indeed the newer branch formats can't be built on older
releases.

Filed

  https://bugs.edge.launchpad.net/launchpad-code/+bug/603615

about this.

Depending on how we fix this we could then not put the new classes in to
bzr-builder.

> A compromise be to say "merge-part" is not available unless you
> are using a new-enough bzr. (Again, probably >= 2.0 with the help of a
> backport in bzr-builder.)

I don't fancy that, but it would be an option, yes.

Thanks,

James

Revision history for this message
Daniel Hahler (blueyed) wrote :

I am looking forward to using this, but wonder why it used with branching ("merge") and not nesting ("nest")?

Andrew wrote:
> have a recipe that specifies "current trunk + packaging from $recent-packaging".

That's the most common use case and looks like "nesting", not merging.

As far as I understand the following will take of this / make it behave like "nest":
> I think I would like to see catching UnrelatedBranches, and then if
> a subdir is specified, try again with NULL_REVISION as the base.

Revision history for this message
Andrew Bennetts (spiv) wrote :

Daniel: I agree, nest-part is probably a better description for users. It happens that the implementation in bzr reuses a bunch of merge infrastructure, but I think it's fair to say that's an implementation detail that is irrelevant to users. So I've updated the patch accordingly, thank you!

James: So what is the plan for dealing or not with older bzr versions for now? The relevant bzr change is with PQM for landing in 2.2 right now. I'll going resubmit this patch for merging without any backwards compat for bzr < 2.2 included, but if you do want it I can make an additional proposal to copy the MergeIntoMerge bits into bzr-builder. At this point I'm basically just asking for a clear "Yes, do that" or "No, don't bother" instruction and then I'll go and do as you say, the current plan is a bit unclear with bug 603615 not going anywhere atm.

(Because the trunk branch has changed since I first created this proposal I'm going to mark this one as rejected and create a new one. But I really just resubmitting this patch.)

Revision history for this message
Andrew Bennetts (spiv) wrote :

The new merge proposal is at <https://code.edge.launchpad.net/~spiv/bzr-builder/merge-subdirs-479705/+merge/31251>. For some reason LP won't let me mark this proposal as Rejected, though :(

review: Disapprove
70. By Andrew Bennetts

Rename merge-part instruction to nest-part.

71. By Andrew Bennetts

Merge trunk.

72. By Andrew Bennetts

Change the syntax of nest-part to have the revspec at the end, and make it optional.

73. By Andrew Bennetts

Tweak plugin help text.

Unmerged revisions

73. By Andrew Bennetts

Tweak plugin help text.

72. By Andrew Bennetts

Change the syntax of nest-part to have the revspec at the end, and make it optional.

71. By Andrew Bennetts

Merge trunk.

70. By Andrew Bennetts

Rename merge-part instruction to nest-part.

69. By Andrew Bennetts

Rename 'merge-into' instruction to 'merge-part'.

68. By Andrew Bennetts

Reduce cruft slightly.

67. By Andrew Bennetts

Bump format, add tests for parsing of merge-into, revert unused change to allow 'merge' instructions to take subpaths.

66. By Andrew Bennetts

Expand docstrings, and allow specifying a target subdir.

65. By Andrew Bennetts

Define a new 'merge-into' instruction.

64. By Andrew Bennetts

Make merge_unrelated_branch work using my merge-into-merger branch of bzrlib.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'TODO'
2--- TODO 2009-04-15 14:10:19 +0000
3+++ TODO 2010-08-13 01:14:42 +0000
4@@ -1,6 +1,3 @@
5-- Implement building the source package from the tree, including adding
6- the changelog entry with the desired version.
7-- Implement uploading to a PPA.
8 - Documentation.
9 - Work out how to get tarballs for non-native packages if we want that.
10 - Decide on error on existing target directory vs. re-use and pull changes.
11
12=== modified file '__init__.py'
13--- __init__.py 2010-08-13 01:14:42 +0000
14+++ __init__.py 2010-08-13 01:14:42 +0000
15@@ -21,7 +21,7 @@
16
17 A recipe is just a text file that starts with a line such as::
18
19- # bzr-builder format 0.2 deb-version 1.0+{revno}-{revno:packaging}
20+ # bzr-builder format 0.3 deb-version 1.0+{revno}-{revno:packaging}
21
22 The format specifier is there to allow the syntax to be changed in later
23 versions, and the meaning of "deb-version" will be explained later.
24@@ -70,6 +70,20 @@
25 for the revisionspec is identical to that taken by the "--revision" argument
26 to many bzr commands. See "bzr help revisionspec" for details.
27
28+You can also merge specific subdirectories from a branch with a "nest-part"
29+line like
30+
31+nest-part packaging lp:~foo-dev/foo/packaging debian
32+
33+which specifies that the only the debian/ subdirectory should be merged. This
34+works even if the branches share no revision history. You can optionally
35+specify the subdirectory and revision in the target with a line like
36+
37+nest-part libfoo lp:libfoo src lib/foo tag:release-1.2
38+
39+which will put the "src" directory of libfoo in "lib/foo", using the revision
40+of libfoo tagged "release-1.2".
41+
42 It is also possible to run an arbitrary command at a particular point in the
43 construction process. For example::
44
45@@ -104,14 +118,28 @@
46
47 * {time} will be substituted with the current date and time, such as
48 200908191512.
49+ * {date} will be substituted with just the current date, such as
50+ 20090819.
51 * {revno} will be the revno of the base branch (the first specified).
52 * {revno:<branch name>} will be substituted with the revno for the
53 branch named <branch name> in the recipe.
54+ * {debupstream} will be replaced by the upstream portion of the version
55+ number taken from debian/changelog in the final tree. If when the
56+ tree is built the top of debian/changelog has a version number of
57+ "1.0-1" then this would evaluate to "1.0".
58+
59+Instruction syntax summary:
60+
61+ * nest NAME BRANCH TARGET-DIR [REVISION]
62+ * merge NAME BRANCH [REVISION]
63+ * nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]
64+ * run COMMAND
65
66 Format versions:
67
68 0.1 - original format.
69 0.2 - added "run" instruction.
70+ 0.3 - added "nest-part" instruction.
71 """
72
73 if __name__ == '__main__':
74@@ -123,382 +151,10 @@
75 shell=True, env={"BZR_PLUGIN_PATH": dir})
76 sys.exit(retcode)
77
78-import datetime
79-from email import utils
80-import os
81-import pwd
82-import re
83-import socket
84-import shutil
85-import subprocess
86-import tempfile
87-
88-from debian_bundle import changelog
89-
90-from bzrlib import (
91- errors,
92- trace,
93- transport,
94- )
95-from bzrlib.commands import Command, register_command
96-from bzrlib.option import Option
97-
98-from bzrlib.plugins.builder.recipe import (
99- build_manifest,
100- build_tree,
101- RecipeParser,
102- resolve_revisions,
103- )
104-
105-
106-def write_manifest_to_path(path, base_branch):
107- parent_dir = os.path.dirname(path)
108- if parent_dir != '' and not os.path.exists(parent_dir):
109- os.makedirs(parent_dir)
110- manifest_f = open(path, 'wb')
111- try:
112- manifest_f.write(build_manifest(base_branch))
113- finally:
114- manifest_f.close()
115-
116-
117-def get_branch_from_recipe_file(recipe_file):
118- recipe_transport = transport.get_transport(os.path.dirname(recipe_file))
119- try:
120- recipe_contents = recipe_transport.get_bytes(
121- os.path.basename(recipe_file))
122- except errors.NoSuchFile:
123- raise errors.BzrCommandError("Specified recipe does not exist: "
124- "%s" % recipe_file)
125- parser = RecipeParser(recipe_contents, filename=recipe_file)
126- return parser.parse()
127-
128-
129-def get_old_recipe(if_changed_from):
130- old_manifest_transport = transport.get_transport(os.path.dirname(
131- if_changed_from))
132- try:
133- old_manifest_contents = old_manifest_transport.get_bytes(
134- os.path.basename(if_changed_from))
135- except errors.NoSuchFile:
136- return None
137- old_recipe = RecipeParser(old_manifest_contents,
138- filename=if_changed_from).parse()
139- return old_recipe
140-
141-
142-def get_maintainer():
143- """
144- Create maintainer string using the same algorithm as in dch
145- """
146- env = os.environ
147- regex = re.compile(r"^(.*)\s+<(.*)>$")
148-
149- # Split email and name
150- if 'DEBEMAIL' in env:
151- match_obj = regex.match(env['DEBEMAIL'])
152- if match_obj:
153- if not 'DEBFULLNAME' in env:
154- env['DEBFULLNAME'] = match_obj.group(1)
155- env['DEBEMAIL'] = match_obj.group(2)
156- if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env:
157- if 'EMAIL' in env:
158- match_obj = regex.match(env['EMAIL'])
159- if match_obj:
160- if not 'DEBFULLNAME' in env:
161- env['DEBFULLNAME'] = match_obj.group(1)
162- env['EMAIL'] = match_obj.group(2)
163-
164- # Get maintainer's name
165- if 'DEBFULLNAME' in env:
166- maintainer = env['DEBFULLNAME']
167- elif 'NAME' in env:
168- maintainer = env['NAME']
169- else:
170- # Use password database if no data in environment variables
171- try:
172- maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos)
173- except KeyError, AttributeError:
174- # TBD: Use last changelog entry value
175- maintainer = "bzr-builder"
176-
177- # Get maintainer's mail address
178- if 'DEBEMAIL' in env:
179- email = env['DEBEMAIL']
180- elif 'EMAIL' in env:
181- email = env['EMAIL']
182- else:
183- addr = None
184- if os.path.exists('/etc/mailname'):
185- f = open('/etc/mailname')
186- try:
187- addr = f.readline().strip()
188- finally:
189- f.close()
190- if not addr:
191- addr = socket.getfqdn()
192- if addr:
193- user = pwd.getpwuid(os.getuid()).pw_name
194- if not user:
195- addr = None
196- else:
197- addr = "%s@%s" % (user, addr)
198-
199- if addr:
200- email = addr
201- else:
202- # TBD: Use last changelog entry value
203- email = "none@example.org"
204-
205- return (maintainer, email)
206-
207-
208-def add_changelog_entry(base_branch, basedir, distribution=None,
209- package=None):
210- debian_dir = os.path.join(basedir, "debian")
211- if not os.path.exists(debian_dir):
212- os.makedirs(debian_dir)
213- cl_path = os.path.join(debian_dir, "changelog")
214- if os.path.exists(cl_path):
215- cl_f = open(cl_path)
216- try:
217- cl = changelog.Changelog(file=cl_f)
218- finally:
219- cl_f.close()
220- else:
221- cl = changelog.Changelog()
222- if len(cl._blocks) > 0:
223- if distribution is None:
224- distribution = cl._blocks[0].distributions.split()[0]
225- if package is None:
226- package = cl._blocks[0].package
227- if "{debupstream}" in base_branch.deb_version:
228- cl_version = cl._blocks[0].version
229- base_branch.deb_version = base_branch.deb_version.replace(
230- "{debupstream}", cl_version.upstream_version)
231- else:
232- if package is None:
233- raise errors.BzrCommandError("No previous changelog to "
234- "take the package name from, and --package not "
235- "specified.")
236- if "{debupstream}" in base_branch.deb_version:
237- raise errors.BzrCommandError("No previous changelog to "
238- "take the upstream version from - {debupstream} was "
239- "used.")
240- if distribution is None:
241- distribution = "jaunty"
242- # Use debian packaging environment variables
243- # or default values if they don't exist
244- author = "%s <%s>" % get_maintainer()
245-
246- date = utils.formatdate(localtime=True)
247- cl.new_block(package=package, version=base_branch.deb_version,
248- distributions=distribution, urgency="low",
249- changes=['', ' * Auto build.', ''],
250- author=author, date=date)
251- cl_f = open(cl_path, 'wb')
252- try:
253- cl.write_to_open_file(cl_f)
254- finally:
255- cl_f.close()
256-
257-
258-def build_source_package(basedir):
259- trace.note("Building the source package")
260- command = ["/usr/bin/debuild", "--no-tgz-check", "-i", "-I", "-S",
261- "-uc", "-us"]
262- proc = subprocess.Popen(command, cwd=basedir,
263- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
264- stdin=subprocess.PIPE)
265- proc.stdin.close()
266- retcode = proc.wait()
267- if retcode != 0:
268- output = proc.stdout.read()
269- raise errors.BzrCommandError("Failed to build the source package: "
270- "%s" % output)
271-
272-
273-def sign_source_package(basedir, key_id):
274- trace.note("Signing the source package")
275- command = ["/usr/bin/debsign", "-S", "-k%s" % key_id]
276- proc = subprocess.Popen(command, cwd=basedir,
277- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
278- stdin=subprocess.PIPE)
279- proc.stdin.close()
280- retcode = proc.wait()
281- if retcode != 0:
282- output = proc.stdout.read()
283- raise errors.BzrCommandError("Signing the package failed: "
284- "%s" % output)
285-
286-
287-def dput_source_package(basedir, target):
288- trace.note("Uploading the source package")
289- command = ["/usr/bin/debrelease", "-S", "--dput", target]
290- proc = subprocess.Popen(command, cwd=basedir,
291- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
292- stdin=subprocess.PIPE)
293- proc.stdin.close()
294- retcode = proc.wait()
295- if retcode != 0:
296- output = proc.stdout.read()
297- raise errors.BzrCommandError("Uploading the package failed: "
298- "%s" % output)
299-
300-
301-class cmd_build(Command):
302- """Build a tree based on a 'recipe'.
303-
304- Pass the name of the recipe file and the directory to work in.
305-
306- See "bzr help builder" for more information on what a recipe is.
307- """
308- takes_args = ["recipe_file", "working_directory"]
309- takes_options = [
310- Option('manifest', type=str, argname="path",
311- help="Path to write the manifest to."),
312- Option('if-changed-from', type=str, argname="path",
313- help="Only build if the outcome would be different "
314- "to that specified in the specified manifest."),
315- ]
316-
317- def _substitute_stuff(self, recipe_file, if_changed_from):
318- """Common code to substitute stuff
319-
320- :return: A tuple with (retcode, base_branch). If retcode is None
321- then the command execution should continue.
322- """
323- base_branch = get_branch_from_recipe_file(recipe_file)
324- time = datetime.datetime.utcnow()
325- base_branch.substitute_time(time)
326- old_recipe = None
327- if if_changed_from is not None:
328- old_recipe = get_old_recipe(if_changed_from)
329- # Save the unsubstituted version for dailydeb.
330- self._template_version = base_branch.deb_version
331- changed = resolve_revisions(base_branch, if_changed_from=old_recipe)
332- if not changed:
333- trace.note("Unchanged")
334- return 0, base_branch
335- return None, base_branch
336-
337- def run(self, recipe_file, working_directory, manifest=None,
338- if_changed_from=None):
339- result, base_branch = self._substitute_stuff(recipe_file,
340- if_changed_from)
341- if result is not None:
342- return result
343- manifest_path = manifest or os.path.join(working_directory,
344- "bzr-builder.manifest")
345- build_tree(base_branch, working_directory)
346- write_manifest_to_path(manifest_path, base_branch)
347-
348-
349-register_command(cmd_build)
350-
351-
352-class cmd_dailydeb(cmd_build):
353- """Build a deb based on a 'recipe'.
354-
355- See "bzr help builder" for more information on what a recipe is.
356-
357- If you do not specify a working directory then a temporary
358- directory will be used and it will be removed when the command
359- finishes.
360- """
361-
362- takes_options = cmd_build.takes_options + [
363- Option("package", type=str,
364- help="The package name to use in the changelog entry. "
365- "If not specified then the package from the "
366- "previous changelog entry will be used, so it "
367- "must be specified if there is no changelog."),
368- Option("distribution", type=str,
369- help="The distribution to target. If not specified "
370- "then the same distribution as the last entry "
371- "in debian/changelog will be used."),
372- Option("dput", type=str, argname="target",
373- help="dput the built package to the specified "
374- "dput target."),
375- Option("key-id", type=str, short_name="k",
376- help="Sign the packages with the specified GnuPG key, "
377- "must be specified if you use --dput."),
378- ]
379-
380- takes_args = ["recipe_file", "working_basedir?"]
381-
382- def run(self, recipe_file, working_basedir=None, manifest=None,
383- if_changed_from=None, package=None, distribution=None,
384- dput=None, key_id=None):
385-
386- if dput is not None and key_id is None:
387- raise errors.BzrCommandError("You must specify --key-id if you "
388- "specify --dput.")
389- result, base_branch = self._substitute_stuff(recipe_file,
390- if_changed_from)
391- if result is not None:
392- return result
393- if working_basedir is None:
394- temp_dir = tempfile.mkdtemp(prefix="bzr-builder-")
395- working_basedir = temp_dir
396- else:
397- temp_dir = None
398- if not os.path.exists(working_basedir):
399- os.makedirs(working_basedir)
400- self._calculate_package_name(recipe_file, package)
401- working_directory = os.path.join(working_basedir,
402- "%s-%s" % (self._package_name, self._template_version))
403- try:
404- # we want to use a consistent package_dir always to support
405- # updates in place, but debuild etc want PACKAGE-UPSTREAMVERSION
406- # on disk, so we build_tree with the unsubstituted version number
407- # and do a final rename-to step before calling into debian build
408- # tools. We then rename the working dir back.
409- manifest_path = os.path.join(working_directory, "debian",
410- "bzr-builder.manifest")
411- build_tree(base_branch, working_directory)
412- write_manifest_to_path(manifest_path, base_branch)
413- # Add changelog also substitutes {debupstream}.
414- add_changelog_entry(base_branch, working_directory,
415- distribution=distribution, package=package)
416- package_dir = self._calculate_package_dir(base_branch,
417- working_basedir)
418- # working_directory -> package_dir: after this debian stuff works.
419- os.rename(working_directory, package_dir)
420- try:
421- build_source_package(package_dir)
422- if key_id is not None:
423- sign_source_package(package_dir, key_id)
424- if dput is not None:
425- dput_source_package(package_dir, dput)
426- finally:
427- # package_dir -> working_directory
428- os.rename(package_dir, working_directory)
429- # Note that this may write a second manifest.
430- if manifest is not None:
431- write_manifest_to_path(manifest, base_branch)
432- finally:
433- if temp_dir is not None:
434- shutil.rmtree(temp_dir)
435-
436- def _calculate_package_dir(self, base_branch, working_basedir):
437- """Calculate the directory name that should be used while debuilding."""
438- version = base_branch.deb_version
439- if "-" in version:
440- version = version[:version.rindex("-")]
441- package_basedir = "%s-%s" % (self._package_name, version)
442- package_dir = os.path.join(working_basedir, package_basedir)
443- return package_dir
444-
445- def _calculate_package_name(self, recipe_file, package):
446- """Calculate the directory name that should be used while debuilding."""
447- recipe_name = os.path.basename(recipe_file)
448- if recipe_name.endswith(".recipe"):
449- recipe_name = recipe_name[:-len(".recipe")]
450- self._package_name = package or recipe_name
451-
452-
453-register_command(cmd_dailydeb)
454+
455+from bzrlib.commands import plugin_cmds
456+plugin_cmds.register_lazy("cmd_build", [], "bzrlib.plugins.builder.cmds")
457+plugin_cmds.register_lazy("cmd_dailydeb", [], "bzrlib.plugins.builder.cmds")
458
459
460 def test_suite():
461
462=== added file 'cmds.py'
463--- cmds.py 1970-01-01 00:00:00 +0000
464+++ cmds.py 2010-08-13 01:14:42 +0000
465@@ -0,0 +1,492 @@
466+# bzr-builder: a bzr plugin to constuct trees based on recipes
467+# Copyright 2009 Canonical Ltd.
468+
469+# This program is free software: you can redistribute it and/or modify it
470+# under the terms of the GNU General Public License version 3, as published
471+# by the Free Software Foundation.
472+
473+# This program is distributed in the hope that it will be useful, but
474+# WITHOUT ANY WARRANTY; without even the implied warranties of
475+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
476+# PURPOSE. See the GNU General Public License for more details.
477+
478+# You should have received a copy of the GNU General Public License along
479+# with this program. If not, see <http://www.gnu.org/licenses/>.
480+
481+"""Subcommands provided by bzr-builder."""
482+
483+import datetime
484+from email import utils
485+import errno
486+import os
487+import pwd
488+import re
489+import socket
490+import shutil
491+import subprocess
492+import tempfile
493+
494+try:
495+ from debian import changelog
496+except ImportError:
497+ # In older versions of python-debian the main package was named
498+ # debian_bundle
499+ from debian_bundle import changelog
500+
501+from bzrlib import (
502+ errors,
503+ trace,
504+ transport,
505+ )
506+from bzrlib.commands import Command
507+from bzrlib.option import Option
508+
509+from bzrlib.plugins.builder.recipe import (
510+ build_tree,
511+ DEBUPSTREAM_VAR,
512+ RecipeParser,
513+ resolve_revisions,
514+ )
515+
516+
517+# The default distribution used by add_changelog_entry()
518+DEFAULT_UBUNTU_DISTRIBUTION = "lucid"
519+
520+
521+class MissingDependency(errors.BzrError):
522+ pass
523+
524+
525+def write_manifest_to_path(path, base_branch):
526+ parent_dir = os.path.dirname(path)
527+ if parent_dir != '' and not os.path.exists(parent_dir):
528+ os.makedirs(parent_dir)
529+ manifest_f = open(path, 'wb')
530+ try:
531+ manifest_f.write(str(base_branch))
532+ finally:
533+ manifest_f.close()
534+
535+
536+def get_branch_from_recipe_file(recipe_file):
537+ recipe_transport = transport.get_transport(os.path.dirname(recipe_file))
538+ try:
539+ recipe_contents = recipe_transport.get_bytes(
540+ os.path.basename(recipe_file))
541+ except errors.NoSuchFile:
542+ raise errors.BzrCommandError("Specified recipe does not exist: "
543+ "%s" % recipe_file)
544+ parser = RecipeParser(recipe_contents, filename=recipe_file)
545+ return parser.parse()
546+
547+
548+def get_old_recipe(if_changed_from):
549+ old_manifest_transport = transport.get_transport(os.path.dirname(
550+ if_changed_from))
551+ try:
552+ old_manifest_contents = old_manifest_transport.get_bytes(
553+ os.path.basename(if_changed_from))
554+ except errors.NoSuchFile:
555+ return None
556+ old_recipe = RecipeParser(old_manifest_contents,
557+ filename=if_changed_from).parse()
558+ return old_recipe
559+
560+
561+def get_maintainer():
562+ """Create maintainer string using the same algorithm as in dch.
563+ """
564+ env = os.environ
565+ regex = re.compile(r"^(.*)\s+<(.*)>$")
566+
567+ # Split email and name
568+ if 'DEBEMAIL' in env:
569+ match_obj = regex.match(env['DEBEMAIL'])
570+ if match_obj:
571+ if not 'DEBFULLNAME' in env:
572+ env['DEBFULLNAME'] = match_obj.group(1)
573+ env['DEBEMAIL'] = match_obj.group(2)
574+ if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env:
575+ if 'EMAIL' in env:
576+ match_obj = regex.match(env['EMAIL'])
577+ if match_obj:
578+ if not 'DEBFULLNAME' in env:
579+ env['DEBFULLNAME'] = match_obj.group(1)
580+ env['EMAIL'] = match_obj.group(2)
581+
582+ # Get maintainer's name
583+ if 'DEBFULLNAME' in env:
584+ maintainer = env['DEBFULLNAME']
585+ elif 'NAME' in env:
586+ maintainer = env['NAME']
587+ else:
588+ # Use password database if no data in environment variables
589+ try:
590+ maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos)
591+ except (KeyError, AttributeError):
592+ # TBD: Use last changelog entry value
593+ maintainer = "bzr-builder"
594+
595+ # Get maintainer's mail address
596+ if 'DEBEMAIL' in env:
597+ email = env['DEBEMAIL']
598+ elif 'EMAIL' in env:
599+ email = env['EMAIL']
600+ else:
601+ addr = None
602+ if os.path.exists('/etc/mailname'):
603+ f = open('/etc/mailname')
604+ try:
605+ addr = f.readline().strip()
606+ finally:
607+ f.close()
608+ if not addr:
609+ addr = socket.getfqdn()
610+ if addr:
611+ user = pwd.getpwuid(os.getuid()).pw_name
612+ if not user:
613+ addr = None
614+ else:
615+ addr = "%s@%s" % (user, addr)
616+
617+ if addr:
618+ email = addr
619+ else:
620+ # TBD: Use last changelog entry value
621+ email = "none@example.org"
622+
623+ return (maintainer, email)
624+
625+
626+def add_changelog_entry(base_branch, basedir, distribution=None,
627+ package=None, author_name=None, author_email=None,
628+ append_version=None):
629+ debian_dir = os.path.join(basedir, "debian")
630+ if not os.path.exists(debian_dir):
631+ os.makedirs(debian_dir)
632+ cl_path = os.path.join(debian_dir, "changelog")
633+ file_found = False
634+ if os.path.exists(cl_path):
635+ file_found = True
636+ cl_f = open(cl_path)
637+ try:
638+ contents = cl_f.read()
639+ finally:
640+ cl_f.close()
641+ cl = changelog.Changelog(file=contents)
642+ else:
643+ cl = changelog.Changelog()
644+ if len(cl._blocks) > 0:
645+ if distribution is None:
646+ distribution = cl._blocks[0].distributions.split()[0]
647+ if package is None:
648+ package = cl._blocks[0].package
649+ if DEBUPSTREAM_VAR in base_branch.deb_version:
650+ cl_version = cl._blocks[0].version
651+ base_branch.substitute_debupstream(cl_version)
652+ else:
653+ if file_found:
654+ if len(contents.strip()) > 0:
655+ reason = ("debian/changelog didn't contain any "
656+ "parseable stanzas")
657+ else:
658+ reason = "debian/changelog was empty"
659+ else:
660+ reason = "debian/changelog was not present"
661+ if package is None:
662+ raise errors.BzrCommandError("No previous changelog to "
663+ "take the package name from, and --package not "
664+ "specified: %s." % reason)
665+ if DEBUPSTREAM_VAR in base_branch.deb_version:
666+ raise errors.BzrCommandError("No previous changelog to "
667+ "take the upstream version from as %s was "
668+ "used: %s." % (DEBUPSTREAM_VAR, reason))
669+ if distribution is None:
670+ distribution = DEFAULT_UBUNTU_DISTRIBUTION
671+ # Use debian packaging environment variables
672+ # or default values if they don't exist
673+ if author_name is None or author_email is None:
674+ author_name, author_email = get_maintainer()
675+ author = "%s <%s>" % (author_name, author_email)
676+
677+ date = utils.formatdate(localtime=True)
678+ version = base_branch.deb_version
679+ if append_version is not None:
680+ version += append_version
681+ try:
682+ changelog.Version(version)
683+ except (changelog.VersionError, ValueError), e:
684+ raise errors.BzrCommandError("Invalid deb-version: %s: %s"
685+ % (version, e))
686+ cl.new_block(package=package, version=version,
687+ distributions=distribution, urgency="low",
688+ changes=['', ' * Auto build.', ''],
689+ author=author, date=date)
690+ cl_f = open(cl_path, 'wb')
691+ try:
692+ cl.write_to_open_file(cl_f)
693+ finally:
694+ cl_f.close()
695+
696+
697+def calculate_package_dir(base_branch, package_name, working_basedir):
698+ """Calculate the directory name that should be used while debuilding."""
699+ version = base_branch.deb_version
700+ if "-" in version:
701+ version = version[:version.rindex("-")]
702+ package_basedir = "%s-%s" % (package_name, version)
703+ package_dir = os.path.join(working_basedir, package_basedir)
704+ return package_dir
705+
706+
707+def _run_command(command, basedir, msg, error_msg,
708+ not_installed_msg=None):
709+ """ Run a command in a subprocess.
710+
711+ :param command: list with command and parameters
712+ :param msg: message to display to the user
713+ :param error_msg: message to display if something fails.
714+ :param not_installed_msg: the message to display if the command
715+ isn't available.
716+ """
717+ trace.note(msg)
718+ # Hide output if -q is in use.
719+ quiet = trace.is_quiet()
720+ if quiet:
721+ kwargs = {"stderr": subprocess.STDOUT, "stdout": subprocess.PIPE}
722+ else:
723+ kwargs = {}
724+ try:
725+ proc = subprocess.Popen(command, cwd=basedir,
726+ stdin=subprocess.PIPE, **kwargs)
727+ except OSError, e:
728+ if e.errno != errno.ENOENT:
729+ raise
730+ if not_installed_msg is None:
731+ raise
732+ raise MissingDependency(msg=not_installed_msg)
733+ output = proc.communicate()
734+ if proc.returncode != 0:
735+ if quiet:
736+ raise errors.BzrCommandError("%s: %s" % (error_msg, output))
737+ else:
738+ raise errors.BzrCommandError(error_msg)
739+
740+
741+def build_source_package(basedir):
742+ command = ["/usr/bin/debuild", "--no-tgz-check", "-i", "-I", "-S",
743+ "-uc", "-us"]
744+ _run_command(command, basedir,
745+ "Building the source package",
746+ "Failed to build the source package",
747+ not_installed_msg="debuild is not installed, please install "
748+ "the devscripts package.")
749+
750+
751+def sign_source_package(basedir, key_id):
752+ command = ["/usr/bin/debsign", "-S", "-k%s" % key_id]
753+ _run_command(command, basedir,
754+ "Signing the source package",
755+ "Signing the package failed",
756+ not_installed_msg="debsign is not installed, please install "
757+ "the devscripts package.")
758+
759+
760+def dput_source_package(basedir, target):
761+ command = ["/usr/bin/debrelease", "-S", "--dput", target]
762+ _run_command(command, basedir,
763+ "Uploading the source package",
764+ "Uploading the package failed",
765+ not_installed_msg="debrelease is not installed, please "
766+ "install the devscripts package.")
767+
768+
769+class cmd_build(Command):
770+ """Build a tree based on a 'recipe'.
771+
772+ Pass the name of the recipe file and the directory to work in.
773+
774+ See "bzr help builder" for more information on what a recipe is.
775+ """
776+ takes_args = ["recipe_file", "working_directory"]
777+ takes_options = [
778+ Option('manifest', type=str, argname="path",
779+ help="Path to write the manifest to."),
780+ Option('if-changed-from', type=str, argname="path",
781+ help="Only build if the outcome would be different "
782+ "to that specified in the specified manifest."),
783+ ]
784+
785+ def _get_prepared_branch_from_recipe(self, recipe_file,
786+ if_changed_from=None):
787+ """Common code to prepare a branch and do substitutions.
788+
789+ :param recipe_file: a path to a recipe file to work from.
790+ :param if_changed_from: an optional path to a manifest to
791+ compare the recipe against.
792+ :return: A tuple with (retcode, base_branch). If retcode is None
793+ then the command execution should continue.
794+ """
795+ base_branch = get_branch_from_recipe_file(recipe_file)
796+ time = datetime.datetime.utcnow()
797+ base_branch.substitute_time(time)
798+ old_recipe = None
799+ if if_changed_from is not None:
800+ old_recipe = get_old_recipe(if_changed_from)
801+ # Save the unsubstituted version for dailydeb.
802+ self._template_version = base_branch.deb_version
803+ changed = resolve_revisions(base_branch, if_changed_from=old_recipe)
804+ if not changed:
805+ trace.note("Unchanged")
806+ return 0, base_branch
807+ return None, base_branch
808+
809+ def run(self, recipe_file, working_directory, manifest=None,
810+ if_changed_from=None):
811+ result, base_branch = self._get_prepared_branch_from_recipe(recipe_file,
812+ if_changed_from=if_changed_from)
813+ if result is not None:
814+ return result
815+ manifest_path = manifest or os.path.join(working_directory,
816+ "bzr-builder.manifest")
817+ build_tree(base_branch, working_directory)
818+ write_manifest_to_path(manifest_path, base_branch)
819+
820+
821+class cmd_dailydeb(cmd_build):
822+ """Build a deb based on a 'recipe'.
823+
824+ See "bzr help builder" for more information on what a recipe is.
825+
826+ If you do not specify a working directory then a temporary
827+ directory will be used and it will be removed when the command
828+ finishes.
829+ """
830+
831+ takes_options = cmd_build.takes_options + [
832+ Option("package", type=str,
833+ help="The package name to use in the changelog entry. "
834+ "If not specified then the package from the "
835+ "previous changelog entry will be used, so it "
836+ "must be specified if there is no changelog."),
837+ Option("distribution", type=str,
838+ help="The distribution to target. If not specified "
839+ "then the same distribution as the last entry "
840+ "in debian/changelog will be used."),
841+ Option("dput", type=str, argname="target",
842+ help="dput the built package to the specified "
843+ "dput target."),
844+ Option("key-id", type=str, short_name="k",
845+ help="Sign the packages with the specified GnuPG key. "
846+ "Must be specified if you use --dput."),
847+ Option("no-build",
848+ help="Just ready the source package and don't "
849+ "actually build it."),
850+ Option("watch-ppa", help="Watch the PPA the package was "
851+ "dput to and exit with 0 only if it builds and "
852+ "publishes successfully."),
853+ Option("append-version", type=str, help="Append the "
854+ "specified string to the end of the version used "
855+ "in debian/changelog."),
856+ ]
857+
858+ takes_args = ["recipe_file", "working_basedir?"]
859+
860+ def run(self, recipe_file, working_basedir=None, manifest=None,
861+ if_changed_from=None, package=None, distribution=None,
862+ dput=None, key_id=None, no_build=None, watch_ppa=False,
863+ append_version=None):
864+
865+ if dput is not None and key_id is None:
866+ raise errors.BzrCommandError("You must specify --key-id if you "
867+ "specify --dput.")
868+ if watch_ppa:
869+ if not dput:
870+ raise errors.BzrCommandError(
871+ "cannot watch a ppa without doing dput.")
872+ else:
873+ # Check we can calculate a PPA url.
874+ target_from_dput(dput)
875+
876+ result, base_branch = self._get_prepared_branch_from_recipe(recipe_file,
877+ if_changed_from=if_changed_from)
878+ if result is not None:
879+ return result
880+ if working_basedir is None:
881+ temp_dir = tempfile.mkdtemp(prefix="bzr-builder-")
882+ working_basedir = temp_dir
883+ else:
884+ temp_dir = None
885+ if not os.path.exists(working_basedir):
886+ os.makedirs(working_basedir)
887+ package_name = self._calculate_package_name(recipe_file, package)
888+ working_directory = os.path.join(working_basedir,
889+ "%s-%s" % (package_name, self._template_version))
890+ try:
891+ # we want to use a consistent package_dir always to support
892+ # updates in place, but debuild etc want PACKAGE-UPSTREAMVERSION
893+ # on disk, so we build_tree with the unsubstituted version number
894+ # and do a final rename-to step before calling into debian build
895+ # tools. We then rename the working dir back.
896+ manifest_path = os.path.join(working_directory, "debian",
897+ "bzr-builder.manifest")
898+ build_tree(base_branch, working_directory)
899+ write_manifest_to_path(manifest_path, base_branch)
900+ # Add changelog also substitutes {debupstream}.
901+ add_changelog_entry(base_branch, working_directory,
902+ distribution=distribution, package=package,
903+ append_version=append_version)
904+ package_dir = calculate_package_dir(base_branch,
905+ package_name, working_basedir)
906+ # working_directory -> package_dir: after this debian stuff works.
907+ os.rename(working_directory, package_dir)
908+ if no_build:
909+ if manifest is not None:
910+ write_manifest_to_path(manifest, base_branch)
911+ return 0
912+ try:
913+ build_source_package(package_dir)
914+ if key_id is not None:
915+ sign_source_package(package_dir, key_id)
916+ if dput is not None:
917+ dput_source_package(package_dir, dput)
918+ finally:
919+ # package_dir -> working_directory
920+ # FIXME: may fail in error unwind, masking the original exception.
921+ os.rename(package_dir, working_directory)
922+ # Note that this may write a second manifest.
923+ if manifest is not None:
924+ write_manifest_to_path(manifest, base_branch)
925+ finally:
926+ if temp_dir is not None:
927+ shutil.rmtree(temp_dir)
928+ if watch_ppa:
929+ from bzrlib.plugins.builder.ppa import watch
930+ target = target_from_dput(dput)
931+ if not watch(target, self.package, base_branch.deb_version):
932+ return 2
933+
934+ def _calculate_package_name(self, recipe_file, package):
935+ """Calculate the directory name that should be used while debuilding."""
936+ recipe_name = os.path.basename(recipe_file)
937+ if recipe_name.endswith(".recipe"):
938+ recipe_name = recipe_name[:-len(".recipe")]
939+ return package or recipe_name
940+
941+
942+def target_from_dput(dput):
943+ """Convert a dput specification to a LP API specification.
944+
945+ :param dput: A dput command spec like ppa:team-name.
946+ :return: A LP API target like team-name/ppa.
947+ """
948+ ppa_prefix = 'ppa:'
949+ if not dput.startswith(ppa_prefix):
950+ raise errors.BzrCommandError('%r does not appear to be a PPA. '
951+ 'A dput target like \'%suser[/name]\' must be used.'
952+ % (dput, ppa_prefix))
953+ base, _, suffix = dput[len(ppa_prefix):].partition('/')
954+ if not suffix:
955+ suffix = 'ppa'
956+ return base + '/' + suffix
957+
958
959=== added file 'ppa.py'
960--- ppa.py 1970-01-01 00:00:00 +0000
961+++ ppa.py 2010-08-13 01:14:42 +0000
962@@ -0,0 +1,124 @@
963+# ppa support for bzr builder.
964+#
965+# Copyright: Canonical Ltd. (C) 2009
966+#
967+# This program is free software: you can redistribute it and/or modify it
968+# under the terms of the GNU General Public License version 3, as published
969+# by the Free Software Foundation.
970+
971+# This program is distributed in the hope that it will be useful, but
972+# WITHOUT ANY WARRANTY; without even the implied warranties of
973+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
974+# PURPOSE. See the GNU General Public License for more details.
975+
976+# You should have received a copy of the GNU General Public License along
977+# with this program. If not, see <http://www.gnu.org/licenses/>.
978+
979+import os
980+import time
981+
982+
983+from launchpadlib.launchpad import (
984+ Launchpad,
985+ EDGE_SERVICE_ROOT,
986+ )
987+from launchpadlib.credentials import Credentials
988+
989+from bzrlib import (
990+ errors,
991+ trace,
992+ )
993+
994+
995+def get_lp():
996+ credentials = Credentials()
997+ oauth_file = os.path.expanduser('~/.cache/launchpadlib/bzr-builder')
998+ if os.path.exists(oauth_file):
999+ f = open(oauth_file)
1000+ try:
1001+ credentials.load(f)
1002+ finally:
1003+ f.close()
1004+ launchpad = Launchpad(credentials, EDGE_SERVICE_ROOT)
1005+ else:
1006+ launchpad = Launchpad.get_token_and_login('bzr-builder',
1007+ EDGE_SERVICE_ROOT)
1008+ f = open(oauth_file, 'wb')
1009+ try:
1010+ launchpad.credentials.save(f)
1011+ finally:
1012+ f.close()
1013+ return launchpad
1014+
1015+
1016+def watch(owner_name, archive_name, package_name, version):
1017+ """Watch a package build.
1018+
1019+ :return: True once the package built and published, or False if it fails
1020+ or there is a timeout waiting.
1021+ """
1022+ version = str(version)
1023+ trace.note("Logging into Launchpad")
1024+
1025+ launchpad = get_lp()
1026+ owner = launchpad.people[owner_name]
1027+ archive = owner.getPPAByName(name=archive_name)
1028+ end_states = ['FAILEDTOBUILD', 'FULLYBUILT']
1029+ important_arches = ['amd64', 'i386', 'armel']
1030+ trace.note("Waiting for version %s of %s to build." % (version, package_name))
1031+ start = time.time()
1032+ while True:
1033+ sourceRecords = list(archive.getPublishedSources(
1034+ source_name=package_name, version=version))
1035+ if not sourceRecords:
1036+ if time.time() - 900 > start:
1037+ # Over 15 minutes and no source yet, upload FAIL.
1038+ raise errors.BzrCommandError("No source record in %s/%s for "
1039+ "package %s=%s after 15 minutes." % (owner_name,
1040+ archive_name, package_name, version))
1041+ return False
1042+ trace.note("Source not available yet - waiting.")
1043+ time.sleep(60)
1044+ continue
1045+ pkg = sourceRecords[0]
1046+ if pkg.status.lower() not in ('published', 'pending'):
1047+ trace.note("Package status: %s" % (pkg.status,))
1048+ time.sleep(60)
1049+ continue
1050+ # FIXME: LP should export this as an attribute.
1051+ source_id = pkg.self_link.rsplit('/', 1)[1]
1052+ buildSummaries = archive.getBuildSummariesForSourceIds(
1053+ source_ids=[source_id])[source_id]
1054+ if buildSummaries['status'] in end_states:
1055+ break
1056+ if buildSummaries['status'] == 'NEEDSBUILD':
1057+ # We ignore non-virtual PPA architectures that are sparsely
1058+ # supplied with buildds.
1059+ missing = []
1060+ for build in buildSummaries['builds']:
1061+ arch = build['arch_tag']
1062+ if arch in important_arches:
1063+ missing.append(arch)
1064+ if not missing:
1065+ break
1066+ extra = ' on ' + ', '.join(missing)
1067+ else:
1068+ extra = ''
1069+ trace.note("%s is still in %s%s" % (pkg.display_name,
1070+ buildSummaries['status'], extra))
1071+ time.sleep(60)
1072+ trace.note("%s is now %s" % (pkg.display_name, buildSummaries['status']))
1073+ result = True
1074+ if pkg.status.lower() != 'published':
1075+ result = False # should this perhaps keep waiting?
1076+ if buildSummaries['status'] != 'FULLYBUILT':
1077+ if buildSummaries['status'] == 'NEEDSBUILD':
1078+ # We're stopping early cause the important_arches are built.
1079+ builds = pkg.getBuilds()
1080+ for build in builds:
1081+ if build.arch_tag in important_arches:
1082+ if build.buildstate != 'Successfully built':
1083+ result = False
1084+ else:
1085+ result = False
1086+ return result
1087
1088=== modified file 'recipe.py'
1089--- recipe.py 2010-08-13 01:14:42 +0000
1090+++ recipe.py 2010-08-13 01:14:42 +0000
1091@@ -22,22 +22,53 @@
1092 bzrdir,
1093 errors,
1094 merge,
1095+ revision,
1096 revisionspec,
1097 tag,
1098+ trace,
1099 transport,
1100- ui,
1101 urlutils,
1102 )
1103
1104
1105+try:
1106+ from debian import changelog
1107+except ImportError:
1108+ from debian_bundle import changelog
1109+
1110+
1111 def subprocess_setup():
1112 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
1113
1114
1115 MERGE_INSTRUCTION = "merge"
1116+NEST_PART_INSTRUCTION = "nest-part"
1117 NEST_INSTRUCTION = "nest"
1118 RUN_INSTRUCTION = "run"
1119
1120+TIME_VAR = "{time}"
1121+DATE_VAR = "{date}"
1122+REVNO_VAR = "{revno}"
1123+REVNO_PARAM_VAR = "{revno:%s}"
1124+DEBUPSTREAM_VAR = "{debupstream}"
1125+
1126+ok_to_preserve = [DEBUPSTREAM_VAR]
1127+# The variables that don't require substitution in their name
1128+simple_vars = [TIME_VAR, DATE_VAR, REVNO_VAR, DEBUPSTREAM_VAR]
1129+
1130+
1131+def check_expanded_deb_version(base_branch):
1132+ checked_version = base_branch.deb_version
1133+ for token in ok_to_preserve:
1134+ checked_version = checked_version.replace(token, "")
1135+ if "{" in checked_version:
1136+ available_tokens = simple_vars
1137+ for name in base_branch.list_branch_names():
1138+ available_tokens.append(REVNO_PARAM_VAR % name)
1139+ raise errors.BzrCommandError("deb-version not fully "
1140+ "expanded: %s. Valid substitutions are: %s"
1141+ % (base_branch.deb_version, available_tokens))
1142+
1143
1144 class CommandFailedError(errors.BzrError):
1145
1146@@ -113,10 +144,10 @@
1147 # We do a "pull"
1148 if tree_to is not None:
1149 # FIXME: should these pulls overwrite?
1150- result = tree_to.pull(br_from, stop_revision=revision_id,
1151+ tree_to.pull(br_from, stop_revision=revision_id,
1152 possible_transports=possible_transports)
1153 else:
1154- result = br_to.pull(br_from, stop_revision=revision_id,
1155+ br_to.pull(br_from, stop_revision=revision_id,
1156 possible_transports=possible_transports)
1157 tree_to = br_to.bzrdir.create_workingtree()
1158 # Ugh, we have to assume that the caller replaces their reference
1159@@ -146,7 +177,7 @@
1160 return tree_to, br_to
1161
1162
1163-def merge_branch(child_branch, tree_to, br_to):
1164+def merge_branch(child_branch, tree_to, br_to, possible_transports=None):
1165 """Merge the branch specified by child_branch.
1166
1167 :param child_branch: the RecipeBranch to retrieve the branch and revision to
1168@@ -154,82 +185,129 @@
1169 :param tree_to: the WorkingTree to merge in to.
1170 :param br_to: the Branch to merge in to.
1171 """
1172+ if child_branch.branch is None:
1173+ child_branch.branch = branch.Branch.open(child_branch.url,
1174+ possible_transports=possible_transports)
1175+ child_branch.branch.lock_read()
1176+ try:
1177+ tag._merge_tags_if_possible(child_branch.branch, br_to)
1178+ if child_branch.revspec is not None:
1179+ merge_revspec = revisionspec.RevisionSpec.from_string(
1180+ child_branch.revspec)
1181+ try:
1182+ merge_revid = merge_revspec.as_revision_id(
1183+ child_branch.branch)
1184+ except errors.InvalidRevisionSpec, e:
1185+ # Give the user a hint if they didn't mean to speciy
1186+ # a revspec.
1187+ e.extra = (". Did you not mean to specify a revspec "
1188+ "at the end of the merge line?")
1189+ raise e
1190+ else:
1191+ merge_revid = child_branch.branch.last_revision()
1192+ child_branch.revid = merge_revid
1193+ merger = merge.Merger.from_revision_ids(None, tree_to, merge_revid,
1194+ other_branch=child_branch.branch, tree_branch=br_to)
1195+ merger.merge_type = merge.Merge3Merger
1196+ if (merger.base_rev_id == merger.other_rev_id and
1197+ merger.other_rev_id is not None):
1198+ # Nothing to do.
1199+ return
1200+ conflict_count = merger.do_merge()
1201+ merger.set_pending()
1202+ if conflict_count:
1203+ # FIXME: better reporting
1204+ raise errors.BzrCommandError("Conflicts from merge")
1205+ tree_to.commit("Merge %s" %
1206+ urlutils.unescape_for_display(
1207+ child_branch.url, 'utf-8'))
1208+ finally:
1209+ child_branch.branch.unlock()
1210+
1211+
1212+def nest_part_branch(op, child_branch, tree_to, br_to, subpath,
1213+ target_subdir=None):
1214+ """Merge the branch subdirectory specified by child_branch.
1215+
1216+ :param child_branch: the RecipeBranch to retrieve the branch and revision to
1217+ merge from.
1218+ :param tree_to: the WorkingTree to merge in to.
1219+ :param br_to: the Branch to merge in to.
1220+ :param subpath: only merge files from branch that are from this path.
1221+ e.g. subpath='/debian' will only merge changes from that directory.
1222+ :param target_subdir: (optional) directory in target to merge that
1223+ subpath into. Defaults to basename of subpath.
1224+ """
1225 merge_from = branch.Branch.open(child_branch.url)
1226 merge_from.lock_read()
1227- try:
1228- pb = ui.ui_factory.nested_progress_bar()
1229- try:
1230- tag._merge_tags_if_possible(merge_from, br_to)
1231- if child_branch.revspec is not None:
1232- merge_revspec = revisionspec.RevisionSpec.from_string(
1233- child_branch.revspec)
1234- merge_revid = merge_revspec.as_revision_id(merge_from)
1235- else:
1236- merge_revid = merge_from.last_revision()
1237- child_branch.revid = merge_revid
1238- merger = merge.Merger.from_revision_ids(pb, tree_to, merge_revid,
1239- other_branch=merge_from, tree_branch=br_to)
1240- merger.merge_type = merge.Merge3Merger
1241- if (merger.base_rev_id == merger.other_rev_id and
1242- merger.other_rev_id is not None):
1243- # Nothing to do.
1244- return
1245- conflict_count = merger.do_merge()
1246- merger.set_pending()
1247- if conflict_count:
1248- # FIXME: better reporting
1249- raise errors.BzrCommandError("Conflicts from merge")
1250- tree_to.commit("Merge %s" %
1251- urlutils.unescape_for_display(
1252- child_branch.url, 'utf-8'))
1253- finally:
1254- pb.finished()
1255- finally:
1256- merge_from.unlock()
1257-
1258-
1259-def update_branch(base_branch, tree_to, br_to, to_transport):
1260- from_location = base_branch.url
1261- accelerator_tree, br_from = bzrdir.BzrDir.open_tree_or_branch(
1262- from_location)
1263- br_from.lock_read()
1264+ op.add_cleanup(merge_from.unlock)
1265+ if child_branch.revspec is not None:
1266+ merge_revspec = revisionspec.RevisionSpec.from_string(
1267+ child_branch.revspec)
1268+ merge_revid = merge_revspec.as_revision_id(merge_from)
1269+ else:
1270+ merge_revid = merge_from.last_revision()
1271+ child_branch.revid = merge_revid
1272+ other_tree = merge_from.basis_tree()
1273+ other_tree.lock_read()
1274+ op.add_cleanup(other_tree.unlock)
1275+ if target_subdir is None:
1276+ target_subdir = os.path.basename(subpath)
1277+ merger = merge.MergeIntoMerger(this_tree=tree_to, other_tree=other_tree,
1278+ other_branch=merge_from, target_subdir=target_subdir,
1279+ source_subpath=subpath, other_rev_id=merge_revid)
1280+ merger.set_base_revision(revision.NULL_REVISION, merge_from)
1281+ conflict_count = merger.do_merge()
1282+ merger.set_pending()
1283+ if conflict_count:
1284+ # FIXME: better reporting
1285+ raise errors.BzrCommandError("Conflicts from merge")
1286+ tree_to.commit("Merge %s of %s" %
1287+ (subpath, urlutils.unescape_for_display(child_branch.url, 'utf-8')))
1288+
1289+
1290+def update_branch(base_branch, tree_to, br_to, to_transport,
1291+ possible_transports=None):
1292+ if base_branch.branch is None:
1293+ base_branch.branch = branch.Branch.open(base_branch.url,
1294+ possible_transports=possible_transports)
1295+ base_branch.branch.lock_read()
1296 try:
1297 if base_branch.revspec is not None:
1298 revspec = revisionspec.RevisionSpec.from_string(
1299 base_branch.revspec)
1300- revision_id = revspec.as_revision_id(br_from)
1301+ revision_id = revspec.as_revision_id(base_branch.branch)
1302 else:
1303- revision_id = br_from.last_revision()
1304+ revision_id = base_branch.branch.last_revision()
1305 base_branch.revid = revision_id
1306- tree_to, br_to = pull_or_branch(tree_to, br_to, br_from,
1307+ tree_to, br_to = pull_or_branch(tree_to, br_to, base_branch.branch,
1308 to_transport, revision_id,
1309- accelerator_tree=accelerator_tree,
1310- possible_transports=[to_transport])
1311+ possible_transports=possible_transports)
1312 finally:
1313- br_from.unlock()
1314+ base_branch.branch.unlock()
1315 return tree_to, br_to
1316
1317
1318 def _resolve_revisions_recurse(new_branch, substitute_revno,
1319 if_changed_from=None):
1320 changed = False
1321- br_from = branch.Branch.open(new_branch.url)
1322- br_from.lock_read()
1323+ new_branch.branch = branch.Branch.open(new_branch.url)
1324+ new_branch.branch.lock_read()
1325 try:
1326 if new_branch.revspec is not None:
1327 revspec = revisionspec.RevisionSpec.from_string(
1328 new_branch.revspec)
1329- revision_id = revspec.as_revision_id(br_from)
1330+ revision_id = revspec.as_revision_id(new_branch.branch)
1331 else:
1332- revision_id = br_from.last_revision()
1333+ revision_id = new_branch.branch.last_revision()
1334 new_branch.revid = revision_id
1335 def get_revno():
1336 try:
1337- revno = br_from.revision_id_to_revno(revision_id)
1338+ revno = new_branch.branch.revision_id_to_revno(revision_id)
1339 return str(revno)
1340 except errors.NoSuchRevision:
1341 # We need to load and use the full revno map after all
1342- result = br_from.get_revision_id_to_revno_map().get(
1343+ result = new_branch.branch.get_revision_id_to_revno_map().get(
1344 revision_id)
1345 if result is None:
1346 return result
1347@@ -242,14 +320,13 @@
1348 changed_revspec = revisionspec.RevisionSpec.from_string(
1349 if_changed_from.revspec)
1350 changed_revision_id = changed_revspec.as_revision_id(
1351- br_from)
1352+ new_branch.branch)
1353 else:
1354- changed_revision_id = br_from.last_revision()
1355+ changed_revision_id = new_branch.branch.last_revision()
1356 if revision_id != changed_revision_id:
1357 changed = True
1358 for index, instruction in enumerate(new_branch.child_branches):
1359 child_branch = instruction.recipe_branch
1360- nest_location = instruction.nest_path
1361 if_changed_child = None
1362 if if_changed_from is not None:
1363 if_changed_child = if_changed_from.child_branches[index].recipe_branch
1364@@ -261,7 +338,7 @@
1365 changed = child_changed
1366 return changed
1367 finally:
1368- br_from.unlock()
1369+ new_branch.branch.unlock()
1370
1371
1372 def resolve_revisions(base_branch, if_changed_from=None):
1373@@ -289,30 +366,20 @@
1374 if_changed_from=if_changed_from_revisions)
1375 if not changed:
1376 changed = changed_revisions
1377- ok_to_preserve = ["{debupstream}"]
1378- checked_version = base_branch.deb_version
1379- for token in ok_to_preserve:
1380- checked_version = checked_version.replace(token, "")
1381- if "{" in checked_version:
1382- raise errors.BzrCommandError("deb-version not fully "
1383- "expanded: %s" % base_branch.deb_version)
1384+ check_expanded_deb_version(base_branch)
1385 if if_changed_from is not None and not changed:
1386 return False
1387 return True
1388
1389
1390-def build_tree(base_branch, target_path):
1391- """Build the RecipeBranch at a path.
1392-
1393- Follow the instructions embodied in RecipeBranch and build a tree
1394- based on them rooted at target_path. If target_path exists and
1395- is the root of the branch then the branch will be updated based on
1396- what the RecipeBranch requires.
1397-
1398- :param base_branch: a RecipeBranch to build.
1399- :param target_path: the path to the base of the desired output.
1400- """
1401- to_transport = transport.get_transport(target_path)
1402+def _build_inner_tree(base_branch, target_path, possible_transports=None):
1403+ revision_of = ""
1404+ if base_branch.revspec is not None:
1405+ revision_of = "revision '%s' of " % base_branch.revspec
1406+ trace.note("Retrieving %s'%s' to put at '%s'."
1407+ % (revision_of, base_branch.url, target_path))
1408+ to_transport = transport.get_transport(target_path,
1409+ possible_transports=possible_transports)
1410 try:
1411 tree_to, br_to = bzrdir.BzrDir.open_tree_or_branch(target_path)
1412 # Should we commit any changes in the tree here? If we don't
1413@@ -327,7 +394,7 @@
1414 br_to.lock_write()
1415 try:
1416 tree_to, br_to = update_branch(base_branch, tree_to, br_to,
1417- to_transport)
1418+ to_transport, possible_transports=possible_transports)
1419 for instruction in base_branch.child_branches:
1420 instruction.apply(target_path, tree_to, br_to)
1421 finally:
1422@@ -339,49 +406,25 @@
1423 tree_to.unlock()
1424
1425
1426-def _add_child_branches_to_manifest(child_branches, indent_level):
1427- manifest = ""
1428- for instruction in child_branches:
1429- child_branch = instruction.recipe_branch
1430- nest_location = instruction.nest_path
1431- if child_branch is None:
1432- manifest += "%s%s %s\n" % (" " * indent_level, RUN_INSTRUCTION,
1433- nest_location)
1434- else:
1435- assert child_branch.revid is not None, "Branch hasn't been built"
1436- if nest_location is not None:
1437- manifest += "%s%s %s %s %s revid:%s\n" % \
1438- (" " * indent_level, NEST_INSTRUCTION,
1439- child_branch.name,
1440- child_branch.url, nest_location,
1441- child_branch.revid)
1442- manifest += _add_child_branches_to_manifest(
1443- child_branch.child_branches, indent_level+1)
1444- else:
1445- manifest += "%s%s %s %s revid:%s\n" % \
1446- (" " * indent_level, MERGE_INSTRUCTION,
1447- child_branch.name,
1448- child_branch.url, child_branch.revid)
1449- return manifest
1450-
1451-
1452-def build_manifest(base_branch):
1453- manifest = "# bzr-builder format %s deb-version " % str(base_branch.format)
1454- # TODO: should we store the expanded version that was used?
1455- manifest += "%s\n" % (base_branch.deb_version,)
1456- assert base_branch.revid is not None, "Branch hasn't been built"
1457- manifest += "%s revid:%s\n" % (base_branch.url, base_branch.revid)
1458- manifest += _add_child_branches_to_manifest(base_branch.child_branches, 0)
1459- # Sanity check.
1460- # TODO: write a function that compares the result of this parse with
1461- # the branch that we built it from.
1462- RecipeParser(manifest).parse()
1463- return manifest
1464+def build_tree(base_branch, target_path, possible_transports=None):
1465+ """Build the RecipeBranch at a path.
1466+
1467+ Follow the instructions embodied in RecipeBranch and build a tree
1468+ based on them rooted at target_path. If target_path exists and
1469+ is the root of the branch then the branch will be updated based on
1470+ what the RecipeBranch requires.
1471+
1472+ :param base_branch: a RecipeBranch to build.
1473+ :param target_path: the path to the base of the desired output.
1474+ """
1475+ trace.note("Building tree.")
1476+ _build_inner_tree(base_branch, target_path,
1477+ possible_transports=possible_transports)
1478
1479
1480 class ChildBranch(object):
1481 """A child branch in a recipe.
1482-
1483+
1484 If the nest path is not None it is the path relative to the recipe branch
1485 where the child branch should be placed. If it is None then the child
1486 branch should be merged instead of nested.
1487@@ -390,8 +433,8 @@
1488 def __init__(self, recipe_branch, nest_path=None):
1489 self.recipe_branch = recipe_branch
1490 self.nest_path = nest_path
1491-
1492- def apply(self, target_path, tree_to, br_to):
1493+
1494+ def apply(self, target_path, tree_to, br_to, possible_transports=None):
1495 raise NotImplementedError(self.apply)
1496
1497 def as_tuple(self):
1498@@ -400,8 +443,9 @@
1499
1500 class CommandInstruction(ChildBranch):
1501
1502- def apply(self, target_path, tree_to, br_to):
1503+ def apply(self, target_path, tree_to, br_to, possible_transports=None):
1504 # it's a command
1505+ trace.note("Running '%s' in '%s'." % (self.nest_path, target_path))
1506 proc = subprocess.Popen(self.nest_path, cwd=target_path,
1507 preexec_fn=subprocess_setup, shell=True, stdin=subprocess.PIPE)
1508 proc.communicate()
1509@@ -411,16 +455,36 @@
1510
1511 class MergeInstruction(ChildBranch):
1512
1513+ def apply(self, target_path, tree_to, br_to, possible_transports=None):
1514+ revision_of = ""
1515+ if self.recipe_branch.revspec is not None:
1516+ revision_of = "revision '%s' of " % self.recipe_branch.revspec
1517+ trace.note("Merging %s'%s' in to '%s'."
1518+ % (revision_of, self.recipe_branch.url, target_path))
1519+ merge_branch(self.recipe_branch, tree_to, br_to,
1520+ possible_transports=possible_transports)
1521+
1522+
1523+class NestPartInstruction(ChildBranch):
1524+
1525+ def __init__(self, recipe_branch, subpath, target_subdir):
1526+ ChildBranch.__init__(self, recipe_branch)
1527+ self.subpath = subpath
1528+ self.target_subdir = target_subdir
1529+
1530 def apply(self, target_path, tree_to, br_to):
1531- merge_branch(self.recipe_branch, tree_to, br_to)
1532+ from bzrlib.cleanup import OperationWithCleanups
1533+ op = OperationWithCleanups(nest_part_branch)
1534+ op.run(self.recipe_branch, tree_to, br_to, self.subpath,
1535+ self.target_subdir)
1536
1537
1538 class NestInstruction(ChildBranch):
1539
1540- def apply(self, target_path, tree_to, br_to):
1541- # FIXME: pass possible_transports around
1542- build_tree(self.recipe_branch,
1543- target_path=os.path.join(target_path, self.nest_path))
1544+ def apply(self, target_path, tree_to, br_to, possible_transports=None):
1545+ _build_inner_tree(self.recipe_branch,
1546+ target_path=os.path.join(target_path, self.nest_path),
1547+ possible_transports=possible_transports)
1548
1549
1550 class RecipeBranch(object):
1551@@ -430,9 +494,12 @@
1552 root branch), and optionally child branches that are either merged
1553 or nested.
1554
1555- The child_branches attribute is a list of tuples of ChildBranch objects.
1556+ The child_branches attribute is a list of tuples of ChildBranch objects.
1557 The revid attribute records the revid that the url and revspec resolved
1558 to when the RecipeBranch was built, or None if it has not been built.
1559+
1560+ :ivar revid: after this recipe branch has been built this is set to the
1561+ revision ID that was merged/nested from the branch at self.url.
1562 """
1563
1564 def __init__(self, name, url, revspec=None):
1565@@ -448,6 +515,7 @@
1566 self.revspec = revspec
1567 self.child_branches = []
1568 self.revid = None
1569+ self.branch = None
1570
1571 def merge_branch(self, branch):
1572 """Merge a child branch in to this one.
1573@@ -456,6 +524,18 @@
1574 """
1575 self.child_branches.append(MergeInstruction(branch))
1576
1577+ def nest_part_branch(self, branch, subpath=None, target_subdir=None):
1578+ """Merge subdir of a child branch into this one.
1579+
1580+ :param branch: the RecipeBranch to merge.
1581+ :param subpath: only merge files from branch that are from this path.
1582+ e.g. subpath='/debian' will only merge changes from that directory.
1583+ :param target_subdir: (optional) directory in target to merge that
1584+ subpath into. Defaults to basename of subpath.
1585+ """
1586+ self.child_branches.append(
1587+ NestPartInstruction(branch, subpath, target_subdir))
1588+
1589 def nest_branch(self, location, branch):
1590 """Nest a child branch in to this one.
1591
1592@@ -492,10 +572,32 @@
1593 if ((child_branch is None and other_child_branch is not None)
1594 or (child_branch is not None and other_child_branch is None)):
1595 return True
1596- if child_branch.different_shape_to(other_child_branch):
1597+ # if child_branch is None then other_child_branch must be
1598+ # None too, meaning that they are both run instructions,
1599+ # we would compare their nest locations (commands), but
1600+ # that has already been done, so just guard
1601+ if (child_branch is not None
1602+ and child_branch.different_shape_to(other_child_branch)):
1603 return True
1604 return False
1605
1606+ def _list_child_names(self):
1607+ child_names = []
1608+ for instruction in self.child_branches:
1609+ child_branch = instruction.recipe_branch
1610+ if child_branch is None:
1611+ continue
1612+ child_names += child_branch.list_branch_names()
1613+ return child_names
1614+
1615+ def list_branch_names(self):
1616+ """List all of the branch names under this one.
1617+
1618+ :return: a list of the branch names.
1619+ :rtype: list(str)
1620+ """
1621+ return [self.name] + self._list_child_names()
1622+
1623
1624 class BaseRecipeBranch(RecipeBranch):
1625 """The RecipeBranch that is at the root of a recipe."""
1626@@ -521,9 +623,9 @@
1627 needed.
1628 """
1629 if branch_name is None:
1630- subst_string = "{revno}"
1631+ subst_string = REVNO_VAR
1632 else:
1633- subst_string = "{revno:%s}" % branch_name
1634+ subst_string = REVNO_PARAM_VAR % branch_name
1635 if subst_string in self.deb_version:
1636 revno = get_revno_cb()
1637 if revno is None:
1638@@ -537,9 +639,75 @@
1639
1640 :param time: a datetime.datetime with the desired time.
1641 """
1642- if "{time}" in self.deb_version:
1643- self.deb_version = self.deb_version.replace("{time}",
1644+ if TIME_VAR in self.deb_version:
1645+ self.deb_version = self.deb_version.replace(TIME_VAR,
1646 time.strftime("%Y%m%d%H%M"))
1647+ if DATE_VAR in self.deb_version:
1648+ self.deb_version = self.deb_version.replace(DATE_VAR,
1649+ time.strftime("%Y%m%d"))
1650+
1651+ def substitute_debupstream(self, version):
1652+ """Substitute {debupstream} in to deb_version if needed.
1653+
1654+ :param version: the Version object to take the upstream version
1655+ from.
1656+ """
1657+ if DEBUPSTREAM_VAR in self.deb_version:
1658+ # Should we include the epoch?
1659+ self.deb_version = self.deb_version.replace(DEBUPSTREAM_VAR,
1660+ version.upstream_version)
1661+
1662+ def _add_child_branches_to_manifest(self, child_branches, indent_level):
1663+ manifest = ""
1664+ for instruction in child_branches:
1665+ child_branch = instruction.recipe_branch
1666+ nest_location = instruction.nest_path
1667+ if child_branch is None:
1668+ manifest += "%s%s %s\n" % (" " * indent_level, RUN_INSTRUCTION,
1669+ nest_location)
1670+ else:
1671+ if child_branch.revid is not None:
1672+ revid_part = " revid:%s" % child_branch.revid
1673+ elif child_branch.revspec is not None:
1674+ revid_part = " %s" % child_branch.revspec
1675+ else:
1676+ revid_part = ""
1677+ if nest_location is not None:
1678+ manifest += "%s%s %s %s %s%s\n" % \
1679+ (" " * indent_level, NEST_INSTRUCTION,
1680+ child_branch.name,
1681+ child_branch.url, nest_location,
1682+ revid_part)
1683+ manifest += self._add_child_branches_to_manifest(
1684+ child_branch.child_branches, indent_level+1)
1685+ else:
1686+ manifest += "%s%s %s %s%s\n" % \
1687+ (" " * indent_level, MERGE_INSTRUCTION,
1688+ child_branch.name,
1689+ child_branch.url, revid_part)
1690+ return manifest
1691+
1692+
1693+ def __str__(self):
1694+ manifest = "# bzr-builder format %s deb-version " % str(self.format)
1695+ # TODO: should we store the expanded version that was used?
1696+ manifest += "%s\n" % (self.deb_version,)
1697+ if self.revid is not None:
1698+ manifest += "%s revid:%s\n" % (self.url, self.revid)
1699+ elif self.revspec is not None:
1700+ manifest += "%s %s\n" % (self.url, self.revspec)
1701+ else:
1702+ manifest += "%s\n" % (self.url,)
1703+ manifest += self._add_child_branches_to_manifest(self.child_branches,
1704+ 0)
1705+ # Sanity check.
1706+ # TODO: write a function that compares the result of this parse with
1707+ # the branch that we built it from.
1708+ RecipeParser(manifest).parse()
1709+ return manifest
1710+
1711+ def list_branch_names(self):
1712+ return self._list_child_names()
1713
1714
1715 class RecipeParseError(errors.BzrError):
1716@@ -550,6 +718,13 @@
1717 problem=problem)
1718
1719
1720+class ForbiddenInstructionError(RecipeParseError):
1721+
1722+ def __init__(self, filename, line, char, problem, instruction_name=None):
1723+ RecipeParseError.__init__(self, filename, line, char, problem)
1724+ self.instruction_name = instruction_name
1725+
1726+
1727 class RecipeParser(object):
1728 """Parse a recipe.
1729
1730@@ -560,7 +735,7 @@
1731 eol_char = "\n"
1732 digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
1733
1734- NEWEST_VERSION = 0.2
1735+ NEWEST_VERSION = 0.3
1736
1737 def __init__(self, f, filename=None):
1738 """Create a RecipeParser.
1739@@ -578,9 +753,12 @@
1740 if filename is None:
1741 self.filename = "recipe"
1742
1743- def parse(self):
1744+ def parse(self, forbidden_instructions=None):
1745 """Parse the recipe.
1746
1747+ :param forbidden_instructions: a list of instructions that you
1748+ don't want to allow. Defaults to None allowing them all.
1749+ :type forbidden_instructions: list(str) or None
1750 :return: a RecipeBranch representing the recipe.
1751 """
1752 self.lines = self.text.split("\n")
1753@@ -588,12 +766,21 @@
1754 self.line_index = 0
1755 self.current_line = self.lines[self.line_index]
1756 self.current_indent_level = 0
1757+ self.seen_nicks = set()
1758+ self.seen_paths = {".": 1}
1759 (version, deb_version) = self.parse_header()
1760 self.version = version
1761 last_instruction = None
1762 active_branches = []
1763 last_branch = None
1764 while self.line_index < len(self.lines):
1765+ if self.is_blankline():
1766+ self.new_line()
1767+ continue
1768+ comment = self.parse_comment_line()
1769+ if comment is not None:
1770+ self.new_line()
1771+ continue
1772 old_indent_level = self.parse_indent()
1773 if old_indent_level is not None:
1774 if (old_indent_level < self.current_indent_level
1775@@ -605,10 +792,6 @@
1776 else:
1777 unindent = self.current_indent_level - old_indent_level
1778 active_branches = active_branches[:unindent]
1779- comment = self.parse_comment_line()
1780- if comment is not None:
1781- self.new_line()
1782- continue
1783 if last_instruction is None:
1784 url = self.take_to_whitespace("branch to start from")
1785 revspec = self.parse_optional_revspec()
1786@@ -618,7 +801,8 @@
1787 active_branches = [last_branch]
1788 last_instruction = ""
1789 else:
1790- instruction = self.parse_instruction()
1791+ instruction = self.parse_instruction(
1792+ forbidden_instructions=forbidden_instructions)
1793 if instruction == RUN_INSTRUCTION:
1794 self.parse_whitespace("the command")
1795 command = self.take_to_newline().strip()
1796@@ -629,13 +813,25 @@
1797 url = self.parse_branch_url()
1798 if instruction == NEST_INSTRUCTION:
1799 location = self.parse_branch_location()
1800- revspec = self.parse_optional_revspec()
1801+ if instruction == NEST_PART_INSTRUCTION:
1802+ path = self.parse_subpath()
1803+ target_subdir = self.parse_optional_path()
1804+ if target_subdir == '':
1805+ target_subdir = None
1806+ revspec = None
1807+ else:
1808+ revspec = self.parse_optional_revspec()
1809+ else:
1810+ revspec = self.parse_optional_revspec()
1811 self.new_line()
1812 last_branch = RecipeBranch(branch_id, url, revspec=revspec)
1813 if instruction == NEST_INSTRUCTION:
1814 active_branches[-1].nest_branch(location, last_branch)
1815- else:
1816+ elif instruction == MERGE_INSTRUCTION:
1817 active_branches[-1].merge_branch(last_branch)
1818+ elif instruction == NEST_PART_INSTRUCTION:
1819+ active_branches[-1].nest_part_branch(
1820+ last_branch, path, target_subdir)
1821 last_instruction = instruction
1822 if len(active_branches) == 0:
1823 self.throw_parse_error("Empty recipe")
1824@@ -655,18 +851,28 @@
1825 self.new_line()
1826 return version, deb_version
1827
1828- def parse_instruction(self):
1829+ def parse_instruction(self, forbidden_instructions=None):
1830 if self.version < 0.2:
1831 options = (MERGE_INSTRUCTION, NEST_INSTRUCTION)
1832 options_str = "'%s' or '%s'" % options
1833- else:
1834+ elif self.version < 0.3:
1835 options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, RUN_INSTRUCTION)
1836 options_str = "'%s', '%s' or '%s'" % options
1837+ else:
1838+ options = (MERGE_INSTRUCTION, NEST_INSTRUCTION,
1839+ NEST_PART_INSTRUCTION, RUN_INSTRUCTION)
1840+ options_str = "'%s', '%s', '%s' or '%s'" % options
1841 instruction = self.peek_to_whitespace()
1842 if instruction is None:
1843 self.throw_parse_error("End of line while looking for %s"
1844 % options_str)
1845 if instruction in options:
1846+ if forbidden_instructions is not None:
1847+ if instruction in forbidden_instructions:
1848+ self.throw_parse_error("The '%s' instruction is "
1849+ "forbidden." % instruction,
1850+ cls=ForbiddenInstructionError,
1851+ instruction_name=instruction)
1852 self.take_chars(len(instruction))
1853 return instruction
1854 self.throw_parse_error("Expecting %s, got '%s'"
1855@@ -674,7 +880,15 @@
1856
1857 def parse_branch_id(self):
1858 self.parse_whitespace("the branch id")
1859- branch_id = self.take_to_whitespace("the branch id")
1860+ branch_id = self.peek_to_whitespace()
1861+ if branch_id is None:
1862+ self.throw_parse_error("End of line while looking for the "
1863+ "branch id")
1864+ if branch_id in self.seen_nicks:
1865+ self.throw_parse_error("'%s' was already used to identify "
1866+ "a branch." % branch_id)
1867+ self.take_chars(len(branch_id))
1868+ self.seen_nicks.add(branch_id)
1869 return branch_id
1870
1871 def parse_branch_url(self):
1872@@ -685,8 +899,34 @@
1873 def parse_branch_location(self):
1874 # FIXME: Needs a better term
1875 self.parse_whitespace("the location to nest")
1876- location = self.take_to_whitespace("the location to nest")
1877- return location
1878+ location = self.peek_to_whitespace()
1879+ if location is None:
1880+ self.throw_parse_error("End of line while looking for the "
1881+ "location to nest")
1882+ norm_location = os.path.normpath(location)
1883+ if norm_location in self.seen_paths:
1884+ self.throw_parse_error("The path '%s' is a duplicate of "
1885+ "the one used on line %d." % (location,
1886+ self.seen_paths[norm_location]))
1887+ if os.path.isabs(norm_location):
1888+ self.throw_parse_error("Absolute paths are not allowed: %s"
1889+ % location)
1890+ if norm_location.startswith(".."):
1891+ self.throw_parse_error("Paths outside the current directory "
1892+ "are not allowed: %s" % location)
1893+ self.take_chars(len(location))
1894+ self.seen_paths[norm_location] = self.line_index + 1
1895+ return location
1896+
1897+ def parse_subpath(self):
1898+ self.parse_whitespace("the subpath to merge")
1899+ location = self.take_to_whitespace("the subpath to merge")
1900+ return location
1901+
1902+ def parse_revspec(self):
1903+ self.parse_whitespace("the revspec")
1904+ revspec = self.take_to_whitespace("the revspec")
1905+ return revspec
1906
1907 def parse_optional_revspec(self):
1908 self.parse_whitespace(None, require=False)
1909@@ -695,9 +935,18 @@
1910 self.take_chars(len(revspec))
1911 return revspec
1912
1913- def throw_parse_error(self, problem):
1914- raise RecipeParseError(self.filename, self.line_index + 1,
1915- self.index + 1, problem)
1916+ def parse_optional_path(self):
1917+ self.parse_whitespace(None, require=False)
1918+ path = self.peek_to_whitespace()
1919+ if path is not None:
1920+ self.take_chars(len(path))
1921+ return path
1922+
1923+ def throw_parse_error(self, problem, cls=None, **kwargs):
1924+ if cls is None:
1925+ cls = RecipeParseError
1926+ raise cls(self.filename, self.line_index + 1,
1927+ self.index + 1, problem, **kwargs)
1928
1929 def throw_expecting_error(self, expected, actual):
1930 self.throw_parse_error("Expecting '%s', got '%s'"
1931@@ -720,6 +969,12 @@
1932 else:
1933 self.current_line = self.lines[self.line_index]
1934
1935+ def is_blankline(self):
1936+ whitespace = self.peek_whitespace()
1937+ if whitespace is None:
1938+ return True
1939+ return self.peek_char(skip=len(whitespace)) is None
1940+
1941 def take_char(self):
1942 if self.index >= len(self.current_line):
1943 return None
1944@@ -789,6 +1044,18 @@
1945 actual = self.peek_char()
1946 return ret
1947
1948+ def peek_whitespace(self):
1949+ ret = ""
1950+ char = self.peek_char()
1951+ if char is None:
1952+ return char
1953+ count = 0
1954+ while char is not None and char in self.whitespace_chars:
1955+ ret += char
1956+ count += 1
1957+ char = self.peek_char(skip=count)
1958+ return ret
1959+
1960 def parse_word(self, expected, require_whitespace=True):
1961 self.parse_whitespace("'%s'" % expected, require=require_whitespace)
1962 length = len(expected)
1963@@ -865,10 +1132,14 @@
1964 return text
1965
1966 def parse_comment_line(self):
1967- if self.peek_char() is None:
1968- return ""
1969- if self.peek_char() != "#":
1970+ whitespace = self.peek_whitespace()
1971+ if whitespace is None:
1972+ return ""
1973+ if self.peek_char(skip=len(whitespace)) is None:
1974+ return ""
1975+ if self.peek_char(skip=len(whitespace)) != "#":
1976 return None
1977+ self.parse_whitespace(None, require=False)
1978 comment = self.current_line[self.index:]
1979 self.index += len(comment)
1980 return comment
1981
1982=== modified file 'setup.py'
1983--- setup.py 2009-08-25 11:43:42 +0000
1984+++ setup.py 2010-08-13 01:14:42 +0000
1985@@ -2,15 +2,16 @@
1986
1987 from distutils.core import setup
1988
1989-setup(name="bzr-builder",
1990- version="0.1",
1991- description="Turn a recipe in to a bzr branch",
1992- author="James Westby",
1993- author_email="james.westby@canonical.com",
1994- license="GNU GPL v3",
1995- url="http://launchpad.net/bzr-builder",
1996- packages=['bzrlib.plugins.builder',
1997- 'bzrlib.plugins.builder.tests',
1998- ],
1999- package_dir={'bzrlib.plugins.builder': '.'},
2000- )
2001+if __name__ == '__main__':
2002+ setup(name="bzr-builder",
2003+ version="0.2",
2004+ description="Turn a recipe in to a bzr branch",
2005+ author="James Westby",
2006+ author_email="james.westby@canonical.com",
2007+ license="GNU GPL v3",
2008+ url="http://launchpad.net/bzr-builder",
2009+ packages=['bzrlib.plugins.builder',
2010+ 'bzrlib.plugins.builder.tests',
2011+ ],
2012+ package_dir={'bzrlib.plugins.builder': '.'},
2013+ )
2014
2015=== modified file 'tests/__init__.py'
2016--- tests/__init__.py 2009-04-16 20:23:39 +0000
2017+++ tests/__init__.py 2010-08-13 01:14:42 +0000
2018@@ -20,9 +20,10 @@
2019 loader = TestUtil.TestLoader()
2020 suite = TestSuite()
2021 testmod_names = [
2022- 'test_blackbox',
2023- 'test_recipe',
2024+ 'blackbox',
2025+ 'ppa',
2026+ 'recipe',
2027 ]
2028- suite.addTest(loader.loadTestsFromModuleNames(["%s.%s" % (__name__, i)
2029+ suite.addTest(loader.loadTestsFromModuleNames(["%s.test_%s" % (__name__, i)
2030 for i in testmod_names]))
2031 return suite
2032
2033=== modified file 'tests/test_blackbox.py'
2034--- tests/test_blackbox.py 2010-08-13 01:14:42 +0000
2035+++ tests/test_blackbox.py 2010-08-13 01:14:42 +0000
2036@@ -55,7 +55,7 @@
2037 self.build_tree(["source/a"])
2038 source.add(["a"])
2039 revid = source.commit("one")
2040- self.run_bzr("build recipe working")
2041+ self.run_bzr("build -q recipe working")
2042 self.failUnlessExists("working/a")
2043 tree = workingtree.WorkingTree.open("working")
2044 self.assertEqual(revid, tree.last_revision())
2045@@ -71,7 +71,7 @@
2046 self.build_tree(["source/a"])
2047 source.add(["a"])
2048 revid = source.commit("one")
2049- self.run_bzr("build recipe working --manifest manifest")
2050+ self.run_bzr("build -q recipe working --manifest manifest")
2051 self.failUnlessExists("working/a")
2052 self.failUnlessExists("manifest")
2053 self.check_file_contents("manifest", "# bzr-builder format 0.1 "
2054@@ -83,7 +83,7 @@
2055 source = self.make_branch_and_tree("source")
2056 self.build_tree(["source/a"])
2057 source.add(["a"])
2058- revid = source.commit("one")
2059+ source.commit("one")
2060 out, err = self.run_bzr("build recipe working "
2061 "--if-changed-from manifest")
2062
2063@@ -111,7 +111,7 @@
2064 "deb-version 1\nsource 1\n")])
2065 self.build_tree_contents([("old-manifest", "# bzr-builder format 0.1 "
2066 "deb-version 1\nsource revid:foo\n")])
2067- out, err = self.run_bzr("build recipe working --manifest manifest "
2068+ out, err = self.run_bzr("build -q recipe working --manifest manifest "
2069 "--if-changed-from old-manifest")
2070 self.failUnlessExists("working/a")
2071 self.failUnlessExists("manifest")
2072@@ -130,7 +130,7 @@
2073 revid = source.commit("one")
2074 self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2075 "deb-version 1\nsource 1\n")])
2076- out, err = self.run_bzr("dailydeb test.recipe working "
2077+ out, err = self.run_bzr("dailydeb -q test.recipe working "
2078 "--manifest manifest --package foo")
2079 self.failIfExists("working/a")
2080 package_root = "working/foo-1/"
2081@@ -149,13 +149,14 @@
2082 cl_f = open(cl_path)
2083 try:
2084 line = cl_f.readline()
2085- self.assertEqual("foo (1) jaunty; urgency=low\n", line)
2086+ self.assertEqual("foo (1) lucid; urgency=low\n", line)
2087 finally:
2088 cl_f.close()
2089
2090 def test_cmd_dailydeb_no_work_dir(self):
2091 #TODO: define a test feature for debuild and require it here.
2092- self.permit_dir('/') # Allow the made working dir to be accessed.
2093+ if getattr(self, "permit_dir", None) is not None:
2094+ self.permit_dir('/') # Allow the made working dir to be accessed.
2095 source = self.make_branch_and_tree("source")
2096 self.build_tree(["source/a", "source/debian/"])
2097 self.build_tree_contents([("source/debian/rules",
2098@@ -163,15 +164,16 @@
2099 ("source/debian/control",
2100 "Source: foo\nMaintainer: maint maint@maint.org\n")])
2101 source.add(["a", "debian/", "debian/rules", "debian/control"])
2102- revid = source.commit("one")
2103+ source.commit("one")
2104 self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2105 "deb-version 1\nsource 1\n")])
2106- out, err = self.run_bzr("dailydeb test.recipe "
2107+ out, err = self.run_bzr("dailydeb -q test.recipe "
2108 "--manifest manifest --package foo")
2109
2110 def test_cmd_dailydeb_if_changed_from_non_existant(self):
2111 #TODO: define a test feature for debuild and require it here.
2112- self.permit_dir('/') # Allow the made working dir to be accessed.
2113+ if getattr(self, "permit_dir", None) is not None:
2114+ self.permit_dir('/') # Allow the made working dir to be accessed.
2115 source = self.make_branch_and_tree("source")
2116 self.build_tree(["source/a", "source/debian/"])
2117 self.build_tree_contents([("source/debian/rules",
2118@@ -179,10 +181,10 @@
2119 ("source/debian/control",
2120 "Source: foo\nMaintainer: maint maint@maint.org\n")])
2121 source.add(["a", "debian/", "debian/rules", "debian/control"])
2122- revid = source.commit("one")
2123+ source.commit("one")
2124 self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2125 "deb-version 1\nsource 1\n")])
2126- out, err = self.run_bzr("dailydeb test.recipe "
2127+ out, err = self.run_bzr("dailydeb -q test.recipe "
2128 "--manifest manifest --package foo --if-changed-from bar")
2129
2130 def make_simple_package(self):
2131@@ -201,12 +203,29 @@
2132 source.commit("one")
2133 return source
2134
2135+ def test_cmd_dailydeb_no_build(self):
2136+ self.make_simple_package()
2137+ self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2138+ "deb-version 1\nsource 1\n")])
2139+ out, err = self.run_bzr("dailydeb -q test.recipe "
2140+ "--manifest manifest --no-build working")
2141+ new_cl_contents = ("package (1) unstable; urgency=low\n\n"
2142+ " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ")
2143+ f = open("working/test-1/debian/changelog")
2144+ try:
2145+ actual_cl_contents = f.read()
2146+ finally:
2147+ f.close()
2148+ self.assertStartsWith(actual_cl_contents, new_cl_contents)
2149+ for fn in os.listdir("working"):
2150+ self.assertFalse(fn.endswith(".changes"))
2151+
2152 def test_cmd_dailydeb_with_package_from_changelog(self):
2153 #TODO: define a test feature for debuild and require it here.
2154- source = self.make_simple_package()
2155+ self.make_simple_package()
2156 self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2157 "deb-version 1\nsource 1\n")])
2158- out, err = self.run_bzr("dailydeb test.recipe "
2159+ out, err = self.run_bzr("dailydeb -q test.recipe "
2160 "--manifest manifest --if-changed-from bar working")
2161 new_cl_contents = ("package (1) unstable; urgency=low\n\n"
2162 " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ")
2163@@ -218,10 +237,10 @@
2164 self.assertStartsWith(actual_cl_contents, new_cl_contents)
2165
2166 def test_cmd_dailydeb_with_upstream_version_from_changelog(self):
2167- source = self.make_simple_package()
2168+ self.make_simple_package()
2169 self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2170 "deb-version {debupstream}-2\nsource 1\n")])
2171- out, err = self.run_bzr("dailydeb test.recipe working")
2172+ out, err = self.run_bzr("dailydeb -q test.recipe working")
2173 new_cl_contents = ("package (0.1-2) unstable; urgency=low\n\n"
2174 " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ")
2175 f = open("working/test-{debupstream}-2/debian/changelog")
2176@@ -230,3 +249,30 @@
2177 finally:
2178 f.close()
2179 self.assertStartsWith(actual_cl_contents, new_cl_contents)
2180+
2181+ def test_cmd_dailydeb_with_append_version(self):
2182+ self.make_simple_package()
2183+ self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2184+ "deb-version 1\nsource 1\n")])
2185+ out, err = self.run_bzr("dailydeb -q test.recipe working "
2186+ "--append-version ~ppa1")
2187+ new_cl_contents = ("package (1~ppa1) unstable; urgency=low\n\n"
2188+ " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ")
2189+ f = open("working/test-1/debian/changelog")
2190+ try:
2191+ actual_cl_contents = f.read()
2192+ finally:
2193+ f.close()
2194+ self.assertStartsWith(actual_cl_contents, new_cl_contents)
2195+
2196+ def test_cmd_dailydeb_with_invalid_version(self):
2197+ source = self.make_branch_and_tree("source")
2198+ self.build_tree(["source/a"])
2199+ source.add(["a"])
2200+ revid = source.commit("one")
2201+ self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 "
2202+ "deb-version $\nsource 1\n")])
2203+ err = self.run_bzr("dailydeb -q test.recipe working --package foo",
2204+ retcode=3)[1]
2205+ self.assertEqual("bzr: ERROR: Invalid deb-version: $: "
2206+ "Could not parse version: $\n", err)
2207
2208=== added file 'tests/test_ppa.py'
2209--- tests/test_ppa.py 1970-01-01 00:00:00 +0000
2210+++ tests/test_ppa.py 2010-08-13 01:14:42 +0000
2211@@ -0,0 +1,28 @@
2212+# bzr-builder: a bzr plugin to construct trees based on recipes
2213+# Copyright 2009 Canonical Ltd.
2214+
2215+# This program is free software: you can redistribute it and/or modify it
2216+# under the terms of the GNU General Public License version 3, as published
2217+# by the Free Software Foundation.
2218+
2219+# This program is distributed in the hope that it will be useful, but
2220+# WITHOUT ANY WARRANTY; without even the implied warranties of
2221+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2222+# PURPOSE. See the GNU General Public License for more details.
2223+
2224+# You should have received a copy of the GNU General Public License along
2225+# with this program. If not, see <http://www.gnu.org/licenses/>.
2226+
2227+from bzrlib.plugins.builder.cmds import target_from_dput
2228+from bzrlib.tests import (
2229+ TestCase,
2230+ )
2231+
2232+
2233+class TestTargetFromDPut(TestCase):
2234+
2235+ def test_default_ppa(self):
2236+ self.assertEqual('team-name/ppa', target_from_dput('ppa:team-name'))
2237+
2238+ def test_named_ppa(self):
2239+ self.assertEqual('team/ppa2', target_from_dput('ppa:team/ppa2'))
2240
2241=== modified file 'tests/test_recipe.py'
2242--- tests/test_recipe.py 2010-08-13 01:14:42 +0000
2243+++ tests/test_recipe.py 2010-08-13 01:14:42 +0000
2244@@ -27,26 +27,28 @@
2245 )
2246 from bzrlib.plugins.builder.recipe import (
2247 BaseRecipeBranch,
2248- build_manifest,
2249 build_tree,
2250 ensure_basedir,
2251+ ForbiddenInstructionError,
2252 pull_or_branch,
2253 RecipeParser,
2254 RecipeBranch,
2255 RecipeParseError,
2256 resolve_revisions,
2257+ RUN_INSTRUCTION,
2258 )
2259
2260
2261 class RecipeParserTests(TestCaseInTempDir):
2262
2263 deb_version = "0.1-{revno}"
2264- basic_header = ("# bzr-builder format 0.2 deb-version "
2265+ basic_header = ("# bzr-builder format 0.3 deb-version "
2266 + deb_version +"\n")
2267- basic_header_and_branch = basic_header + "http://foo.org/\n"
2268+ basic_branch = "http://foo.org/"
2269+ basic_header_and_branch = basic_header + basic_branch + "\n"
2270
2271- def get_recipe(self, recipe_text):
2272- return RecipeParser(recipe_text).parse()
2273+ def get_recipe(self, recipe_text, **kwargs):
2274+ return RecipeParser(recipe_text).parse(**kwargs)
2275
2276 def assertParseError(self, line, char, problem, callable, *args,
2277 **kwargs):
2278@@ -55,6 +57,7 @@
2279 self.assertEqual(line, exc.line)
2280 self.assertEqual(char, exc.char)
2281 self.assertEqual("recipe", exc.filename)
2282+ return exc
2283
2284 def check_recipe_branch(self, branch, name, url, revspec=None,
2285 num_child_branches=0, revid=None):
2286@@ -130,8 +133,8 @@
2287 self.basic_header + "http://foo.org/ 2 foo")
2288
2289 def tests_rejects_unknown_instruction(self):
2290- self.assertParseError(3, 1, "Expecting 'merge', 'nest' or 'run', "
2291- "got 'cat'", self.get_recipe,
2292+ self.assertParseError(3, 1, "Expecting 'merge', 'nest', 'nest-part' "
2293+ "or 'run', got 'cat'", self.get_recipe,
2294 self.basic_header + "http://foo.org/\n" + "cat")
2295
2296 def test_rejects_merge_no_name(self):
2297@@ -149,6 +152,21 @@
2298 "got 'bar'", self.get_recipe,
2299 self.basic_header_and_branch + "merge foo url 2 bar")
2300
2301+ def test_rejects_nest_part_no_name(self):
2302+ self.assertParseError(3, 11, "End of line while looking for "
2303+ "the branch id", self.get_recipe,
2304+ self.basic_header_and_branch + "nest-part ")
2305+
2306+ def test_rejects_nest_part_no_url(self):
2307+ self.assertParseError(3, 15, "End of line while looking for "
2308+ "the branch url", self.get_recipe,
2309+ self.basic_header_and_branch + "nest-part foo ")
2310+
2311+ def test_rejects_nest_part_no_subpath(self):
2312+ self.assertParseError(3, 22, "End of line while looking for "
2313+ "the subpath to merge", self.get_recipe,
2314+ self.basic_header_and_branch + "nest-part foo url:// ")
2315+
2316 def test_rejects_nest_no_name(self):
2317 self.assertParseError(3, 6, "End of line while looking for "
2318 "the branch id", self.get_recipe,
2319@@ -202,6 +220,12 @@
2320 self.assertParseError(3, 1, "Empty recipe", self.get_recipe,
2321 self.basic_header)
2322
2323+ def test_rejects_non_unique_ids(self):
2324+ self.assertParseError(4, 7, "'foo' was already used to identify "
2325+ "a branch.", self.get_recipe,
2326+ self.basic_header_and_branch + "merge foo url\n"
2327+ + "merge foo other-url\n")
2328+
2329 def test_builds_simplest_recipe(self):
2330 base_branch = self.get_recipe(self.basic_header_and_branch)
2331 self.check_base_recipe_branch(base_branch, "http://foo.org/")
2332@@ -220,6 +244,42 @@
2333 self.assertEqual(None, location)
2334 self.check_recipe_branch(child_branch, "bar", "http://bar.org")
2335
2336+ def test_builds_recipe_with_nest_part(self):
2337+ base_branch = self.get_recipe(self.basic_header_and_branch
2338+ + "nest-part bar http://bar.org some/path")
2339+ self.check_base_recipe_branch(base_branch, "http://foo.org/",
2340+ num_child_branches=1)
2341+ instruction = base_branch.child_branches[0]
2342+ self.assertEqual(None, instruction.nest_path)
2343+ self.check_recipe_branch(
2344+ instruction.recipe_branch, "bar", "http://bar.org")
2345+ self.assertEqual("some/path", instruction.subpath)
2346+ self.assertEqual(None, instruction.target_subdir)
2347+
2348+ def test_builds_recipe_with_nest_part_subdir(self):
2349+ base_branch = self.get_recipe(self.basic_header_and_branch
2350+ + "nest-part bar http://bar.org some/path target-subdir")
2351+ self.check_base_recipe_branch(base_branch, "http://foo.org/",
2352+ num_child_branches=1)
2353+ instruction = base_branch.child_branches[0]
2354+ self.assertEqual(None, instruction.nest_path)
2355+ self.check_recipe_branch(
2356+ instruction.recipe_branch, "bar", "http://bar.org")
2357+ self.assertEqual("some/path", instruction.subpath)
2358+ self.assertEqual("target-subdir", instruction.target_subdir)
2359+
2360+ def test_builds_recipe_with_nest_part_subdir_and_revspec(self):
2361+ base_branch = self.get_recipe(self.basic_header_and_branch
2362+ + "nest-part bar http://bar.org some/path target-subdir 1234")
2363+ self.check_base_recipe_branch(base_branch, "http://foo.org/",
2364+ num_child_branches=1)
2365+ instruction = base_branch.child_branches[0]
2366+ self.assertEqual(None, instruction.nest_path)
2367+ self.check_recipe_branch(
2368+ instruction.recipe_branch, "bar", "http://bar.org", "1234")
2369+ self.assertEqual("some/path", instruction.subpath)
2370+ self.assertEqual("target-subdir", instruction.target_subdir)
2371+
2372 def test_builds_recipe_with_nest(self):
2373 base_branch = self.get_recipe(self.basic_header_and_branch
2374 + "nest bar http://bar.org baz")
2375@@ -306,6 +366,60 @@
2376 self.assertEqual(None, child_branch)
2377 self.assertEqual("touch test", command)
2378
2379+ def test_accepts_blank_line_during_nest(self):
2380+ base_branch = self.get_recipe(self.basic_header_and_branch
2381+ + "nest foo http://bar.org bar\n merge baz baz.org\n\n"
2382+ " merge zap zap.org\n")
2383+ self.check_base_recipe_branch(base_branch, self.basic_branch,
2384+ num_child_branches=1)
2385+ nested_branch, location = base_branch.child_branches[0].as_tuple()
2386+ self.assertEqual("bar", location)
2387+ self.check_recipe_branch(nested_branch, "foo", "http://bar.org",
2388+ num_child_branches=2)
2389+ child_branch, location = nested_branch.child_branches[0].as_tuple()
2390+ self.assertEqual(None, location)
2391+ self.check_recipe_branch(child_branch, "baz", "baz.org")
2392+ child_branch, location = nested_branch.child_branches[1].as_tuple()
2393+ self.assertEqual(None, location)
2394+ self.check_recipe_branch(child_branch, "zap", "zap.org")
2395+
2396+ def test_accepts_blank_line_at_start_of_nest(self):
2397+ base_branch = self.get_recipe(self.basic_header_and_branch
2398+ + "nest foo http://bar.org bar\n\n merge baz baz.org\n")
2399+ self.check_base_recipe_branch(base_branch, self.basic_branch,
2400+ num_child_branches=1)
2401+ nested_branch, location = base_branch.child_branches[0].as_tuple()
2402+ self.assertEqual("bar", location)
2403+ self.check_recipe_branch(nested_branch, "foo", "http://bar.org",
2404+ num_child_branches=1)
2405+ child_branch, location = nested_branch.child_branches[0].as_tuple()
2406+ self.assertEqual(None, location)
2407+ self.check_recipe_branch(child_branch, "baz", "baz.org")
2408+
2409+ def test_accepts_blank_line_as_only_thing_in_nest(self):
2410+ base_branch = self.get_recipe(self.basic_header_and_branch
2411+ + "nest foo http://bar.org bar\n\nmerge baz baz.org\n")
2412+ self.check_base_recipe_branch(base_branch, self.basic_branch,
2413+ num_child_branches=2)
2414+ nested_branch, location = base_branch.child_branches[0].as_tuple()
2415+ self.assertEqual("bar", location)
2416+ self.check_recipe_branch(nested_branch, "foo", "http://bar.org")
2417+ child_branch, location = base_branch.child_branches[1].as_tuple()
2418+ self.assertEqual(None, location)
2419+ self.check_recipe_branch(child_branch, "baz", "baz.org")
2420+
2421+ def test_accepts_comment_line_with_any_number_of_spaces(self):
2422+ base_branch = self.get_recipe(self.basic_header_and_branch
2423+ + "nest foo http://bar.org bar\n #foo\nmerge baz baz.org\n")
2424+ self.check_base_recipe_branch(base_branch, self.basic_branch,
2425+ num_child_branches=2)
2426+ nested_branch, location = base_branch.child_branches[0].as_tuple()
2427+ self.assertEqual("bar", location)
2428+ self.check_recipe_branch(nested_branch, "foo", "http://bar.org")
2429+ child_branch, location = base_branch.child_branches[1].as_tuple()
2430+ self.assertEqual(None, location)
2431+ self.check_recipe_branch(child_branch, "baz", "baz.org")
2432+
2433 def test_old_format_rejects_run(self):
2434 header = ("# bzr-builder format 0.1 deb-version "
2435 + self.deb_version +"\n")
2436@@ -313,6 +427,45 @@
2437 , self.get_recipe, header + "http://foo.org/\n"
2438 + "run touch test \n")
2439
2440+ def test_error_on_forbidden_instructions(self):
2441+ exc = self.assertParseError(3, 1, "The 'run' instruction is "
2442+ "forbidden.", self.get_recipe, self.basic_header_and_branch
2443+ + "run touch test\n",
2444+ forbidden_instructions=[RUN_INSTRUCTION])
2445+ self.assertTrue(isinstance(exc, ForbiddenInstructionError))
2446+ self.assertEqual("run", exc.instruction_name)
2447+
2448+ def test_error_on_duplicate_path(self):
2449+ exc = self.assertParseError(3, 15, "The path '.' is a duplicate "
2450+ "of the one used on line 1.", self.get_recipe,
2451+ self.basic_header_and_branch + "nest nest url .\n")
2452+
2453+ def test_error_on_duplicate_path_with_another_nest(self):
2454+ exc = self.assertParseError(4, 16, "The path 'foo' is a duplicate "
2455+ "of the one used on line 3.", self.get_recipe,
2456+ self.basic_header_and_branch + "nest nest url foo\n"
2457+ + "nest nest2 url foo\n")
2458+
2459+ def test_duplicate_path_check_uses_normpath(self):
2460+ exc = self.assertParseError(3, 15, "The path 'foo/..' is a duplicate "
2461+ "of the one used on line 1.", self.get_recipe,
2462+ self.basic_header_and_branch + "nest nest url foo/..\n")
2463+
2464+ def test_error_absolute_path(self):
2465+ exc = self.assertParseError(3, 15, "Absolute paths are not allowed: "
2466+ "/etc/passwd", self.get_recipe, self.basic_header_and_branch
2467+ + "nest nest url /etc/passwd\n")
2468+
2469+ def test_error_simple_parent_dir(self):
2470+ exc = self.assertParseError(3, 15, "Paths outside the current "
2471+ "directory are not allowed: ../foo", self.get_recipe,
2472+ self.basic_header_and_branch + "nest nest url ../foo\n")
2473+
2474+ def test_error_complex_parent_dir(self):
2475+ exc = self.assertParseError(3, 15, "Paths outside the current "
2476+ "directory are not allowed: ./foo/../..", self.get_recipe,
2477+ self.basic_header_and_branch + "nest nest url ./foo/../..\n")
2478+
2479
2480 class BuildTreeTests(TestCaseWithTransport):
2481
2482@@ -352,7 +505,7 @@
2483 def test_build_tree_single_branch_existing_branch(self):
2484 source = self.make_branch_and_tree("source")
2485 revid = source.commit("one")
2486- target = self.make_branch_and_tree("target")
2487+ self.make_branch_and_tree("target")
2488 base_branch = BaseRecipeBranch("source", "1", 0.2)
2489 build_tree(base_branch, "target")
2490 self.failUnlessExists("target")
2491@@ -360,15 +513,17 @@
2492 self.assertEqual(revid, tree.last_revision())
2493 self.assertEqual(revid, base_branch.revid)
2494
2495+ def make_source_branch(self, relpath):
2496+ """Make a branch with one file and one commit."""
2497+ source1 = self.make_branch_and_tree(relpath)
2498+ self.build_tree([relpath + "/a"])
2499+ source1.add(["a"])
2500+ source1.commit("one")
2501+ return source1
2502+
2503 def test_build_tree_nested(self):
2504- source1 = self.make_branch_and_tree("source1")
2505- self.build_tree(["source1/a"])
2506- source1.add(["a"])
2507- source1_rev_id = source1.commit("one")
2508- source2 = self.make_branch_and_tree("source2")
2509- self.build_tree(["source2/a"])
2510- source2.add(["a"])
2511- source2_rev_id = source2.commit("one")
2512+ source1_rev_id = self.make_source_branch("source1").last_revision()
2513+ source2_rev_id = self.make_source_branch("source2").last_revision()
2514 base_branch = BaseRecipeBranch("source1", "1", 0.2)
2515 nested_branch = RecipeBranch("nested", "source2")
2516 base_branch.nest_branch("sub", nested_branch)
2517@@ -382,10 +537,8 @@
2518 self.assertEqual(source2_rev_id, nested_branch.revid)
2519
2520 def test_build_tree_merged(self):
2521- source1 = self.make_branch_and_tree("source1")
2522- self.build_tree(["source1/a"])
2523- source1.add(["a"])
2524- source1_rev_id = source1.commit("one")
2525+ source1 = self.make_source_branch("source1")
2526+ source1_rev_id = source1.last_revision()
2527 source2 = source1.bzrdir.sprout("source2").open_workingtree()
2528 self.build_tree_contents([("source2/a", "other change")])
2529 source2_rev_id = source2.commit("one")
2530@@ -403,11 +556,60 @@
2531 self.assertEqual(source1_rev_id, base_branch.revid)
2532 self.assertEqual(source2_rev_id, merged_branch.revid)
2533
2534+ def test_build_tree_nest_part(self):
2535+ """A recipe can specify a merge of just part of an unrelated tree."""
2536+ source1 = self.make_source_branch("source1")
2537+ source2 = self.make_source_branch("source2")
2538+ source1.lock_read()
2539+ self.addCleanup(source1.unlock)
2540+ source1_rev_id = source1.last_revision()
2541+ # Add 'b' to source2.
2542+ self.build_tree_contents([
2543+ ("source2/b", "new file"), ("source2/not-b", "other file")])
2544+ source2.add(["b", "not-b"])
2545+ source2_rev_id = source2.commit("two")
2546+ base_branch = BaseRecipeBranch("source1", "1", 0.2)
2547+ merged_branch = RecipeBranch("merged", "source2")
2548+ # Merge just 'b' from source2; 'a' is untouched.
2549+ base_branch.nest_part_branch(merged_branch, "b")
2550+ build_tree(base_branch, "target")
2551+ file_id = source1.path2id("a")
2552+ self.check_file_contents("target/a", source1.get_file_text(file_id))
2553+ self.check_file_contents("target/b", "new file")
2554+ self.assertNotInWorkingTree("not-b", "target")
2555+ self.assertEqual(source1_rev_id, base_branch.revid)
2556+ self.assertEqual(source2_rev_id, merged_branch.revid)
2557+
2558+ def test_build_tree_nest_part_explicit_target(self):
2559+ """A recipe can specify a merge of just part of an unrelated tree into
2560+ a specific subdirectory of the target tree.
2561+ """
2562+ source1 = self.make_branch_and_tree("source1")
2563+ self.build_tree(["source1/dir/"])
2564+ source1.add(["dir"])
2565+ source1.commit("one")
2566+ source2 = self.make_source_branch("source2")
2567+ source1.lock_read()
2568+ self.addCleanup(source1.unlock)
2569+ source1_rev_id = source1.last_revision()
2570+ # Add 'b' to source2.
2571+ self.build_tree_contents([
2572+ ("source2/b", "new file"), ("source2/not-b", "other file")])
2573+ source2.add(["b", "not-b"])
2574+ source2_rev_id = source2.commit("two")
2575+ base_branch = BaseRecipeBranch("source1", "1", 0.2)
2576+ merged_branch = RecipeBranch("merged", "source2")
2577+ # Merge just 'b' from source2; 'a' is untouched.
2578+ base_branch.nest_part_branch(merged_branch, "b", "dir/b")
2579+ build_tree(base_branch, "target")
2580+ self.check_file_contents("target/dir/b", "new file")
2581+ self.assertNotInWorkingTree("dir/not-b", "target")
2582+ self.assertEqual(source1_rev_id, base_branch.revid)
2583+ self.assertEqual(source2_rev_id, merged_branch.revid)
2584+
2585 def test_build_tree_merge_twice(self):
2586- source1 = self.make_branch_and_tree("source1")
2587- self.build_tree(["source1/a"])
2588- source1.add(["a"])
2589- source1_rev_id = source1.commit("one")
2590+ source1 = self.make_source_branch("source1")
2591+ source1_rev_id = source1.last_revision()
2592 source2 = source1.bzrdir.sprout("source2").open_workingtree()
2593 self.build_tree_contents([("source2/a", "other change")])
2594 source2_rev_id = source2.commit("one")
2595@@ -436,10 +638,7 @@
2596 self.assertEqual(source3_rev_id, merged_branch2.revid)
2597
2598 def test_build_tree_merged_with_conflicts(self):
2599- source1 = self.make_branch_and_tree("source1")
2600- self.build_tree(["source1/a"])
2601- source1.add(["a"])
2602- source1_rev_id = source1.commit("one")
2603+ source1 = self.make_source_branch("source1")
2604 source2 = source1.bzrdir.sprout("source2").open_workingtree()
2605 self.build_tree_contents([("source2/a", "other change\n")])
2606 source2_rev_id = source2.commit("one")
2607@@ -448,7 +647,7 @@
2608 base_branch = BaseRecipeBranch("source1", "1", 0.2)
2609 merged_branch = RecipeBranch("merged", "source2")
2610 base_branch.merge_branch(merged_branch)
2611- e = self.assertRaises(errors.BzrCommandError, build_tree,
2612+ self.assertRaises(errors.BzrCommandError, build_tree,
2613 base_branch, "target")
2614 self.failUnlessExists("target")
2615 tree = workingtree.WorkingTree.open("target")
2616@@ -465,10 +664,8 @@
2617 self.assertEqual(source2_rev_id, merged_branch.revid)
2618
2619 def test_build_tree_with_revspecs(self):
2620- source1 = self.make_branch_and_tree("source1")
2621- self.build_tree(["source1/a"])
2622- source1.add(["a"])
2623- source1_rev_id = source1.commit("one")
2624+ source1 = self.make_source_branch("source1")
2625+ source1_rev_id = source1.last_revision()
2626 source2 = source1.bzrdir.sprout("source2").open_workingtree()
2627 self.build_tree_contents([("source2/a", "other change\n")])
2628 source2_rev_id = source2.commit("one")
2629@@ -622,6 +819,20 @@
2630 self.assertEqual(revid, tree.last_revision())
2631 self.assertEqual(revid, base_branch.revid)
2632
2633+ def test_error_on_merge_revspec(self):
2634+ # See bug 416950
2635+ source = self.make_branch_and_tree("source")
2636+ revid = source.commit("one")
2637+ base_branch = BaseRecipeBranch("source", "1", 0.2)
2638+ merged_branch = RecipeBranch("merged", "source", revspec="debian")
2639+ base_branch.merge_branch(merged_branch)
2640+ e = self.assertRaises(errors.InvalidRevisionSpec,
2641+ build_tree, base_branch, "target")
2642+ self.assertTrue(str(e).startswith("Requested revision: 'debian' "
2643+ "does not exist in branch: "))
2644+ self.assertTrue(str(e).endswith(". Did you not mean to specify a "
2645+ "revspec at the end of the merge line?"))
2646+
2647
2648 class ResolveRevisionsTests(TestCaseWithTransport):
2649
2650@@ -704,7 +915,7 @@
2651
2652 def test_changed_command(self):
2653 source =self.make_branch_and_tree("source")
2654- revid = source.commit("one")
2655+ source.commit("one")
2656 branch1 = BaseRecipeBranch("source", "{revno}", 0.2)
2657 branch2 = BaseRecipeBranch("source", "{revno}", 0.2)
2658 branch1.run_command("touch test1")
2659@@ -713,6 +924,17 @@
2660 if_changed_from=branch2))
2661 self.assertEqual("source", branch1.url)
2662
2663+ def test_unchanged_command(self):
2664+ source =self.make_branch_and_tree("source")
2665+ source.commit("one")
2666+ branch1 = BaseRecipeBranch("source", "{revno}", 0.2)
2667+ branch2 = BaseRecipeBranch("source", "{revno}", 0.2)
2668+ branch1.run_command("touch test1")
2669+ branch2.run_command("touch test1")
2670+ self.assertEqual(False, resolve_revisions(branch1,
2671+ if_changed_from=branch2))
2672+ self.assertEqual("source", branch1.url)
2673+
2674 def test_substitute(self):
2675 source =self.make_branch_and_tree("source")
2676 revid1 = source.commit("one")
2677@@ -737,13 +959,20 @@
2678 resolve_revisions(branch1)
2679 self.assertEqual("{debupstream}-2", branch1.deb_version)
2680
2681-
2682-class BuildManifestTests(TestCaseInTempDir):
2683+ def test_subsitute_not_fully_expanded(self):
2684+ source =self.make_branch_and_tree("source")
2685+ source.commit("one")
2686+ source.commit("two")
2687+ branch1 = BaseRecipeBranch("source", "{revno:packaging}", 0.2)
2688+ self.assertRaises(errors.BzrCommandError, resolve_revisions, branch1)
2689+
2690+
2691+class StringifyTests(TestCaseInTempDir):
2692
2693 def test_simple_manifest(self):
2694 base_branch = BaseRecipeBranch("base_url", "1", 0.1)
2695 base_branch.revid = "base_revid"
2696- manifest = build_manifest(base_branch)
2697+ manifest = str(base_branch)
2698 self.assertEqual("# bzr-builder format 0.1 deb-version 1\n"
2699 "base_url revid:base_revid\n", manifest)
2700
2701@@ -759,7 +988,7 @@
2702 merged_branch = RecipeBranch("merged", "merged_url")
2703 merged_branch.revid = "merged_revid"
2704 base_branch.merge_branch(merged_branch)
2705- manifest = build_manifest(base_branch)
2706+ manifest = str(base_branch)
2707 self.assertEqual("# bzr-builder format 0.2 deb-version 2\n"
2708 "base_url revid:base_revid\n"
2709 "nest nested1 nested1_url nested revid:nested1_revid\n"
2710@@ -770,11 +999,40 @@
2711 base_branch = BaseRecipeBranch("base_url", "1", 0.2)
2712 base_branch.revid = "base_revid"
2713 base_branch.run_command("touch test")
2714- manifest = build_manifest(base_branch)
2715+ manifest = str(base_branch)
2716 self.assertEqual("# bzr-builder format 0.2 deb-version 1\n"
2717 "base_url revid:base_revid\n"
2718 "run touch test\n", manifest)
2719
2720+ def test_recipe_with_no_revspec(self):
2721+ base_branch = BaseRecipeBranch("base_url", "1", 0.1)
2722+ manifest = str(base_branch)
2723+ self.assertEqual("# bzr-builder format 0.1 deb-version 1\n"
2724+ "base_url\n", manifest)
2725+
2726+ def test_recipe_with_tag_revspec(self):
2727+ base_branch = BaseRecipeBranch("base_url", "1", 0.1,
2728+ revspec="tag:foo")
2729+ manifest = str(base_branch)
2730+ self.assertEqual("# bzr-builder format 0.1 deb-version 1\n"
2731+ "base_url tag:foo\n", manifest)
2732+
2733+ def test_recipe_with_child(self):
2734+ base_branch = BaseRecipeBranch("base_url", "2", 0.2)
2735+ nested_branch1 = RecipeBranch("nested1", "nested1_url",
2736+ revspec="tag:foo")
2737+ base_branch.nest_branch("nested", nested_branch1)
2738+ nested_branch2 = RecipeBranch("nested2", "nested2_url")
2739+ nested_branch1.nest_branch("nested2", nested_branch2)
2740+ merged_branch = RecipeBranch("merged", "merged_url")
2741+ base_branch.merge_branch(merged_branch)
2742+ manifest = str(base_branch)
2743+ self.assertEqual("# bzr-builder format 0.2 deb-version 2\n"
2744+ "base_url\n"
2745+ "nest nested1 nested1_url nested tag:foo\n"
2746+ " nest nested2 nested2_url nested2\n"
2747+ "merge merged merged_url\n", manifest)
2748+
2749
2750 class RecipeBranchTests(TestCaseInTempDir):
2751
2752@@ -817,6 +1075,14 @@
2753 base_branch.substitute_time(time)
2754 self.assertEqual("1-197001010000", base_branch.deb_version)
2755
2756+ def test_substitute_date(self):
2757+ time = datetime.datetime.utcfromtimestamp(1)
2758+ base_branch = BaseRecipeBranch("base_url", "1-{date}", 0.2)
2759+ base_branch.substitute_time(time)
2760+ self.assertEqual("1-19700101", base_branch.deb_version)
2761+ base_branch.substitute_time(time)
2762+ self.assertEqual("1-19700101", base_branch.deb_version)
2763+
2764 def test_substitute_revno(self):
2765 base_branch = BaseRecipeBranch("base_url", "1", 0.2)
2766 base_branch.substitute_revno(None, None)
2767@@ -838,3 +1104,17 @@
2768 self.assertEqual("3", base_branch.deb_version)
2769 base_branch.substitute_revno("foo", lambda: "3")
2770 self.assertEqual("3", base_branch.deb_version)
2771+
2772+ def test_list_branch_names(self):
2773+ base_branch = BaseRecipeBranch("base_url", "1", 0.2)
2774+ base_branch.merge_branch(RecipeBranch("merged", "merged_url"))
2775+ nested_branch = RecipeBranch("nested", "nested_url")
2776+ nested_branch.merge_branch(
2777+ RecipeBranch("merged_into_nested", "another_url"))
2778+ base_branch.nest_branch("subdir", nested_branch)
2779+ base_branch.merge_branch(
2780+ RecipeBranch("another_nested", "yet_another_url"))
2781+ base_branch.run_command("a command")
2782+ self.assertEqual(
2783+ ["merged", "nested", "merged_into_nested", "another_nested"],
2784+ base_branch.list_branch_names())

Subscribers

People subscribed via source and target branches