Merge lp:~achiang/hydrazine/batch-operations into lp:hydrazine
- batch-operations
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 93 |
Proposed branch: | lp:~achiang/hydrazine/batch-operations |
Merge into: | lp:hydrazine |
Diff against target: |
243 lines (+120/-23) 2 files modified
README (+23/-1) bugclient (+97/-22) |
To merge this branch: | bzr merge lp:~achiang/hydrazine/batch-operations |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool | Needs Fixing | ||
Martin Packman (community) | Needs Information | ||
Review via email: mp+78702@code.launchpad.net |
Commit message
Description of the change
batchify existing commands
Lots of user-visible documentation written, and even more developer documentation in the individual commits.
Please take a look and let me know what you think.
Depends on:
https:/
Thanks.
Martin Pool (mbp) wrote : | # |
This would be a great feature to have, thanks achiang!
[fix] Please mention this in the readme.
[aside] It might be nice to actually split out the concept of 'active filter' into an object so that we can refine it later, but that can be changed later on.
> What kind of performance do you get against staging with the batch command?
> Currently having an lp_save in each command isn't do bad because it's generally limited by your typing speed, but if changing importance, validity and tags on 100 bugs means 300 saves, I don't know if that's will get us yelled at.
I think the thing that _will_ get us yelled at is if a slip of the finger in a batch command ends up with thousands of bugs accidentally being closed. So that's probably best done through [not having any bugs ;-)] and making it clear what's going to be done before anything happens, or at least before too much of it happens to be irretrievable. For instance we could print 'setting 213 bugs to status closed' as we start.
With performance: generally speaking having a new feature that's slow is better than not having the feature at all, as long as it's not done in a way that will be inordinately hard to improve later. And, in this case, we're automating something previously done manually so even slow operation can only help.
Each lp_save will do a blocking round trip, so if you update 100 bugs it will take some time (depending on rtt). Launchpad doesn't have a 'update multiple bugs' rpc, so to do better we either need to add new apis or (perhaps more likely) overlap multiple requests. The latter may be hard from lplib, and we might need eventually to go to twisted or golang or whatever, but either of them is out of scope.
So in short: good question, but it shouldn't block merging this imo.
[aside] This is getting to the point where it needs some testing, preferably against a mock Launchpad.
[aside] Perhaps hydrazine should print a 'what's new' when it starts.
[fix] 'filter tags' doesn't seem to be implemented?
- 100. By Alex Chiang
-
bugclient: merge with trunk to get NamedEnum() class
Get latest NamedEnum() class from trunk, and convert our old filter
enum to use it properly. - 101. By Alex Chiang
-
README: updated to reflect new batch and filter commands
Update the readme, now that select_new is gone.
Add some helpful examples on how to use filter and batch.
Alex Chiang (achiang) wrote : | # |
Hello gz,
> This looks really interesting. So the main aim of the change is to generalise the way of showing new bugs into a new 'filter' command.
Actually, processing new bugs was quite a minor consideration, it just happens that the new batch and filter commands can be used to do so (hopefully a good sign for the general soundness of the design!).
The main impetus is that I'm involved with projects where we target 30--60 bugs to a milestone, and as developers fix them, they move status to Fix Committed. After the project is QA'ed and released, the milestone is closed, but now we have to change the status on all those bugs to Fix Released.
I have future plans to extend the filters even further, so we can do things like, "select bugs marked fix committed last week", but that will obviously wait for a future MP.
Like poolie, I would prefer to shelve the performance discussion for a later MP as well.
I've updated README as requested by poolie.
Would love it if folks could re-review, thanks!
Martin Pool (mbp) wrote : | # |
thanks for the doc updates.
72 +Caution, without the 'filter' command, you will operate on ALL
73 +active bugs in your pillar, including bugs already Fix Committed.
[fix] That sounds kind of risky. Would it be at all practical to have it just fail if there is no filter? I think it would, just by after the 'for f' block
else:
print 'No filter set?'
return
aside from that it looks good - thanks!
If you just fix that thing, please go ahead and land it.
- 102. By Alex Chiang
-
bugclient: require a filter before doing batch commands
This requirement makes life a little safer. Without it, we had the
potential to do a batch operation on ALL active bugs in the pillar,
which includes bugs with status 'Fix Committed'.If we require a filter, that can mitigate this little pitfall.
Alex Chiang (achiang) wrote : | # |
Fixed, and now landing, per poolie's approval.
Thanks all!
Preview Diff
1 | === modified file 'README' |
2 | --- README 2010-09-02 01:16:39 +0000 |
3 | +++ README 2011-10-12 03:20:22 +0000 |
4 | @@ -20,13 +20,35 @@ |
5 | |
6 | ./bugclient |
7 | pillar bzr |
8 | - select_new |
9 | + filter status new |
10 | + batch |
11 | bug 1234 |
12 | triage confirmed high +dirstate |
13 | next |
14 | |
15 | ``triage`` can set the importance, status, and tags in one go. |
16 | |
17 | +In the example above, we use the plain ``batch`` command after selecting all |
18 | +bugs with status New, and iterate over them one at a time, with ``next``. |
19 | + |
20 | +A more interesting example of how to use the batch command might be to |
21 | +change the status of many bugs after a project has released a milestone:: |
22 | + |
23 | + ./bugclient |
24 | + pillar hydrazine |
25 | + filter milestone 0.2 |
26 | + filter status Fix Committed # note, you may apply multiple filters |
27 | + batch status Fix Released |
28 | + |
29 | +Or perhaps you would like to tag multiple bugs at once:: |
30 | + |
31 | + ./bugclient |
32 | + pillar hydrazine |
33 | + filter importance wishlist |
34 | + batch tag +bluesky |
35 | + |
36 | +See the help for ``filter`` and ``batch`` for more details. |
37 | + |
38 | feed-pqm |
39 | ======== |
40 | |
41 | |
42 | === modified file 'bugclient' |
43 | --- bugclient 2011-10-11 02:39:41 +0000 |
44 | +++ bugclient 2011-10-12 03:20:22 +0000 |
45 | @@ -38,6 +38,7 @@ |
46 | 'Incomplete', 'Invalid', 'New']) |
47 | importance_enum = NamedEnum(['Critical', 'High', 'Medium', 'Low', |
48 | 'Wishlist', 'Undecided']) |
49 | +filter_enum = NamedEnum(['milestone', 'status', 'tags', 'clear']) |
50 | |
51 | class HydrazineCmd(cmd.Cmd): |
52 | |
53 | @@ -47,10 +48,61 @@ |
54 | self.current_bug_number = None |
55 | self.pillar = None |
56 | self.task_list = None |
57 | + self.filters = {} |
58 | |
59 | def _connect(self): |
60 | self.session = hydrazine.create_session() |
61 | |
62 | + def do_batch(self, line): |
63 | + """Perform batch operation on current pillar's bugs. |
64 | +Use the 'filter' command to limit the batch command's scope of bugs. |
65 | + |
66 | +example: |
67 | + filter status new |
68 | + batch # load New bugs; iterate manually with next |
69 | + batch show # show information on all New bugs |
70 | + batch status confirmed # change all New bugs to Confirmed |
71 | + |
72 | +Caution, there is very little error checking. You are about to commit |
73 | +a lot of changes to a lot of bugs. Double-check your command! |
74 | + """ |
75 | + kwargs = {} |
76 | + if not self.filters: |
77 | + print 'No filter set?' |
78 | + return |
79 | + for f in self.filters: |
80 | + if f == "milestone": |
81 | + kwargs[f] = "%s/+milestone/%s" % (self.pillar, self.filters[f]) |
82 | + else: |
83 | + kwargs[f] = self.filters[f] |
84 | + # XXX: is this the best sort order? |
85 | + kwargs['order_by'] = '-datecreated' |
86 | + self.task_list = self.pillar.searchTasks(**kwargs) |
87 | + |
88 | + try: |
89 | + first_bug_task = self.task_list[0] |
90 | + self.search_index = 0 |
91 | + self._select_bug(first_bug_task.bug, "quiet") |
92 | + except IndexError: |
93 | + print "No bugtasks found" |
94 | + return |
95 | + |
96 | + args = line.split() |
97 | + if not args: |
98 | + print "Task list loaded" |
99 | + self._show_bug(self.bug) |
100 | + return |
101 | + |
102 | + # XXX: blind pass-through for now; need to prevent commands that |
103 | + # make no sense in batch mode, such as 'official_tags', etc. |
104 | + for tasks in self.task_list: |
105 | + getattr(self, "do_" + args[0])(' '.join(args[1:])) |
106 | + self.do_next("quiet") |
107 | + |
108 | + # go back to beginning of task_list to do more batch commands |
109 | + self.search_index = 0 |
110 | + self._select_bug(self.task_list[0].bug, "quiet") |
111 | + |
112 | def do_bug(self, bug_number): |
113 | """Open bug by number""" |
114 | try: |
115 | @@ -106,6 +158,36 @@ |
116 | def do_EOF(self, what): |
117 | return True |
118 | |
119 | + def do_filter(self, line): |
120 | + """Add a filter for batch operations |
121 | + |
122 | +examples: |
123 | + filter milestone M1 # milestone named 'M1' |
124 | + filter status new # all New bugs |
125 | + filter tags a b c # tags 'a', 'b', and 'c' |
126 | + filter clear # remove all filters |
127 | + """ |
128 | + args = line.split() |
129 | + if not filter_enum.get(args[0]): |
130 | + print 'Invalid filter. Valid filters are:' |
131 | + _show_columnated(filter_enum.get_all()) |
132 | + return |
133 | + |
134 | + if args[0] == "clear": |
135 | + self.filters = {} |
136 | + return |
137 | + elif args[0] == "status": |
138 | + if status_enum.get(' '.join(args[1:])) == None: |
139 | + print 'invalid status, valid values are:' |
140 | + _show_columnated(status_enum.get_all()) |
141 | + return |
142 | + else: |
143 | + # Normalize status, and toss old junk |
144 | + args[1] = status_enum.get(' '.join(args[1:])) |
145 | + del args[2:] |
146 | + |
147 | + self.filters[args[0]] = ' '.join(args[1:]) |
148 | + |
149 | def do_importance(self, line): |
150 | """Set importance""" |
151 | task = self._needs_single_task() |
152 | @@ -116,10 +198,10 @@ |
153 | print 'invalid importance, valid values are:' |
154 | _show_columnated(importance_enum.get_all()) |
155 | return |
156 | - print 'changing importance %s => %s' % (task.importance, new_importance) |
157 | + print '#%d changing importance %s => %s' % (self.bug.id, task.importance, new_importance) |
158 | task.importance = new_importance |
159 | - print '**** before save' |
160 | if opts.debug: |
161 | + print '**** before save' |
162 | print task._wadl_resource._definition.representation |
163 | try: |
164 | task.lp_save() |
165 | @@ -128,10 +210,10 @@ |
166 | if opts.debug: |
167 | print task._wadl_resource._definition.representation |
168 | |
169 | - def do_next(self, ignored): |
170 | + def do_next(self, show): |
171 | """Go to the next bug in the list""" |
172 | if self.task_list is None: |
173 | - print 'no list loaded; use select_new etc' |
174 | + print 'no list loaded; use a filter and batch to load a list' |
175 | return |
176 | self.search_index += 1 |
177 | try: |
178 | @@ -139,7 +221,9 @@ |
179 | except IndexError: |
180 | print "End of bug list" |
181 | return |
182 | - self._select_bug(bug_task.bug) |
183 | + if show == "": |
184 | + show = "show" |
185 | + self._select_bug(bug_task.bug, show) |
186 | |
187 | def do_official_tags(self, ignored): |
188 | """Show the official tags for the current pillar.""" |
189 | @@ -181,19 +265,6 @@ |
190 | task.target = new_target |
191 | task.lp_save() |
192 | |
193 | - def do_select_new(self, ignored): |
194 | - """Select the list of new bugs in the current pillar""" |
195 | - if self._needs_pillar(): return |
196 | - self.task_list = self.pillar.searchTasks(status="New", |
197 | - order_by=['-datecreated']) |
198 | - self.task_list_index = 0 |
199 | - try: |
200 | - first_bug_task = self.task_list[0] |
201 | - self.search_index = 0 |
202 | - except IndexError: |
203 | - print "No bugtasks found" |
204 | - self._select_bug(first_bug_task.bug) |
205 | - |
206 | def do_show(self, ignored): |
207 | """Show the header of the current bug""" |
208 | if self._needs_bug(): |
209 | @@ -229,7 +300,7 @@ |
210 | print 'invalid status, valid values are:' |
211 | _show_columnated(status_enum.get_all()) |
212 | return |
213 | - print 'changing status %s => %s' % (task.status, new_status) |
214 | + print '#%d changing status %s => %s' % (self.bug.id, task.status, new_status) |
215 | task.status = new_status |
216 | task.lp_save() |
217 | |
218 | @@ -343,7 +414,10 @@ |
219 | |
220 | @property |
221 | def prompt(self): |
222 | - p = 'hydrazine(%s) ' % (self.short_service_root,) |
223 | + p = "" |
224 | + if self.filters: |
225 | + p = 'Current filters: %s\n' % self.filters |
226 | + p += 'hydrazine(%s) ' % (self.short_service_root,) |
227 | if self.current_bug_number is not None: |
228 | p += '#%d ' % (self.current_bug_number,) |
229 | if self.pillar is not None: |
230 | @@ -355,10 +429,11 @@ |
231 | p = p[:-1] |
232 | return p + '> ' |
233 | |
234 | - def _select_bug(self, the_bug): |
235 | + def _select_bug(self, the_bug, show="show"): |
236 | self.bug = the_bug |
237 | self.current_bug_number = the_bug.id |
238 | - self._show_bug(self.bug) |
239 | + if show == "show": |
240 | + self._show_bug(self.bug) |
241 | |
242 | def _find_pillar(self, pillar_name): |
243 | pillar_collection = self.session.pillars.search(text=pillar_name) |
This looks really interesting. So the main aim of the change is to generalise the way of showing new bugs into a new 'filter' command.
What kind of performance do you get against staging with the batch command? I don't know the launchpad api all that well and am a little concerned that just delegating out to the other do_ methods for each bug is not the best way of changing lots of bugs. Currently having an lp_save in each command isn't do bad because it's generally limited by your typing speed, but if changing importance, validity and tags on 100 bugs means 300 saves, I don't know if that's will get us yelled at.
Merge proposal nit, when you have a branch that depends on a change you've proposed separately, remember to set the 'prerequisite branch' option so those changes aren't show in the diff.