Merge lp:~jelmer/bzr/pre_post_command_hooks into lp:bzr

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 6441
Proposed branch: lp:~jelmer/bzr/pre_post_command_hooks
Merge into: lp:bzr
Diff against target: 662 lines (+212/-36)
18 files modified
bzrlib/branch.py (+14/-7)
bzrlib/builtins.py (+2/-2)
bzrlib/bzrdir.py (+13/-7)
bzrlib/commands.py (+10/-0)
bzrlib/controldir.py (+4/-4)
bzrlib/mutabletree.py (+9/-1)
bzrlib/plugins/weave_fmt/branch.py (+3/-1)
bzrlib/remote.py (+7/-1)
bzrlib/smart/bzrdir.py (+1/-1)
bzrlib/tests/per_bzrdir/test_bzrdir.py (+1/-1)
bzrlib/tests/per_controldir/test_controldir.py (+2/-2)
bzrlib/tests/per_controldir_colo/test_unsupported.py (+1/-1)
bzrlib/tests/test_commands.py (+79/-0)
bzrlib/tests/test_foreign.py (+7/-2)
bzrlib/tests/test_transform.py (+38/-0)
bzrlib/transform.py (+9/-5)
doc/en/admin-guide/migration.txt (+1/-1)
doc/en/release-notes/bzr-2.5.txt (+11/-0)
To merge this branch: bzr merge lp:~jelmer/bzr/pre_post_command_hooks
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
bzr-core Pending
Review via email: mp+87254@code.launchpad.net

This proposal supersedes a proposal from 2011-12-22.

Commit message

Add pre_command and post_command hooks.

Description of the change

This branch introduces two new command hooks, 'pre_command' and 'post_command', that are triggered prior and after a command is run.

Both hooks are handed the command object that is being run.

This is split out from a branch by Brian, which also added support for suppression exceptions.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

That looks good, thanks.

[fix] This should be mentioned in news and whatsnew.

[idea] Perhaps the whole "call hooks, log failures" should be split
out to a separate function, in hooks.py, since it's called twice here
and perhaps also in other places.

[query] This will mean that if the hooks fail, they will fail totally
silently. I can see how we would want to perhaps not make the command
fail entirely, especially as the hooks may come from out-of-date
plugins, but total silence seems too far the other way. Perhaps you
can give the user at least a single-line warning "pre_command hook %r
failed: %r"?

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

Thanks for working on this. I'll echo Martin's comments, but have two additional ones as well:

I'd rather see the hooks take care of handling exceptions, and logging exceptions quietly if they want to.

You might want to use cmd.outf.write() rather than "print", since IIRC print writes to the actual stdout during selftest.

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

Added mention to the news.
Moved hook execution to new method in HookPoint.run(), that logs hook failure with trace.warning().
Added tests for HookPoint.run()

And to answer Jelmer's points:

> I'd rather see the hooks take care of handling exceptions, and
> logging exceptions quietly if they want to.

My concern, along with all hooks, is that errors in the hooks shouldn't interfere with the actual operations.

> You might want to use cmd.outf.write() rather than "print", since IIRC
> print writes to the actual stdout during selftest.

Oops — those prints weren't supposed to be in there at all.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

On 07/11/11 14:41, Brian de Alwis wrote:
> Added mention to the news.
> Moved hook execution to new method in HookPoint.run(), that logs hook
failure with trace.warning().
> Added tests for HookPoint.run()
>
> And to answer Jelmer's points:
>
>> I'd rather see the hooks take care of handling exceptions, and
>> logging exceptions quietly if they want to.
>
> My concern, along with all hooks, is that errors in the hooks shouldn't
interfere with the actual operations.
In some situations it may be required that hooks are run, since not
running them would break other things. E.g. what if I wanted to add a
hook to "bzr commit" that raises BzrCommandError when there were certain
files in my working tree that should prevent committing? If there was
some bug in my hook I would rather have bzr dump a stracktrace rather
than silently discarding the error and letting me commit.

I'd rather leave the decision of whether failure is fatal or not to the
hook itself.

Cheers,

Jelmer

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

That's a good point Jelmer. Perhaps the hook-definer could specifically enable a set of allowed exceptions?

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

Oh, I forgot to add: my preference is to catch and "suppress" exceptions from hooks: there's nothing worse than having a bzr command throw an exception and look like it's failed, to only find that it's actually a harmless error from a hook. This has happened to me a few times when branching, pushing, and pulling.

Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

@Brian: I agree with jelmer, in the general case (all over the bzr code base) we don't catch exceptions preventively. Catching an exception should always be the "exception", not the norm.

If a hook *can* harmlessly fail then it's up to this hook to catch the exception(s). Having a bzr command involve failing hooks and still succeed is likely to mask bugs which can be serious. If such exceptions are expected, they should be handled specifically, it's likely that many "harmless" exceptions will require a specific handling too.

Registering "allowed" exceptions... I've used that for our test servers but that was a for a very specific case: exceptions related to sockets being shut down and inherently required because they could happen in the client or the server under circumstances outside of our control.

But even with this case, I'm not convinced it's worth generalising the practice. I seem to recall that C++ went this route at one time and a lot of people complained (it has a lot of scalability issues).

In a related spirit, I don't quite get the point about passing an exception to the post hook, this smells like a recipe for bad juju:
1) If the command fails and raises an exception, something bad has happened and the command probably encountered an error, the hook should not even be called in this case,
2) I don't want to make exceptions that a command can raise part of the command API (and passing them to the hook makes them part of the API). We claim to maintain API compatibility and I don't even want to start thinking about maintaining compatibility for exceptions and even less about how we would deprecate them ;)

