Merge lp:~qbzr-dev/qbzr/commit_data into lp:~qbzr-dev/qbzr/trunk

Proposed by Alexander Belchenko
Status: Merged
Merged at revision: not available
Proposed branch: lp:~qbzr-dev/qbzr/commit_data
Merge into: lp:~qbzr-dev/qbzr/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~qbzr-dev/qbzr/commit_data
Reviewer Review Type Date Requested Status
Gary van der Merwe Approve
Vincent Ladeuil Pending
Review via email: mp+10040@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Alexander Belchenko (bialix) wrote :

Major refactoring of our internal code that saves/restore/uncommit commit message. Now it becomes more generic and we can easily extend CommitData to save/restore bugfixes ids etc.

The next step is actually implement support for save/restore bugfixes ids data.

Revision history for this message
Alexander Belchenko (bialix) wrote :

I've finished this work. Now this branch supports [commit_data] section in branch.conf with following data:

* message
* bugs
* old_revid - old tip (uncommitted revision)
* new_revid - new tip (after uncommit)

[old_revid, new_revid) -- is uncommitted chain.

Code for support commit_data has been reworked to be as much universal as possible, so in the future it could be reused for other clients. Code for decoding bug urls back to bug numbers or bug ids extracted to separate bugs.py module and also can be reused by other clients. There is enough unittest IMO, but of course there is might be room for improvements.

It works fine in my manual testing. Actually I'm starting to dogfood it.

I'll wait couple of days for review and then planning to land it if nobody will object.

Revision history for this message
Alexander Belchenko (bialix) wrote :

Vincent: if you'll have a chance to look at the final code -- it will be great. I've addressed some of your recent feedback.

Revision history for this message
Alexander Belchenko (bialix) wrote :

Oh, actually I have to Resubmit this proposal to refresh the diff. Please, use recent code from my branch for review.

Revision history for this message
Gary van der Merwe (garyvdm) wrote :

I think that this work is excellent. I approve.

How ever I would like for Vicent to also have a look, as he may have some input on how we make things common with gtk. Maybe we can have a common uncommit hook.

