[view] Add view that show diff (content and commit message) between the current revision of a patch and an obsolete one.

This depends on ticket #6354109 in cubicweb-vcsfile.

Note that new views use bootstrap CSS.

(closes #2168025)

authorRabah Meradi <rabah.meradi@logilab.fr>
changeset655ee937d6a4
branchdefault
phasepublic
hiddenno
parent revision#a2376a41e407 [sobjects] Do not generate notifications on state change when coming from 'outdated' state
child revision#b43bc8c6d780 Version 2.3.0
files modified by this revision
__pkginfo__.py
cubicweb-vcreview.spec
data/cubes.vcreview.css
data/cubes.vcreview.js
debian/control
i18n/en.po
i18n/fr.po
views/primary.py
# HG changeset patch
# User Rabah Meradi <rabah.meradi@logilab.fr>
# Date 1434615079 -7200
# Thu Jun 18 10:11:19 2015 +0200
# Node ID 655ee937d6a4a09eae7072795f6a9cb1996e7148
# Parent a2376a41e407dba7e40c7ae770c717605654a56e
[view] Add view that show diff (content and commit message) between the current revision of a patch and an obsolete one.

This depends on ticket #6354109 in cubicweb-vcsfile.

Note that new views use bootstrap CSS.

(closes #2168025)

diff --git a/__pkginfo__.py b/__pkginfo__.py
@@ -18,11 +18,11 @@
1             'Programming Language :: Python',
2             'Programming Language :: JavaScript',
3      ]
4 
5  __depends__ =  {'cubicweb': '>= 3.19.0',
6 -                'cubicweb-vcsfile': '>= 2.0.0',
7 +                'cubicweb-vcsfile': '>= 2.2.0',
8                  'cubicweb-comment': '>= 1.8.0',
9                  'cubicweb-task': None,
10                  'cubicweb-nosylist': None,
11                  }
12 
diff --git a/cubicweb-vcreview.spec b/cubicweb-vcreview.spec
@@ -19,11 +19,11 @@
13  BuildArch:      noarch
14  BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-buildroot
15 
16  BuildRequires:  %{python} %{python}-setuptools
17  Requires:       cubicweb >= 3.19.0
18 -Requires:       cubicweb-vcsfile >= 2.0.0
19 +Requires:       cubicweb-vcsfile >= 2.2.0
20  Requires:       cubicweb-comment >= 1.8.0
21  Requires:       cubicweb-task 
22  Requires:       cubicweb-nosylist
23 
24  %description
diff --git a/data/cubes.vcreview.css b/data/cubes.vcreview.css
@@ -131,6 +131,14 @@
25  .vcreview_task input.task_is_done:hover {
26      cursor: pointer;
27  }
28  .vcreview_task h5 {
29      font-size: 90%;
30 -}
31 \ No newline at end of file
32 +}
33 +
34 +/* Revision comparison form styling */
35 +
36 +.icon-diff {
37 +    display: block;
38 +    left: 25px; top: 25px;
39 +    height: 25px; width: 25px;
40 +}
diff --git a/data/cubes.vcreview.js b/data/cubes.vcreview.js
@@ -2,17 +2,18 @@
41   *  :organization: Logilab
42   *  :copyright: 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
43   *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
44   */
45 
46 +cw.cubes.vcreview = new Namespace('cw.cubes.vcreview');
47 +
48 
49  /* this function is called on to add activity from inlined form
50   *
51   * It calls the [add|eid]_activity method on the jsoncontroller and [re]load
52   * only the view for the added or edited activity
53   */
54 -
55  function addActivity(eid, parentcreated, context) {
56      validateForm(
57  	'has_activityForm' + eid, null,
58  	function(result, formid, cbargs) {
59  	    if (parentcreated) {
@@ -41,5 +42,40 @@
60          });
61          return false;
62      });
63  });
64 
65 +jQuery.extend(cw.cubes.vcreview, {
66 +    /*
67 +     * Make an ajax call to retreive the diff.
68 +     */
69 +    getDiff: function(rev, current) {
70 +        data = {'rev1': rev,
71 +                'rev2': current,
72 +                'vid': 'vcreview.patch.revisions_diff'
73 +        };
74 +        $.get(BASE_URL, data, function(response) {
75 +            $('#diff-div').html(response);
76 +        }) ;
77 +        $('.list-group-item').removeClass('list-group-item-danger', 'list-group-item-success');
78 +        $('#rev-' + rev).addClass('list-group-item-danger');
79 +        $('#rev-' + current).addClass('list-group-item-success');
80 +    },
81 +
82 +
83 +    /*
84 +     * Add a trigger to the compare link.
85 +     *
86 +     * Its arguments are:
87 +     *
88 +     * `eids`, a list of eids of the patch revisions.
89 +     * The current revision's eid is at index 0.
90 +     */
91 +    addClickTrigger: function(eids) {
92 +        var current = eids[0];
93 +        $('.icon-diff').each(function (i){
94 +            $(this).click(function() {
95 +                cw.cubes.vcreview.getDiff(eids[i+1], current);
96 +            });
97 +        });
98 +    }
99 +});
diff --git a/debian/control b/debian/control
@@ -11,11 +11,11 @@
100 
101  Package: cubicweb-vcreview
102  Architecture: all
103  Depends:
104   cubicweb-common (>= 3.19.0),
105 - cubicweb-vcsfile (>= 2.0.0),
106 + cubicweb-vcsfile (>= 2.2.0),
107   cubicweb-comment (>= 1.8.0),
108   cubicweb-task,
109   cubicweb-nosylist,
110   ${python:Depends},
111   ${misc:Depends},
diff --git a/i18n/en.po b/i18n/en.po
@@ -36,19 +36,28 @@
112  msgstr ""
113 
114  msgid "Automatically set new patches in the pending-review state"
115  msgstr ""
116 
117 +msgid "Content"
118 +msgstr ""
119 +
120 +msgid "Diff"
121 +msgstr ""
122 +
123  # schema pot file, generated on 2011-02-10 18:08:42
124  #
125  # singular and plural forms for each entity type
126  msgid "InsertionPoint"
127  msgstr "Insertion point"
128 
129  msgid "InsertionPoint_plural"
130  msgstr "Insertion points"
131 
132 +msgid "Message"
133 +msgstr ""
134 +
135  msgid "My review worklist"
136  msgstr ""
137 
138  msgid "New InsertionPoint"
139  msgstr ""
@@ -145,19 +154,25 @@
140  msgstr ""
141 
142  msgid "author"
143  msgstr ""
144 
145 +msgid "by"
146 +msgstr ""
147 +
148  # subject and object forms for each relation type
149  # (no object form for final or symmetric relation types)
150  msgctxt "Task"
151  msgid "comments_object"
152  msgstr ""
153 
154  msgid "committers"
155  msgstr ""
156 
157 +msgid "created on"
158 +msgstr ""
159 +
160  msgid "creating Task (Patch %(linkto)s has_activity Task)"
161  msgstr "creating task for %(linkto)s"
162 
163  msgid "ctxcomponents_vcreview.activitysection"
164  msgstr "activity table"
@@ -270,10 +285,13 @@
165  msgstr "identifier"
166 
167  msgid "name of the patch in the repository"
168  msgstr ""
169 
170 +msgid "no diff"
171 +msgstr ""
172 +
173  msgid "obsolete"
174  msgstr ""
175 
176  msgid "outdated"
177  msgstr ""
@@ -407,10 +425,13 @@
178 
179  msgctxt "Repository"
180  msgid "use_global_groups"
181  msgstr ""
182 
183 +msgid "vcreview.patch.revisions"
184 +msgstr "revisions"
185 +
186  msgid "vcreview.patch.tab_head"
187  msgstr "tip"
188 
189  msgid "vcreview.patch.tab_main"
190  msgstr "description"
diff --git a/i18n/fr.po b/i18n/fr.po
@@ -38,19 +38,28 @@
191  msgid "Automatically set new patches in the pending-review state"
192  msgstr ""
193  "Mettre les nouveaux patches automatiquement dans l'état \"en attente de revue"
194  "\""
195 
196 +msgid "Content"
197 +msgstr "Contenu"
198 +
199 +msgid "Diff"
200 +msgstr ""
201 +
202  # schema pot file, generated on 2011-02-10 18:08:42
203  #
204  # singular and plural forms for each entity type
205  msgid "InsertionPoint"
206  msgstr "Point d'insertion"
207 
208  msgid "InsertionPoint_plural"
209  msgstr "Point d'insertion"
210 
211 +msgid "Message"
212 +msgstr ""
213 +
214  msgid "My review worklist"
215  msgstr "Revue de code en attente"
216 
217  msgid "New InsertionPoint"
218  msgstr "nouveau point d'insertion"
@@ -150,19 +159,25 @@
219  msgstr "demander modification"
220 
221  msgid "author"
222  msgstr "auteur"
223 
224 +msgid "by"
225 +msgstr "par"
226 +
227  # subject and object forms for each relation type
228  # (no object form for final or symmetric relation types)
229  msgctxt "Task"
230  msgid "comments_object"
231  msgstr ""
232 
233  msgid "committers"
234  msgstr "committeurs"
235 
236 +msgid "created on"
237 +msgstr "crée le"
238 +
239  msgid "creating Task (Patch %(linkto)s has_activity Task)"
240  msgstr "création d'un tâche pour le patch %(linkto)s"
241 
242  msgid "ctxcomponents_vcreview.activitysection"
243  msgstr "table des tâches"
@@ -276,10 +291,13 @@
244  msgstr "identifiant"
245 
246  msgid "name of the patch in the repository"
247  msgstr "nom du patch dans l'entrepôt"
248 
249 +msgid "no diff"
250 +msgstr "aucune différence"
251 +
252  msgid "obsolete"
253  msgstr ""
254 
255  msgid "outdated"
256  msgstr ""
@@ -413,10 +431,13 @@
257 
258  msgctxt "Repository"
259  msgid "use_global_groups"
260  msgstr ""
261 
262 +msgid "vcreview.patch.revisions"
263 +msgstr "révisions du patch"
264 +
265  msgid "vcreview.patch.tab_head"
266  msgstr "tip"
267 
268  msgid "vcreview.patch.tab_main"
269  msgstr "description"
diff --git a/views/primary.py b/views/primary.py
@@ -16,17 +16,18 @@
270  """cubicweb-vcreview primary views and adapters for the web ui"""
271 
272  __docformat__ = "restructuredtext en"
273  _ = unicode
274 
275 +from difflib import unified_diff
276 +
277  from logilab.mtconverter import xml_escape
278 
279  from cubicweb import tags
280 -from cubicweb.predicates import score_entity, is_instance
281 -from cubicweb.view import EntityView
282 -from cubicweb.schema import display_name
283 -from cubicweb.web.views import tabs, primary, ibreadcrumbs, uicfg
284 +from cubicweb.predicates import score_entity, is_instance, one_line_rset
285 +from cubicweb.view import EntityView, View
286 +from cubicweb.web.views import tabs, ibreadcrumbs, uicfg
287 
288  from cubes.vcsfile.views import primary as vcsfile
289  from cubes.vcreview.views import final_patch_states_rql
290  from cubes.vcreview.site_cubicweb import COMPONENT_CONTEXT
291 
@@ -45,11 +46,12 @@
292  class PatchPrimaryView(tabs.TabbedPrimaryView):
293      """Main Patch primary view"""
294      __select__ = is_instance('Patch')
295 
296      tabs = [_('vcreview.patch.tab_main'),
297 -            _('vcreview.patch.tab_head')]
298 +            _('vcreview.patch.tab_head'),
299 +            _('vcreview.patch.revisions')]
300      default_tab = 'vcreview.patch.tab_main'
301 
302      def render_entity_title(self, entity):
303          self.w(u'<h1>%s <span class="state">[%s]</span></h1>'
304                 % (xml_escape(entity.dc_title()),
@@ -61,20 +63,10 @@
305      __regid__ = 'vcreview.patch.tab_main'
306      __select__ = is_instance('Patch')
307 
308      def render_entity_attributes(self, entity):
309          super(PatchPrimaryTab, self).render_entity_attributes(entity)
310 -        self.w(u'<h4>%s</h4>' % self._cw._('Patch revisions'))
311 -        rset = self._cw.execute(
312 -            'Any R,RA,RB,RC,RD,R ORDERBY RC DESC '
313 -            'WHERE X eid %(x)s, X patch_revision R,'
314 -            'R author RA, R branch RB, R creation_date RC, R description RD',
315 -            {'x': entity.eid})
316 -        _, __ = self._cw._, self._cw.__
317 -        self.wview('table', rset,
318 -                   headers=[__('Revision'), __('author'), __('branch'),
319 -                            __('creation_date'), __('commit message')])
320 
321  class PatchHeadTab(EntityView):
322      """Revision view of the tip most version of the patch
323 
324      (with comment and task)"""
@@ -85,10 +77,125 @@
325          tip = entity.tip()
326          if tip:
327              tip.view('primary', w=self.w)
328 
329 
330 +class PatchRevisions(EntityView):
331 +    """A view that allow to view the diff between two revisions of the same
332 +    patch.
333 +    """
334 +    __regid__ = 'vcreview.patch.revisions'
335 +    __select__ = (is_instance('Patch')
336 +                  & score_entity(lambda x: len(x.patch_revision) > 1))
337 +
338 +    def entity_call(self, entity):
339 +        w = self.w
340 +        self._cw.add_css('cubes.vcreview.css')
341 +        self._cw.add_js('cubes.vcreview.js')
342 +        self._cw.add_css('pygments.css')
343 +        w(tags.h4(self._cw._('Patch revisions')))
344 +        w(u'<ul class="list-group">')
345 +        parent_eid = entity.patch_revision[0].eid
346 +        eids = []
347 +        for i, rev in enumerate(entity.patch_revision):
348 +            eids.append(rev.eid)
349 +            w(u'<li class="list-group-item" id="rev-%s">' % rev.eid)
350 +            w(rev.view('incontext'))
351 +            if i != 0:
352 +                w(u'<div class="pull-right">')
353 +                w(u'<span title="%s">' % self._cw._('Diff'))
354 +                w(u'<a class="icon-diff" href="#"><span class="icon-code"></span></a>')
355 +                w(u'</span>')
356 +                w(u'</div>')
357 +            w(u'<div>')
358 +            w(u'<span class="text-muted">%s %s %s %s' % (
359 +                self._cw._('created on'), rev.printable_value('creation_date'),
360 +                self._cw._('by'), rev.author))
361 +            if rev.parent_revision:
362 +                parent = rev.parent_revision[0]
363 +                w(u' (%s %s)' % (self._cw.__('parent_revision'), parent.view('oneline')))
364 +            w(u'</span>')
365 +            w(u'</div>')
366 +            w(u'</li>')
367 +        w(u'</ul>')
368 +
369 +        self._cw.add_onload(u'cw.cubes.vcreview.addClickTrigger(%s);' % eids)
370 +        self.w(tags.div(klass='clear'))
371 +        self.w(tags.div(id="diff-div", klass="content"))
372 +
373 +
374 +class PatchRevisionsDiff(View):
375 +    """A view that show a diff between two revisions.
376 +    The revisions eid are found in the request parameters.
377 +    The parameters are rev1 for the first revision and rev2
378 +    for the second revision.
379 +    """
380 +    __regid__ = 'vcreview.patch.revisions_diff'
381 +    templatable = False
382 +
383 +    def call(self):
384 +        form = self._cw.form
385 +        if 'rev1' not in form or 'rev2' not in form:
386 +            return
387 +        rev1 = self._cw.entity_from_eid(form['rev1'])
388 +        rev2 = self._cw.entity_from_eid(form['rev2'])
389 +        transformer = rev1._cw_mtc_transform
390 +        message_diff = self.get_message_diff(rev1, rev2)
391 +        content_diff = self.get_content_diff(rev1, rev2)
392 +        self.w(tags.h4(self._cw._('Diff')))
393 +        self.w(tags.h5(self._cw._('Message')))
394 +        self.write_diff(transformer, message_diff, self.w)
395 +        self.w(tags.h5(self._cw._('Content')))
396 +        self.write_diff(transformer, content_diff, self.w)
397 +
398 +    def get_content_diff(self, rev1, rev2):
399 +        """Return the diff between two revisions.
400 +
401 +        :param Revision rev1: the first revision
402 +        :param Revision rev2: the second revision
403 +        """
404 +        repo = rev1.from_repository[0]
405 +        assert repo == rev2.from_repository[0]
406 +        diff = self._cw.call_service(
407 +            'vcsfile.export-rev-diff', repo_eid=repo.eid,
408 +            rev1=rev1.changeset, rev2=rev2.changeset)
409 +        return diff or u''
410 +
411 +    @staticmethod
412 +    def get_message_diff(rev1, rev2):
413 +        """Return the diff between the commit messages of
414 +        the two revisions. It return a diff in the `text/x-diff`
415 +        format
416 +
417 +        :param Revision rev1: the first revision
418 +        :param Revision rev2: the second revision
419 +        """
420 +        message_one = rev1.description.split('\n')
421 +        message_two = rev2.description.split('\n')
422 +        first_message = rev1.view('shorttext')
423 +        second_message = rev2.view('shorttext')
424 +        diffcontent = unified_diff(
425 +            message_one, message_two, fromfile=first_message,
426 +            tofile=second_message, lineterm='')
427 +        return u'\n'.join(diffcontent)
428 +
429 +    def write_diff(self, transformer, diff, w):
430 +        """Transform the diff which is supposed to be in format
431 +        `text/x-diff` to the HTML format and write the result
432 +        to the given writer.
433 +
434 +        :param transformer: the transformer tu use to do the conversion
435 +        :param diff: the diff
436 +        :param w: the list to which to append the HTML content
437 +        """
438 +        if diff:
439 +            html = transformer(diff, 'text/x-diff', 'text/html', 'utf8')
440 +            w(html)
441 +        else:
442 +            w(tags.div(self._cw._('no diff'), klass='alert alert-warning'))
443 +
444 +
445  #_pvs.tag_object_of(('*', 'patch_revision', '*'), 'hidden') # in breadcrumbs
446 
447  # repository primary view ######################################################
448 
449  _pvs.tag_subject_of(('Repository', 'repository_committer', '*'), 'attributes')
obsoletes