These hooks are very valuable, let's keep them easy to use.

Also, _setup_run use a hack because we couldn't use 'with' at the time it was introduced, I'd rather use it now that duplicating the hack.

I had to re-read _hook_run twice before realising you were not calling run() there. I then understood why:

8 + self._hook_run()
9 self._setup_run()

was also confusing (that's because hacks are confusing ;).

In a nutshell: hooks, so far, enforce almost no API nor behaviour. As a consequence, we've been able to explore very different use cases for them (have a look at Merger.hooks['merge_file_content'] in merge.py in _compute_transform and _do_merge_contents for a use case which will probably *never* use HookPoint.run()).

I'm very hesitant about restricting the hook usages without more obvious (and uncontroversial) benefits.

I hope I'm not sounding to negative here, I really think we need these hooks.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal

Hmm, thinking more about it.

If HookPoint.run() was named run_ignoring_exceptions() and we have enough use cases to triangulate (at least 3 then, excluding tests ;), I may change my mind.

More than five already existing use cases and I may even agree ;0)

I still think post_hook shouldn't be called if the command raises an exception though (one more reason being that the hooks will have no guarantee about what is available to them).

Ping me on IRC tomorrow (UTC time) if you want to discuss this proposal more interactively.

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> Perhaps the hook-definer could specifically enable a set of allowed exceptions?

Well, there is already a mechanism for them to do that, which is to just catch the exceptions they think are non-fatal. Though perhaps we want to centralize this to make it more consistent across hook functions than it would be if they all did their own thing.

One way to handle this is to have a hook-calling function that checks a (global?) config variable as to what to do with exceptions. By default I think it should let them propagate up, but you could give options to just print them or to squash them entirely. I wouldn't encourage people to set that permanently because the exception may indicate a serious problem, but at least this would give them an escape if the warning is known to be just noise.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

On 07/11/11 17:02, Brian de Alwis wrote:
> That's a good point Jelmer. Perhaps the hook-definer could specifically enable a set of allowed exceptions?
Something like that would make sense. Perhaps this should be a different
merge proposal though?

The pre/post command hooks seem pretty uncontroversial and the code and
tests look good, so let's land that and then see if there are ways in
which exceptions from hooks can be handled.

Cheers,

Jelmer

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

> The pre/post command hooks seem pretty uncontroversial and the code and
> tests look good, so let's land that and then see if there are ways in
> which exceptions from hooks can be handled.

+1

Revision history for this message
Martin Pool (mbp) wrote : Posted in a previous version of this proposal

Hi,

I like the way you separated out the generic run methods.

[tweak] I would suggest renaming .run(), which is a bit generic especially right next to Command.run() into I don't know perhaps Hook.run_all() or Hook.fire().

[query] I see you're still proceeding to run the post-command hooks when one of the pre-command hooks fails, and Hook.run() is running all of the hooks even if one fails. I think we really need to get to grips with this question.

A- I agree it is annoying when a plugin causes a mysterious failure.

B- On the other hand just blindly continuing on after an exception can have bad consequences: ^C is ignored, or the knock-on error masks the real problem (eg 'TooManyConcurrentRequests'), or in the worst case the program ploughs into the ground because of retrying operations that can't safely be retried. I think generally speaking our bias is now to just stop when something goes wrong, but I do recognize A.

I can think of a few options:

- add a configuration option to continue after errors (though, by putting the work on the user this is not really a great solution)
- whitelist some particular exceptions - for me the most obvious would be AttributeError which almost always just means some code is out of date and it can be fairly safely ignored

What do you think?

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

Hi Brian,

Are you still working on this?

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

Sorry Jelmer, but I haven't had much time to work on it further. I'll have some time after Dec 20.

I think it sounds best to:

  * rename Hooks.run() to Hooks.fire() that returns a boolean. If True, then all hooks passed with no exceptions raised. If False, then one or more hooks raised an exception.

  * add a boolean "suppress_exceptions" to Hooks that defaults to false and, if true, provides the behaviour I've currently implemented

  * add a list "passed_exceptions" to contain a list of exceptions that should be passed-through when suppress_exceptions = True (e.g, BzrCommandError)

suppress_exceptions and passed_exceptions are configured at hook definition time; the creator of the hook knows best what to allow.

If you really think AttributeError should be ignorable, we could also add one more field:

  * add a list "ignored_exceptions" to contain a list of exceptions that not affect the return result of Hooks.fire(). The exception will still be logged.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

On 12/22/2011 11:49 PM, Brian de Alwis wrote:
> Brian de Alwis has proposed merging
lp:~slyguy/bzr/pre_post_command_hooks into lp:bzr.
>
> Requested reviews:
> Vincent Ladeuil (vila)
>
> For more details, see:
> https://code.launchpad.net/~slyguy/bzr/pre_post_command_hooks/+merge/86763
>
> This branch introduces two new command hooks, 'pre_command' and
'post_command', that are triggered prior and after a command is run.
>
> pre_command is provided the command object. Any pre_command hook can
abort the command by raising BzrCommandError.
>
> post_command is provided the command object and any exception raised
from the execution of the command. post_command hooks cannot affect the
raising of the exception.
>
> HookPoint can be configured to suppress exceptions when the hooks are
fired, and can also mark certain exception classes as to be passed
through. Any exceptions raised when triggering the hooks are quietly logged.
Hi Brian,

