Kiln » TortoiseHg » TortoiseHg
Clone URL:  
Pushed to one repository · View In Graph Contained in 1.9, 1.9.1, and 1.9.2

add hgview sources

Changeset 4f3155fd9442

Parent aa5eebcf2f3c

by Adrian Buehlmann

Changes to 46 files · Browse files at 4f3155fd9442 Showing diff from parent aa5eebcf2f3c Diff from another changeset...

Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​__init__.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@@ -0,0 +1,29 @@
+# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""mercurial interactive history viewer + +Its purpose is similar to the hgk tool of mercurial, and it has been +written with efficiency in mind when dealing with big repositories +(it can happily be used to browse Linux kernel source code +repository). +""" + +# monkey patch to support older hg versions +from mercurial import changelog, filelog +if not hasattr(changelog.changelog, '__len__'): + changelog.changelog.__len__ = changelog.changelog.count +if not hasattr(filelog.filelog, '__len__'): + filelog.filelog.__len__ = filelog.filelog.count
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​config.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*- +# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# pylint: disable-msg=C0103 + +""" +Module for managing configuration parameters of hgview using Hg's +configuration system +""" +import os +import re + +def cached(meth): + """ + decorator to cache config values once they are read + """ + name = meth.func_name + def wrapper(self, *args, **kw): + if name in self._cache: + return self._cache[name] + res = meth(self, *args, **kw) + self._cache[name] = res + return res + wrapper.__doc__ = meth.__doc__ + return wrapper + +class HgConfig(object): + """ + Class managing user configuration from hg standard configuration system (.hgrc) + """ + def __init__(self, ui, section="hgview"): + self.ui = ui + self.section = section + self._cache = {} + + @cached + def getFont(self): + """ + font: default font used to display diffs and files. Use Qt4 format. + """ + return self.ui.config(self.section, 'font', 'Monospace') + + @cached + def getFontSize(self, default=9): + """ + fontsize: text size in file content viewer + """ + return int(self.ui.config(self.section, 'fontsize', default)) + + @cached + def getDotRadius(self, default=8): + """ + dotradius: radius (in pixels) of the dot in the revision graph + """ + r = self.ui.config(self.section, 'dotradius', default) + return int(r) + + @cached + def getUsers(self): + """ + users: path of the file holding users configurations + """ + users = {} + aliases = {} + usersfile = self.ui.config(self.section, 'users', + os.path.join('~', ".hgusers")) + cfgfile = None + if usersfile: + try: + cfgfile = open(os.path.expanduser(usersfile)) + except IOError: + cfgfile = None + + if cfgfile: + currid = None + for line in cfgfile: + line = line.strip() + if not line or line.startswith('#'): + continue + cmd, val = line.split('=', 1) + if cmd == 'id': + currid = val + if currid in users: + print "W: user %s is defined several times" % currid + users[currid] = {'aliases': set()} + elif cmd == "alias": + users[currid]['aliases'].add(val) + if val in aliases: + print ("W: alias %s is used in several " + "user definitions" % val) + aliases[val] = currid + else: + users[currid][cmd] = val + return users, aliases + + @cached + def getFileModifiedColor(self, default='blue'): + """ + filemodifiedcolor: display color of a modified file + """ + return self.ui.config(self.section, 'filemodifiedcolor', default) + @cached + def getFileRemovedColor(self, default='red'): + """ + fileremovedcolor: display color of a removed file + """ + return self.ui.config(self.section, 'fileremovededcolor', default) + @cached + def getFileDeletedColor(self, default='darkred'): + """ + filedeletedcolor: display color of a deleted file + """ + return self.ui.config(self.section, 'filedeletedcolor', default) + @cached + def getFileAddedColor(self, default='green'): + """ + fileaddedcolor: display color of an added file + """ + return self.ui.config(self.section, 'fileaddedcolor', default) + + @cached + def getRowHeight(self, default=20): + """ + rowheight: height (in pixels) on a row of the revision table + """ + return int(self.ui.config(self.section, 'rowheight', default)) + + @cached + def getHideFindDelay(self, default=10000): + """ + hidefinddelay: delay (in ms) after which the find bar will disappear + """ + return int(self.ui.config(self.section, 'hidefindddelay', default)) + + @cached + def getFillingStep(self, default=300): + """ + fillingstep: number of nodes 'loaded' at a time when updating repo graph log + """ + return int(self.ui.config(self.section, 'fillingstep', default)) + + @cached + def getChangelogColumns(self, default=None): + """ + changelogcolumns: ordered list of displayed columns in changelog views; + defaults to ID, Branch, Log, Author, Date, Tags + """ + cols = self.ui.config(self.section, 'changelogcolumns', default) + if cols is None: + return None + return [col.strip() for col in cols.split(',') if col.strip()] + + @cached + def getFilelogColumns(self, default=None): + """ + filelogcolumns: ordered list of displayed columns in filelog views; + defaults to ID, Log, Author, Date + """ + cols = self.ui.config(self.section, 'filelogcolumns', default) + if cols is None: + return None + return [col.strip() for col in cols.split(',') if col.strip()] + + @cached + def getDisplayDiffStats(self, default="no"): + """ + displaydiffstats: flag controllong the appearance of the + 'Diff' column in a revision's file list + """ + val = str(self.ui.config(self.section, 'displaydiffstats', default)) + return val.lower() in ['true', 'yes', '1', 'on'] + + @cached + def getMaxFileSize(self, default=100000): + """ + maxfilesize: max size of a file (for diff computations, display content, etc.) + """ + return int(self.ui.config(self.section, 'maxfilesize', default)) + + @cached + def getDiffBGColor(self, default='white'): + """ + diffbgcolor: background color of diffs + """ + return self.ui.config(self.section, 'diffbgcolor', default) + + @cached + def getDiffFGColor(self, default='black'): + """ + difffgcolor: text color of diffs + """ + return self.ui.config(self.section, 'difffgcolor', default) + + @cached + def getDiffPlusColor(self, default='green'): + """ + diffpluscolor: text color of added lines in diffs + """ + return self.ui.config(self.section, 'diffpluscolor', default) + + @cached + def getDiffMinusColor(self, default='red'): + """ + diffminuscolor: text color of removed lines in diffs + """ + return self.ui.config(self.section, 'diffminuscolor', default) + + @cached + def getDiffSectionColor(self, default='magenta'): + """ + diffsectioncolor: text color of new section in diffs + """ + return self.ui.config(self.section, 'diffsectioncolor', default) + + @cached + def getMQFGColor(self, default='#ff8183'): + """ + mqfgcolor: bg color to highlight mq patches + """ + return self.ui.config(self.section, 'mqfgcolor', default) + + @cached + def getMQHideTags(self, default=False): + """ + mqhidetags: hide mq tags + """ + return self.ui.config(self.section, 'mqhidetags', default) + + + +_HgConfig = HgConfig +# HgConfig is instanciated only once (singleton) +# +# this 'factory' is used to manage this (not using heavy guns of +# metaclass or so) +_hgconfig = None +def HgConfig(ui): + """Factory to instanciate HgConfig class as a singleton + """ + # pylint: disable-msg=E0102 + global _hgconfig + if _hgconfig is None: + _hgconfig = _HgConfig(ui) + return _hgconfig + + +def get_option_descriptions(rest=False): + """ + Extract options descriptions (docstrings of HgConfig methods) + """ + options = [] + for attr in dir(_HgConfig): + if attr.startswith('get'): + meth = getattr(_HgConfig, attr) + if callable(meth): + doc = meth.__doc__ + if doc and doc.strip(): + doc = doc.strip() + if rest: + doc = re.sub(r' *(?P<arg>.*) *: *(?P<desc>.*)', r'``\1`` \2', doc.strip()) + doc = ' '.join(doc.split()) # remove \n and other multiple whitespaces + options.append(doc) + return options +
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​decorators.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*- +""" +Some useful decorator functions +""" +import time + +def timeit(func): + """Decorator used to time the execution of a function""" + def timefunc(*args, **kwargs): + """wrapper""" + t_1 = time.time() + t_2 = time.clock() + res = func(*args, **kwargs) + t_3 = time.clock() + t_4 = time.time() + print "%s: %.2fms (time) %.2fms (clock)" % \ + (func.func_name, 1000*(t_3 - t_2), 1000*(t_4 - t_1)) + return res + return timefunc
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​hggraph.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
@@ -0,0 +1,477 @@
+# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""helper functions and classes to ease hg revision graph building + +Based on graphlog's algorithm, with insipration stolen to TortoiseHg +revision grapher. +""" + +from cStringIO import StringIO +import difflib + +from mercurial.node import nullrev +from mercurial import patch, util, match + +import hgviewlib # force apply monkeypatches +from hgviewlib.util import tounicode, isbfile + +def diff(repo, ctx1, ctx2=None, files=None): + """ + Compute the diff of files between 2 changectx + """ + if ctx2 is None: + ctx2 = ctx1.parents()[0] + if files is None: + m = match.always(repo.root, repo.getcwd()) + else: + m = match.exact(repo.root, repo.getcwd(), files) + # try/except for the sake of hg compatibility (API changes between + # 1.0 and 1.1) + try: + out = StringIO() + patch.diff(repo, ctx2.node(), ctx1.node(), match=m, fp=out) + diffdata = out.getvalue() + except: + diffdata = '\n'.join(patch.diff(repo, ctx2.node(), ctx1.node(), + match=m)) + # XXX how to deal diff encodings? + try: + diffdata = unicode(diffdata, "utf-8") + except UnicodeError: + # XXX use a default encoding from config? + diffdata = unicode(diffdata, "iso-8859-15", 'ignore') + return diffdata + + +def __get_parents(repo, rev, branch=None): + """ + Return non-null parents of `rev`. If branch is given, only return + parents that belongs to names branch `branch` (beware that this is + much slower). + """ + if not branch: + if rev is None: + return [x.rev() for x in repo.changectx(None).parents() if x] + return [x for x in repo.changelog.parentrevs(rev) if x != nullrev] + if rev is None: + return [x.rev() for x in repo.changectx(None).parents() \ + if x and repo.changectx(rev).branch() == branch] + return [x for x in repo.changelog.parentrevs(rev) \ + if (x != nullrev and repo.changectx(rev).branch() == branch)] + + +def ismerge(ctx): + """ + Return True if the changecontext ctx is a merge mode (should work + with hg 1.0 and 1.2) + """ + if ctx: + return len(ctx.parents()) == 2 and ctx.parents()[1] + return False + +def revision_grapher(repo, start_rev=None, stop_rev=0, branch=None, follow=False): + """incremental revision grapher + + This generator function walks through the revision history from + revision start_rev to revision stop_rev (which must be less than + or equal to start_rev) and for each revision emits tuples with the + following elements: + + - current revision + - column of the current node in the set of ongoing edges + - color of the node (?) + - lines; a list of (col, next_col, color) indicating the edges between + the current row and the next row + - parent revisions of current revision + + If follow is True, only generated the subtree from the start_rev head. + + If branch is set, only generated the subtree for the given named branch. + """ + if start_rev is None and repo.status() == ([],)*7: + start_rev = len(repo.changelog) + assert start_rev is None or start_rev >= stop_rev + curr_rev = start_rev + revs = [] + rev_color = {} + nextcolor = 0 + while curr_rev is None or curr_rev >= stop_rev: + # Compute revs and next_revs. + if curr_rev not in revs: + if branch: + ctx = repo.changectx(curr_rev) + if ctx.branch() != branch: + if curr_rev is None: + curr_rev = len(repo.changelog) + else: + curr_rev -= 1 + yield None + continue + + # New head. + if start_rev and follow and curr_rev != start_rev: + curr_rev -= 1 + continue + revs.append(curr_rev) + rev_color[curr_rev] = curcolor = nextcolor + nextcolor += 1 + p_revs = __get_parents(repo, curr_rev, branch) + while p_revs: + rev0 = p_revs[0] + if rev0 < stop_rev or rev0 in rev_color: + break + rev_color[rev0] = curcolor + p_revs = __get_parents(repo, rev0, branch) + curcolor = rev_color[curr_rev] + rev_index = revs.index(curr_rev) + next_revs = revs[:] + + # Add parents to next_revs. + parents = __get_parents(repo, curr_rev, branch) + parents_to_add = [] + if len(parents) > 1: + preferred_color = None + else: + preferred_color = curcolor + for parent in parents: + if parent not in next_revs: + parents_to_add.append(parent) + if parent not in rev_color: + if preferred_color: + rev_color[parent] = preferred_color + preferred_color = None + else: + rev_color[parent] = nextcolor + nextcolor += 1 + preferred_color = None + + # parents_to_add.sort() + next_revs[rev_index:rev_index + 1] = parents_to_add + + lines = [] + for i, rev in enumerate(revs): + if rev in next_revs: + color = rev_color[rev] + lines.append( (i, next_revs.index(rev), color) ) + elif rev == curr_rev: + for parent in parents: + color = rev_color[parent] + lines.append( (i, next_revs.index(parent), color) ) + + yield (curr_rev, rev_index, curcolor, lines, parents) + revs = next_revs + if curr_rev is None: + curr_rev = len(repo.changelog) + else: + curr_rev -= 1 + + +def filelog_grapher(repo, path): + ''' + Graph the ancestry of a single file (log). Deletions show + up as breaks in the graph. + ''' + filerev = len(repo.file(path)) - 1 + fctx = repo.filectx(path, fileid=filerev) + rev = fctx.rev() + + flog = fctx.filelog() + heads = [repo.filectx(path, fileid=flog.rev(x)).rev() for x in flog.heads()] + assert rev in heads + heads.remove(rev) + + revs = [] + rev_color = {} + nextcolor = 0 + _paths = {} + + while rev >= 0: + # Compute revs and next_revs + if rev not in revs: + revs.append(rev) + rev_color[rev] = nextcolor ; nextcolor += 1 + curcolor = rev_color[rev] + index = revs.index(rev) + next_revs = revs[:] + + # Add parents to next_revs + fctx = repo.filectx(_paths.get(rev, path), changeid=rev) + for pfctx in fctx.parents(): + _paths[pfctx.rev()] = pfctx.path() + parents = [pfctx.rev() for pfctx in fctx.parents()]# if f.path() == path] + parents_to_add = [] + for parent in parents: + if parent not in next_revs: + parents_to_add.append(parent) + if len(parents) > 1: + rev_color[parent] = nextcolor ; nextcolor += 1 + else: + rev_color[parent] = curcolor + parents_to_add.sort() + next_revs[index:index + 1] = parents_to_add + + lines = [] + for i, nrev in enumerate(revs): + if nrev in next_revs: + color = rev_color[nrev] + lines.append( (i, next_revs.index(nrev), color) ) + elif nrev == rev: + for parent in parents: + color = rev_color[parent] + lines.append( (i, next_revs.index(parent), color) ) + + pcrevs = [pfc.rev() for pfc in fctx.parents()] + yield (fctx.rev(), index, curcolor, lines, pcrevs, + _paths.get(fctx.rev(), path)) + revs = next_revs + + if revs: + rev = max(revs) + else: + rev = -1 + if heads and rev <= heads[-1]: + rev = heads.pop() + +class GraphNode(object): + """ + Simple class to encapsulate e hg node in the revision graph. Does + nothing but declaring attributes. + """ + def __init__(self, rev, xposition, color, lines, parents, ncols=None, + extra=None): + self.rev = rev + self.x = xposition + self.color = color + if ncols is None: + ncols = len(lines) + self.cols = ncols + self.parents = parents + self.bottomlines = lines + self.toplines = [] + self.extra = extra + +class Graph(object): + """ + Graph object to ease hg repo navigation. The Graph object + instanciate a `revision_grapher` generator, and provide a `fill` + method to build the graph progressively. + """ + #@timeit + def __init__(self, repo, grapher, maxfilesize=100000): + self.maxfilesize = maxfilesize + self.repo = repo + self.maxlog = len(self.repo.changelog) + self.grapher = grapher + self.nodes = [] + self.nodesdict = {} + self.max_cols = 0 + + def build_nodes(self, nnodes=None, rev=None): + """ + Build up to `nnodes` more nodes in our graph, or build as many + nodes required to reach `rev`. + + If both rev and nnodes are set, build as many nodes as + required to reach rev plus nnodes more. + """ + if self.grapher is None: + return False + stopped = False + mcol = [self.max_cols] + for vnext in self.grapher: + if vnext is None: + continue + nrev, xpos, color, lines, parents = vnext[:5] + if nrev >= self.maxlog: + continue + gnode = GraphNode(nrev, xpos, color, lines, parents, + extra=vnext[5:]) + if self.nodes: + gnode.toplines = self.nodes[-1].bottomlines + self.nodes.append(gnode) + self.nodesdict[nrev] = gnode + mcol.append(gnode.cols) + if rev is not None and nrev <= rev: + rev = None # we reached rev, switching to nnode counter + if rev is None: + if nnodes is not None: + nnodes -= 1 + if not nnodes: + break + else: + break + else: + self.grapher = None + stopped = True + + self.max_cols = max(mcol) + return not stopped + + def isfilled(self): + return self.grapher is None + + def fill(self, step=100): + """ + Return a generator that fills the graph by bursts of `step` + more nodes at each iteration. + """ + while self.build_nodes(step): + yield len(self) + yield len(self) + + def __getitem__(self, idx): + if isinstance(idx, slice): + # XXX TODO: ensure nodes are built + return self.nodes.__getitem__(idx) + if idx >= len(self.nodes): + # build as many graph nodes as required to answer the + # requested idx + self.build_nodes(idx) + if idx > len(self): + return self.nodes[-1] + return self.nodes[idx] + + def __len__(self): + # len(graph) is the number of actually built graph nodes + return len(self.nodes) + + def index(self, rev): + if len(self) == 0: # graph is empty, let's build some nodes + self.build_nodes(10) + if rev is not None and rev < self.nodes[-1].rev: + self.build_nodes(self.nodes[-1].rev - rev) + if rev in self.nodesdict: + return self.nodes.index(self.nodesdict[rev]) + return -1 + + def fileflags(self, filename, rev): + """ + Return a couple of flags ('=', '+', '-' or '?') depending on the nature + of the diff for filename between rev and its parents. + """ + ctx = self.repo.changectx(rev) + flags = [] + for p in ctx.parents(): + changes = self.repo.status(p.node(), ctx.node())[:5] + # changes = modified, added, removed, deleted, unknown + for flag, lst in zip(["=", "+", "-", "-", "?"], changes): + if filename in lst: + if flag == "+": + renamed = ctx.filectx(filename).renamed() + if renamed: + flags.append(renamed) + break + flags.append(flag) + break + else: + flags.append('') + return flags + + def fileflag(self, filename, rev): + """ + Return a flag (see fileflags) between rev and its first parent + """ + return self.fileflags(filename, rev)[0] + + def filename(self, rev): + return self.nodesdict[rev].extra[0] + + def filedata(self, filename, rev, mode='diff'): + """XXX written under dubious encoding assumptions + """ + # XXX This really begins to be a dirty mess... + data = "" + flag = self.fileflag(filename, rev) + ctx = self.repo.changectx(rev) + try: + fctx = ctx.filectx(filename) + except LookupError: + fctx = None # may happen for renamed files? + + if isbfile(filename): + data = "[bfile]\n" + if fctx: + data = fctx.data() + data += "footprint: %s\n" % data + return "+", data + if flag not in ('-', '?'): + if fctx is None:# or fctx.node() is None: + return '', None + if fctx.size() > self.maxfilesize: + data = "file too big" + return flag, data + if flag == "+" or mode == 'file': + flag = '+' + # return the whole file + data = fctx.data() + if util.binary(data): + data = "binary file" + else: # tries to convert to unicode + data = tounicode(data) + elif flag == "=" or isinstance(mode, int): + flag = "=" + if isinstance(mode, int): + parentctx = self.repo.changectx(mode) + else: + parent = self.fileparent(filename, rev) + parentctx = self.repo.changectx(parent) + # return the diff but the 3 first lines + data = diff(self.repo, ctx, parentctx, files=[filename]) + data = u'\n'.join(data.splitlines()[3:]) + elif flag == '': + data = '' + else: # file renamed + oldname, node = flag + newdata = fctx.data().splitlines() + olddata = self.repo.filectx(oldname, fileid=node) + olddata = olddata.data().splitlines() + data = list(difflib.unified_diff(olddata, newdata, oldname, + filename))[2:] + if data: + flag = "=" + else: + data = newdata + flag = "+" + data = u'\n'.join(tounicode(elt) for elt in data) + return flag, data + + def fileparent(self, filename, rev): + if rev is not None: + node = self.repo.changelog.node(rev) + else: + node = self.repo.changectx(rev).node() + for parent in self.nodesdict[rev].parents: + pnode = self.repo.changelog.node(parent) + changes = self.repo.status(pnode, node)[:5] + allchanges = [] + [allchanges.extend(e) for e in changes] + if filename in allchanges: + return parent + return None + +if __name__ == "__main__": + # pylint: disable-msg=C0103 + import sys + from mercurial import ui, hg + u = ui.ui() + r = hg.repository(u, sys.argv[1]) + if len(sys.argv) == 3: + rg = filelog_grapher(r, sys.argv[2]) + else: + rg = revision_grapher(r) + g = Graph(r, rg) +
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​hgviewhelp.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@@ -0,0 +1,223 @@
+# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +""" +help messages for hgview +""" + +help_msg = """ +hgview: a visual hg log viewer +============================== + +This command will launch the hgview log navigator, allowing to +visually browse in the hg graph log, search in logs, and display diff +between arbitrary revisions of a file, with simple support for mq and +bigfile extensions. + +If a filename is given, launch the filelog diff viewer for this file, +and with the '-n' option, launch the filelog navigator for the file. + +With the '-r' option, launch the manifest viewer for the given revision. + +Revlog graph +------------ + +The main revision graph display the repository history as a graph, +sorted by revision number. + +The color of the node of each revision depends on the named branch the +revision belongs to. + +The color of the links (between nodes) is randomly chosen. + +The position of the working directory is marked on the graph using a +small sunny icon as node marker. If the working directory has local +modifications, a *virtual* is added in the graph with a special sign +icon (with no revision number). Modified, added and removed files are +listed and browsable as a normal changeset node. + +Note that if the working directoy is in merge state, there will be 2 +revisions marked as modified in the graph (since the working directory +is then a son of both the merged nodes). + +mq support +~~~~~~~~~~ + +There is a simple support for the mq extension. Applied patches are +seen in the revlog graph with a special arrow icon. Unapplied patches +are *not* in the revlog graph (since they are not mercurial +changesets). + +When the currently selected revision is an applied patch, the revision +metadata display (see below) area point this by showing an additional +line with coloured background listing all available patches (applied +or not, so if you cannot see the content of an unapplied patch, you +are aware there are unapplied patches, as long as there is at leat one +applied patch). Current patch is displayed using bold font; unapplied +patches are displayed in italic. + + +Revision metadata display +------------------------- + +The area where current revision's metadata is displayed +(description, parents revisions, etc.) may contain two kinds of hyperlink: + +- when the hyperlink is the **changeset ID**, it allows you to + directly go to the given revision, + +- when the hyperlink is the **revision number** (on merge nodes only), + it means that you can change the other revision used to comput + the diff. This allows you to compare the merged node with each + of its parents, or even with the common ancestor of these 2 + nodes. + + +Revision's modified file list +----------------------------- + +The file list diplay the list of modified files. The diff +displayed is the one of the selected file, between the selected +revision and its parent revision. + +On a merge node, by default, only files which are different from +both its parents are listed here. However, you can display the +list of all modified files by double-clickig the file list column +header. + + +Quickbars +--------- + +Quickbars are tollbar that appear when asked for by hitting it's +keybord shortcut. Only one quickbar can be displayed at a time. + +When a quickbar is visible, hitting the Esc key make it disappear. + +The goto quickbar +~~~~~~~~~~~~~~~~~ + +This toolbar appears when hitting Ctrl+G. It allows you to jump to a +given revision. The destination revision can be entered by: + +- it's revision number (negative values allowed, count from tip) +- it's changeset ID (short or long) +- a tag name (with completion) +- a branch name +- an empty string; means "goto current working directory" + +The search quickbar +~~~~~~~~~~~~~~~~~~~ + +This toolbar appears when hitting Ctrl+F or / (if not in goto toolbar). + +It allows you to type a string to be searched for: + +- in the currently displayed revision commit message (with highlight-as-you-type) +- in the currently displayed file or diff (with highlight-as-you-type) + +Hitting the "Search next" button starts a background task for searching among the whole +revision log, starting from the current position (selected revision +and file). + + +Keyboard shortcuts +------------------ + +**Up/Down** + go to next/previous revision + +**MidButton** + go to the common ancestor of the clicked revision and the currently selected one + + +**Left/Right** + display previous/next files of the current changeset + +**Ctrl+F** or **/** + display the search 'quickbar' + +**Ctrl+G** + display the goto 'quickbar' + +**Esc** + exit or hide the visible 'quickbar' + +**Enter** + run the diff viewer for the currently selected file (display diff + between revisions) + +**Alt+Enter** + run the filelog navigator + +**Shift+Enter** + run the manifest viewer for the displayed revision + +**Ctrl+R** + reread repo; note that by default, repo will be automatically + reloaded if it is modified (due to a commit, a pull, etc.) + +**Alt+Up/Down** + display previous/next diff block + +**Alt+Left/Right** + go to previous/next visited revision (in navigation history) + +**Backspace** + set current revision the current start revision (hide any revision above it) + +**Shit+Backspace** + clear the start revision value + + + """ + +def get_options_helpmsg(rest=False): + """display hgview full list of configuration options + """ + from config import get_option_descriptions + options = get_option_descriptions(rest) + msg = """ +Configuration options +===================== + +These should be set under the [hgview] section of the hgrc config file. + +""" + msg += '\n'.join(["- " + v for v in options]) + '\n' + msg += """ +The 'users' config statement should be the path of a file +describing users, like:: + + --8<------------------------------------------- + # file ~/.hgusers + id=david + alias=david.douard@logilab.fr + alias=david@logilab.fr + alias=David Douard <david.douard@logilab.fr> + color=#FF0000 + id=ludal + alias=ludovic.aubry@logilab.fr + alias=ludal@logilab.fr + alias=Ludovic Aubry <ludovic.aubry@logilab.fr> + color=#00FF00 + --8<------------------------------------------- + +This allow to make several 'authors' under the same name, with the +same color, in the graphlog browser. + """ + return msg + +long_help_msg = help_msg + get_options_helpmsg()
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​__init__.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*- +# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# +# make sure the Qt rc files are converted into python modules, then load them +# this must be done BEFORE other hgqv qt4 modules are loaded. +import os +import os.path as osp +import sys +import mx.DateTime as dt + +def should_rebuild(srcfile, pyfile): + return not osp.isfile(pyfile) or osp.isfile(srcfile) and \ + osp.getmtime(pyfile) < osp.getmtime(srcfile) + +# automatically load resource module, creating it on the fly if +# required +curdir = osp.dirname(__file__) +pyfile = osp.join(curdir, "hgqv_rc.py") +rcfile = osp.join(curdir, "hgqv.qrc") +if should_rebuild(rcfile, pyfile): + if os.system('pyrcc4 %s -o %s' % (rcfile, pyfile)): + print "ERROR: Cannot convert the resource file '%s' into a python module." + print "Please check the PyQt 'pyrcc4' tool is installed, or do it by hand running:" + print "pyrcc4 %s -o %s" % (rcfile, pyfile) + +# load icons from resource and store them in a dict, no matter their +# extension (.svg or .png) +from PyQt4 import QtCore +from PyQt4 import QtGui, uic +connect = QtCore.QObject.connect +SIGNAL = QtCore.SIGNAL +Qt = QtCore.Qt +import hgqv_rc + + +_icons = {} +def _load_icons(): + t = dt.today() + x = t.month == 12 and t.day in (24,25) + d = QtCore.QDir(':/icons') + for icn in d.entryList(): + name, ext = osp.splitext(str(icn)) + if name not in _icons or ext == ".svg": + _icons[name] = QtGui.QIcon(':/icons/%s' % icn) + if x: + for name in _icons: + if name.endswith('_x'): + _icons[name[:-2]] = _icons[name] + +def icon(name): + """ + Return a QIcon for the resource named 'name.(svg|png)' (the given + 'name' parameter must *not* provide the extension). + """ + if not _icons: + _load_icons() + return _icons.get(name) + + +# dirty hack to please PyQt4 uic +import hgrepoview, hgfileview +sys.modules['hgrepoview'] = hgrepoview +sys.modules['hgfileview'] = hgfileview +sys.modules['hgqv_rc'] = hgqv_rc + +def setup_font_substitutions(): + # be sure monospace default font for diffs have a decent substitution + # on MacOS + QtGui.QFont.insertSubstitutions('monospace', ['monaco', 'courier new'])
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​blockmatcher.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*- +# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +""" +Qt4 widgets to display diffs as blocks +""" +import sys, os + +from PyQt4 import QtGui, QtCore +from PyQt4.QtCore import Qt, SIGNAL + +class BlockList(QtGui.QWidget): + """ + A simple widget to be 'linked' to the scrollbar of a diff text + view. + + It represents diff blocks with coloured rectangles, showing + currently viewed area by a semi-transparant rectangle sliding + above them. + """ + def __init__(self, *args): + QtGui.QWidget.__init__(self, *args) + self._blocks = set() + self._minimum = 0 + self._maximum = 100 + self.blockTypes = {'+': QtGui.QColor(0xA0, 0xFF, 0xB0, ),#0xa5), + '-': QtGui.QColor(0xFF, 0xA0, 0xA0, ),#0xa5), + 'x': QtGui.QColor(0xA0, 0xA0, 0xFF, ),#0xa5), + } + self._sbar = None + self._value = 0 + self._pagestep = 10 + self._vrectcolor = QtGui.QColor(0x00, 0x00, 0x55, 0x25) + self._vrectbordercolor = self._vrectcolor.darker() + self.sizePolicy().setControlType(QtGui.QSizePolicy.Slider) + self.setMinimumWidth(20) + + def clear(self): + self._blocks = set() + + def addBlock(self, typ, alo, ahi): + self._blocks.add((typ, alo, ahi)) + + def setMaximum(self, maximum): + self._maximum = maximum + self.update() + self.emit(SIGNAL('rangeChanged(int, int)'), + self._minimum, self._maximum) + + def setMinimum(self, minimum): + self._minimum = minimum + self.update() + self.emit(SIGNAL('rangeChanged(int, int)'), + self._minimum, self._maximum) + + def setRange(self, minimum, maximum): + self._minimum = minimum + self._maximum = maximum + self.update() + self.emit(SIGNAL('rangeChanged(int, int)'), + self._minimum, self._maximum) + + def setValue(self, val): + if val != self._value: + self._value = val + self.update() + self.emit(SIGNAL('valueChanged(int)'), val) + + def setPageStep(self, pagestep): + if pagestep != self._pagestep: + self._pagestep = pagestep + self.update() + self.emit(SIGNAL('pageStepChanged(int)'), pagestep) + + def linkScrollBar(self, sbar): + """ + Make the block list displayer be linked to the scrollbar + """ + self._sbar = sbar + self.setUpdatesEnabled(False) + self.setMaximum(sbar.maximum()) + self.setMinimum(sbar.minimum()) + self.setPageStep(sbar.pageStep()) + self.setValue(sbar.value()) + self.setUpdatesEnabled(True) + self.connect(sbar, SIGNAL('valueChanged(int)'), self.setValue) + self.connect(sbar, SIGNAL('rangeChanged(int, int)'), self.setRange) + self.connect(self, SIGNAL('valueChanged(int)'), sbar.setValue) + self.connect(self, SIGNAL('rangeChanged(int, int)'), sbar.setRange) + self.connect(self, SIGNAL('pageStepChanged(int)'), sbar.setPageStep) + + def syncPageStep(self): + self.setPageStep(self._sbar.pageStep()) + + def paintEvent(self, event): + w = self.width() - 1 + h = self.height() + p = QtGui.QPainter(self) + p.scale(1.0, float(h)/(self._maximum - self._minimum + self._pagestep)) + p.setPen(Qt.NoPen) + for typ, alo, ahi in self._blocks: + p.save() + p.setBrush(self.blockTypes[typ]) + p.drawRect(1, alo, w-1, ahi-alo) + p.restore() + + p.save() + p.setPen(self._vrectbordercolor) + p.setBrush(self._vrectcolor) + p.drawRect(0, self._value, w, self._pagestep) + p.restore() + +class BlockMatch(BlockList): + """ + A simpe widget to be linked to 2 file views (text areas), + displaying 2 versions of a same file (diff). + + It will show graphically matching diff blocks between the 2 text + areas. + """ + def __init__(self, *args): + QtGui.QWidget.__init__(self, *args) + self._blocks = set() + self._minimum = {'left': 0, 'right': 0} + self._maximum = {'left': 100, 'right': 100} + self.blockTypes = {'+': QtGui.QColor(0xA0, 0xFF, 0xB0, ),#0xa5), + '-': QtGui.QColor(0xFF, 0xA0, 0xA0, ),#0xa5), + 'x': QtGui.QColor(0xA0, 0xA0, 0xFF, ),#0xa5), + } + self._sbar = {} + self._value = {'left': 0, 'right': 0} + self._pagestep = {'left': 10, 'right': 10} + self._vrectcolor = QtGui.QColor(0x00, 0x00, 0x55, 0x25) + self._vrectbordercolor = self._vrectcolor.darker() + self.sizePolicy().setControlType(QtGui.QSizePolicy.Slider) + self.setMinimumWidth(20) + + def nDiffs(self): + return len(self._blocks) + + def showDiff(self, delta): + ps_l = float(self._pagestep['left']) + ps_r = float(self._pagestep['right']) + mv_l = self._value['left'] + mv_r = self._value['right'] + Mv_l = mv_l + ps_l + Mv_r = mv_r + ps_r + + vblocks = [] + blocks = sorted(self._blocks, key=lambda x:(x[1],x[3],x[2],x[4])) + for i, (typ, alo, ahi, blo, bhi) in enumerate(blocks): + if (mv_l<=alo<=Mv_l or mv_l<=ahi<=Mv_l or + mv_r<=blo<=Mv_r or mv_r<=bhi<=Mv_r): + break + else: + i = -1 + i += delta + + if i < 0: + return -1 + if i >= len(blocks): + return 1 + typ, alo, ahi, blo, bhi = blocks[i] + self.setValue(alo, "left") + self.setValue(blo, "right") + if i == 0: + return -1 + if i == len(blocks)-1: + return 1 + return 0 + + def nextDiff(self): + return self.showDiff(+1) + + def prevDiff(self): + return self.showDiff(-1) + + def addBlock(self, typ, alo, ahi, blo=None, bhi=None): + if bhi is None: + bhi = ahi + if blo is None: + blo = alo + self._blocks.add((typ, alo, ahi, blo, bhi)) + + def paintEvent(self, event): + w = self.width() + h = self.height() + p = QtGui.QPainter(self) + p.setRenderHint(p.Antialiasing) + + ps_l = float(self._pagestep['left']) + ps_r = float(self._pagestep['right']) + v_l = self._value['left'] + v_r = self._value['right'] + + # we do integer divisions here cause the pagestep is the + # integer number of fully displayed text lines + scalel = self._sbar['left'].height()//ps_l + scaler = self._sbar['right'].height()//ps_r + + ml = v_l + Ml = v_l + ps_l + mr = v_r + Mr = v_r + ps_r + + p.setPen(Qt.NoPen) + for typ, alo, ahi, blo, bhi in self._blocks: + if not (ml<=alo<=Ml or ml<=ahi<=Ml or mr<=blo<=Mr or mr<=bhi<=Mr): + continue + p.save() + p.setBrush(self.blockTypes[typ]) + + path = QtGui.QPainterPath() + path.moveTo(0, scalel * (alo - ml)) + path.cubicTo(w/3.0, scalel * (alo - ml), + 2*w/3.0, scaler * (blo - mr), + w, scaler * (blo - mr)) + path.lineTo(w, scaler * (bhi - mr) + 2) + path.cubicTo(2*w/3.0, scaler * (bhi - mr) + 2, + w/3.0, scalel * (ahi - ml) + 2, + 0, scalel * (ahi - ml) + 2) + path.closeSubpath() + p.drawPath(path) + + p.restore() + + def setMaximum(self, maximum, side): + self._maximum[side] = maximum + self.update() + self.emit(SIGNAL('rangeChanged(int, int, const QString &)'), + self._minimum[side], self._maximum[side], side) + + def setMinimum(self, minimum, side): + self._minimum[side] = minimum + self.update() + self.emit(SIGNAL('rangeChanged(int, int, const QString &)'), + self._minimum[side], self._maximum[side], side) + + def setRange(self, minimum, maximum, side=None): + if side is None: + if self.sender() == self._sbar['left']: + side = 'left' + else: + side = 'right' + self._minimum[side] = minimum + self._maximum[side] = maximum + self.update() + self.emit(SIGNAL('rangeChanged(int, int, const QString &)'), + self._minimum[side], self._maximum[side], side) + + def setValue(self, val, side=None): + if side is None: + if self.sender() == self._sbar['left']: + side = 'left' + else: + side = 'right' + if val != self._value[side]: + self._value[side] = val + self.update() + self.emit(SIGNAL('valueChanged(int, const QString &)'), val, side) + + def setPageStep(self, pagestep, side): + if pagestep != self._pagestep[side]: + self._pagestep[side] = pagestep + self.update() + self.emit(SIGNAL('pageStepChanged(int, const QString &)'), + pagestep, side) + + def syncPageStep(self): + for side in ['left', 'right']: + self.setPageStep(self._sbar[side].pageStep(), side) + + def resizeEvent(self, event): + self.syncPageStep() + + def linkScrollBar(self, sb, side): + """ + Make the block list displayer be linked to the scrollbar + """ + if self._sbar is None: + self._sbar = {} + self._sbar[side] = sb + self.setUpdatesEnabled(False) + self.setMaximum(sb.maximum(), side) + self.setMinimum(sb.minimum(), side) + self.setPageStep(sb.pageStep(), side) + self.setValue(sb.value(), side) + self.setUpdatesEnabled(True) + self.connect(sb, SIGNAL('valueChanged(int)'), self.setValue) + self.connect(sb, SIGNAL('rangeChanged(int, int)'), self.setRange) + + self.connect(self, SIGNAL('valueChanged(int, const QString &)'), + lambda v, s: side==s and sb.setValue(v)) + self.connect(self, SIGNAL('rangeChanged(int, int, const QString )'), + lambda v1, v2, s: side==s and sb.setRange(v1, v2)) + self.connect(self, SIGNAL('pageStepChanged(int, const QString )'), + lambda v, s: side==s and sb.setPageStep(v)) + +if __name__ == '__main__': + a = QtGui.QApplication([]) + f = QtGui.QFrame() + l = QtGui.QHBoxLayout(f) + + sb1 = QtGui.QScrollBar() + sb2 = QtGui.QScrollBar() + + w0 = BlockList() + w0.addBlock('-', 200, 300) + w0.addBlock('-', 450, 460) + w0.addBlock('x', 500, 501) + w0.linkScrollBar(sb1) + + w1 = BlockMatch() + w1.addBlock('+', 12, 42) + w1.addBlock('+', 55, 142) + w1.addBlock('-', 200, 300) + w1.addBlock('-', 330, 400, 450, 460) + w1.addBlock('x', 420, 450, 500, 501) + w1.linkScrollBar(sb1, 'left') + w1.linkScrollBar(sb2, 'right') + + w2 = BlockList() + w2.addBlock('+', 12, 42) + w2.addBlock('+', 55, 142) + w2.addBlock('x', 420, 450) + w2.linkScrollBar(sb2) + + l.addWidget(sb1) + l.addWidget(w0) + l.addWidget(w1) + l.addWidget(w2) + l.addWidget(sb2) + + w0.setRange(0, 1200) + w0.setPageStep(100) + w1.setRange(0, 1200, 'left') + w1.setRange(0, 1200, 'right') + w1.setPageStep(100, 'left') + w1.setPageStep(100, 'right') + w2.setRange(0, 1200) + w2.setPageStep(100) + + print "sb1=", sb1.minimum(), sb1.maximum(), sb1.pageStep() + print "sb2=", sb2.minimum(), sb2.maximum(), sb2.pageStep() + + f.show() + a.exec_() +
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​filediffviewer.ui Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
@@ -0,0 +1,163 @@
+<ui version="4.0" > + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>620</width> + <height>546</height> + </rect> + </property> + <property name="windowTitle" > + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget" > + <property name="geometry" > + <rect> + <x>0</x> + <y>33</y> + <width>620</width> + <height>513</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout" > + <property name="margin" > + <number>0</number> + </property> + <item> + <widget class="QSplitter" name="splitter" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <widget class="QWidget" name="layoutWidget" > + <layout class="QHBoxLayout" name="horizontalLayout" > + <property name="spacing" > + <number>0</number> + </property> + <item> + <widget class="HgRepoView" name="tableView_revisions_left" > + <property name="alternatingRowColors" > + <bool>true</bool> + </property> + <property name="selectionMode" > + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior" > + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid" > + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="HgRepoView" name="tableView_revisions_right" > + <property name="alternatingRowColors" > + <bool>true</bool> + </property> + <property name="selectionMode" > + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior" > + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid" > + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="layoutWidget" > + <layout class="QHBoxLayout" name="horizontalLayout_2" > + <property name="spacing" > + <number>0</number> + </property> + <item> + <widget class="QFrame" name="frame" > + <property name="frameShape" > + <enum>QFrame::NoFrame</enum> + </property> + <property name="frameShadow" > + <enum>QFrame::Raised</enum> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QToolBar" name="toolBar" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>121</width> + <height>33</height> + </rect> + </property> + <property name="windowTitle" > + <string>toolBar</string> + </property> + <attribute name="toolBarArea" > + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak" > + <bool>true</bool> + </attribute> + <addaction name="actionClose" /> + <addaction name="actionReload" /> + </widget> + <widget class="QToolBar" name="toolBar_edit" > + <property name="geometry" > + <rect> + <x>121</x> + <y>0</y> + <width>499</width> + <height>33</height> + </rect> + </property> + <property name="windowTitle" > + <string>toolBar_2</string> + </property> + <attribute name="toolBarArea" > + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak" > + <bool>false</bool> + </attribute> + </widget> + <action name="actionClose" > + <property name="text" > + <string>Close</string> + </property> + <property name="shortcut" > + <string>Ctrl+Q</string> + </property> + </action> + <action name="actionReload" > + <property name="text" > + <string>Reload</string> + </property> + <property name="shortcut" > + <string>Ctrl+R</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>HgRepoView</class> + <extends>QTableView</extends> + <header>hgrepoview.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>tableView_revisions_left</tabstop> + <tabstop>tableView_revisions_right</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​fileviewer.ui Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@@ -0,0 +1,108 @@
+<ui version="4.0" > + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>481</width> + <height>438</height> + </rect> + </property> + <property name="windowTitle" > + <string>hg FileViewer</string> + </property> + <widget class="QWidget" name="centralwidget" > + <property name="geometry" > + <rect> + <x>0</x> + <y>33</y> + <width>481</width> + <height>405</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout" > + <property name="margin" > + <number>2</number> + </property> + <item> + <widget class="QSplitter" name="splitter" > + <property name="orientation" > + <enum>Qt::Vertical</enum> + </property> + <widget class="HgRepoView" name="tableView_revisions" > + <property name="alternatingRowColors" > + <bool>true</bool> + </property> + <property name="selectionMode" > + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior" > + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid" > + <bool>false</bool> + </property> + <property name="gridStyle" > + <enum>Qt::NoPen</enum> + </property> + </widget> + <widget class="HgFileView" native="1" name="textView" /> + </widget> + </item> + </layout> + </widget> + <widget class="QToolBar" name="toolBar_edit" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>481</width> + <height>33</height> + </rect> + </property> + <property name="windowTitle" > + <string>toolBar</string> + </property> + <attribute name="toolBarArea" > + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak" > + <bool>false</bool> + </attribute> + <addaction name="actionClose" /> + <addaction name="actionReload" /> + </widget> + <action name="actionClose" > + <property name="text" > + <string>Close</string> + </property> + <property name="shortcut" > + <string>Ctrl+Q</string> + </property> + </action> + <action name="actionReload" > + <property name="text" > + <string>Reload</string> + </property> + <property name="shortcut" > + <string>Ctrl+R</string> + </property> + </action> + </widget> + <customwidgets> + <customwidget> + <class>HgRepoView</class> + <extends>QTableView</extends> + <header>hgrepoview.h</header> + </customwidget> + <customwidget> + <class>HgFileView</class> + <extends>QWidget</extends> + <header>hgfileview.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui>
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​helpviewer.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@@ -0,0 +1,47 @@
+# -*- coding: iso-8859-1 -*- +#!/usr/bin/env python +# main.py - qt4-based hg rev log browser +# +# Copyright (C) 2007-2010 Logilab. All rights reserved. +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +""" +Help window for hgview +""" +import sys, os +import re + +from PyQt4 import QtCore, QtGui, Qsci + +from hgviewlib.qt4 import icon as geticon +from hgviewlib.qt4.hgdialogmixin import HgDialogMixin +from hgviewlib.hgviewhelp import help_msg, get_options_helpmsg + +Qt = QtCore.Qt +bold = QtGui.QFont.Bold +connect = QtCore.QObject.connect +SIGNAL = QtCore.SIGNAL + +from docutils.core import publish_string + +class HelpViewer(QtGui.QDialog, HgDialogMixin): + """hgview simple help viewer""" + _uifile = 'helpviewer.ui' + def __init__(self, repo, parent=None): + self.repo = repo + QtGui.QDialog.__init__(self, parent) + HgDialogMixin.__init__(self) + data = help_msg + get_options_helpmsg(rest=True) + + html = publish_string(data, writer_name='html') + self.textBrowser.setText(html) + + # must be redefined cause it's a QDialog + def accept(self): + QtGui.QDialog.accept(self) + + def reject(self): + QtGui.QDialog.reject(self) + +
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​helpviewer.ui Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@@ -0,0 +1,66 @@
+<ui version="4.0" > + <class>Dialog</class> + <widget class="QDialog" name="Dialog" > + <property name="geometry" > + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle" > + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" > + <item> + <widget class="QTextBrowser" name="textBrowser" /> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox" > + <property name="orientation" > + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons" > + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>Dialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel" > + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel" > + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>Dialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel" > + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel" > + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​hgdialogmixin.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*- +# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# +# make sur the Qt rc files are converted into python modules, then load them +# this must be done BEFORE other hgview qt4 modules are loaded. +import os +import os.path as osp +import sys + +from PyQt4 import QtCore +from PyQt4 import QtGui, uic +connect = QtCore.QObject.connect +SIGNAL = QtCore.SIGNAL +Qt = QtCore.Qt + +from hgviewlib.config import HgConfig +from hgviewlib.qt4 import should_rebuild + +class HgDialogMixin(object): + """ + Mixin for QDialogs defined from a .ui file, wich automates the + setup of the UI from the ui file, and the loading of user + preferences. + The main class must define a '_ui_file' class attribute. + """ + def __init__(self): + # self.repo must be defined in actual class before calling __init__ + assert self.repo is not None + self.load_config() + + _path = osp.dirname(__file__) + uifile = osp.join(_path, self._uifile) + pyfile = uifile.replace(".ui", "_ui.py") + if should_rebuild(uifile, pyfile): + os.system('pyuic4 %s -o %s' % (uifile, pyfile)) + try: + modname = osp.splitext(osp.basename(uifile))[0] + "_ui" + modname = "hgviewlib.qt4.%s" % modname + mod = __import__(modname, fromlist=['*']) + classnames = [x for x in dir(mod) if x.startswith('Ui_')] + if len(classnames) == 1: + ui_class = getattr(mod, classnames[0]) + elif 'Ui_MainWindow' in classnames: + ui_class = getattr(mod, 'Ui_MainWindow') + else: + raise ValueError("Can't determine which main class to use in %s" % modname) + except ImportError: + ui_class, base_class = uic.loadUiType(uifile) + + if ui_class not in self.__class__.__bases__: + # hacking by adding the form class from ui file or pyuic4 + # generated module because we cannot use metaclass here, + # due to "QObject" not being a subclass of "object" + self.__class__.__bases__ = self.__class__.__bases__ + (ui_class,) + self.setupUi(self) + self.load_ui() + self.disab_shortcuts = [] + + def load_ui(self): + # we explicitely create a QShortcut so we can disable it + # when a "helper context toolbar" is activated (which can be + # closed hitting the Esc shortcut) + self.esc_shortcut = QtGui.QShortcut(self) + self.esc_shortcut.setKey(Qt.Key_Escape) + connect(self.esc_shortcut, SIGNAL('activated()'), + self.maybeClose) + self._quickbars = [] + + def attachQuickBar(self, qbar): + qbar.setParent(self) + self._quickbars.append(qbar) + connect(qbar, SIGNAL('escShortcutDisabled(bool)'), + self.setShortcutsEnabled) + self.addToolBar(Qt.BottomToolBarArea, qbar) + connect(qbar, SIGNAL('visible'), + self.ensureOneQuickBar) + + def setShortcutsEnabled(self, enabled=True): + for sh in self.disab_shortcuts: + sh.setEnabled(enabled) + + def ensureOneQuickBar(self): + tb = self.sender() + for w in self._quickbars: + if w is not tb: + w.hide() + + def maybeClose(self): + for w in self._quickbars: + if w.isVisible(): + w.cancel() + break + else: + self.close() + + def load_config(self): + cfg = HgConfig(self.repo.ui) + fontstr = cfg.getFont() + font = QtGui.QFont() + try: + if not font.fromString(fontstr): + raise Exception + except: + print "bad font name '%s'" % fontstr + font.setFamily("Monospace") + font.setFixedPitch(True) + font.setPointSize(10) + self._font = font + + self.rowheight = cfg.getRowHeight() + self.users, self.aliases = cfg.getUsers() + return cfg + + def accept(self): + self.close() + def reject(self): + self.close() + +
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*- +# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +""" +Qt4 dialogs to display hg revisions of a file +""" + +import sys, os +import os.path as osp + +from mercurial import ui, hg, util +from mercurial.revlog import LookupError + +from PyQt4 import QtGui, QtCore, Qsci +from PyQt4.QtCore import Qt + +from hgviewlib.util import tounicode + +from hgviewlib.qt4 import icon as geticon +from hgviewlib.qt4.hgdialogmixin import HgDialogMixin +from hgviewlib.qt4.hgrepomodel import ManifestModel +from hgviewlib.qt4.lexers import get_lexer + +connect = QtCore.QObject.connect +disconnect = QtCore.QObject.disconnect +SIGNAL = QtCore.SIGNAL +nullvariant = QtCore.QVariant() + + +class ManifestViewer(QtGui.QMainWindow, HgDialogMixin): + """ + Qt4 dialog to display all files of a repo at a given revision + """ + _uifile = 'manifestviewer.ui' + def __init__(self, repo, noderev): + self.repo = repo + QtGui.QMainWindow.__init__(self) + HgDialogMixin.__init__(self) + self.setWindowTitle('Hg manifest viewer - %s:%s' % (repo.root, noderev)) + + # hg repo + self.repo = repo + self.rev = noderev + self.setupModels() + + self.createActions() + self.setupTextview() + + def load_config(self): + cfg = HgDialogMixin.load_config(self) + self.max_file_size = cfg.getMaxFileSize() + + def setupModels(self): + self.treemodel = ManifestModel(self.repo, self.rev) + self.treeView.setModel(self.treemodel) + connect(self.treeView.selectionModel(), + SIGNAL('currentChanged(const QModelIndex &, const QModelIndex &)'), + self.fileSelected) + + def createActions(self): + connect(self.actionClose, SIGNAL('triggered()'), + self.close) + self.actionClose.setIcon(geticon('quit')) + + def setupTextview(self): + lay = QtGui.QHBoxLayout(self.mainFrame) + lay.setSpacing(0) + lay.setContentsMargins(0,0,0,0) + sci = Qsci.QsciScintilla(self.mainFrame) + lay.addWidget(sci) + sci.setMarginLineNumbers(1, True) + sci.setMarginWidth(1, '000') + sci.setReadOnly(True) + sci.setFont(self._font) + + sci.SendScintilla(sci.SCI_SETSELEOLFILLED, True) + self.textView = sci + + def fileSelected(self, index, *args): + if not index.isValid(): + return + path = self.treemodel.pathFromIndex(index) + try: + fc = self.repo.changectx(self.rev).filectx(path) + except LookupError: + # may occur when a directory is selected + self.textView.setMarginWidth(1, '00') + self.textView.setText('') + return + + if fc.size() > self.max_file_size: + data = "file too big" + else: + # return the whole file + data = fc.data() + if util.binary(data): + data = "binary file" + else: + data = tounicode(data) + lexer = get_lexer(path, data) + if lexer: + lexer.setFont(self._font) + self.textView.setLexer(lexer) + self._cur_lexer = lexer + nlines = data.count('\n') + self.textView.setMarginWidth(1, str(nlines)+'00') + self.textView.setText(data) + + def setCurrentFile(self, filename): + index = QtCore.QModelIndex() + path = filename.split(osp.sep) + for p in path: + self.treeView.expand(index) + for row in range(self.treemodel.rowCount(index)): + newindex = self.treemodel.index(row, 0, index) + if newindex.internalPointer().data(0) == p: + index = newindex + break + self.treeView.setCurrentIndex(index) + + +if __name__ == '__main__': + from mercurial import ui, hg + from optparse import OptionParser + opt = OptionParser() + opt.add_option('-R', '--repo', + dest='repo', + default='.', + help='Hg repository') + + options, args = opt.parse_args() + if len(args) != 1: + opt.error('Please specify a revision number') + rev = args[0] + + u = ui.ui() + repo = hg.repository(u, options.repo) + app = QtGui.QApplication([]) + + view = ManifestViewer(repo, int(rev)) + view.show() + sys.exit(app.exec_()) +
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​hgqv.qrc Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@@ -0,0 +1,23 @@
+<RCC> + <qresource prefix="/" > + <file>icons/quit.svg</file> + <file>icons/reload.svg</file> + <file>icons/back.svg</file> + <file>icons/forward.svg</file> + <file>icons/left.svg</file> + <file>icons/right.svg</file> + <file>icons/up.svg</file> + <file>icons/down.svg</file> + <file>icons/leftright.svg</file> + <file>icons/close.png</file> + <file>icons/help.svg</file> + <file>icons/find.svg</file> + <file>icons/goto.svg</file> + <file>icons/modified.svg</file> + <file>icons/clean.svg</file> + <file>icons/mqdiff.svg</file> + <file>icons/mqdiff_x.svg</file> + <file>icons/mqpatch.svg</file> + <file>icons/mqpatch_x.svg</file> + </qresource> +</RCC>
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​icons/​README Stacked
 
 
 