I would also like to see that we look for a commit message from the msgeditor commit_message_template hook, so that we can load commit messaged that plugins that implement this provide. This can be done at a later stage though.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '__init__.py'
2--- __init__.py 2009-08-11 18:52:11 +0000
3+++ __init__.py 2009-08-12 16:08:39 +0000
4@@ -101,13 +101,15 @@
5
6
7 def post_uncommit_hook(local, master, old_revno, old_tip, new_revno, hook_new_tip):
8+ from bzrlib.plugins.qbzr.lib.commit_data import CommitData
9 branch = local or master
10- message = branch.repository.get_revision(old_tip).message
11- config = branch.get_config()
12- config.set_user_option('qbzr_commit_message', message.strip())
13+ ci_data = CommitData(branch=branch)
14+ ci_data.set_data_on_uncommit(old_tip, hook_new_tip)
15+ ci_data.save()
16
17 from bzrlib.branch import Branch
18-Branch.hooks.install_named_hook('post_uncommit', post_uncommit_hook, 'Remember uncomitted message for qcommit')
19+Branch.hooks.install_named_hook('post_uncommit', post_uncommit_hook,
20+ 'Remember uncomitted revision data for qcommit')
21
22
23 def load_tests(basic_tests, module, loader):
24
25=== modified file 'lib/commit.py'
26--- lib/commit.py 2009-08-11 18:47:59 +0000
27+++ lib/commit.py 2009-08-12 16:08:39 +0000
28@@ -24,6 +24,7 @@
29
30 from bzrlib.plugins.qbzr.lib.spellcheck import SpellCheckHighlighter, SpellChecker
31 from bzrlib.plugins.qbzr.lib.autocomplete import get_wordlist_builder
32+from bzrlib.plugins.qbzr.lib.commit_data import CommitData
33 from bzrlib.plugins.qbzr.lib.diff import (
34 DiffButtons,
35 show_diff,
36@@ -216,6 +217,9 @@
37 dialog = dialog,
38 parent = parent)
39 self.tree = tree
40+ self.ci_data = CommitData(tree=tree)
41+ self.ci_data.load()
42+
43 self.is_bound = bool(tree.branch.get_bound_location())
44 self.has_pending_merges = len(tree.get_parent_ids())>1
45
46@@ -301,7 +305,7 @@
47 spell_highlighter = SpellCheckHighlighter(self.message.document(),
48 spell_checker)
49
50- self.restore_message()
51+ self.restore_commit_data()
52 if message:
53 self.message.setText(message)
54 grid.addWidget(self.message, 0, 0, 1, 2)
55@@ -406,7 +410,7 @@
56
57 # Try to be smart: if there is no saved message
58 # then set focus on Edit Area; otherwise on OK button.
59- if self.get_saved_message():
60+ if self.ci_data['message']:
61 self.buttonbox.setFocus()
62 else:
63 self.message.setFocus()
64@@ -517,34 +521,32 @@
65 self.custom_author = self.author.text()
66 self.author.setText(self.default_author)
67
68- def get_saved_message(self):
69- config = self.tree.branch.get_config()._get_branch_data_config()
70- return config.get_user_option('qbzr_commit_message')
71-
72- def restore_message(self):
73- message = self.get_saved_message()
74+ def restore_commit_data(self):
75+ message = self.ci_data['message']
76 if message:
77 self.message.setText(message)
78
79- def save_message(self):
80- if self.tree.branch.control_files.get_physical_lock_status() or \
81- self.tree.branch.is_locked():
82+ def save_commit_data(self):
83+ if (self.tree.branch.control_files.get_physical_lock_status()
84+ or self.tree.branch.is_locked()):
85+ # XXX maybe show this in a GUI MessageBox (information box)???
86 from bzrlib.trace import warning
87- warning("Cannot save commit message because the branch is locked.")
88- else:
89- message = unicode(self.message.toPlainText())
90- config = self.tree.branch.get_config()
91- if message.strip():
92- config.set_user_option('qbzr_commit_message', message)
93- else:
94- if config.get_user_option('qbzr_commit_message'):
95- # FIXME this should delete the config entry, not just set it to ''
96- config.set_user_option('qbzr_commit_message', '')
97+ warning("Cannot save commit data because the branch is locked.")
98+ return
99+ message = unicode(self.message.toPlainText()).strip()
100+ ci_data = CommitData(tree=self.tree)
101+ ci_data.set_data(message=message)
102+ ci_data.save()
103
104- def clear_saved_message(self):
105- config = self.tree.branch.get_config()
106- # FIXME this should delete the config entry, not just set it to ''
107- config.set_user_option('qbzr_commit_message', '')
108+ def wipe_commit_data(self):
109+ if (self.tree.branch.control_files.get_physical_lock_status()
110+ or self.tree.branch.is_locked()):
111+ # XXX maybe show this in a GUI MessageBox (information box)???
112+ from bzrlib.trace import warning
113+ warning("Cannot wipe commit data because the branch is locked.")
114+ return
115+ ci_data = CommitData(tree=self.tree)
116+ ci_data.wipe()
117
118 def do_start(self):
119 args = ["commit"]
120@@ -635,19 +637,19 @@
121 fmodel = self.filelist.tree_filter_model
122 fmodel.setFilter(fmodel.UNVERSIONED, state)
123
124- def _save_or_clear_message(self):
125+ def _save_or_wipe_commit_data(self):
126 if not self.process_widget.is_running():
127 if self.process_widget.finished:
128- self.clear_saved_message()
129+ self.wipe_commit_data()
130 else:
131- self.save_message()
132+ self.save_commit_data()
133
134 def closeEvent(self, event):
135- self._save_or_clear_message()
136+ self._save_or_wipe_commit_data()
137 return SubProcessDialog.closeEvent(self, event)
138
139 def reject(self):
140- self._save_or_clear_message()
141+ self._save_or_wipe_commit_data()
142 return SubProcessDialog.reject(self)
143
144 def update_branch_groupbox(self):
145
146=== added file 'lib/commit_data.py'
147--- lib/commit_data.py 1970-01-01 00:00:00 +0000
148+++ lib/commit_data.py 2009-08-12 16:08:39 +0000
149@@ -0,0 +1,144 @@
150+# -*- coding: utf-8 -*-
151+#
152+# QBzr - Qt frontend to Bazaar commands
153+# Copyright (C) 2009 Alexander Belchenko
154+#
155+# This program is free software; you can redistribute it and/or
156+# modify it under the terms of the GNU General Public License
157+# as published by the Free Software Foundation; either version 2
158+# of the License, or (at your option) any later version.
159+#
160+# This program is distributed in the hope that it will be useful,
161+# but WITHOUT ANY WARRANTY; without even the implied warranty of
162+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
163+# GNU General Public License for more details.
164+#
165+# You should have received a copy of the GNU General Public License
166+# along with this program; if not, write to the Free Software
167+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
168+
169+"""Commit data save/restore support."""
170+
171+
172+class CommitData(object):
173+ """Class to manipulate with commit data.
174+
175+ Hold the data as dictionary and provide dict-like interface.
176+ All strings saved internally as unicode.
177+
178+ Known items for data dict:
179+ message: main commit message.
180+ bugs: list of 'id:number' strings for fixed bugs
181+ authors: list of strings with name(s) of patch author(s)
182+ file_message: dict for per-file commit messages (MySQL),
183+ keys in this dict are filenames/fileids,
184+ values are specific commit messages.
185+ old_revid: old tip revid (before uncommit)
186+ new_revid: new tip revid (after uncommit)
187+
188+ [bialix 20090812] Data saved in branch.conf in [commit_data] section
189+ as plain dict.
190+ """
191+
192+ def __init__(self, branch=None, tree=None):
193+ """Initialize data object attached to some tree.
194+ @param tree: working tree object for commit/uncommit.
195+ @param branch: branch object for commit/uncommit.
196+ """
197+ self._tree = tree
198+ self._branch = branch
199+ self._data = {}
200+
201+ def __nonzero__(self):
202+ """Check if there is some data actually.
203+ @return: True if data dictionary is not empty.
204+ """
205+ return bool(self._data)
206+
207+ def __getitem__(self, key):
208+ """Read the value of specified key via dict-like interface, e.g. a[key].
209+ @param key: key in data dictionary.
210+ @return: value or None if there is no such key.
211+ """
212+ return self._data.get(key)
213+
214+ def __setitem__(self, key, value):
215+ """Set new value for specified key."""
216+ self._data[key] = value
217+
218+ def __delitem__(self, key):
219+ """Delete key from dictionary."""
220+ del self._data[key]
221+
222+ def as_dict(self):
223+ return self._data.copy()
224+
225+ def set_data(self, data=None, **kw):
226+ """Set new data to dictionary (e.g. to save data from commit dialog).
227+ @param data: dictionary with new data.
228+ @param kw: pairs name=value to insert.
229+ """
230+ if data:
231+ self._data.update(data)
232+ for key, value in kw.iteritems():
233+ self._data[key] = value
234+
235+ def set_data_on_uncommit(self, old_revid, new_revid):
236+ """Set data from post_uncommit hook.
237+ @param old_revid: old tip revid (before uncommit)
238+ @param new_revid: new tip revid (after uncommit). Could be None.
239+ """
240+ branch = self._get_branch()
241+ revision = branch.repository.get_revision(old_revid)
242+ # remember revids
243+ self._data['old_revid'] = old_revid
244+ if new_revid is None:
245+ from bzrlib.revision import NULL_REVISION
246+ new_revid = NULL_REVISION
247+ self._data['new_revid'] = new_revid
248+ # set data from revision
249+ self._data['message'] = revision.message
250+
251+ def load(self):
252+ """Load saved data from branch/tree."""
253+ branch = self._get_branch()
254+ config = branch.get_config()
255+ data = config.get_user_option('commit_data')
256+ if data:
257+ self.set_data(data)
258+ else:
259+ # backward compatibility
260+ old_data = config.get_user_option('qbzr_commit_message')
261+ if old_data:
262+ self.set_data(message=old_data)
263+
264+ def save(self):
265+ """Save data to the branch/tree."""
266+ # XXX save should wipe if self._data is empty
267+ branch = self._get_branch()
268+ config = branch.get_config()
269+ config.set_user_option('commit_data', self._data)
270+ # clear old data
271+ if config.get_user_option('qbzr_commit_message'):
272+ config.set_user_option('qbzr_commit_message', '')
273+
274+ def wipe(self):
275+ """Delete saved data from branch/tree config."""
276+ branch = self._get_branch()
277+ config = branch.get_config()
278+ config.set_user_option('commit_data', {})
279+ # clear old data
280+ if config.get_user_option('qbzr_commit_message'):
281+ config.set_user_option('qbzr_commit_message', '')
282+
283+ def _get_branch(self):
284+ """Return branch object if either branch or tree was specified on init.
285+ Raise BzrInternalError otherwise.
286+ """
287+ if self._branch:
288+ return self._branch
289+ if self._tree:
290+ return self._tree.branch
291+ # too bad
292+ from bzrlib import errors
293+ raise errors.BzrInternalError("CommitData has no saved branch or tree.")
294
295=== added file 'lib/tests/Makefile'
296--- lib/tests/Makefile 1970-01-01 00:00:00 +0000
297+++ lib/tests/Makefile 2009-08-12 14:05:52 +0000
298@@ -0,0 +1,2 @@
299+test:
300+ $(MAKE) -C ../../ test
301
302=== modified file 'lib/tests/__init__.py'
303--- lib/tests/__init__.py 2009-07-09 23:27:53 +0000
304+++ lib/tests/__init__.py 2009-08-12 09:44:09 +0000
305@@ -26,6 +26,7 @@
306 'mock',
307 'test_autocomplete',
308 #'test_diffview', - broken by API changes
309+ 'test_commit_data',
310 'test_extra_isignored',
311 'test_extra_isversioned',
312 'test_i18n',
313
314=== added file 'lib/tests/test_commit_data.py'
315--- lib/tests/test_commit_data.py 1970-01-01 00:00:00 +0000
316+++ lib/tests/test_commit_data.py 2009-08-12 15:27:56 +0000
317@@ -0,0 +1,151 @@
318+# -*- coding: utf-8 -*-
319+#
320+# QBzr - Qt frontend to Bazaar commands
321+# Copyright (C) 2009 Alexander Belchenko
322+#
323+# This program is free software; you can redistribute it and/or
324+# modify it under the terms of the GNU General Public License
325+# as published by the Free Software Foundation; either version 2
326+# of the License, or (at your option) any later version.
327+#
328+# This program is distributed in the hope that it will be useful,
329+# but WITHOUT ANY WARRANTY; without even the implied warranty of
330+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
331+# GNU General Public License for more details.
332+#
333+# You should have received a copy of the GNU General Public License
334+# along with this program; if not, write to the Free Software
335+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
336+
337+"""Tests for commit data object and operations."""
338+
339+from bzrlib.tests import TestCase, TestCaseWithTransport
340+from bzrlib.plugins.qbzr.lib.commit_data import (
341+ CommitData,
342+ )
343+
344+
345+class TestCommitDataBase(TestCase):
346+
347+ def test_empty(self):
348+ d = CommitData()
349+ self.assertFalse(bool(d))
350+ self.assertEqual(None, d['message'])
351+ self.assertEqual({}, d.as_dict())
352+
353+ def test_set_data(self):
354+ d = CommitData()
355+ d.set_data({'message': 'foo bar'})
356+ self.assertTrue(bool(d))
357+ self.assertEqual('foo bar', d['message'])
358+ self.assertEqual({'message': 'foo bar'}, d.as_dict())
359+ #
360+ d = CommitData()
361+ d.set_data(message='foo bar')
362+ self.assertTrue(bool(d))
363+ self.assertEqual('foo bar', d['message'])
364+ self.assertEqual({'message': 'foo bar'}, d.as_dict())
365+ #
366+ d = CommitData()
367+ d.set_data({'fixes': 'lp:123456'}, message='foo bar')
368+ self.assertTrue(bool(d))
369+ self.assertEqual('foo bar', d['message'])
370+ self.assertEqual({'message': 'foo bar',
371+ 'fixes': 'lp:123456',
372+ }, d.as_dict())
373+
374+
375+class TestCommitDataWithTree(TestCaseWithTransport):
376+
377+ def test_set_data_on_uncommit(self):
378+ wt = self.make_branch_and_tree('.')
379+ revid1 = wt.commit(message='1')
380+ # imitate uncommit in branch with only one revision
381+ d = CommitData(branch=wt.branch)
382+ d.set_data_on_uncommit(revid1, None)
383+ self.assertEqual({'message': '1',
384+ 'old_revid': revid1,
385+ 'new_revid': 'null:',
386+ }, d.as_dict())
387+ #
388+ revid2 = wt.commit(message='2')
389+ # imitate uncommit in branch with several revisions
390+ d = CommitData(branch=wt.branch)
391+ d.set_data_on_uncommit(revid2, revid1)
392+ self.assertEqual({'message': '2',
393+ 'old_revid': revid2,
394+ 'new_revid': revid1,
395+ }, d.as_dict())
396+
397+ def test_io(self):
398+ wt = self.make_branch_and_tree('.')
399+ # load nothing
400+ d = CommitData(tree=wt)
401+ d.load()
402+ self.assertEqual({}, d.as_dict())
403+ #
404+ # save data
405+ d = CommitData(tree=wt)
406+ d.set_data(message='spam', old_revid='foo', new_revid='bar')
407+ d.save()
408+ # check branch.conf
409+ cfg = wt.branch.get_config()
410+ self.assertEqual({'message': 'spam',
411+ 'old_revid': 'foo',
412+ 'new_revid': 'bar',
413+ }, cfg.get_user_option('commit_data'))
414+ #
415+ # load data later
416+ d = CommitData(tree=wt)
417+ d.load()
418+ self.assertEqual({'message': 'spam',
419+ 'old_revid': 'foo',
420+ 'new_revid': 'bar',
421+ }, d.as_dict())
422+ #
423+ # wipe the data in the end
424+ d = CommitData(tree=wt)
425+ d.wipe()
426+ # check branch.conf
427+ cfg = wt.branch.get_config()
428+ self.assertEqual({}, cfg.get_user_option('commit_data'))
429+
430+ def test_io_old_data_transition(self):
431+ # we should handle old data (i.e. qbzr_commit_message) gracefully
432+ wt = self.make_branch_and_tree('.')
433+ cfg = wt.branch.get_config()
434+ cfg.set_user_option('qbzr_commit_message', 'spam')
435+ # load
436+ d = CommitData(tree=wt)
437+ d.load()
438+ self.assertEqual({'message': 'spam',
439+ }, d.as_dict())
440+ #
441+ # if here both old and new then prefer new
442+ cfg.set_user_option('commit_data', {'foo': 'bar'})
443+ d = CommitData(tree=wt)
444+ d.load()
445+ self.assertEqual({'foo': 'bar',
446+ }, d.as_dict())
447+ #
448+ # on save we should clear old data
449+ d = CommitData(tree=wt)
450+ d.set_data(message='eggs', old_revid='foo', new_revid='bar')
451+ d.save()
452+ # check branch.conf
453+ cfg = wt.branch.get_config()
454+ self.assertEqual('', cfg.get_user_option('qbzr_commit_message'))
455+ self.assertEqual({'message': 'eggs',
456+ 'old_revid': 'foo',
457+ 'new_revid': 'bar',
458+ }, cfg.get_user_option('commit_data'))
459+ #
460+ # on wipe we should clear old data too
461+ cfg = wt.branch.get_config()
462+ cfg.set_user_option('qbzr_commit_message', 'spam')
463+ d = CommitData(tree=wt)
464+ d.wipe()
465+ # check branch.conf
466+ cfg = wt.branch.get_config()
467+ self.assertEqual('', cfg.get_user_option('qbzr_commit_message'))
468+ self.assertEqual({}, cfg.get_user_option('commit_data'))

Subscribers

People subscribed via source and target branches

to status/vote changes: