Merge lp:~parthm/bzr/173274-export-hooks into lp:bzr

Proposed by Parth Malwankar
Status: Work in progress
Proposed branch: lp:~parthm/bzr/173274-export-hooks
Merge into: lp:bzr
Diff against target: 215 lines (+158/-3)
4 files modified
NEWS (+4/-0)
bzrlib/export/__init__.py (+93/-3)
bzrlib/hooks.py (+5/-0)
bzrlib/tests/test_export.py (+56/-0)
To merge this branch: bzr merge lp:~parthm/bzr/173274-export-hooks
Reviewer Review Type Date Requested Status
Andrew Bennetts Needs Fixing
Review via email: mp+35919@code.launchpad.net

Commit message

support for pre_export and post_export hooks.

Description of the change

=== Fixes Bug #173274 ===
This patch adds support for pre_export and post_export hooks.

The 'return _exporters[format](...)' call in bzrlib.export.export didn't really return a value. Trivially fixed that also.

To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

144 +# FIXME: As export is a directory based module (as against a py file), our
145 +# lazy registering doesn't see 'hooks' in the first tuple. To work around
146 +# this directly list __init__ below. This can possibly be cleaner.
147 +known_hooks.register_lazy(('bzrlib.export.__init__', 'hooks'),
148 + 'bzrlib.export', 'ExportHooks')

I don't understand why this is necessary... oh, I see. There's a bug in _LazyObjectGetter. I've submitted <https://code.edge.launchpad.net/~spiv/bzr/hooks-refactoring/+merge/36101> to fix this, and also make known_hooks registration a little neater. Until that lands this is an ok workaround.

36 - return _exporters[format](tree, dest, root, subdir, filtered=filtered,
37 - per_file_timestamps=per_file_timestamps)
38 + for hook in hooks['pre_export']:
39 + hook(ExportHookParams(tree, dest, format, root, subdir,
40 + filtered, per_file_timestamps))
41 + _exporters[format](tree, dest, root, subdir, filtered=filtered,
42 + per_file_timestamps=per_file_timestamps)
43 + for hook in hooks['post_export']:
44 + hook(ExportHookParams(tree, dest, format, root, subdir,
45 + filtered, per_file_timestamps))

Is this actually sufficient to provide the functionality requested in bug 173274? Specifically, it requests a hook that allows modifying the tree to export, e.g. by running autoconf, and having the new, unversioned (and probably ignored) files that generates be included, and possibly include modifications to versioned files too. I don't think this implementation allows that, as I believe that regardless of the pre_export hook it will still export "the last committed revision", as "bzr help export" says.

Well, the pre-hook could commit a new revision with the changes, and the post-hook could uncommit it... that would be a really ugly hack, and a plugin could already wrap cmd_export to do that sort of hack.

So, two changes I'd like to see:

 * support for somehow allowing selected unversioned files in the tree to be included in the export. Perhaps by changing the Tree object that is being exported?
 * tests that demonstrate that support, so that we can be confident the use case in bug 173274 is satisfied :)

review: Needs Fixing
Revision history for this message
Parth Malwankar (parthm) wrote :

> * support for somehow allowing selected unversioned files in the tree to be
> included in the export. Perhaps by changing the Tree object that is being
> exported?
> * tests that demonstrate that support, so that we can be confident the use
> case in bug 173274 is satisfied :)

Taking bug #619478 into account, maybe a --all or --unversioned flag for 'bzr export'? I can create a patch for that before we try to land this in case one of the above seems like a good solution. Thoughts?

Thanks for the review.

lp:~parthm/bzr/173274-export-hooks updated
5436. By Parth Malwankar

merged in changes from trunk

Revision history for this message
Martin Pool (mbp) wrote :

On 22 September 2010 01:21, Parth Malwankar <email address hidden> wrote:
>
>>  * support for somehow allowing selected unversioned files in the tree to be
>> included in the export.  Perhaps by changing the Tree object that is being
>> exported?
>>  * tests that demonstrate that support, so that we can be confident the use
>> case in bug 173274 is satisfied :)
>
> Taking bug #619478 into account, maybe a --all or --unversioned flag for 'bzr export'? I can create a patch for that before we try to land this in case one of the above seems like a good solution. Thoughts?

That sounds good. I'd used --uncommitted, to be consistent with merge.

--
Martin

Revision history for this message
Vincent Ladeuil (vila) wrote :

>>>>> Martin Pool <email address hidden> writes:

<snip/>

    >> Taking bug #619478 into account, maybe a --all or --unversioned
    >> flag for 'bzr export'? I can create a patch for that before we
    >> try to land this in case one of the above seems like a good
    >> solution. Thoughts?

    > That sounds good. I'd used --uncommitted, to be consistent with merge.

+1

Handling 'junk' files may become possible in the near future and using
'--uncommitted' to encompass all the kinds of unversioned file seems to
be the best bet.

   Vincent

Revision history for this message
John A Meinel (jameinel) wrote :

Just mentioning that "--uncommitted" sounds to me like "give me the
content of the *versioned* files that hasn't been committed yet". Which
is how merge uses it. Specifically, you don't get junk files when you
'merge --uncomitted'.

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

John A Meinel wrote:
> Just mentioning that "--uncommitted" sounds to me like "give me the
> content of the *versioned* files that hasn't been committed yet". Which
> is how merge uses it. Specifically, you don't get junk files when you
> 'merge --uncomitted'.

Yes, I think uncommitted-but-versioned isn't what the use case is asking for.
It is asking for a way to get some (all?) unversioned files included (and
probably uncommitted changes too).