This seems to add the ability to suppress or not suppress exceptions per
hookpoint. Per our prior discussion, I really think this should be
per-hook, as some hooks are more important than others. I.e. wh

Cheers,

Jelmer

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

Hi Jelmer.

On 2011-12-22, at 6:13 PM, Jelmer Vernooij <email address hidden> wrote:
> This seems to add the ability to suppress or not suppress exceptions per hookpoint. Per our prior discussion, I really think this should be
> per-hook, as some hooks are more important than others. I.e. wh

Your message was cut off — I'd really like to understand your use case.

But I should first point out that this mechanism is opt-in and does not change any existing behaviour. Hook providers are free to continue passing through all exceptions. The change simplifies life for hook points that want more control.

If a hook point wants to be able to raise other exceptions, that changes the definition of the hook as the hook firing points may require adaptation.

Or put another way, hook points define an API where the definer undertakes to notify hooks at certain principled points during the execution. For a hook to unilaterally expand the exception pass-through is a change of API that the definer was programmed against.

If a hook needs different handling, perhaps it indicates a need for several hook pints, each with different semantics.

Brian.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

Hi Brian,

On 12/23/2011 01:22 PM, Brian de Alwis wrote:
> On 2011-12-22, at 6:13 PM, Jelmer Vernooij <email address hidden> wrote:
>> This seems to add the ability to suppress or not suppress exceptions
per hookpoint. Per our prior discussion, I really think this should be
>> per-hook, as some hooks are more important than others. I.e. wh
>
> Your message was cut off — I'd really like to understand your use case.
>
> But I should first point out that this mechanism is opt-in and does not
change any existing behaviour. Hook providers are free to continue
passing through all exceptions. The change simplifies life for hook
points that want more control.
Whether we do or don't want to catch exceptions depends on the hook I
think, not on the hook point. I.e. it's fine to ignore exceptions in
hooks that are purely informational, like pre commit hooks that submit
to cia.vc. On the other hand, you could also have a pre commit hook that
ran the testsuite and prevented commits unless the test suite passed.
Unknown exceptions in the first hook are fine. In the second hook,
they're a problem - and they should result in a traceback.

Likewise, I can imagine people wanting to have some mandatory
pre_command_hooks which aren't simply ignored if they have problems.
> If a hook point wants to be able to raise other exceptions, that
changes the definition of the hook as the hook firing points may require
adaptation.
If it wants to raise exceptions that are caught in particular way, then
that does indeed change the definition the hook a bit. Exceptions that
aren't caught are perfectly valid too, though - to indicate serious
issues in the plugin. It might be fine for those exceptions to be
ignored in some situations, but if that is the case depends on the hook.

In other words, I would imagine something like this:

Branch.hooks['post_branch_tip_change'].install_hook(report_tip_change,
"tip change", suppress_exceptions=ue)
>
>
> Or put another way, hook points define an API where the definer
undertakes to notify hooks at certain principled points during the
execution. For a hook to unilaterally expand the exception pass-through
is a change of API that the definer was programmed against.
>
> If a hook needs different handling, perhaps it indicates a need for
several hook pints, each with different semantics.
I think this is potentially true for almost all hook points. Duplicating
the number of hook points we have doesn't seem useful.

Cheers,

Jelmer

Revision history for this message
Brian de Alwis (slyguy) wrote : Posted in a previous version of this proposal

> Whether we do or don't want to catch exceptions depends on the hook I
> think, not on the hook point. I.e. it's fine to ignore exceptions in
> hooks that are purely informational, like pre commit hooks that submit
> to cia.vc. On the other hand, you could also have a pre commit hook that
> ran the testsuite and prevented commits unless the test suite passed.
> Unknown exceptions in the first hook are fine. In the second hook,
> they're a problem - and they should result in a traceback.

I disagree — I believe it should be the defined of the hook point that
decides whether the hooks are informational vs advisory. So in your
second case, I think that outcome should be communicated by a known
exception, like BzrAbortCommitException.

As a committer you have the final say, of course, and I've run out of
time to work on this further anyways. Feel free to modify the patch
as you see fit!

Brian.

Revision history for this message
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal

On 12/24/2011 04:55 AM, Brian de Alwis wrote:
>> Whether we do or don't want to catch exceptions depends on the hook I
>> think, not on the hook point. I.e. it's fine to ignore exceptions in
>> hooks that are purely informational, like pre commit hooks that submit
>> to cia.vc. On the other hand, you could also have a pre commit hook that
>> ran the testsuite and prevented commits unless the test suite passed.
>> Unknown exceptions in the first hook are fine. In the second hook,
>> they're a problem - and they should result in a traceback.
>
> I disagree — I believe it should be the defined of the hook point that
> decides whether the hooks are informational vs advisory. So in your
> second case, I think that outcome should be communicated by a known
> exception, like BzrAbortCommitException.
I agree that in normal case it should raise some well defined exception.

What I mean is, if there's a bug in the commit hook that makes it raise
an unexpected exception (e.g. SyntaxError), then that exception should
be raised all the way through and not be caught and ignored. If such an
exception is ignored, then that can mean a policy that is enforced by
the plugin hook is ignored.

> As a committer you have the final say, of course, and I've run out of
> time to work on this further anyways. Feel free to modify the patch
> as you see fit!
Sorry, I didn't mean to be discouraging. :-/ I'll see if we can at least
land the pre_command / post_command bit of your branch.

Cheers,

Jelmer

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

Good to land, sorry for the delay.

