Kiln » TortoiseHg » TortoiseHg
Clone URL:  
Pushed to one repository · View In Graph Contained in 1.1, 1.1.1, and 1.1.2

Merge with stable

Changeset f0abb760ff7f

Parents b74c6392c828

Parents 38a939b2e4c0

by Steve Borho

Changes to 13 files · Browse files at f0abb760ff7f Showing diff from parent b74c6392c828 38a939b2e4c0 Diff from another changeset...

 
50
51
52
 
 
 
53
54
55
 
50
51
52
53
54
55
56
57
58
@@ -50,6 +50,9 @@
  Launch the web server dialog in a separate process   :guilabel:`Shelve`   Launch the shelve tool in a separate process + :guilabel:`Patch Branch` + Toggles the display of the Patch Branch pane. This button is only + visible when the pbranch extension has been enabled by the user.   :guilabel:`Load more`   Load the next N revisions into the graph   :guilabel:`Load all`
 
230
231
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
@@ -230,3 +230,44 @@
 remote_repository_url combination).    .. vim: noet ts=4 + +pbranch +======= + +Patch Branches (`pbranch <http://mercurial.selenic.com/wiki/PatchBranchExtension>`_) +is a way to develop a series of patches for submission into a main +repo. It is based on topic branches, one per patch, and is thus highly +suitable for collaborative and/or long-term patch development and +maintenance. + +'A detailed manual <http://arrenbrecht.ch/mercurial/pbranch/>'_ can be found online. + +It adds a number of commands which can be listed with +:command:`hg help pbranch`:: + + pbackout - backs out the current patch branch (undoes all its changes) + pdiff - prints the final diff for the current or given patch branch + peditmessage - edit the patch message + pemail - send patches by email + pexport - exports patches + pextdiff - combines pdiff and extdiff + pgraph - print an ASCII art rendering of the patch dependency graph + pmerge - merge pending heads from dependencies into patch branches + pmessage - print the patch message(s) + pnew - start a new patch branch + pstatus - print status of current (or given) patch branch + reapply - reverts the working copy of all files touched by REV to REV + +**Installation** + +To test the use of this plugin, you can specify it on the Mercurial +command line like this:: + + hg --config "extensions.pbranch=" pstatus + +You may want to add it to your Mercurial.ini or a repository's hgrc like this:: + + [extensions] + pbranch= + +If you do this, you can omit the --config command-line option.
 
194
195
196
 
 
 
 
 
 
 
 
 
 
197
198
199
 
201
202
203
204
 
 
 
 
 
205
206
207
 
343
344
345
346
347
348
349
 
422
423
424
425
 
426
427
428
 
578
579
580
581
 
582
583
584
 
590
591
592
593
 
594
595
596
 
793
794
795
796
 
797
798
799
 
805
806
807
808
 
809
810
 
811
812
 
813
814
 
815
816
817
818
 
819
820
821
822
 
823
824
825
 
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
 
211
212
213
 
214
215
216
217
218
219
220
221
 
357
358
359
 
360
361
362
 
435
436
437
 
438
439
440
441
 
591
592
593
 
594
595
596
597
 
603
604
605
 
606
607
608
609
 
806
807
808
 
809
810
811
812
 
818
819
820
 
821
822
 
823
824
 
825
826
 
827
828
829
830
 
831
832
833
834
 