I'm not sure exactly what would best suit the use case (bzr export of a tree
including 'autoconf' output). Typically you wouldn't want to export all ignored
files (e.g. usually you don't want to ship: tags, *.pyc, *.o, ...). Perhaps the
hypothetical 'precious' designation is good enough, but I'm not sure it's an
exact fit. So I suspect a complete, polished solution may involve some extra
complexity (perhaps just some config in bzrrules to define some files as
unversioned_exportable)...

But that's policy. In terms of mechanism, we need a hook or other API that
gives a plugin a way to:

  * cause selected unversioned files to be included in the export
  * cause uncommitted versions of versioned files to be included in the export

Then we should be able to support whatever exact behaviour is needed.

Revision history for this message
Vincent Ladeuil (vila) wrote :

ping, this branch is now a pre-requisite of https://code.edge.launchpad.net/~gz/bzr/transport_post_connect_hook/+merge/38431

What's the status here ?

Revision history for this message
Martin Packman (gz) wrote :

Ah, this one isn't a pre-requisite for me, I just used Parth as a reference and inspiration. :)

Unmerged revisions

5436. By Parth Malwankar

merged in changes from trunk

5435. By Parth Malwankar

added NEWS entry. fixed typo.

5434. By Parth Malwankar

added pre-export and post-export hooks + tests.

5433. By Parth Malwankar

removed unwanted return from export function

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-09-21 10:48:23 +0000
3+++ NEWS 2010-09-21 16:04:46 +0000
4@@ -16,6 +16,10 @@
5 New Features
6 ************
7
8+* Added ``pre_export`` and ``post_export`` hooks. This allows plugins
9+ to register custom handlers which will be invoked before/after the
10+ tree is exported. (Parth Malwankar, #173274)
11+
12 Bug Fixes
13 *********
14
15
16=== modified file 'bzrlib/export/__init__.py'
17--- bzrlib/export/__init__.py 2010-03-25 09:39:03 +0000
18+++ bzrlib/export/__init__.py 2010-09-21 16:04:46 +0000
19@@ -20,7 +20,11 @@
20 """
21
22 import os
23-import bzrlib.errors as errors
24+
25+from bzrlib import (
26+ errors,
27+ hooks as _mod_hooks,
28+ )
29
30 # Maps format name => export function
31 _exporters = {}
32@@ -103,8 +107,14 @@
33 raise errors.NoSuchExportFormat(format)
34 tree.lock_read()
35 try:
36- return _exporters[format](tree, dest, root, subdir, filtered=filtered,
37- per_file_timestamps=per_file_timestamps)
38+ for hook in hooks['pre_export']:
39+ hook(ExportHookParams(tree, dest, format, root, subdir,
40+ filtered, per_file_timestamps))
41+ _exporters[format](tree, dest, root, subdir, filtered=filtered,
42+ per_file_timestamps=per_file_timestamps)
43+ for hook in hooks['post_export']:
44+ hook(ExportHookParams(tree, dest, format, root, subdir,
45+ filtered, per_file_timestamps))
46 finally:
47 tree.unlock()
48
49@@ -176,6 +186,86 @@
50 yield entry
51
52
53+class ExportHooks(_mod_hooks.Hooks):
54+ """A dictionary mapping hook name to a list of callables for export hooks.
55+
56+ e.g. ['pre_export'] is the list of items to be called before bzr
57+ starts exporting files.
58+ """
59+
60+ def __init__(self):
61+ """Create the default hooks.
62+
63+ These are all empty initially, because by default nothing should get
64+ notified.
65+ """
66+ _mod_hooks.Hooks.__init__(self)
67+ self.create_hook(_mod_hooks.HookPoint('pre_export',
68+ "Called with argument ExportHookParams before Bazaar "
69+ "exports the tree. ExportHookParams has the attributes "
70+ "(tree, dest, format, root, subdir, filtered, "
71+ "per_file_timestamps). The first argument is the tree to be "
72+ "exported, other arguments correspond to command "
73+ "line options specified by the user for the export command.",
74+ (2, 3), None))
75+ self.create_hook(_mod_hooks.HookPoint('post_export',
76+ "Called with argument ExportHookParams after Bazaar has "
77+ "exported the tree. ExportHookParams has the attributes "
78+ "(tree, dest, format, root, subdir, filtered, "
79+ "per_file_timestamps). The first argument is the tree to be "
80+ "exported, other arguments correspond to command "
81+ "line options specified by the user for the export command.",
82+ (2, 3), None))
83+
84+
85+class ExportHookParams(object):
86+ """Object holding parameters passed to post_export hooks.
87+
88+ :ivar tree: The tree to export
89+ :ivar dest: The destination where the files etc. should be put
90+ :ivar format: The format (dir, zip, etc)
91+ :ivar root: Name of the root directory inside the exported file
92+ :ivar subdir: The subdirectory to export.
93+ :ivar filtered: If True, content filtering is applied to the
94+ files exported.
95+ :ivar per_file_timestamps: If True, use the timestamp stored in the
96+ tree rather than now().
97+ """
98+
99+ def __init__(self, tree, dest, format=None, root=None, subdir=None,
100+ filtered=False, per_file_timestamps=False):
101+ """Create a group of post_export hook parameters.
102+
103+ :param tree: The tree to export
104+ :param dest: The destination where the files etc. should be put
105+ :param format: The format (dir, zip, etc)
106+ :param root: Name of the root directory inside the exported file
107+ :param subdir: The subdirectory to export.
108+ :param filtered: If True, content filtering is applied to the
109+ files exported.
110+ :param per_file_timestamps: If True, use the timestamp stored in the
111+ tree rather than now().
112+ """
113+ self.tree = tree
114+ self.dest = dest
115+ self.format = format
116+ self.root = root
117+ self.subdir = subdir
118+ self.filtered = filtered
119+ self.per_file_timestamps = per_file_timestamps
120+
121+ def __eq__(self, other):
122+ return self.__dict__ == other.__dict__
123+
124+ def __repr__(self):
125+ return "<%s(%s, %s, %s, %s, %s, %s, %s)>" % (self.__class__.__name__,
126+ self.tree, self.dest, self.format, self.root,
127+ self.subdir, self.filtered, self.per_file_timestamps)
128+
129+
130+hooks = ExportHooks()
131+
132+
133 register_lazy_exporter(None, [], 'bzrlib.export.dir_exporter', 'dir_exporter')
134 register_lazy_exporter('dir', [], 'bzrlib.export.dir_exporter', 'dir_exporter')
135 register_lazy_exporter('tar', ['.tar'], 'bzrlib.export.tar_exporter', 'tar_exporter')
136
137=== modified file 'bzrlib/hooks.py'
138--- bzrlib/hooks.py 2010-08-28 06:13:48 +0000
139+++ bzrlib/hooks.py 2010-09-21 16:04:46 +0000
140@@ -40,6 +40,11 @@
141 'BzrDirHooks')
142 known_hooks.register_lazy(('bzrlib.commands', 'Command.hooks'),
143 'bzrlib.commands', 'CommandHooks')
144+# FIXME: As export is a directory based module (as against a py file), our
145+# lazy registering doesn't see 'hooks' in the first tuple. To work around
146+# this directly list __init__ below. This can possibly be cleaner.
147+known_hooks.register_lazy(('bzrlib.export.__init__', 'hooks'),
148+ 'bzrlib.export', 'ExportHooks')
149 known_hooks.register_lazy(('bzrlib.info', 'hooks'),
150 'bzrlib.info', 'InfoHooks')
151 known_hooks.register_lazy(('bzrlib.lock', 'Lock.hooks'), 'bzrlib.lock',
152
153=== modified file 'bzrlib/tests/test_export.py'
154--- bzrlib/tests/test_export.py 2010-04-14 00:11:32 +0000
155+++ bzrlib/tests/test_export.py 2010-09-21 16:04:46 +0000
156@@ -121,3 +121,59 @@
157 t = self.get_transport('target')
158 self.assertEqual(a_time, t.stat('a').st_mtime)
159 self.assertEqual(b_time, t.stat('b').st_mtime)
160+
161+
162+class TestHooks(tests.TestCaseWithTransport):
163+
164+ def build_tree_and_export(self):
165+ """Build tree and run export."""
166+ self.build_tree(['a/', 'a/b', 'a/c'])
167+ wt = self.make_branch_and_tree('.')
168+ wt.add(['a', 'a/b', 'a/c'])
169+ export.export(wt, 'target', format="dir")
170+
171+ def assertIsExportHookParams(self, params):
172+ """Ensure params is a valid ExportHookParams instance."""
173+ self.assertIsInstance(params, export.ExportHookParams)
174+ attrs = ['tree', 'dest', 'format', 'root', 'subdir',
175+ 'filtered', 'per_file_timestamps']
176+ for a in attrs:
177+ self.assertTrue(hasattr(params, a),
178+ 'Attribute "%s" not found in ExportHookParam' % a)
179+
180+ def test_constructor(self):
181+ """Check that creating a ExportHooks instance has the right defaults.
182+ """
183+ hooks = export.ExportHooks()
184+ self.assertTrue("pre_export" in hooks, "pre_export not in %s" % hooks)
185+ self.assertTrue("post_export" in hooks, "post_export not in %s" % hooks)
186+
187+ def test_installed_hooks_are_ExportHooks(self):
188+ """The installed hooks object should be a ExportHooks.
189+ """
190+ # The installed hooks are saved in self._preserved_hooks.
191+ self.assertIsInstance(self._preserved_hooks[export.__init__][1],
192+ export.__init__.ExportHooks)
193+
194+ def test_post_export_hook(self):
195+ """Ensure that post_export hook is invoked with the right args.
196+ """
197+ calls = []
198+ export.hooks.install_named_hook('post_export', calls.append, None)
199+ self.assertLength(0, calls)
200+ self.build_tree_and_export()
201+ self.assertLength(1, calls)
202+ params = calls[0]
203+ self.assertIsExportHookParams(params)
204+
205+ def test_pre_export_hook(self):
206+ """Ensure that pre_export hook is invoked with the right args.
207+ """
208+ calls = []
209+ export.hooks.install_named_hook('pre_export', calls.append, None)
210+ self.assertLength(0, calls)
211+ self.build_tree_and_export()
212+ self.assertLength(1, calls)
213+ params = calls[0]
214+ self.assertIsExportHookParams(params)
215+