1
@@ -0,0 +1,1 @@
+Most of the icons used here are from the Tango Icon Theme. Some of them have been modified.
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
Added image
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​icons/​help.svg Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_svg "http://www.w3.org/2000/svg"> + <!ENTITY ns_xlink "http://www.w3.org/1999/xlink"> +]> +<svg version="1.1" id="Layer_1" xmlns="&ns_svg;" xmlns:xlink="&ns_xlink;" width="128" height="128" viewBox="0 0 128 128" + overflow="visible" enable-background="new 0 0 128 128" xml:space="preserve"> + <path opacity="0.2" d="M3,64c0,34.188,27.813,62,62,62c34.188,0,62-27.813,62-62S99.188,2,65,2C30.813,2,3,29.813,3,64z"/> + <path opacity="0.2" d="M4,64c0,33.636,27.364,61,61,61s61-27.364,61-61S98.636,3,65,3S4,30.364,4,64z"/> + <linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="53.6196" y1="-10.749" x2="79.1196" y2="154.5011"> + <stop offset="0" style="stop-color:#295AA6"/> + <stop offset="0.3319" style="stop-color:#27569F"/> + <stop offset="0.8192" style="stop-color:#224C8B"/> + <stop offset="1" style="stop-color:#1F4782"/> + </linearGradient> + <path fill="url(#XMLID_5_)" d="M4,63c0,33.636,27.364,61,61,61s61-27.364,61-61S98.636,2,65,2S4,29.364,4,63z"/> + <linearGradient id="XMLID_6_" gradientUnits="userSpaceOnUse" x1="69.3184" y1="125.5117" x2="64.0683" y2="49.5123"> + <stop offset="0" style="stop-color:#2C60B0"/> + <stop offset="0.3199" style="stop-color:#2A5CA9"/> + <stop offset="0.7896" style="stop-color:#245195"/> + <stop offset="1" style="stop-color:#214B8A"/> + </linearGradient> + <circle fill="url(#XMLID_6_)" cx="65" cy="63" r="60"/> + <linearGradient id="XMLID_7_" gradientUnits="userSpaceOnUse" x1="17.7852" y1="24.6035" x2="124.5362" y2="98.6042"> + <stop offset="0" style="stop-color:#728FBA"/> + <stop offset="1" style="stop-color:#3C6199"/> + </linearGradient> + <path fill="url(#XMLID_7_)" d="M57.067,53.727C86,43,123.648,50.273,123.648,50.273C117.811,23.25,93.771,3,65,3 + C31.863,3,5,29.863,5,63c0,8.284,1.679,16.176,4.715,23.354C9.715,86.355,23.271,66.256,57.067,53.727z"/> + <path fill="#4C7AC2" d="M65,122C32.03,122,5.283,95.406,5.013,62.5C5.011,62.668,5,62.832,5,63c0,33.137,26.863,60,60,60 + c33.137,0,60-26.863,60-60c0-0.168-0.012-0.332-0.013-0.5C124.717,95.406,97.97,122,65,122z"/> + <linearGradient id="XMLID_8_" gradientUnits="userSpaceOnUse" x1="5" y1="33.3506" x2="125" y2="33.3506"> + <stop offset="0" style="stop-color:#90A5C4"/> + <stop offset="0.4719" style="stop-color:#D0E0FB"/> + <stop offset="0.8119" style="stop-color:#ACBAD5"/> + <stop offset="1" style="stop-color:#9DAAC5"/> + </linearGradient> + <path fill="url(#XMLID_8_)" d="M65,4.101c32.97,0,59.717,26.595,59.987,59.5c0.001-0.167,0.013-0.333,0.013-0.5 + c0-33.138-26.863-60-60-60c-33.137,0-60,26.862-60,60c0,0.167,0.011,0.333,0.013,0.5C5.283,30.695,32.03,4.101,65,4.101z"/> + <path fill="#FFFFFF" d="M40.86,25.273c7.22-2.143,14.779-3.723,22.336-3.723c12.974,0,25.721,5.754,25.721,20.194 + c0,15.794-20.531,22.111-19.178,36.891H55.864c0-10.718,4.175-16.585,8.236-21.21c4.174-4.625,8.349-8.01,8.349-13.877 + c0-7.444-6.431-10.266-12.974-10.266c-5.979,0-11.958,2.031-17.373,4.514L40.86,25.273z M54.962,85.855h15.794v15.793H54.962 + V85.855z"/> +</svg>
Change 1 of 1 Show Entire File tortoisehg/​hgqt/​hgviewlib/​qt4/​icons/​left.svg Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="345.32999" + height="345.32999" + id="svg2" + sodipodi:version="0.32" + inkscape:version="0.46" + sodipodi:docname="left.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0"> + <defs + id="defs4"> + <marker + inkscape:stockid="EmptyTriangleInL" + orient="auto" + refY="0" + refX="0" + id="EmptyTriangleInL" + style="overflow:visible"> + <path + id="path3310" + d="M 5.77,0 L -2.88,5 L -2.88,-5 L 5.77,0 z" + style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none" + transform="matrix(-0.8,0,0,-0.8,4.8,0)" /> + </marker> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 526.18109 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="744.09448 : 526.18109 : 1" + inkscape:persp3d-origin="372.04724 : 350.78739 : 1" + id="perspective10" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.6292675" + inkscape:cx="162.63104" + inkscape:cy="175.19921" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1189" + inkscape:window-height="767" + inkscape:window-x="0" + inkscape:window-y="0" + showguides="true" + inkscape:guide-bbox="true"> + <sodipodi:guide + orientation="1,0" + position="237.98396,631.10233" + id="guide3977" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-237.99891,-394.73638)"> + <g + id="g4070" + transform="translate(27.2422,68.587853)"> + <g + id="g4023" /> + <path + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.19999981;stroke-dasharray:none;stroke-opacity:1" + d="M 265.95591,479.63572 L 265.95591,517.98928 L 527.31536,517.98928 L 527.31536,479.63572 L 265.95591,479.63572 z" + id="path3973" /> + <path + sodipodi:nodetypes="ccc" + style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:30;marker-start:none;stroke-miterlimit:4;stroke-dasharray:none" + d="M 353.78909,587.89066 L 252.99891,498.81352 L 353.78909,409.73638" + id="path4037" /> + </g> + </g> +</svg>
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes
This file's diff was not loaded because this changeset is very large. Load changes