news^W don't forget details ;)

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/branch.py'
2--- bzrlib/branch.py 2012-01-04 17:12:42 +0000
3+++ bzrlib/branch.py 2012-01-18 19:52:54 +0000
4@@ -2038,6 +2038,8 @@
5 :param name: Name of colocated branch to create, if any
6 :return: a branch in this format
7 """
8+ if name is None:
9+ name = a_bzrdir._get_selected_branch()
10 mutter('creating branch %r in %s', self, a_bzrdir.user_url)
11 branch_transport = a_bzrdir.get_branch_transport(self, name=name)
12 control_files = lockable_files.LockableFiles(branch_transport,
13@@ -2060,6 +2062,8 @@
14 def open(self, a_bzrdir, name=None, _found=False, ignore_fallbacks=False,
15 found_repository=None, possible_transports=None):
16 """See BranchFormat.open()."""
17+ if name is None:
18+ name = a_bzrdir._get_selected_branch()
19 if not _found:
20 format = BranchFormatMetadir.find_format(a_bzrdir, name=name)
21 if format.__class__ != self.__class__:
22@@ -2305,12 +2309,13 @@
23 mutter('creating branch reference in %s', a_bzrdir.user_url)
24 if a_bzrdir._format.fixed_components:
25 raise errors.IncompatibleFormat(self, a_bzrdir._format)
26+ if name is None:
27+ name = a_bzrdir._get_selected_branch()
28 branch_transport = a_bzrdir.get_branch_transport(self, name=name)
29 branch_transport.put_bytes('location',
30 target_branch.user_url)
31 branch_transport.put_bytes('format', self.as_string())
32- branch = self.open(
33- a_bzrdir, name, _found=True,
34+ branch = self.open(a_bzrdir, name, _found=True,
35 possible_transports=[target_branch.bzrdir.root_transport])
36 self._run_post_branch_init_hooks(a_bzrdir, name, branch)
37 return branch
38@@ -2342,6 +2347,8 @@
39 a_bzrdir.
40 :param possible_transports: An optional reusable transports list.
41 """
42+ if name is None:
43+ name = a_bzrdir._get_selected_branch()
44 if not _found:
45 format = BranchFormatMetadir.find_format(a_bzrdir, name=name)
46 if format.__class__ != self.__class__:
47@@ -2351,8 +2358,7 @@
48 location = self.get_reference(a_bzrdir, name)
49 real_bzrdir = controldir.ControlDir.open(
50 location, possible_transports=possible_transports)
51- result = real_bzrdir.open_branch(name=name,
52- ignore_fallbacks=ignore_fallbacks,
53+ result = real_bzrdir.open_branch(ignore_fallbacks=ignore_fallbacks,
54 possible_transports=possible_transports)
55 # this changes the behaviour of result.clone to create a new reference
56 # rather than a copy of the content of the branch.
57@@ -2445,10 +2451,11 @@
58 """Create new branch object at a particular location."""
59 if a_bzrdir is None:
60 raise ValueError('a_bzrdir must be supplied')
61- else:
62- self.bzrdir = a_bzrdir
63+ if name is None:
64+ raise ValueError('name must be supplied')
65+ self.bzrdir = a_bzrdir
66 self._user_transport = self.bzrdir.transport.clone('..')
67- if name is not None:
68+ if name != "":
69 self._user_transport.set_segment_parameter(
70 "branch", urlutils.escape(name))
71 self._base = self._user_transport.base
72
73=== modified file 'bzrlib/builtins.py'
74--- bzrlib/builtins.py 2012-01-16 14:45:48 +0000
75+++ bzrlib/builtins.py 2012-01-18 19:52:54 +0000
76@@ -1444,13 +1444,13 @@
77 else:
78 dir = controldir.ControlDir.open_containing(location)[0]
79 try:
80- active_branch = dir.open_branch(name=None)
81+ active_branch = dir.open_branch(name="")
82 except errors.NotBranchError:
83 active_branch = None
84 branches = dir.get_branches()
85 names = {}
86 for name, branch in branches.iteritems():
87- if name is None:
88+ if name == "":
89 continue
90 active = (active_branch is not None and
91 active_branch.base == branch.base)
92
93=== modified file 'bzrlib/bzrdir.py'
94--- bzrlib/bzrdir.py 2011-12-22 19:54:56 +0000
95+++ bzrlib/bzrdir.py 2012-01-18 19:52:54 +0000
96@@ -829,7 +829,9 @@
97
98 def destroy_branch(self, name=None):
99 """See BzrDir.create_branch."""
100- if name is not None:
101+ if name is None:
102+ name = self._get_selected_branch()
103+ if name != "":
104 raise errors.NoColocatedBranchSupport(self)
105 self.transport.delete_tree('branch')
106
107@@ -887,7 +889,9 @@
108
109 def get_branch_transport(self, branch_format, name=None):
110 """See BzrDir.get_branch_transport()."""
111- if name is not None:
112+ if name is None:
113+ name = self._get_selected_branch()
114+ if name != "":
115 raise errors.NoColocatedBranchSupport(self)
116 # XXX: this shouldn't implicitly create the directory if it's just
117 # promising to get a transport -- mbp 20090727
118@@ -1021,7 +1025,7 @@
119 :param name: Optional branch name to use
120 :return: Relative path to branch
121 """
122- if name is None:
123+ if name == "":
124 return 'branch'
125 return urlutils.join('branches', name.encode("utf-8"))
126
127@@ -1056,7 +1060,7 @@
128 if name is None:
129 name = self._get_selected_branch()
130 path = self._get_branch_path(name)
131- if name is not None:
132+ if name != "":
133 self.control_files.lock_write()
134 try:
135 branches = self._read_branch_list()
136@@ -1073,17 +1077,19 @@
137 """See ControlDir.get_branches."""
138 ret = {}
139 try:
140- ret[None] = self.open_branch()
141+ ret[""] = self.open_branch(name="")
142 except (errors.NotBranchError, errors.NoRepositoryPresent):
143 pass
144
145 for name in self._read_branch_list():
146- ret[name] = self.open_branch(name.decode('utf-8'))
147+ ret[name] = self.open_branch(name=name.decode('utf-8'))
148
149 return ret
150
151 def get_branch_transport(self, branch_format, name=None):
152 """See BzrDir.get_branch_transport()."""
153+ if name is None:
154+ name = self._get_selected_branch()
155 path = self._get_branch_path(name)
156 # XXX: this shouldn't implicitly create the directory if it's just
157 # promising to get a transport -- mbp 20090727
158@@ -1093,7 +1099,7 @@
159 branch_format.get_format_string()
160 except NotImplementedError:
161 raise errors.IncompatibleFormat(branch_format, self._format)
162- if name is not None:
163+ if name != "":
164 try:
165 self.transport.mkdir('branches', mode=self._get_mkdir_mode())
166 except errors.FileExists:
167
168=== modified file 'bzrlib/commands.py'
169--- bzrlib/commands.py 2011-12-18 12:46:49 +0000
170+++ bzrlib/commands.py 2012-01-18 19:52:54 +0000
171@@ -689,11 +689,15 @@
172 """
173 class_run = self.run
174 def run(*args, **kwargs):
175+ for hook in Command.hooks['pre_command']:
176+ hook(self)
177 self._operation = cleanup.OperationWithCleanups(class_run)
178 try:
179 return self._operation.run_simple(*args, **kwargs)
180 finally:
181 del self._operation
182+ for hook in Command.hooks['post_command']:
183+ hook(self)
184 self.run = run
185
186 def run(self):
187@@ -787,6 +791,12 @@
188 " is safe to mutate - e.g. to remove a command. "
189 "list_commands should return the updated set of command names.",
190 (1, 17))
191+ self.add_hook('pre_command',
192+ "Called prior to executing a command. Called with the command "
193+ "object.", (2, 5))
194+ self.add_hook('post_command',
195+ "Called after executing a command. Called with the command "
196+ "object.", (2, 5))
197
198 Command.hooks = CommandHooks()
199
200
201=== modified file 'bzrlib/controldir.py'
202--- bzrlib/controldir.py 2012-01-03 13:47:01 +0000
203+++ bzrlib/controldir.py 2012-01-18 19:52:54 +0000
204@@ -116,7 +116,7 @@
205 :return: Dictionary mapping branch names to instances.
206 """
207 try:
208- return { None: self.open_branch() }
209+ return { "": self.open_branch() }
210 except (errors.NotBranchError, errors.NoRepositoryPresent):
211 return {}
212
213@@ -294,9 +294,9 @@
214 :return: Name of the branch selected by the user, or None.
215 """
216 branch = self.root_transport.get_segment_parameters().get("branch")
217- if branch is not None:
218- branch = urlutils.unescape(branch)
219- return branch
220+ if branch is None:
221+ branch = ""
222+ return urlutils.unescape(branch)
223
224 def has_workingtree(self):
225 """Tell if this controldir contains a working tree.
226
227=== modified file 'bzrlib/mutabletree.py'
228--- bzrlib/mutabletree.py 2012-01-06 14:09:04 +0000
229+++ bzrlib/mutabletree.py 2012-01-18 19:52:54 +0000
230@@ -520,10 +520,18 @@
231 "called with a bzrlib.mutabletree.PostCommitHookParams object. "
232 "The mutable tree the commit was performed on is available via "
233 "the mutable_tree attribute of that object.", (2, 0))
234+ self.add_hook('pre_transform',
235+ "Called before a tree transform on this tree. The hook is called "
236+ "with the tree that is being transformed and the transform.",
237+ (2, 5))
238 self.add_hook('post_build_tree',
239 "Called after a completely new tree is built. The hook is "
240 "called with the tree as its only argument.", (2, 5))
241-
242+ self.add_hook('post_transform',
243+ "Called after a tree transform has been performed on a tree. "
244+ "The hook is called with the tree that is being transformed and "
245+ "the transform.",
246+ (2, 5))
247
248 # install the default hooks into the MutableTree class.
249 MutableTree.hooks = MutableTreeHooks()
250
251=== modified file 'bzrlib/plugins/weave_fmt/branch.py'
252--- bzrlib/plugins/weave_fmt/branch.py 2011-12-19 13:23:58 +0000
253+++ bzrlib/plugins/weave_fmt/branch.py 2012-01-18 19:52:54 +0000
254@@ -138,7 +138,9 @@
255 def open(self, a_bzrdir, name=None, _found=False, ignore_fallbacks=False,
256 found_repository=None, possible_transports=None):
257 """See BranchFormat.open()."""
258- if name is not None:
259+ if name is None:
260+ name = a_bzrdir._get_selected_branch()
261+ if name != "":
262 raise errors.NoColocatedBranchSupport(self)
263 if not _found:
264 # we are being called directly and must probe.
265
266=== modified file 'bzrlib/remote.py'
267--- bzrlib/remote.py 2012-01-04 16:02:14 +0000
268+++ bzrlib/remote.py 2012-01-18 19:52:54 +0000
269@@ -625,6 +625,8 @@
270
271 def create_branch(self, name=None, repository=None,
272 append_revisions_only=None):
273+ if name is None:
274+ name = self._get_selected_branch()
275 # as per meta1 formats - just delegate to the format object which may
276 # be parameterised.
277 real_branch = self._format.get_branch_format().initialize(self,
278@@ -724,6 +726,8 @@
279
280 def open_branch(self, name=None, unsupported=False,
281 ignore_fallbacks=False, possible_transports=None):
282+ if name is None:
283+ name = self._get_selected_branch()
284 if unsupported:
285 raise NotImplementedError('unsupported flag support not implemented yet.')
286 if self._next_open_branch_result is not None:
287@@ -3102,6 +3106,8 @@
288
289 def initialize(self, a_bzrdir, name=None, repository=None,
290 append_revisions_only=None):
291+ if name is None:
292+ name = a_bzrdir._get_selected_branch()
293 # 1) get the network name to use.
294 if self._custom_format:
295 network_name = self._custom_format.network_name()
296@@ -3122,7 +3128,7 @@
297 # Creating on a remote bzr dir.
298 # 2) try direct creation via RPC
299 path = a_bzrdir._path_for_remote_call(a_bzrdir._client)
300- if name is not None:
301+ if name != "":
302 # XXX JRV20100304: Support creating colocated branches
303 raise errors.NoColocatedBranchSupport(self)
304 verb = 'BzrDir.create_branch'
305
306=== modified file 'bzrlib/smart/bzrdir.py'
307--- bzrlib/smart/bzrdir.py 2011-12-19 13:23:58 +0000
308+++ bzrlib/smart/bzrdir.py 2012-01-18 19:52:54 +0000
309@@ -268,7 +268,7 @@
310 self.transport_from_client_path(path))
311 format = branch.network_format_registry.get(network_name)
312 bzrdir.branch_format = format
313- result = format.initialize(bzrdir)
314+ result = format.initialize(bzrdir, name="")
315 rich_root, tree_ref, external_lookup = self._format_to_capabilities(
316 result.repository._format)
317 branch_format = result._format.network_name()
318
319=== modified file 'bzrlib/tests/per_bzrdir/test_bzrdir.py'
320--- bzrlib/tests/per_bzrdir/test_bzrdir.py 2011-12-12 12:09:50 +0000
321+++ bzrlib/tests/per_bzrdir/test_bzrdir.py 2012-01-18 19:52:54 +0000
322@@ -693,5 +693,5 @@
323 raise TestNotApplicable('Format does not support colocation')
324 reference = branch.BranchReferenceFormat().initialize(
325 repo.bzrdir, target_branch=target_branch)
326- self.assertEqual(set([None, 'foo']),
327+ self.assertEqual(set(["", 'foo']),
328 set(repo.bzrdir.get_branches().keys()))
329
330=== modified file 'bzrlib/tests/per_controldir/test_controldir.py'
331--- bzrlib/tests/per_controldir/test_controldir.py 2011-12-27 12:18:36 +0000
332+++ bzrlib/tests/per_controldir/test_controldir.py 2012-01-18 19:52:54 +0000
333@@ -1195,7 +1195,7 @@
334 def test_get_branches(self):
335 repo = self.make_repository('branch-1')
336 target_branch = repo.bzrdir.create_branch()
337- self.assertEqual([None], repo.bzrdir.get_branches().keys())
338+ self.assertEqual([""], repo.bzrdir.get_branches().keys())
339
340 def test_create_repository(self):
341 # a bzrdir can construct a repository for itself.
342@@ -1342,7 +1342,7 @@
343 raise TestSkipped("Can't initialize %r on transport %r"
344 % (self.bzrdir_format, t))
345 dir = bzrdir.BzrDir.open(t.base)
346- self.assertIs(None, dir._get_selected_branch())
347+ self.assertEqual(u"", dir._get_selected_branch())
348
349 def test_root_transport(self):
350 dir = self.make_bzrdir('.')
351
352=== modified file 'bzrlib/tests/per_controldir_colo/test_unsupported.py'
353--- bzrlib/tests/per_controldir_colo/test_unsupported.py 2011-12-11 04:07:08 +0000
354+++ bzrlib/tests/per_controldir_colo/test_unsupported.py 2012-01-18 19:52:54 +0000
355@@ -73,4 +73,4 @@
356 made_control = self.make_bzrdir_with_repo()
357 made_control.create_branch()
358 self.assertEqual(made_control.get_branches().keys(),
359- [None])
360+ [""])
361
362=== modified file 'bzrlib/tests/test_commands.py'
363--- bzrlib/tests/test_commands.py 2011-12-09 14:24:21 +0000
364+++ bzrlib/tests/test_commands.py 2012-01-18 19:52:54 +0000
365@@ -25,6 +25,7 @@
366 errors,
367 option,
368 tests,
369+ trace,
370 )
371 from bzrlib.commands import display_command
372 from bzrlib.tests import TestSkipped
373@@ -370,3 +371,81 @@
374 cmds = list(commands.all_command_names())
375 self.assertEqual(['called'], hook_calls)
376 self.assertSubset(['foo', 'bar'], cmds)
377+
378+class TestPreAndPostCommandHooks(tests.TestCase):
379+ class TestError(StandardError):
380+ __doc__ = """A test exception."""
381+
382+ def test_pre_and_post_hooks(self):
383+ hook_calls = []
384+
385+ def pre_command(cmd):
386+ self.assertEqual([], hook_calls)
387+ hook_calls.append('pre')
388+
389+ def post_command(cmd):
390+ self.assertEqual(['pre', 'run'], hook_calls)
391+ hook_calls.append('post')
392+
393+ def run(cmd):
394+ self.assertEqual(['pre'], hook_calls)
395+ hook_calls.append('run')
396+
397+ self.overrideAttr(builtins.cmd_rocks, 'run', run)
398+ commands.install_bzr_command_hooks()
399+ commands.Command.hooks.install_named_hook(
400+ "pre_command", pre_command, None)
401+ commands.Command.hooks.install_named_hook(
402+ "post_command", post_command, None)
403+
404+ self.assertEqual([], hook_calls)
405+ self.run_bzr(['rocks', '-Oxx=12', '-Oyy=foo'])
406+ self.assertEqual(['pre', 'run', 'post'], hook_calls)
407+
408+ def test_post_hook_provided_exception(self):
409+ hook_calls = []
410+
411+ def post_command(cmd):
412+ hook_calls.append('post')
413+
414+ def run(cmd):
415+ hook_calls.append('run')
416+ raise self.TestError()
417+
418+ self.overrideAttr(builtins.cmd_rocks, 'run', run)
419+ commands.install_bzr_command_hooks()
420+ commands.Command.hooks.install_named_hook(
421+ "post_command", post_command, None)
422+
423+ self.assertEqual([], hook_calls)
424+ self.assertRaises(self.TestError, commands.run_bzr, [u'rocks'])
425+ self.assertEqual(['run', 'post'], hook_calls)
426+
427+ def test_pre_command_error(self):
428+ """Ensure an BzrCommandError in pre_command aborts the command"""
429+
430+ hook_calls = []
431+
432+ def pre_command(cmd):
433+ hook_calls.append('pre')
434+ # verify that all subclasses of BzrCommandError caught too
435+ raise errors.BzrOptionError()
436+
437+ def post_command(cmd, e):
438+ self.fail('post_command should not be called')
439+
440+ def run(cmd):
441+ self.fail('command should not be called')
442+
443+ self.overrideAttr(builtins.cmd_rocks, 'run', run)
444+ commands.install_bzr_command_hooks()
445+ commands.Command.hooks.install_named_hook(
446+ "pre_command", pre_command, None)
447+ commands.Command.hooks.install_named_hook(
448+ "post_command", post_command, None)
449+
450+ self.assertEqual([], hook_calls)
451+ self.assertRaises(errors.BzrCommandError,
452+ commands.run_bzr, [u'rocks'])
453+ self.assertEqual(['pre'], hook_calls)
454+
455
456=== modified file 'bzrlib/tests/test_foreign.py'
457--- bzrlib/tests/test_foreign.py 2011-12-19 19:15:58 +0000
458+++ bzrlib/tests/test_foreign.py 2012-01-18 19:52:54 +0000
459@@ -240,6 +240,8 @@
460
461 def open(self, a_bzrdir, name=None, _found=False, ignore_fallbacks=False,
462 found_repository=None):
463+ if name is None:
464+ name = a_bzrdir._get_selected_branch()
465 if not _found:
466 raise NotImplementedError
467 try:
468@@ -251,7 +253,8 @@
469 return DummyForeignVcsBranch(_format=self,
470 _control_files=control_files,
471 a_bzrdir=a_bzrdir,
472- _repository=found_repository)
473+ _repository=found_repository,
474+ name=name)
475 except errors.NoSuchFile:
476 raise errors.NotBranchError(path=transport.base)
477
478@@ -317,7 +320,9 @@
479
480 def open_branch(self, name=None, unsupported=False, ignore_fallbacks=True,
481 possible_transports=None):
482- if name is not None:
483+ if name is None:
484+ name = self._get_selected_branch()
485+ if name != "":
486 raise errors.NoColocatedBranchSupport(self)
487 return self._format.get_branch_format().open(self, _found=True)
488
489
490=== modified file 'bzrlib/tests/test_transform.py'
491--- bzrlib/tests/test_transform.py 2011-11-17 13:45:49 +0000
492+++ bzrlib/tests/test_transform.py 2012-01-18 19:52:54 +0000
493@@ -60,6 +60,7 @@
494 pathjoin,
495 )
496 from bzrlib.merge import Merge3Merger, Merger
497+from bzrlib.mutabletree import MutableTree
498 from bzrlib.tests import (
499 features,
500 TestCaseInTempDir,
501@@ -3717,3 +3718,40 @@
502 remaining_conflicts.pop())
503 self.assertLength(1, warnings)
504 self.assertStartsWith(warnings[0], 'donttouchmypreciouuus')
505+
506+
507+class TestTransformHooks(tests.TestCaseWithTransport):
508+
509+ def setUp(self):
510+ super(TestTransformHooks, self).setUp()
511+ self.wt = self.make_branch_and_tree('.')
512+ os.chdir('..')
513+
514+ def get_transform(self):
515+ transform = TreeTransform(self.wt)
516+ self.addCleanup(transform.finalize)
517+ return transform, transform.root
518+
519+ def test_pre_commit_hooks(self):
520+ calls = []
521+ def record_pre_transform(tree, tt):
522+ calls.append((tree, tt))
523+ MutableTree.hooks.install_named_hook('pre_transform',
524+ record_pre_transform, "Pre transform")
525+ transform, root = self.get_transform()
526+ old_root_id = transform.tree_file_id(root)
527+ transform.apply()
528+ self.assertEqual(old_root_id, self.wt.get_root_id())
529+ self.assertEquals([(self.wt, transform)], calls)
530+
531+ def test_post_commit_hooks(self):
532+ calls = []
533+ def record_post_transform(tree, tt):
534+ calls.append((tree, tt))
535+ MutableTree.hooks.install_named_hook('post_transform',
536+ record_post_transform, "Post transform")
537+ transform, root = self.get_transform()
538+ old_root_id = transform.tree_file_id(root)
539+ transform.apply()
540+ self.assertEqual(old_root_id, self.wt.get_root_id())
541+ self.assertEquals([(self.wt, transform)], calls)
542
543=== modified file 'bzrlib/transform.py'
544--- bzrlib/transform.py 2011-12-19 17:39:35 +0000
545+++ bzrlib/transform.py 2012-01-18 19:52:54 +0000
546@@ -50,6 +50,7 @@
547 ExistingLimbo, ImmortalLimbo, NoFinalPath,
548 UnableCreateSymlink)
549 from bzrlib.filters import filtered_output_bytes, ContentFilterContext
550+from bzrlib.mutabletree import MutableTree
551 from bzrlib.osutils import (
552 delete_any,
553 file_kind,
554@@ -57,7 +58,6 @@
555 pathjoin,
556 sha_file,
557 splitpath,
558- supports_executable,
559 )
560 from bzrlib.progress import ProgressPhase
561 from bzrlib.symbol_versioning import (
562@@ -156,6 +156,8 @@
563 """
564 if self._tree is None:
565 return
566+ for hook in MutableTree.hooks['post_transform']:
567+ hook(self._tree, self)
568 self._tree.unlock()
569 self._tree = None
570
571@@ -230,7 +232,7 @@
572 irrelevant.
573
574 """
575- new_roots = [k for k, v in self._new_parent.iteritems() if v is
576+ new_roots = [k for k, v in self._new_parent.iteritems() if v ==
577 ROOT_PARENT]
578 if len(new_roots) < 1:
579 return
580@@ -626,7 +628,7 @@
581 for trans_id in self._new_parent:
582 seen = set()
583 parent_id = trans_id
584- while parent_id is not ROOT_PARENT:
585+ while parent_id != ROOT_PARENT:
586 seen.add(parent_id)
587 try:
588 parent_id = self.final_parent(parent_id)
589@@ -642,7 +644,7 @@
590 """If parent directories are versioned, children must be versioned."""
591 conflicts = []
592 for parent_id, children in by_parent.iteritems():
593- if parent_id is ROOT_PARENT:
594+ if parent_id == ROOT_PARENT:
595 continue
596 if self.final_file_id(parent_id) is not None:
597 continue
598@@ -741,7 +743,7 @@
599 """Children must have a directory parent"""
600 conflicts = []
601 for parent_id, children in by_parent.iteritems():
602- if parent_id is ROOT_PARENT:
603+ if parent_id == ROOT_PARENT:
604 continue
605 no_children = True
606 for child_id in children:
607@@ -1722,6 +1724,8 @@
608 calculating one.
609 :param _mover: Supply an alternate FileMover, for testing
610 """
611+ for hook in MutableTree.hooks['pre_transform']:
612+ hook(self._tree, self)
613 if not no_conflicts:
614 self._check_malformed()
615 child_pb = ui.ui_factory.nested_progress_bar()
616
617=== modified file 'doc/en/admin-guide/migration.txt'
618--- doc/en/admin-guide/migration.txt 2010-08-13 19:08:57 +0000
619+++ doc/en/admin-guide/migration.txt 2012-01-18 19:52:54 +0000
620@@ -47,7 +47,7 @@
621 Bazaar's `svn`_ plugin provides tools for interaction with Subversion
622 projects. In fact, Bazaar can be used transparently with projects stored in
623 Subversion, but that is beyond the scope of this document. (See
624-http://doc.bazaar.canonical.com/en/migration/foreign/bzr-on-svn-projects.html for
625+http://doc.bazaar.canonical.com/migration/en/foreign/bzr-on-svn-projects.html for
626 more on that subject.) What is relevant here is the ``svn-import`` command
627 provided by that plugin. This can import an entire subversion repository
628 including tags and branches, particularly if they are stored in Subversion's
629
630=== modified file 'doc/en/release-notes/bzr-2.5.txt'
631--- doc/en/release-notes/bzr-2.5.txt 2012-01-16 17:24:42 +0000
632+++ doc/en/release-notes/bzr-2.5.txt 2012-01-18 19:52:54 +0000
633@@ -26,12 +26,19 @@
634 .. Improvements to existing commands, especially improved performance
635 or memory usage, or better results.
636
637+* Two new command hooks, ``pre_command`` and ``post_command``,
638+ provide notification before and after a command has been run.
639+ (Brian de Alwis, Jelmer Vernooij)
640+
641 Bug Fixes
642 *********
643
644 .. Fixes for situations where bzr would previously crash or give incorrect
645 or undesirable results.
646
647+* Test for equality instead of object identity where ROOT_PARENT is concerned.
648+ (Wouter van Heyst, #881142)
649+
650 Documentation
651 *************
652
653@@ -49,6 +56,10 @@
654 .. Major internal changes, unlikely to be visible to users or plugin
655 developers, but interesting for bzr developers.
656
657+* ``MutableTree`` has two new hooks ``pre_transform`` and
658+ ``post_transform`` that are called for tree transform operations.
659+ (Jelmer Vernooij, #912084)
660+
661 Testing
662 *******
663