Merge lp:~jelmer/bzr/pre_post_command_hooks into lp:bzr
- pre_post_command_hooks
- Merge into bzr.dev
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 |
Related bugs: |
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.
Martin Pool (mbp) wrote : Posted in a previous version of this proposal | # |
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.
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.
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
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?
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.
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.
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.
Vincent Ladeuil (vila) wrote : Posted in a previous version of this proposal | # |
Hmm, thinking more about it.
If HookPoint.run() was named run_ignoring_
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.
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.
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
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
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 'TooManyConcurr
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?
Jelmer Vernooij (jelmer) wrote : Posted in a previous version of this proposal | # |
Hi Brian,
Are you still working on this?
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_
* 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_
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:/
>
> 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
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.
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.
"tip change", suppress_
>
>
> 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
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 BzrAbortCommitE
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.
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 BzrAbortCommitE
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
Vincent Ladeuil (vila) wrote : | # |
Good to land, sorry for the delay.
news^W don't forget details ;)
Jelmer Vernooij (jelmer) wrote : | # |
sent to pqm by email
Preview Diff
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 |
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"?