835
836
837
838
@@ -194,6 +194,16 @@
    def load_patch_details(self, patchfile):   'Load specified patch details into buffer and file list' + pf = open(patchfile) + self.load_patch_details_from_file_object(pf, patchfile) + + def load_patch_details_from_file_object(self, pf, patchfile, isTemp=False): + """ Load patch details into buffer and file list + :param pf: open file object + :param patchfile: path and name of patch file + :param isTemp: if True, then pf is a temporary file + and patchfile does not exist + """   self._filelist.clear()   self._filelist.append(('*', _('[All Files]'), ''))   @@ -201,7 +211,11 @@
  self.currev = -1   self.curphunks = {}   self.curpatch = patchfile - pf = open(self.curpatch) + if isTemp: + # pf is a temporary, so update panel cache while we can + patch_ctx = csinfo.patchctx(patchfile, self.repo, patchHandle=pf) + self.summarypanel.update(patch_ctx, self.patchstyle) + pf.seek(0)   def get_path(a, b):   type = (a == '/dev/null') and 'A' or 'M'   type = (b == '/dev/null') and 'R' or type @@ -343,7 +357,6 @@
  tags, lines = self.prepare_diff(lines, offset, wfile)   for l in lines:   buf.insert(eob, l) -   # inserts the tags   for name, p0, p1 in tags:   i0 = buf.get_iter_at_offset(p0) @@ -422,7 +435,7 @@
  tag = 'red'   l = hglib.diffexpand(l)   else: - tag = 'black' + tag = 'normal'   l = hglib.diffexpand(l)   l = l+"\n"   length = len(l.decode('utf-8')) @@ -578,7 +591,7 @@
  def widget_func(widget, item, markups):   def linkwidget(revnum, revid, summary, highlight=None, branch=None):   # revision label - opts = dict(underline='single', color='blue') + opts = dict(underline='single', color=gtklib.BLUE)   if highlight:   opts['weight'] = 'bold'   rev = '%s (%s)' % (gtklib.markup(revnum, **opts), @@ -590,7 +603,7 @@
  # summary & branch label   sum = gtklib.markup(summary)   if branch: - sum = gtklib.markup(branch, color='black', + sum = gtklib.markup(branch, color=gtklib.NORMAL,   background=gtklib.PGREEN) + ' ' + sum   sumlabel = gtk.Label()   sumlabel.set_markup(sum) @@ -793,7 +806,7 @@
  tag_table = self._buffer.get_tag_table()     tag_table.add(make_texttag('diff', font=self.rawfonts['fontdiff'])) - tag_table.add(make_texttag('blue', foreground='blue')) + tag_table.add(make_texttag('blue', foreground=gtklib.BLUE))   if self.colorstyle == 'background':   tag_table.add(make_texttag('red',   paragraph_background=gtklib.PRED)) @@ -805,21 +818,21 @@
  else:   tag_table.add(make_texttag('red', foreground=gtklib.DRED))   tag_table.add(make_texttag('green', foreground=gtklib.DGREEN)) - tag_table.add(make_texttag('black', foreground='black')) + tag_table.add(make_texttag('normal', foreground=gtklib.NORMAL))   tag_table.add(make_texttag('greybg', - paragraph_background='grey', + paragraph_background=gtklib.CHANGE_HEADER,   weight=pango.WEIGHT_BOLD)) - tag_table.add(make_texttag('yellowbg', background='yellow')) + tag_table.add(make_texttag('yellowbg', background=gtklib.YELLOW))   - issuelink_tag = make_texttag('issuelink', foreground='blue', + issuelink_tag = make_texttag('issuelink', foreground=gtklib.BLUE,   underline=pango.UNDERLINE_SINGLE)   issuelink_tag.connect('event', self.issuelink_event)   tag_table.add(issuelink_tag) - urllink_tag = make_texttag('urllink', foreground='blue', + urllink_tag = make_texttag('urllink', foreground=gtklib.BLUE,   underline=pango.UNDERLINE_SINGLE)   urllink_tag.connect('event', self.urllink_event)   tag_table.add(urllink_tag) - csetlink_tag = make_texttag('csetlink', foreground='blue', + csetlink_tag = make_texttag('csetlink', foreground=gtklib.BLUE,   underline=pango.UNDERLINE_SINGLE)   csetlink_tag.connect('event', self.csetlink_event)   tag_table.add(csetlink_tag)
 
136
137
138
139
 
 
 
 
 
 
 
140
141
142
143
 
 
 
 
 
144
145
146
147
148
149
150
 
 
 
 
151
152
153
 
350
351
352
353
 
354
355
356
 
357
358
359
 
470
471
472
 
 
 
 
 
 
 
473
474
475
 
481
482
483
484
 
 
485
486
487
 
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
 
363
364
365
 
366
367
368
 
369
370
371
372
 
483
484
485
486
487
488
489
490
491
492
493
494
495
 
501
502
503
 
504
505
506
507
508
@@ -136,18 +136,31 @@
   class patchctx(object):   - def __init__(self, patchpath, repo): + def __init__(self, patchpath, repo, patchHandle=None): + """ Read patch context from file + :param patchHandle: If set, then the patch is a temporary. + The provided handle is used to read the patch and + the patchpath contains the name of the patch. + The handle is NOT closed. + """   self._path = patchpath   self._patchname = os.path.basename(patchpath)   self._repo = repo - pf = open(patchpath) + if patchHandle: + pf = patchHandle + pf_start_pos = pf.tell() + else: + pf = open(patchpath)   try:   data = patch.extract(self._repo.ui, pf)   tmpfile, msg, user, date, branch, node, p1, p2 = data   if tmpfile:   os.unlink(tmpfile)   finally: - pf.close() + if patchHandle: + pf.seek(pf_start_pos) + else: + pf.close()   if not msg and hasattr(repo, 'mq'):   # attempt to get commit message   from hgext import mq @@ -350,10 +363,10 @@
  elif item in ('revnum', 'p4', 'svn'):   return str(value)   elif item in ('rawbranch', 'branch'): - return gtklib.markup(' %s ' % value, color='black', + return gtklib.markup(' %s ' % value, color=gtklib.BLACK,   background=gtklib.PGREEN)   elif item in ('rawtags', 'tags'): - opts = dict(color='black', background=gtklib.PYELLOW) + opts = dict(color=gtklib.BLACK, background=gtklib.PYELLOW)   tags = [gtklib.markup(' %s ' % tag, **opts) for tag in value]   return ' '.join(tags)   elif item in ('desc', 'summary', 'user', 'shortuser', @@ -470,6 +483,13 @@
  return self.info.get_widget(item, self, self.ctx, self.custom, **kargs)     def update(self, target=None, custom=None, repo=None): + self.ctx = None + if type(target) == patchctx: + # If a patchctx is specified as target, use it instead + # of creating a context from revision or patch file + self.ctx = target + target = None + self.target = None   if target is None:   target = self.target   if target is not None: @@ -481,7 +501,8 @@
  repo = self.repo   if repo is not None:   self.repo = repo - self.ctx = create_context(repo, target) + if self.ctx is None: + self.ctx = create_context(repo, target)   if self.ctx is None:   return False # cannot update   return True
 
37
38
39
 
 
 
40
41
42
 
44
45
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
48
49
 
37
38
39
40
41
42
43
44
45
 
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
@@ -37,6 +37,9 @@
 DRED = '#900000'  DGREEN = '#006400'  DBLUE = '#000090' +DYELLOW = '#6A6A00' +DORANGE = '#AA5000' +# DORANGE = '#FF8000'    PRED = '#ffcccc'  PGREEN = '#aaffaa' @@ -44,6 +47,102 @@
 PYELLOW = '#ffffaa'  PORANGE = '#ffddaa'   +RED = 'red' +GREEN = 'green' +BLUE = 'blue' +YELLOW = 'yellow' +BLACK = 'black' +WHITE = 'white' +GREY = 'grey' + +NORMAL = BLACK +NEW_REV_COLOR = DGREEN +CHANGE_HEADER = GREY + +UP_ARROW_COLOR = '#feaf3e' +DOWN_ARROW_COLOR = '#8ae234' +STAR_COLOR = '#fce94f' +CELL_GREY = '#2e3436' +STATUS_HEADER = '#DDDDDD' +STATUS_REJECT_BACKGROUND = '#EEEEEE' +STATUS_REJECT_FOREGROUND = '#888888' + +# line colors +MAINLINE_COLOR = ( 0.0, 0.0, 0.0 ) +LINE_COLORS = [ + ( 1.0, 0.0, 0.0 ), + ( 1.0, 1.0, 0.0 ), + ( 0.0, 1.0, 0.0 ), + ( 0.0, 1.0, 1.0 ), + ( 0.0, 0.0, 1.0 ), + ( 1.0, 0.0, 1.0 ), + ] + +def get_gtk_colors(): + color_scheme = gtk.settings_get_default().get_property('gtk-color-scheme') + colors = {} + for color in color_scheme.split('\n'): + color = color.strip() + if color: + name, color = color.split(':') + colors[name.strip()] = gtk.gdk.color_parse(color.strip()) + return colors + +def _init_colors(): + global NORMAL, MAINLINE_COLOR + + gtk_colors = get_gtk_colors() + + try: + normal = gtk_colors['fg_color'] + except KeyError: + # TODO: find out how to log such errors + pass + else: + NORMAL = str(normal) + MAINLINE_COLOR = ( + normal.red_float, + normal.green_float, + normal.blue_float + ) + + # adjust colors for a dark color scheme: + if normal.value > 0.5: + global RED, GREEN, BLUE, BLACK, WHITE, \ + DRED, DGREEN, DBLUE, DYELLOW, DORANGE, \ + PRED, PGREEN, PBLUE, PYELLOW, PORANGE, \ + NEW_REV_COLOR, LINE_COLORS, CHANGE_HEADER + + RED = PRED + GREEN = NEW_REV_COLOR = PGREEN + BLUE = PBLUE + PRED = DRED + DRED = '#FF6161' +# DRED, PRED = PRED, DRED + DGREEN, PGREEN = PGREEN, DGREEN + DBLUE, PBLUE = PBLUE, DBLUE + DYELLOW, PYELLOW = PYELLOW, DYELLOW + DORANGE, PORANGE = PORANGE, DORANGE + BLACK, WHITE = WHITE, BLACK + + CHANGE_HEADER = '#404040' + + LINE_COLORS = [ + ( 1.0, 0.3804, 0.3804 ), + ( 1.0, 1.0, 0.0 ), + ( 0.0, 1.0, 0.0 ), + ( 0.0, 1.0, 1.0 ), + ( 0.2902, 0.4863, 0.851 ), + ( 1.0, 0.3882, 1.0 ), + ] + + # TODO: dark color scheme for: + # UP_ARROW_COLOR, DOWN_ARROW_COLOR, STAR_COLOR, + # CELL_GREY, STATUS_HEADER, STATUS_REJECT_BACKGROUND, + # STATUS_REJECT_FOREGROUND + +_init_colors() +  def set_tortoise_icon(window, thgicon):   ico = paths.get_tortoise_icon(thgicon)   if ico: window.set_icon_from_file(ico)
 
196
197
198
199
 
200
201
202
 
196
197
198
 
199
200
201
202
@@ -196,7 +196,7 @@
  self.buf = gtk.TextBuffer()   self.buf.create_tag('removed', foreground=gtklib.DRED)   self.buf.create_tag('added', foreground=gtklib.DGREEN) - self.buf.create_tag('position', foreground='#FF8000') + self.buf.create_tag('position', foreground=gtklib.DORANGE)   self.buf.create_tag('header', foreground=gtklib.DBLUE)   diffview = gtk.TextView(self.buf)   scroller.add(diffview)
 
406
407
408
409
 
410
411
412
 
406
407
408
 
409
410
411
412
@@ -406,7 +406,7 @@
  markup = markup % (gtklib.DRED, 'bold')   icons = {'error': True}   else: - markup = markup % ('black', 'normal') + markup = markup % (gtklib.NORMAL, 'normal')   icons = {}   text = gtklib.markup_escape_text(text)   self.rlabel.set_markup(markup % text)
 
28
29
30
 
31
32
33
 
215
216
217
218
 
 
 
 
 
 
 
 
 
 
 
219
220
221
 
322
323
324
 
 
 
 
 
 
325
326
327
 
334
335
336
337
 
338
339
340
 
817
818
819
820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
822
823
 
891
892
893
894
 
895
 
896
897
898
 
924
925
926
 
 
 
 
 
 
 
927
928
929
 
945
946
947
 
 
948
949
950
 
984
985
986
 
 
 
 
987
988
989
 
1118
1119
1120
 
 
 
 
1121
1122
1123
 
1307
1308
1309
 
1310
1311
1312
 
1518
1519
1520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1521
1522
1523
 
2050
2051
2052
 
 
2053
2054
2055
 
2638
2639
2640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2641
2642
2643
 
28
29
30
31
32
33
34
 
216
217
218
 
219
220
221
222
223
224
225
226
227
228
229
230
231
232
 
333
334
335
336
337
338
339
340
341
342
343
344
 
351
352
353
 
354
355
356
357
 
834
835
836
 
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
 
925
926
927
 
928
929
930
931
932
933
 
959
960
961
962
963
964
965
966
967
968
969
970
971
 
987
988
989
990
991
992
993
994
 
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
 
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
 
1359
1360
1361
1362
1363
1364
1365
 
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
 
2122
2123
2124
2125
2126
2127
2128
2129
 
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
@@ -28,6 +28,7 @@
 from tortoisehg.hgtk import backout, status, hgemail, tagadd, update, merge  from tortoisehg.hgtk import archive, changeset, thgconfig, thgmq, histdetails  from tortoisehg.hgtk import statusbar, bookmark, thgimport +from tortoisehg.hgtk import thgpbranch    MODE_REVRANGE = 0  MODE_FILEPATS = 1 @@ -215,7 +216,17 @@
  tip=_('Show/Hide Patch Queue'),   toggle=True,   icon='menupatch.ico') - tbar += [gtk.SeparatorToolItem(), self.mqtb] + tbar += [self.mqtb] + if 'pbranch' in self.exs: + self.pbranchtb = self.make_toolbutton(gtk.STOCK_DIRECTORY, + _('Patch Branch'), + self.pbranch_clicked, name='pbranch', + tip=_('Show/Hide Patch Branch'), + toggle=True, + icon='menupatch.ico') + tbar += [self.pbranchtb] + if 'mq' in self.exs or 'pbranch' in self.exs: + tbar += [gtk.SeparatorToolItem()]   sep = gtk.SeparatorToolItem()   sep.set_expand(True)   sep.set_draw(False) @@ -322,6 +333,12 @@
  else:   p4menu = []   + if 'pbranch' in self.exs: + pbranch_item = [dict(text=_('Patch Branch'), name='pbranch', ascheck=True, + func=self.pbranch_clicked, check=self.setting_pbranchvis) ] + else: + pbranch_item = [] +   return [   dict(text=_('_View'), subitems=[   dict(text=_('Load more Revisions'), name='load-more', @@ -334,7 +351,7 @@
  ] + sync_bar_item + [   dict(text=_('Filter Bar'), ascheck=True,   func=self.toggle_show_filterbar, check=self.show_filterbar), - ] + mq_item + [ + ] + mq_item + pbranch_item + [   dict(text='----'),   dict(text=_('Refresh'), func=refresh, args=[False],   icon=gtk.STOCK_REFRESH), @@ -817,7 +834,24 @@
  else:   self.goto_rev(revid)   - def repo_invalidated(self, mqwidget): + def pbranch_selected(self, pbranchwidget, revid, patchname): + 'if revid < 0 then the patch is listed in .hg/pgraph but not in repo' + self.stbar.set_text('') + pf = tempfile.TemporaryFile() + try: + try: + pf.writelines(pbranchwidget.pdiff(patchname)) + except (util.Abort, error.RepoError), e: + self.stbar.set_text(str(e)) + return + self.currevid = self.lastrevid = None + pf.seek(0) + self.changeview.load_patch_details_from_file_object(pf, patchname, isTemp=True) + finally: + pf.close() + + def repo_invalidated(self, widget): + 'Emitted from MQWidget and PBranchWidget'   self.reload_log()     def files_dropped(self, mqwidget, files, *args): @@ -891,8 +925,9 @@
  else:   item.set_sensitive(False)   - # enable MQ panel + # enable panels   self.enable_mqpanel() + self.enable_pbranchpanel()     def get_proxy_args(self):   item = self.get_menuitem('use-proxy-server') @@ -924,6 +959,13 @@
  else:   settings['glog-mqpane'] = self.setting_mqhpos   settings['glog-mqvis'] = self.setting_mqvis + if hasattr(self, 'pbranchpaned') and self.pbranchwidget.has_patch(): + curpos = self.pbranchpaned.get_position() + settings['glog-pbranchpane'] = curpos or self.setting_pbranchhpos + settings['glog-pbranchvis'] = bool(curpos) + else: + settings['glog-pbranchpane'] = self.setting_pbranchhpos + settings['glog-pbranchvis'] = self.setting_pbranchvis   settings['branch-color'] = self.graphview.get_property('branch-color')   settings['show-output'] = self.showoutput   settings['show-toolbar'] = self.show_toolbar @@ -945,6 +987,8 @@
  self.setting_hpos = settings.get('glog-hpane', -1)   self.setting_mqhpos = settings.get('glog-mqpane', 140) or 140   self.setting_mqvis = settings.get('glog-mqvis', False) + self.setting_pbranchhpos = settings.get('glog-pbranchpane', 140) or 140 + self.setting_pbranchvis = settings.get('glog-pbranchvis', False)   self.branch_color = settings.get('branch-color', False)   self.showoutput = settings.get('show-output', False)   self.show_toolbar = settings.get('show-toolbar', True) @@ -984,6 +1028,10 @@
  if hasattr(self, 'mqwidget'):   self.mqwidget.refresh()   + # refresh pbranch widget if exists + if hasattr(self, 'pbranchwidget'): + self.pbranchwidget.refresh() +   # force a redraw of the visible rows   self.graphview.hide()   self.graphview.show() @@ -1118,6 +1166,10 @@
  'count': ncount, 'total': ntotal}   self.stbar.set_text(mq_text, name='mq')   + # refresh pbranch widget if exists + if hasattr(self, 'pbranchwidget'): + self.pbranchwidget.refresh() +   # Remember options to next time reload_log is called   self.filteropts = opts   @@ -1307,6 +1359,7 @@
  # prepare statusbar   self.stbar = statusbar.StatusBar()   self.stbar.append_field('mq') + self.stbar.append_field('pbranch')   self.stbar.append_field('filter')   self.stbar.append_field('rev')   @@ -1518,6 +1571,25 @@
  midpane.pack_start(self.graphview)   midpane.show_all()   + # pbranch widget + if 'pbranch' in self.exs: + # create PBranchWidget + self.pbranchwidget = thgpbranch.PBranchWidget( + self, self.repo, self.stbar, accelgroup, self.tooltips) + self.pbranchwidget.connect('patch-selected', self.pbranch_selected) + self.pbranchwidget.connect('repo-invalidated', self.repo_invalidated) + + def wrapframe(widget): + frame = gtk.Frame() + frame.set_shadow_type(gtk.SHADOW_ETCHED_IN) + frame.add(widget) + return frame + self.pbranchpaned = gtk.HPaned() + self.pbranchpaned.add1(wrapframe(self.pbranchwidget)) + self.pbranchpaned.add2(wrapframe(midpane)) + + midpane = self.pbranchpaned +   # MQ widget   if 'mq' in self.exs:   # create MQWidget @@ -2050,6 +2122,8 @@
  self.hpaned.set_position(self.setting_hpos)   if hasattr(self, 'mqpaned') and self.mqtb.get_active():   self.mqpaned.set_position(self.setting_mqhpos) + if hasattr(self, 'pbranchpaned') and self.pbranchtb.get_active(): + self.pbranchpaned.set_position(self.setting_pbranchhpos)     def thgdiff(self, treeview):   'ctrl-d handler' @@ -2638,6 +2712,26 @@
  def mq_clicked(self, widget, *args):   self.enable_mqpanel(widget.get_active())   + def enable_pbranchpanel(self, enable=None): + if not hasattr(self, 'pbranchpaned'): + return + if enable is None: + enable = self.setting_pbranchvis and self.pbranchwidget.has_patch() + oldpos = self.pbranchpaned.get_position() + self.pbranchpaned.set_position(enable and self.setting_pbranchhpos or 0) + if not enable and oldpos: + self.setting_pbranchhpos = oldpos + + # set the state of PBranch toolbutton + if hasattr(self, 'pbranchtb'): + self.pbranchtb.handler_block_by_func(self.pbranch_clicked) + self.cmd_set_active('pbranch', enable) + self.pbranchtb.handler_unblock_by_func(self.pbranch_clicked) + self.cmd_set_sensitive('pbranch', self.pbranchwidget.has_pbranch()) + + def pbranch_clicked(self, widget, data=None): + self.enable_pbranchpanel(widget.get_active()) +   def tree_button_press(self, tree, event):   if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |   gtk.gdk.CONTROL_MASK)):
 
19
20
21
 
 
22
23
24
 
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
 
106
107
 
108
109
110
 
189
190
191
192
 
193
194
195
 
198
199
200
201
 
202
203
204
205
206
 
207
208
209
 
218
219
220
221
 
222
223
224
 
19
20
21
22
23
24
25
26
 
89
90
91
 
 
 
 
 
 
 
 
 
92
93
94
95
96
97
 
98
99
 
100
101
102
103
 
182
183
184
 
185
186
187
188
 
191
192
193
 
194
195
196
197
198
 
199
200
201
202
 
211
212
213
 
214
215
216
217
@@ -19,6 +19,8 @@
 import pango  import cairo   +from tortoisehg.hgtk import gtklib +  # Styles used when rendering revision graph edges  style_SOLID = 0  style_DASHED = 1 @@ -87,24 +89,15 @@
  colours and the fg parameter provides the multiplier that should be   applied to the foreground colours.   """ - mainline_color = ( 0.0, 0.0, 0.0 ) - colours = [ - ( 1.0, 0.0, 0.0 ), - ( 1.0, 1.0, 0.0 ), - ( 0.0, 1.0, 0.0 ), - ( 0.0, 1.0, 1.0 ), - ( 0.0, 0.0, 1.0 ), - ( 1.0, 0.0, 1.0 ), - ]     if isinstance(colour, str):   r, g, b = colour[1:3], colour[3:5], colour[5:7]   colour_rgb = int(r, 16) / 255., int(g, 16) / 255., int(b, 16) / 255.   else:   if colour == 0: - colour_rgb = mainline_color + colour_rgb = gtklib.MAINLINE_COLOR   else: - colour_rgb = colours[colour % len(colours)] + colour_rgb = gtklib.LINE_COLORS[colour % len(gtklib.LINE_COLORS)]     red = (colour_rgb[0] * fg) or bg   green = (colour_rgb[1] * fg) or bg @@ -189,7 +182,7 @@
  # Possible node status   if status != 0:   def draw_arrow(x, y, dir): - self.set_colour(ctx, '#2e3436', 0.0, 1.0) + self.set_colour(ctx, gtklib.CELL_GREY, 0.0, 1.0)   ctx.rectangle(x, y, 2, 5)   ax, ay = x, y + (dir == 'down' and 5 or 0)   inc = 3 * (dir == 'up' and -1 or 1) @@ -198,12 +191,12 @@
  ctx.line_to(ax + 1, ay + inc)   ctx.line_to(ax - 2, ay)   ctx.stroke_preserve() - fillcolor = dir == 'up' and '#feaf3e' or '#8ae234' + fillcolor = dir == 'up' and gtklib.UP_ARROW_COLOR or gtklib.DOWN_ARROW_COLOR   self.set_colour(ctx, fillcolor, 0.0, 1.0)   ctx.fill()     def draw_star(x, y, radius, nodes, offset=False): - self.set_colour(ctx, '#2e3436', 0.0, 1.0) + self.set_colour(ctx, gtklib.CELL_GREY, 0.0, 1.0)   total_nodes = nodes * 2 #inner + outer nodes   angle = 2 * math.pi / total_nodes;   offset = offset and angle / 2 or 0 @@ -218,7 +211,7 @@
  else:   ctx.line_to(arc_x, arc_y)   ctx.stroke_preserve() - self.set_colour(ctx, '#fce94f', 0.0, 1.0) + self.set_colour(ctx, gtklib.STAR_COLOR, 0.0, 1.0)   ctx.fill()     arrow_y = arc_start_position_y - box_size / 4
 
179
180
181
182
 
183
184
185
 
217
218
219
220
 
221
222
223
224
225
226
 
227
228
229
 
295
296
297
298
 
299
300
301
 
303
304
305
306
 
307
308
309
 
179
180
181
 
182
183
184
185
 
217
218
219
 
220
221
222
223
224
225
 
226
227
228
229
 
295
296
297
 
298
299
300
301
 
303
304
305
 
306
307
308
309
@@ -179,7 +179,7 @@
  if parent is None:   parent = ctx.parents()[0].node()   M, A, R = self.repo.status(parent, ctx.node())[:3] - common = dict(color='black') + common = dict(color=gtklib.BLACK)   M = M and gtklib.markup(' %s ' % len(M),   background=gtklib.PORANGE, **common) or ''   A = A and gtklib.markup(' %s ' % len(A), @@ -217,13 +217,13 @@
  bg = gtklib.PORANGE   elif tag in self.mqpatches:   bg = gtklib.PBLUE - style = {'color': 'black', 'background': bg} + style = {'color': gtklib.BLACK, 'background': bg}   tstr += gtklib.markup(' %s ' % tag, **style) + ' '     branch = ctx.branch()   bstr = ''   if self.branchtags.get(branch) == node: - bstr += gtklib.markup(' %s ' % branch, color='black', + bstr += gtklib.markup(' %s ' % branch, color=gtklib.BLACK,   background=gtklib.PGREEN) + ' '     if revid in self.wcparents: @@ -295,7 +295,7 @@
  self.color_func = self.text_color_author     def text_color_default(self, rev, author): - return int(rev) >= self.origtip and 'darkgreen' or 'black' + return int(rev) >= self.origtip and gtklib.NEW_REV_COLOR or gtklib.NORMAL     colors = '''black blue deeppink mediumorchid blue burlywood4 goldenrod   slateblue red2 navy dimgrey'''.split() @@ -303,7 +303,7 @@
    def text_color_author(self, rev, author):   if int(rev) >= self.origtip: - return 'darkgreen' + return gtklib.NEW_REV_COLOR   for re, v in self.author_pats:   if (re.search(author)):   return v
 
53
54
55
56
 
57
58
59
 
411
412
413
414
 
415
416
417
 
420
421
422
423
424
 
 
425
426
427
 
870
871
872
873
 
 
874
875
876
 
920
921
922
923
 
924
925
 
926
927
 
928
929
 
930
931
 
932
933
934
 
1096
1097
1098
1099
 
1100
1101
1102
 
53
54
55
 
56
57
58
59
 
411
412
413
 
414
415
416
417
 
420
421
422
 
 
423
424
425
426
427
 
870
871
872
 
873
874
875
876
877
 
921
922
923
 
924
925
 
926
927
 
928
929
 
930
931
 
932
933
934
935
 
1097
1098
1099
 
1100
1101
1102
1103
@@ -53,7 +53,7 @@
  elif line.startswith('+'):   hunk += gtklib.markup(line, color=gtklib.DGREEN)   elif line.startswith('@@'): - hunk = gtklib.markup(line, color='#FF8000') + hunk = gtklib.markup(line, color=gtklib.DORANGE)   else:   hunk += gtklib.markup(line)   return hunk @@ -411,7 +411,7 @@
  diffcol.add_attribute(cell, 'markup', DM_DISP_TEXT)     # differentiate header chunks - cell.set_property('cell-background', '#DDDDDD') + cell.set_property('cell-background', gtklib.STATUS_HEADER)   diffcol.add_attribute(cell, 'cell_background_set', DM_IS_HEADER)   self.headerfont = self.difffont.copy()   self.headerfont.set_weight(pango.WEIGHT_HEAVY) @@ -420,8 +420,8 @@
  self.rejfont = self.difffont.copy()   self.rejfont.set_weight(pango.WEIGHT_LIGHT)   diffcol.add_attribute(cell, 'font-desc', DM_FONT) - cell.set_property('background', '#EEEEEE') - cell.set_property('foreground', '#888888') + cell.set_property('background', gtklib.STATUS_REJECT_BACKGROUND) + cell.set_property('foreground', gtklib.STATUS_REJECT_FOREGROUND)   diffcol.add_attribute(cell, 'background-set', DM_REJECTED)   diffcol.add_attribute(cell, 'foreground-set', DM_REJECTED)   difftree.append_column(diffcol) @@ -870,7 +870,8 @@
  sel = lambda x: x >= lasthunk or not dmodel[hc+x+1][DM_REJECTED]   newtext = chunks[0].selpretty(sel)   if not selected: - newtext = "<span foreground='#888888'>" + newtext + "</span>" + newtext = "<span foreground='" + gtklib.STATUS_REJECT_FOREGROUND + \ + "'>" + newtext + "</span>"   dmodel[hc][DM_DISP_TEXT] = newtext     def updated_codes(self): @@ -920,15 +921,15 @@
  elif stat == 'R':   text_renderer.set_property('foreground', gtklib.DRED)   elif stat == 'C': - text_renderer.set_property('foreground', 'black') + text_renderer.set_property('foreground', gtklib.NORMAL)   elif stat == '!': - text_renderer.set_property('foreground', 'red') + text_renderer.set_property('foreground', gtklib.RED)   elif stat == '?': - text_renderer.set_property('foreground', '#AA5000') + text_renderer.set_property('foreground', gtklib.DORANGE)   elif stat == 'I': - text_renderer.set_property('foreground', 'black') + text_renderer.set_property('foreground', gtklib.NORMAL)   else: - text_renderer.set_property('foreground', 'black') + text_renderer.set_property('foreground', gtklib.NORMAL)       def rename_file(self, wfile): @@ -1096,7 +1097,7 @@
  else:   buf.create_tag('removed', foreground=gtklib.DRED)   buf.create_tag('added', foreground=gtklib.DGREEN) - buf.create_tag('position', foreground='#FF8000') + buf.create_tag('position', foreground=gtklib.DORANGE)   buf.create_tag('header', foreground=gtklib.DBLUE)     bufiter = buf.get_start_iter()
 
721
722
723
724
 
725
726
 
727
728
 
729
730
731
 
721
722
723
 
724
725
 
726
727
 
728
729
730
731
@@ -721,11 +721,11 @@
    stat = row[MQ_STATUS]   if stat == 'A': - cell.set_property('foreground', 'blue') + cell.set_property('foreground', gtklib.BLUE)   elif stat == 'U': - cell.set_property('foreground', '#909090') + cell.set_property('foreground', gtklib.GREY)   else: - cell.set_property('foreground', 'black') + cell.set_property('foreground', gtklib.NORMAL)     patchname = row[MQ_NAME]   if self.is_qtip(patchname):
Change 1 of 1 Show Entire File tortoisehg/​hgtk/​thgpbranch.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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
@@ -0,0 +1,868 @@
+# thgpbranch.py - embeddable widget for the PatchBranch extension +# +# Copyright 2009 Peer Sommerlund <peer.sommerlund@gmail.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2, incorporated herein by reference. + +import os +import tempfile +import gtk +import gobject + +from mercurial import cmdutil, extensions +from mercurial import commands as hg +import mercurial.ui + +from tortoisehg.util.i18n import _ + +from tortoisehg.hgtk import hgcmd +from tortoisehg.hgtk import update +from tortoisehg.hgtk import gtklib, dialog +from tortoisehg.hgtk.logview import graphcell + +# Patch Branch model enumeration +M_NODE = 0 +M_IN_LINES = 1 +M_OUT_LINES = 2 +M_NAME = 3 +M_STATUS = 4 +M_TITLE = 5 +M_MSG = 6 +M_MSGESC = 7 + +# Patch Branch column enumeration +C_GRAPH = 0 +C_STATUS = 1 +C_NAME = 2 +C_TITLE = 3 +C_MSG = 4 + +class PBranchWidget(gtk.VBox): + + __gproperties__ = { + 'graph-column-visible': (gobject.TYPE_BOOLEAN, + 'Graph', + 'Show graph column', + False, + gobject.PARAM_READWRITE), + 'status-column-visible': (gobject.TYPE_BOOLEAN, + 'Status', + 'Show status column', + False, + gobject.PARAM_READWRITE), + 'name-column-visible': (gobject.TYPE_BOOLEAN, + 'Name', + 'Show name column', + False, + gobject.PARAM_READWRITE), + 'title-column-visible': (gobject.TYPE_BOOLEAN, + 'Title', + 'Show title column', + False, + gobject.PARAM_READWRITE), + 'message-column-visible': (gobject.TYPE_BOOLEAN, + 'Title', + 'Show title column', + False, + gobject.PARAM_READWRITE), + 'show-internal-branches': (gobject.TYPE_BOOLEAN, + 'ShowInternalBranches', + "Show internal branches", + False, + gobject.PARAM_READWRITE) + } + + __gsignals__ = { + 'repo-invalidated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ()), + 'patch-selected': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + (int, # revision number for patch head + str)) # patch name + } + + def __init__(self, parentwin, repo, statusbar, accelgroup=None, tooltips=None): + gtk.VBox.__init__(self) + + self.parent_window = parentwin + self.repo = repo + self.pbranch = extensions.find('pbranch') + self.statusbar = statusbar + + # top toolbar + tbar = gtklib.SlimToolbar(tooltips) + + ## buttons + self.btn = {} + pmergebtn = tbar.append_button(gtk.STOCK_CONVERT, + _('Merge all pending dependencies')) + pmergebtn.connect('clicked', self.pmerge_clicked) + self.btn['pmerge'] = pmergebtn + + pbackoutbtn = tbar.append_button(gtk.STOCK_GO_BACK, + _('Backout current patch branch')) + pbackoutbtn.connect('clicked', self.pbackout_clicked) + self.btn['pbackout'] = pbackoutbtn + + reapplybtn = gtk.ToolButton(gtk.STOCK_GO_FORWARD) + reapplybtn = tbar.append_button(gtk.STOCK_GO_FORWARD, + _('Backport part of a changeset to a dependency')) + reapplybtn.connect('clicked', self.reapply_clicked) + self.btn['reapply'] = reapplybtn + + pnewbtn = tbar.append_button(gtk.STOCK_NEW, + _('Start a new patch branch')) + pnewbtn.connect('clicked', self.pnew_clicked) + self.btn['pnew'] = pnewbtn + + pgraphbtn = tbar.append_button(gtk.STOCK_EDIT, + _('Edit patch dependency graph')) + pgraphbtn.connect('clicked', self.edit_pgraph_clicked) + self.btn['pnew'] = pnewbtn + + ## separator + tbar.append_space() + + ## drop-down menu + menubtn = gtk.MenuToolButton('') + menubtn.set_menu(self.create_view_menu()) + tbar.append_widget(menubtn, padding=0) + self.btn['menu'] = menubtn + def after_init(): + menubtn.child.get_children()[0].hide() + gtklib.idle_add_single_call(after_init) + self.pack_start(tbar, False, False) + + # center pane + mainbox = gtk.VBox() + self.pack_start(mainbox, True, True) + + ## scrolled pane + pane = gtk.ScrolledWindow() + pane.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + pane.set_shadow_type(gtk.SHADOW_IN) + mainbox.pack_start(pane) + + ### patch list + #### patch list model + self.model = gtk.ListStore( + gobject.TYPE_PYOBJECT, # node info + gobject.TYPE_PYOBJECT, # in-lines + gobject.TYPE_PYOBJECT, # out-lines + str, # patch name + str, # patch status + str, # patch title + str, # patch message + str) # patch message escaped + #### patch list view + self.list = gtk.TreeView(self.model) + # To support old PyGTK (<2.12) + if hasattr(self.list, 'set_tooltip_column'): + self.list.set_tooltip_column(M_MSGESC) + self.list.connect('cursor-changed', self.list_sel_changed) + self.list.connect('button-press-event', self.list_pressed) + self.list.connect('row-activated', self.list_row_activated) + self.list.connect('size-allocate', self.list_size_allocated) + + #### patch list columns + self.cols = {} + self.cells = {} + + def addcol(header, col_idx, model_idx=None, right=False, resizable=False, + editable=False, editfunc=None, cell_renderer=None, + properties=[]): + header = (right and '%s ' or ' %s') % header + cell = cell_renderer or gtk.CellRendererText() + if editfunc: + cell.set_property('editable', editable) + cell.connect('edited', editfunc) + col = gtk.TreeViewColumn(header, cell) + if cell_renderer is None: + col.add_attribute(cell, 'text', model_idx) + col.set_resizable(resizable) + col.set_visible(self.get_property(self.col_to_prop(col_idx))) + if right: + col.set_alignment(1) + cell.set_property('xalign', 1) + for (property_name, model_index) in properties: + col.add_attribute(cell, property_name, model_index) + self.list.append_column(col) + self.cols[col_idx] = col + self.cells[col_idx] = cell + + def cell_edited(cell, path, newname): + row = self.model[path] + patchname = row[M_NAME] + if newname != patchname: + self.qrename(newname, patch=patchname) + + #### patch list columns and cell renderers + + addcol(_('Graph'), C_GRAPH, resizable=True, + cell_renderer=graphcell.CellRendererGraph(), + properties=[("node", M_NODE), + ("in-lines",M_IN_LINES), + ("out-lines", M_OUT_LINES)] + ) + addcol(_('St'), C_STATUS, M_STATUS) + addcol(_('Name'), C_NAME, M_NAME, editfunc=cell_edited) + addcol(_('Title'), C_TITLE, M_TITLE) + addcol(_('Message'), C_MSG, M_MSG) + + pane.add(self.list) + + ## command widget + self.cmd = hgcmd.CmdWidget(style=hgcmd.STYLE_COMPACT, + tooltips=tooltips) + mainbox.pack_start(self.cmd, False, False) + + # accelerator + if accelgroup: + # TODO + pass + + ### public functions ### + + def refresh(self): + """ + Refresh the list of patches. + This operation will try to keep selection state. + """ + if not self.pbranch: + return + + # store selected patch name + selname = None + model, paths = self.list.get_selection().get_selected_rows() + if len(paths) > 0: + selname = model[paths[0]][M_NAME] + + # compute model data + self.model.clear() + opts = {'tips': True} + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + graph = mgr.graphforopts(opts) + if not self.get_property('show-internal-branches'): + graph = mgr.patchonlygraph(graph) + names = None + patch_list = graph.topolist(names) + in_lines = [] + if patch_list: + dep_list = [patch_list[0]] + cur_branch = self.repo['.'].branch() + patch_status = {} + for name in patch_list: + patch_status[name] = self.pstatus(name) + for name in patch_list: + parents = graph.deps(name) + + # Node properties + if name in dep_list: + node_column = dep_list.index(name) + else: + node_column = len(dep_list) + node_colour = patch_status[name] and '#ff0000' or 0 + node_status = (name == cur_branch) and 4 or 0 + node = (node_column, node_colour, node_status) + + # Find next dependency list + my_deps = [] + for p in parents: + if p not in dep_list: + my_deps.append(p) + next_dep_list = dep_list[:] + next_dep_list[node_column:node_column+1] = my_deps + + # Dependency lines + shift = len(parents) - 1 + out_lines = [] + for p in parents: + dep_column = next_dep_list.index(p) + colour = 0 # black + if patch_status[p]: + colour = '#ff0000' # red + style = 0 # solid lines + out_lines.append((node_column, dep_column, colour, style)) + for lines in in_lines: + (start_column, end_column, colour, style) = lines + if end_column == node_column: + # Deps to current patch end here + pass + else: + # Find line continuations + dep = dep_list[end_column] + dep_column = next_dep_list.index(dep) + out_lines.append((end_column, dep_column, colour, style)) + + stat = patch_status[name] and 'M' or 'C' # patch status + patchname = name + msg = self.pmessage(name) # summary + if msg: + msg_esc = gtklib.markup_escape_text(msg) # escaped summary (utf-8) + title = msg.split('\n')[0] + else: + msg_esc = None + title = None + self.model.append((node, in_lines, out_lines, patchname, stat, + title, msg, msg_esc)) + # Loop + in_lines = out_lines + dep_list = next_dep_list + + + # restore patch selection + if selname: + iter = self.get_iter_by_patchname(selname) + if iter: + self.list.get_selection().select_iter(iter) + + # update UI sensitives + self.update_sensitives() + + # report status + status_text = '' + idle_text = None + if self.has_patch(): + status_text = self.pending_merges() \ + and _('pending pmerges') \ + or _('no pending pmerges') + self.statusbar.set_text(status_text, 'pbranch') + self.statusbar.set_idle_text(idle_text) + + def pgraph(self): + """ + [pbranch] Execute 'pgraph' command. + + :returns: A list of patches and dependencies + """ + if self.pbranch is None: + return None + opts = {} + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + return mgr.graphforopts(opts) + + def patch_list(self, opts={}): + """List all patches in pbranch dependency DAG""" + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + graph = mgr.graphforopts(opts) + names = None + return graph.topolist(names) + + def pending_merges(self): + """Return True if there are pending pmerge operations""" + for patch in self.patch_list(): + if self.pstatus(patch): + return True + return False + + def pstatus(self, patch_name): + """ + [pbranch] Execute 'pstatus' command. + + :param patch_name: Name of patch-branch + :retv: list of status messages. If empty there is no pending merges + """ + if self.pbranch is None: + return None + status = [] + opts = {} + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + graph = mgr.graphforopts(opts) + heads = self.repo.branchheads(patch_name) + if len(heads) > 1: + status.append(_('needs merge of %i heads\n') % len(heads)) + for dep, through in graph.pendingmerges(patch_name): + if through: + status.append(_('needs merge with %s (through %s)\n') % + (dep, ", ".join(through))) + else: + status.append(_('needs merge with %s\n') % dep) + for dep in graph.pendingrebases(patch_name): + status.append(_('needs update of diff base to tip of %s\n') % dep) + return status + + def pmessage(self, patch_name): + """ + Get patch message + + :param patch_name: Name of patch-branch + :retv: Full patch message. If you extract the first line + you will get the patch title. If the repo does not contain + message or patch, the function returns None + """ + opts = {} + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + try: + return mgr.patchdesc(patch_name) + except: + return None + + def peditmessage(self, patch_name): + """ + Edit patch message + + :param patch_name: Name of patch-branch + """ + if not patch_name in self.patch_list(): + return + cmdline = ['hg', 'peditmessage', patch_name] + self.cmd.execute(cmdline, self.cmd_done) + + def pdiff(self, patch_name): + """ + [pbranch] Execute 'pdiff --tips' command. + + :param patch_name: Name of patch-branch + :retv: list of lines of generated patch + """ + opts = {} + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + graph = mgr.graphattips() + return graph.diff(patch_name, None, opts) + + def pnew_ui(self): + """ + Create new patch. + Propmt user for new patch name. Patch is created + on current branch. + """ + parent = None + title = _('New Patch Name') + new_name = dialog.entry_dialog(parent, title) + if not new_name: + return False + self.pnew(new_name) + return True + + def pnew(self, patch_name): + """ + [pbranch] Execute 'pnew' command. + + :param patch_name: Name of new patch-branch + """ + if self.pbranch is None: + return False + self.pbranch.cmdnew(self.repo.ui, self.repo, patch_name) + self.emit('repo-invalidated') + return True + + def pmerge(self, patch_name=None): + """ + [pbranch] Execute 'pmerge' command. + + :param patch_name: Merge to this patch-branch + """ + if not self.has_patch(): + return + cmdline = ['hg', 'pmerge'] + if patch_name: + cmdline += [patch_name] + else: + cmdline += ['--all'] + self.cmd.execute(cmdline, self.cmd_done) + + def pbackout(self): + """ + [pbranch] Execute 'pbackout' command. + """ + assert False + + def pfinish(self, patch_name): + """ + [pbranch] Execute 'pfinish' command. + + The workdir must be clean. + The patch branch dependencies must be merged. + + :param patch_name: A patch branch (not an internal branch) + """ + # Check preconditions for pfinish + + assert self.is_patch(patch_name) + + pmerge_status = self.pstatus(patch_name) + if pmerge_status != []: + dialog.error_dialog(self.parent_window, + _('Pending Pmerge'), + _('You cannot finish this patch branch unless you pmerge it first.\n' + 'pmerge will solve the following issues with %(patch)s:\n' + '* %(issuelist)s') % + {'patch': patch_name, + 'issuelist': '* '.join(pmerge_status)} + ) + return + + if not self.workdir_is_clean(): + dialog.error_dialog(self.parent_window, + _('Uncommitted Local Changes'), + _('pfinish uses your working directory for temporary work.\n' + 'Please commit your local changes before issuing pfinish.') + ) + return + + if hasattr(self.repo, 'mq') and len(self.repo.mq.applied) > 0: + dialog.error_dialog(self.parent_window, + _('Applied MQ patch'), + _('pfinish must be able to commit, but this is not allowed\n' + 'as long as you have MQ patches applied.') + ) + return + + # Set up environment for mercurial commands + class CmdWidgetUi(mercurial.ui.ui): + def __init__(self, cmdLogWidget): + src = None + super(CmdWidgetUi, self).__init__(src) + self.cmdLogWidget = cmdLogWidget + def write(self, *args): + for a in args: + self.cmdLogWidget.append(str(a)) + def write_err(self, *args): + for a in args: + self.cmdLogWidget.append(str(a), error=True) + def flush(self): + pass + def prompt(self, msg, choices=None, default="y"): + raise util.Abort("Internal Error: prompt not available") + def promptchoice(self, msg, choices, default=0): + raise util.Abort("Internal Error: promptchoice not available") + def getpass(self, prompt=None, default=None): + raise util.Abort("Internal Error: getpass not available") + repo = self.repo + ui = CmdWidgetUi(self.cmd.log) + old_ui = repo.ui + repo.ui = ui + + # Commit patch to dependency + fd, patch_file_name = tempfile.mkstemp(prefix='thg-patch-') + patch_file = os.fdopen(fd, 'w') + patch_file.writelines(self.pdiff(patch_name)) + patch_file.close() + upstream_branch = self.pgraph().deps(patch_name)[0] + hg.update(ui, repo, rev=upstream_branch) + hg.import_(ui, repo, patch_file_name, base='', strip=1) + os.unlink(patch_file_name) + + # Close patch branch + hg.update(ui, repo, rev=patch_name) + hg.merge(ui, repo, upstream_branch) + msg = _('Patch branch finished') + hg.commit(ui, repo, close_branch=True, message=msg) + + # Update GUI + repo.ui = old_ui + self.emit('repo-invalidated') + + def has_pbranch(self): + """ return True if pbranch extension can be used """ + return self.pbranch is not None + + def has_patch(self): + """ return True if pbranch extension is in use on repo """ + return self.has_pbranch() and self.pgraph() != [] + + def is_patch(self, branch_name): + """ return True if branch is a patch. This excludes root branches + and internal diff base branches (for patches with multiple + dependencies. """ + return self.has_pbranch() and self.pgraph().ispatch(branch_name) + + def cur_branch(self): + """ Return branch that workdir belongs to. """ + return self.repo.dirstate.branch() + + def workdir_is_clean(self): + """ return True if the working directory is clean """ + c = self.repo[None] + return not (c.modified() or c.added() or c.removed()) + + ### internal functions ### + + def get_iter_by_patchname(self, name): + """ return iter has specified patch name """ + if name: + for row in self.model: + if row[M_NAME] == name: + return row.iter + return None + + def get_path_by_patchname(self, name): + """ return path has specified patch name """ + iter = self.get_iter_by_patchname(name) + if iter: + return self.model.get_path(iter) + return None + + def update_sensitives(self): + """ Update the sensitives of entire UI """ + def disable_pbranchcmd(): + for name in ('pbackout', 'pmerge', 'pnew', 'reapply'): + self.btn[name].set_sensitive(False) + if self.pbranch: + self.list.set_sensitive(True) + self.btn['menu'].set_sensitive(True) + in_pbranch = True #TODO + is_merge = len(self.repo.parents()) > 1 + self.btn['pmerge'].set_sensitive(in_pbranch) + self.btn['pbackout'].set_sensitive(in_pbranch) + self.btn['pnew'].set_sensitive(not is_merge) + self.btn['reapply'].set_sensitive(True) + else: + self.list.set_sensitive(False) + self.btn['menu'].set_sensitive(False) + disable_pbranchcmd() + + def scroll_to_current(self): + """ + Scroll to current patch in the patch list. + If the patch is selected, it will do nothing. + """ + if self.list.get_selection().count_selected_rows() > 0: + return + curpatch = self.cur_branch() + if not curpatch: + return + path = self.get_path_by_patchname(curpatch) + if path: + self.list.scroll_to_cell(path) + + def show_patch_cmenu(self, list, path): + """Context menu for selected patch""" + row = self.model[path] + + menu = gtk.Menu() + def append(label, handler=None): + item = gtk.MenuItem(label, True) + item.set_border_width(1) + if handler: + item.connect('activate', handler, row) + menu.append(item) + + has_pbranch = self.has_pbranch() + is_current = self.has_patch() and self.cur_branch() == row[M_NAME] + is_patch = self.is_patch(row[M_NAME]) + is_internal = self.pbranch.isinternal(row[M_NAME]) + is_merge = len(self.repo.branchheads(row[M_NAME])) > 1 + + if has_pbranch and not is_merge and not is_internal: + append(_('_new'), self.pnew_activated) + if not is_current: + append(_('_goto (update workdir)'), self.goto_activated) + if is_patch: + append(_('_edit message'), self.edit_message_activated) + append(_('_rename'), self.rename_activated) + append(_('_delete'), self.delete_activated) + append(_('_finish'), self.finish_activated) + + if len(menu.get_children()) > 0: + menu.show_all() + menu.popup(None, None, None, 0, 0) + + def create_view_menu(self): + """Top right menu for selection of columns and + view configuration.""" + menu = gtk.Menu() + def append(item=None, handler=None, check=False, + active=False, sep=False): + if sep: + item = gtk.SeparatorMenuItem() + else: + if isinstance(item, str): + if check: + item = gtk.CheckMenuItem(item) + item.set_active(active) + else: + item = gtk.MenuItem(item) + item.set_border_width(1) + if handler: + item.connect('activate', handler) + menu.append(item) + return item + def colappend(label, col_idx, active=True): + def handler(menuitem): + col = self.cols[col_idx] + col.set_visible(menuitem.get_active()) + propname = self.col_to_prop(col_idx) + item = append(label, handler, check=True, active=active) + self.vmenu[propname] = item + + self.vmenu = {} + + colappend(_('Show graph'), C_GRAPH) + colappend(_('Show status'), C_STATUS, active=False) + colappend(_('Show name'), C_NAME) + colappend(_('Show title'), C_TITLE, active=False) + colappend(_('Show message'), C_MSG, active=False) + + append(sep=True) + + def enable_editable(item): + self.cells[C_NAME].set_property('editable', item.get_active()) + item = append(_('Enable editable cells'), enable_editable, + check=True, active=False) + self.vmenu['editable-cell'] = item + item = append(_("Show internal branches"), lambda item: self.refresh(), + check=True, active=False) + self.vmenu['show-internal-branches'] = item + + menu.show_all() + return menu + + def show_dialog(self, dlg): + """Show modal dialog and block application + See also show_dialog in history.py + """ + dlg.set_transient_for(self.parent_window) + dlg.show_all() + dlg.run() + if gtk.pygtk_version < (2, 12, 0): + # Workaround for old PyGTK (< 2.12.0) issue. + # See background of this: f668034aeda3 + dlg.set_transient_for(None) + + + def update_by_row(self, row): + branch = row[M_NAME] + rev = cmdutil.revrange(self.repo, [branch]) + parents = [x.node() for x in self.repo.parents()] + dialog = update.UpdateDialog(rev[0]) + self.show_dialog(dialog) + self.update_completed(parents) + + def update_completed(self, oldparents): + self.repo.invalidate() + self.repo.dirstate.invalidate() + newparents = [x.node() for x in self.repo.parents()] + if not oldparents == newparents: + self.emit('repo-invalidated') + + def cmd_done(self, returncode, useraborted, noemit=False): + if returncode == 0: + if self.cmd.get_pbar(): + self.cmd.set_result(_('Succeed'), style='ok') + elif useraborted: + self.cmd.set_result(_('Canceled'), style='error') + else: + self.cmd.set_result(_('Failed'), style='error') + self.refresh() + if not noemit: + self.emit('repo-invalidated') + + def do_get_property(self, property): + try: + return self.vmenu[property.name].get_active() + except: + raise AttributeError, 'unknown property %s' % property.name + + def do_set_property(self, property, value): + try: + self.vmenu[property.name].set_active(value) + except: + raise AttributeError, 'unknown property %s' % property.name + + def col_to_prop(self, col_idx): + if col_idx == C_GRAPH: + return 'graph-column-visible' + if col_idx == C_STATUS: + return 'status-column-visible' + if col_idx == C_NAME: + return 'name-column-visible' + if col_idx == C_TITLE: + return 'title-column-visible' + if col_idx == C_MSG: + return 'message-column-visible' + return '' + + ### signal handlers ### + + def list_pressed(self, list, event): + x, y = int(event.x), int(event.y) + pathinfo = list.get_path_at_pos(x, y) + if event.button == 1: + if not pathinfo: + # HACK: clear selection after this function calling, + # against selection by getting focus + def unselect(): + selection = list.get_selection() + selection.unselect_all() + gtklib.idle_add_single_call(unselect) + elif event.button == 3: + if pathinfo: + self.show_patch_cmenu(self.list, pathinfo[0]) + + def list_sel_changed(self, list): + path, focus = list.get_cursor() + row = self.model[path] + patchname = row[M_NAME] + try: + ctx = self.repo[patchname] + revid = ctx.rev() + except hglib.RepoError: + revid = -1 + self.emit('patch-selected', revid, patchname) + + def list_row_activated(self, list, path, column): + self.update_by_row(self.model[path]) + + def list_size_allocated(self, list, req): + if self.has_patch(): + self.scroll_to_current() + + def pbackout_clicked(self, toolbutton): + pass + + def pmerge_clicked(self, toolbutton): + self.pmerge() + + def pnew_clicked(self, toolbutton): + self.pnew_ui() + + def reapply_clicked(self, toolbutton): + pass + + def edit_pgraph_clicked(self, toolbutton): + opts = {} # TODO: How to find user ID + mgr = self.pbranch.patchmanager(self.repo.ui, self.repo, opts) + oldtext = mgr.graphdesc() + # run editor in the repository root + olddir = os.getcwd() + os.chdir(self.repo.root) + newtext = self.repo.ui.edit(oldtext, opts.get('user')) + os.chdir(olddir) + mgr.updategraphdesc(newtext) + + ### context menu signal handlers ### + + def pnew_activated(self, menuitem, row): + """Insert new patch after this row""" + if self.cur_branch() == row[M_NAME]: + self.pnew_ui() + return + # pnew from patch different than current + assert False + if self.wdir_modified(): + # Ask user if current changes should be discarded + # Abort if user does not agree + pass + # remember prev branch + # Update to row[M_NAME] + # pnew_ui + # if aborted, update back to prev branch + pass + + def edit_message_activated(self, menuitem, row): + self.peditmessage(row[M_NAME]) + + def goto_activated(self, menuitem, row): + self.update_by_row(row) + + def delete_activated(self, menuitem, row): + assert False + + def rename_activated(self, menuitem, row): + assert False + + def finish_activated(self, menuitem, row): + self.pfinish(row[M_NAME])