Kiln » TortoiseHg » TortoiseHg
Clone URL:  
Pushed to one repository · View In Graph Contained in 0.8, 0.8.1, and 0.8.2

status, commit, shelve: large performance refactor

Fixes or references: #161, #205, #18, #114, #39
Included in this change were variable & function renames, removal of _

* Removed the 'Show Diff' button. This dialog is complicated enough without it
* Do not read all diffs at startup. Present diffs when a file is selected
* This means chunks are not tied to the diff-model. We must keep their
selection state even when not visible
* Detect large files (1MB default limit) and do not show their diffs
* Keep just one copy of hunk text in the data model (used to be 3)
* fixed shell notifications using repo.wjoin()
* moved some functions around

Changeset 7a6e8c8784f8

Parent 1dd1d6a1cc5b

by Steve Borho

Changes to 3 files · Browse files at 7a6e8c8784f8 Showing diff from parent 1dd1d6a1cc5b Diff from another changeset...

Change 1 of 29 Show Entire File hggtk/​commit.py Stacked
 
91
92
93
94
 
95
96
97
 
127
128
129
130
 
131
132
133
134
135
136
 
137
138
139
140
141
 
142
143
144
 
150
151
152
153
154
155
156
157
158
 
 
 
 
 
 
159
160
161
 
173
174
175
176
 
177
178
179
 
180
181
 
182
183
184
 
216
217
218
219
 
220
221
222
 
229
230
231
232
 
233
234
235
 
243
244
245
246
 
247
248
249
250
251
252
 
 
 
253
254
 
255
256
257
 
258
259
260
 
261
262
263
 
265
266
267
268
269
270
 
 
 
271
272
273
 
274
275
276
277
278
279
 
280
281
282
 
293
294
295
296
 
297
298
299
 
303
304
305
306
 
307
308
309
310
 
 
 
311
312
313
 
323
324
325
326
 
327
328
329
 
 
330
331
332
 
333
334
335
 
340
341
342
343
 
344
345
346
 
348
349
350
351
 
352
353
354
 
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
 
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
 
461
462
463
464
465
466
467
468
 
477
478
479
480
481
 
 
482
483
484
485
486
487
 
488
489
490
 
493
494
495
496
497
498
499
 
501
502
503
504
505
506
507
 
 
 
 
508
509
510
 
511
512
513
514
 
515
516
517
518
519
520
521
 
 
522
523
524
 
527
528
529
530
 
531
532
533
534
535
536
537
 
538
539
540
 
548
549
550
551
 
552
553
554
 
563
564
565
566
 
567
568
569
 
603
604
605
606
 
607
608
609
 
659
660
661
662
 
663
664
665
 
671
672
673
 
674
675
676
 
677
678
679
 
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
 
717
718
719
720
 
721
722
723
724
725
726
727
 
728
729
730
 
735
736
737
738
 
739
740
741
 
773
774
775
776
 
777
778
779
 
91
92
93
 
94
95
96
97
 
127
128
129
 
130
131
132
133
134
135
 
136
137
138
139
140
 
141
142
143
144
 
150
151
152
 
 
 
 
 
 
153
154
155
156
157
158
159
160
161
 
173
174
175
 
176
177
178
 
179
180
 
181
182
183
184
 
216
217
218
 
219
220
221
222
 
229
230
231
 
232
233
234
235
 
243
244
245
 
246
247
248
249
 
 
 
250
251
252
253
 
254
255
256
 
257
258
259
 
260
261
262
263
 
265
266
267
 
 
 
268
269
270
271
272
 
273
274
275
276
277
278
 
279
280
281
282
 
293
294
295
 
296
297
298
299
 
303
304
305
 
306
307
 
 
 
308
309
310
311
312
313
 
323
324
325
 
326
327
 
 
328
329
330
331
 
332
333
334
335
 
340
341
342
 
343
344
345
346
 
348
349
350
 
351
352
353
354
 
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
 
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
 
462
463
464
 
 
465
466
467
 
476
477
478
 
 
479
480
481
482
483
484
485
 
486
487
488
489
 
492
493
494
 
495
496
497
 
499
500
501
 
 
 
 
502
503
504
505
506
507
 
508
509
510
511
 
512
513
514
515
516
517
 
 
518
519
520
521
522
 
525
526
527
 
528
529
530
531
532
533
534
 
535
536
537
538
 
546
547
548
 
549
550
551
552
 
561
562
563
 
564
565
566
567
 
601
602
603
 
604
605
606
607
 
657
658
659
 
660
661
662
663
 
669
670
671
672
673
674
 
675
676
677
678
 
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
 
716
717
718
 
719
720
721
722
723
724
725
 
726
727
728
729
 
734
735
736
 
737
738
739
740
 
772
773
774
 
775
776
777
778
@@ -91,7 +91,7 @@
  self.mode = 'commit'   self.nextbranch = None   self.closebranch = False - self._last_commit_id = None + self.last_commit_id = None   self.qnew = False   self.notify_func = None   @@ -127,18 +127,18 @@
  for entry in self.filemodel :   if entry[FM_STATUS] in 'MAR':   entry[FM_CHECKED] = True - self._update_check_count() + self.update_check_count()   self.opts['check'] = False       def save_settings(self):   settings = GStatus.save_settings(self) - settings['commit-vpane'] = self._vpaned.get_position() + settings['commit-vpane'] = self.vpaned.get_position()   return settings       def load_settings(self, settings): - self.connect('delete-event', self._delete) + self.connect('delete-event', self.delete)   GStatus.load_settings(self, settings)   self._setting_vpos = -1   try: @@ -150,12 +150,12 @@
  def get_tbbuttons(self):   tbbuttons = GStatus.get_tbbuttons(self)   tbbuttons.insert(2, gtk.SeparatorToolItem()) - self._undo_button = self.make_toolbutton(gtk.STOCK_UNDO, _('_Undo'), - self._undo_clicked, tip=_('undo recent commit')) - self._commit_button = self.make_toolbutton(gtk.STOCK_OK, _('_Commit'), - self._commit_clicked, tip=_('commit')) - tbbuttons.insert(2, self._undo_button) - tbbuttons.insert(2, self._commit_button) + self.undo_button = self.make_toolbutton(gtk.STOCK_UNDO, _('_Undo'), + self.undo_clicked, tip=_('undo recent commit')) + self.commit_button = self.make_toolbutton(gtk.STOCK_OK, _('_Commit'), + self.commit_clicked, tip=_('commit')) + tbbuttons.insert(2, self.undo_button) + tbbuttons.insert(2, self.commit_button)   return tbbuttons     @@ -173,12 +173,12 @@
  buf.set_text(model[index][1])   buf.set_modified(False)   - def _first_msg_popdown(self, combo, shown): + def first_msg_popdown(self, combo, shown):   combo.disconnect(self.popupid)   self.popupid = None - self._update_recent_messages() + self.update_recent_messages()   - def _update_recent_messages(self, msg=None): + def update_recent_messages(self, msg=None):   if msg is not None:   self._mru_messages.add(msg)   self.settings.write() @@ -216,7 +216,7 @@
  mbox.pack_start(label, False, False, 2)   self.qnew_name = gtk.Entry()   self.qnew_name.set_width_chars(20) - self.qnew_name.connect('changed', self._qnew_changed) + self.qnew_name.connect('changed', self.qnew_changed)   mbox.pack_start(self.qnew_name, False, False, 2)   else:   self.qnew_name = None @@ -229,7 +229,7 @@
  liststore.append([_('Recent Commit Messages...'), ''])   self.msg_cbbox.set_active(0)   self.popupid = self.msg_cbbox.connect('notify::popup-shown', - self._first_msg_popdown) + self.first_msg_popdown)   self.msg_cbbox.connect('changed', self.changed_cb)   mbox.pack_start(self.msg_cbbox)   vbox.pack_start(mbox, False, False) @@ -243,21 +243,21 @@
  vbox.pack_start(frame)     self.text = gtk.TextView() - self.text.connect('populate-popup', self._msg_add_to_popup) + self.text.connect('populate-popup', self.msg_add_to_popup)   self.text.modify_font(pango.FontDescription(self.fontcomment))   scroller.add(self.text)   - self._vpaned = gtk.VPaned() - self._vpaned.add1(vbox) - self._vpaned.add2(status_body) + self.vpaned = gtk.VPaned() + self.vpaned.add1(vbox) + self.vpaned.add2(status_body)   gobject.idle_add(self.realize_settings) - return self._vpaned + return self.vpaned     def realize_settings(self): - self._vpaned.set_position(self._setting_vpos) + self.vpaned.set_position(self._setting_vpos)     def thgaccept(self, window): - self._commit_clicked(None) + self.commit_clicked(None)     def get_menu_info(self):   """Returns menu info in this order: merge, addrem, unknown, @@ -265,18 +265,18 @@
  """   merge, addrem, unknown, clean, ignored, deleted, unresolved, resolved \   = GStatus.get_menu_info(self) - return (merge + ((_('_commit'), self._commit_file),), - addrem + ((_('_commit'), self._commit_file),), - unknown + ((_('_commit'), self._commit_file),), + return (merge + ((_('_commit'), self.commit_file),), + addrem + ((_('_commit'), self.commit_file),), + unknown + ((_('_commit'), self.commit_file),),   clean,   ignored, - deleted + ((_('_commit'), self._commit_file),), + deleted + ((_('_commit'), self.commit_file),),   unresolved,   resolved,   )     - def _delete(self, window, event): + def delete(self, window, event):   if not self.should_live():   self.destroy()   else: @@ -293,7 +293,7 @@
  res = dialog.run()   if res == gtk.RESPONSE_YES:   begin, end = buf.get_bounds() - self._update_recent_messages(buf.get_text(begin, end)) + self.update_recent_messages(buf.get_text(begin, end))   buf.set_modified(False)   elif res != gtk.RESPONSE_NO:   live = True @@ -303,11 +303,11 @@
      def reload_status(self): - if not self._ready: return False + if not self.ready: return False   success = GStatus.reload_status(self) - self._check_merge() - self._check_patch_queue() - self._check_undo() + self.check_merge() + self.check_patch_queue() + self.check_undo()   self.refresh_branchop()   return success   @@ -323,13 +323,13 @@
  text = _('branch: ') + self.repo[None].branch()   self.branchbutton.set_label(text)   - def _check_undo(self): + def check_undo(self):   can_undo = os.path.exists(self.repo.sjoin("undo")) and \ - self._last_commit_id is not None - self._undo_button.set_sensitive(can_undo) + self.last_commit_id is not None + self.undo_button.set_sensitive(can_undo)     - def _check_merge(self): + def check_merge(self):   self.get_toolbutton(_('Re_vert')).set_sensitive(not self.merging)   self.get_toolbutton(_('_Add')).set_sensitive(not self.merging)   self.get_toolbutton(_('_Remove')).set_sensitive(not self.merging) @@ -340,7 +340,7 @@
  for entry in self.filemodel:   if entry[FM_STATUS] in 'MARD':   entry[FM_CHECKED] = True - self._update_check_count() + self.update_check_count()     # pre-fill commit message   buf = self.text.get_buffer() @@ -348,7 +348,7 @@
  buf.set_modified(False)     - def _check_patch_queue(self): + def check_patch_queue(self):   '''See if an MQ patch is applied, switch to qrefresh mode'''   self.qheader = None   if self.mqmode: @@ -384,38 +384,36 @@
  c_btn.set_tooltip(self.tooltips, _('commit'))   self.branchbutton.set_sensitive(not (self.mqmode or self.qnew))   - def _commit_clicked(self, toolbutton, data=None): - if not self._ready_message(): + def commit_clicked(self, toolbutton, data=None): + if not self.ready_message():   return     if self.merging:   # merges must be committed without specifying file list. - self._hg_commit([]) + self.hg_commit([])   else:   commitable = 'MAR' - addremove_list = self._relevant_files('?!') - if len(addremove_list) and self._should_addremove(addremove_list): + addremove_list = self.relevant_files('?!') + if len(addremove_list) and self.should_addremove(addremove_list):   commitable += '?!'   - commit_list = self._relevant_files(commitable) + commit_list = self.relevant_files(commitable)   if len(commit_list) > 0: - self._commit_selected(commit_list) + self.commit_selected(commit_list)   elif len(self.filemodel) == 0 and self.qnew: - self._commit_selected([]) + self.commit_selected([])   else:   gdialog.Prompt(_('Nothing Commited'),   _('No committable files selected'), self).run()   return   self.reload_status()   shlib.update_thgstatus(self.ui, self.repo.root, wait=True) - shlib.shell_notify([self.cwd] + self._relevant_files('MAR')) + files = [self.repo.wjoin(x) for x in self.relevant_files('MAR')] + shlib.shell_notify(files)   - def _commit_selected(self, files): + def commit_selected(self, files):   # 1a. get list of chunks not rejected - repo, chunks, ui = self.repo, self._shelve_chunks, self.ui - model = self.diff_model - files = [util.pconvert(f) for f in files] - hlist = [x[DM_CHUNK_ID] for x in model if not x[DM_REJECTED]] + repo, ui = self.repo, self.ui     # 2. backup changed files, so we can restore them in the end   backups = {} @@ -424,28 +422,31 @@
  os.mkdir(backupdir)   except OSError, err:   if err.errno != errno.EEXIST: - gdialog.Prompt(_('Commit'), _('Unable to create ') + backupdir, - self).run() + gdialog.Prompt(_('Commit'), + _('Unable to create ') + backupdir, self).run()   return   try:   # backup continues + allchunks = []   for f in files:   if f not in self.modified: continue - fh = self._filechunks.get(f) - if not fh or len(fh) < 2: continue + if f not in self.filechunks: continue + chunks = self.filechunks[f] + if len(chunks) < 2: continue +   # unfiltered files do not go through backup-revert-patch cycle - rejected = [x for x in fh[1:] if model[x][DM_REJECTED]] + rejected = [c for c in chunks[1:] if not c.active]   if len(rejected) == 0: continue + allchunks.extend(chunks)   fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',   dir=backupdir)   os.close(fd) - ui.debug(_('backup %r as %r\n') % (f, tmpname))   util.copyfile(repo.wjoin(f), tmpname)   backups[f] = tmpname     fp = cStringIO.StringIO() - for n, c in enumerate(chunks): - if c.filename() in backups and n in hlist: + for n, c in enumerate(allchunks): + if c.filename() in backups and c.active:   c.write(fp)   dopatch = fp.tell()   fp.seek(0) @@ -461,8 +462,6 @@
  # 3b. (apply)   if dopatch:   try: - ui.debug(_('applying patch\n')) - ui.debug(fp.getvalue())   pfiles = {}   patch.internalpatch(fp, ui, 1, repo.root, files=pfiles)   patch.updatedir(ui, repo, pfiles) @@ -477,14 +476,14 @@
  del fp     # 4. We prepared working directory according to filtered patch. - # Now is the time to delegate the job to commit/qrefresh or the like! - + # Now is the time to delegate the job to commit/qrefresh + # or the like!   # it is important to first chdir to repo root -- we'll call a   # highlevel command with list of pathnames relative to repo root   cwd = os.getcwd()   os.chdir(repo.root)   try: - self._hg_commit(files) + self.hg_commit(files)   finally:   os.chdir(cwd)   @@ -493,7 +492,6 @@
  # 5. finally restore backed-up files   try:   for realname, tmpname in backups.iteritems(): - ui.debug(_('restoring %r to %r\n') % (tmpname, realname))   util.copyfile(tmpname, repo.wjoin(realname))   os.unlink(tmpname)   os.rmdir(backupdir) @@ -501,24 +499,24 @@
  pass     - def _commit_file(self, stat, file): - if self._ready_message(): - if stat not in '?!' or self._should_addremove([file]): - self._hg_commit([file]) + def commit_file(self, stat, file): + if self.ready_message(): + if stat not in '?!' or self.should_addremove([file]): + self.hg_commit([file])   self.reload_status()   shlib.update_thgstatus(self.ui, self.repo.root, wait=True) - shlib.shell_notify([file]) + shlib.shell_notify([self.repo.wjoin(file)])   return True     - def _undo_clicked(self, toolbutton, data=None): + def undo_clicked(self, toolbutton, data=None):   response = gdialog.Confirm(_('Confirm Undo commit'),   [], self, _('Undo last commit')).run()   if response != gtk.RESPONSE_YES:   return   - tip = self._get_tip_rev(True) - if not tip == self._last_commit_id: + tip = self.get_tip_rev(True) + if not tip == self.last_commit_id:   gdialog.Prompt(_('Undo commit'),   _('Unable to undo!\n\n'   'Tip revision differs from last commit.'), @@ -527,14 +525,14 @@
    try:   self.repo.rollback() - self._last_commit_id = None + self.last_commit_id = None   self.reload_status()   except:   gdialog.Prompt(_('Undo commit'),   _('Errors during rollback!'), self).run()     - def _should_addremove(self, files): + def should_addremove(self, files):   if self.test_opt('addremove'):   return True   else: @@ -548,7 +546,7 @@
  return False     - def _ready_message(self): + def ready_message(self):   buf = self.text.get_buffer()   if buf.get_char_count() == 0:   gdialog.Prompt(_('Nothing Commited'), @@ -563,7 +561,7 @@
  gdialog.Prompt(_('Error'),   _('Message format configuration error'),   self).run() - self._msg_config(None) + self.msg_config(None)   return     lines = buf.get_text(buf.get_start_iter(), @@ -603,7 +601,7 @@
  return True     - def _hg_commit(self, files): + def hg_commit(self, files):   if not self.repo.ui.config('ui', 'username'):   gdialog.Prompt(_('Commit: Invalid username'),   _('Your username has not been configured.\n\n' @@ -659,7 +657,7 @@
  cmdline.extend(['--date', self.opts['date']])   cmdline += ['--message', hglib.fromutf(self.opts['message'])]   if self.qnew: - cmdline += [hglib.fromutf(self._get_qnew_name())] + cmdline += [hglib.fromutf(self.get_qnew_name())]   cmdline += [self.repo.wjoin(x) for x in files]   dialog = hgcmd.CmdDialog(cmdline, True)   dialog.set_transient_for(self) @@ -671,9 +669,10 @@
  self.closebranch = False   self.nextbranch = None   self.opts['check'] = True # recheck MAR after commit + self.filechunks = {} # do not keep chunks   buf = self.text.get_buffer()   if buf.get_modified(): - self._update_recent_messages(self.opts['message']) + self.update_recent_messages(self.opts['message'])   buf.set_modified(False)   if self.qnew:   self.qnew_name.set_text('') @@ -682,31 +681,31 @@
  self.qnew = False   elif self.qheader is None:   self.text.set_buffer(gtk.TextBuffer()) - self._last_commit_id = self._get_tip_rev(True) + self.last_commit_id = self.get_tip_rev(True)   if self.notify_func:   self.notify_func(self.notify_args)   - def _get_tip_rev(self, refresh=False): + def get_tip_rev(self, refresh=False):   if refresh:   self.repo.invalidate()   return self.repo['tip'].node()   - def _get_qnew_name(self): + def get_qnew_name(self):   return self.qnew_name and self.qnew_name.get_text().strip() or ''   - def _qnew_changed(self, element): - qnew = bool(self._get_qnew_name()) + def qnew_changed(self, element): + qnew = bool(self.get_qnew_name())   if self.qnew != qnew:   self.qnew = qnew   self.mode = qnew and 'status' or 'commit'   self.reload_status()   self.qnew_name.grab_focus() # set focus back   - def _msg_add_to_popup(self, textview, menu): + def msg_add_to_popup(self, textview, menu):   menu_items = (('----', None), - (_('Paste _Filenames'), self._msg_paste_fnames), - (_('App_ly Format'), self._msg_word_wrap), - (_('C_onfigure Format'), self._msg_config)) + (_('Paste _Filenames'), self.msg_paste_fnames), + (_('App_ly Format'), self.msg_word_wrap), + (_('C_onfigure Format'), self.msg_config))   for label, handler in menu_items:   if label == '----':   menuitem = gtk.SeparatorMenuItem() @@ -717,14 +716,14 @@
  menu.append(menuitem)   menu.show_all()   - def _msg_paste_fnames(self, sender): + def msg_paste_fnames(self, sender):   buf = self.text.get_buffer()   fnames = [ file[FM_PATH_UTF8] for file in self.filemodel   if file[FM_CHECKED] ]   buf.delete_selection(True, True)   buf.insert_at_cursor('\n'.join(fnames))   - def _msg_word_wrap(self, sender): + def msg_word_wrap(self, sender):   try:   sumlen = int(self.repo.ui.config('tortoisehg', 'summarylen', 0))   maxlen = int(self.repo.ui.config('tortoisehg', 'messagewrap', 0)) @@ -735,7 +734,7 @@
  gdialog.Prompt(_('Info required'),   _('Message format needs to be configured'),   self).run() - self._msg_config(None) + self.msg_config(None)   return     buf = self.text.get_buffer() @@ -773,7 +772,7 @@
  lnum += 1   buf.set_text('\n'.join(lines))   - def _msg_config(self, sender): + def msg_config(self, sender):   dlg = thgconfig.ConfigDialog(True)   dlg.show_all()   dlg.focus_field('tortoisehg.summarylen')
Change 1 of 36 Show Entire File hggtk/​status.py Stacked
 
29
30
31
32
33
34
35
36
37
38
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
41
42
 
56
57
58
 
 
 
 
59
60
61
62
63
64
 
65
66
67
 
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
 
123
124
125
126
 
127
128
129
 
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
 
217
218
219
220
221
222
223
224
 
246
247
248
249
250
 
 
251
252
253
254
255
256
257
 
 
 
 
 
258
259
260
 
273
274
275
276
 
277
278
279
 
288
289
290
291
 
292
293
294
295
 
296
297
298
 
306
307
308
309
 
310
311
312
 
334
335
336
337
 
338
339
340
 
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
 
381
382
383
384
 
385
386
387
388
 
 
389
390
391
 
395
396
397
398
399
400
401
402
403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
405
406
 
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
 
595
596
597
598
599
600
601
602
603
604
605
606
607
 
 
 
 
 
608
609
610
 
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
 
689
690
691
692
 
693
694
695
696
697
698
699
 
700
701
702
 
716
717
718
719
 
720
721
722
723
724
 
 
725
726
727
728
 
729
730
731
732
 
733
734
735
736
 
737
738
739
 
750
751
752
753
 
754
755
756
757
 
758
759
760
 
773
774
775
776
 
777
778
779
 
792
793
794
795
 
796
797
798
 
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
869
870
871
872
873
874
875
876
877
878
879
880
881
 
 
882
883
884
885
886
887
 
 
 
 
 
 
 
888
889
 
890
891
892
893
894
895
 
 
 
896
897
898
899
900
901
902
 
 
 
 
 
903
904
905
 
908
909
910
911
912
 
 
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
 
1034
1035
1036
1037
 
1038
1039
1040
 
1045
1046
1047
1048
1049
 
 
1050
1051
1052
1053
1054
1055
1056
 
 
 
 
 
 
 
 
 
 
 
 
1057
1058
1059
1060
 
1061
1062
1063
1064
1065
1066
1067
 
 
1068
1069
 
1070
1071
1072
1073
1074
1075
1076
1077
 
 
1078
1079
1080
1081
 
1082
1083
1084
 
1086
1087
1088
1089
 
1090
1091
1092
 
1130
1131
1132
1133
1134
 
 
1135
1136
 
1137
1138
1139
1140
1141
1142
1143
1144
 
 
1145
1146
1147
1148
 
1149
1150
1151
 
1158
1159
1160
1161
1162
1163
 
 
 
1164
1165
 
1166
1167
 
1168
1169
1170
1171
1172
1173
1174
 
 
1175
1176
1177
 
1196
1197
1198
1199
 
1200
1201
1202
1203
1204
1205
1206
 
 
1207
1208
 
1209
1210
 
1211
1212
1213
 
1224
1225
1226
1227
 
1228
1229
1230
1231
1232
 
1233
1234
1235
 
1239
1240
1241
1242
 
1243
1244
1245
1246
1247
1248
 
1249
1250
1251
1252
1253
1254
 
1255
1256
1257
 
1259
1260
1261
1262
1263
 
 
1264
1265
1266
1267
 
1268
1269
1270
1271
1272
1273
1274
 
 
1275
1276
1277
 
1278
1279
1280
1281
1282
 
1283
1284
1285
 
1289
1290
1291
1292
 
1293
1294
1295
 
1301
1302
1303
1304
 
1305
1306
1307
1308
 
1309
1310
1311
 
1312
1313
1314
 
1317
1318
1319
1320
 
1321
1322
1323
1324
1325
1326
 
1327
1328
1329
1330
1331
 
1332
1333
1334
1335
 
1336
1337
1338
1339
 
1340
1341
1342
1343
1344
 
1345
1346
1347
 
1355
1356
1357
1358
 
1359
1360
1361
 
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
 
86
87
88
89
90
91
92
93
94
95
96
97
 
98
99
100
101
 
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
 
157
158
159
 
160
161
162
163
 
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
 
241
242
243
 
 
244
245
246
 
268
269
270
 
 
271
272
273
274
 
 
 
 
 
275
276
277
278
279
280
281
282
 
295
296
297
 
298
299
300
301
 
310
311
312
 
313
314
315
316
 
317
318
319
320
 
328
329
330
 
331
332
333
334
 
356
357
358
 
359
360
361
362
 
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
 
407
408
409
 
410
411
412
 
 
413
414
415
416
417
 
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
 
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
 
612
613
614
 
615
616
617
 
 
 
618
 
 
619
620
621
622
623
624
625
626
 
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
 
724
725
726
 
727
728
729
730
731
732
733
 
734
735
736
737
 
751
752
753
 
754
755
756
757
 
 
758
759
760
761
762
 
763
764
765
766
 
767
768
769
770
 
771
772
773
774
 
785
786
787
 
788
789
790
791
 
792
793
794
795
 
808
809
810
 
811
812
813
814
 
827
828
829
 
830
831
832
833
 
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
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
 
925
 
 
 
 
 
 
 
926
927
928
929
930
931
932
 
 
 
 
 
933
934
935
936
937
938
939
940
941
942
943
944
945
946
 
 
 
 
 
 
 
 
 
 
 
 
 
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
 
 
975
976
977
978
979
 
980
981
982
983
984
985
986
987
988
989
990
991
 
 
 
 
 
 
 
 
 
 
992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
994
995
996
 
 
 
 
997
998
999
1000
1001
1002
1003
1004
 
1005
1006
 
1007
 
 
 
1008
1009
1010
1011
1012
1013
 
 
 
 
1014
1015
1016
1017
1018
1019
1020
1021
 
1024
1025
1026
 
 
1027
1028
1029
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1031
1032
1033
1034
 
1035
1036
1037
1038
 
1043
1044
1045
 
 
1046
1047
1048
 
 
 
 
 
 
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
 
 
1063
1064
1065
1066
1067
1068
 
 
1069
1070
1071
 
1072
1073
1074
1075
1076
1077
1078
 
 
1079
1080
1081
1082
1083
 
1084
1085
1086
1087
 
1089
1090
1091
 
1092
1093
1094
1095
 
1133
1134
1135
 
 
1136
1137
1138
 
1139
1140
1141
1142
1143
1144
1145
 
 
1146
1147
1148
1149
1150
 
1151
1152
1153
1154
 
1161
1162
1163
 
 
 
1164
1165
1166
1167
 
1168
1169
 
1170
1171
1172
1173
1174
1175
 
 
1176
1177
1178
1179
1180
 
1199
1200
1201
 
1202
1203
1204
1205
1206
1207
 
 
1208
1209
1210
 
1211
1212
 
1213
1214
1215
1216
 
1227
1228
1229
 
1230
1231
1232
1233
1234
 
1235
1236
1237
1238
 
1242
1243
1244
 
1245
1246
1247
1248
1249
1250
 
1251
1252
1253
1254
1255
1256
 
1257
1258
1259
1260
 
1262
1263
1264
 
 
1265
1266
1267
1268
1269
 
1270
1271
1272
1273
1274
1275
 
 
1276
1277
1278
1279
 
1280
1281
1282
1283
1284
 
1285
1286
1287
1288
 
1292
1293
1294
 
1295
1296
1297
1298
 
1304
1305
1306
 
1307
1308
1309
1310
 
1311
1312
1313
 
1314
1315
1316
1317
 
1320
1321
1322
 
1323
1324
1325
1326
1327
1328
 
1329
1330
1331
1332
1333
 
1334
1335
1336
1337
 
1338
1339
1340
1341
 
1342
1343
1344
1345
1346
 
1347
1348
1349
1350
 
1358
1359
1360
 
1361
1362
1363
1364
@@ -29,14 +29,44 @@
 FM_MERGE_STATUS = 4  FM_PARTIAL_SELECTED = 5   -# diff_model row enumerations -DM_REJECTED = 0 -DM_MARKUP = 1 -DM_TEXT = 2 -DM_DISPLAYED = 3 -DM_IS_HEADER = 4 -DM_CHUNK_ID = 5 -DM_FONT = 6 +# diffmodel row enumerations +DM_REJECTED = 0 +DM_DISP_TEXT = 1 +DM_IS_HEADER = 2 +DM_PATH = 3 +DM_CHUNK_ID = 4 +DM_FONT = 5 + +def hunk_markup(text): + 'Format a diff hunk for display in a TreeView row with markup' + hunk = "" + lines = text.splitlines() + for line in lines: + line = gobject.markup_escape_text(hglib.toutf(line[:128])) + if line[-1] != '\n': + line += '\n' + if line.startswith('---') or line.startswith('+++'): + hunk += '<span foreground="#000090">%s</span>' % line + elif line.startswith('-'): + hunk += '<span foreground="#900000">%s</span>' % line + elif line.startswith('+'): + hunk += '<span foreground="#006400">%s</span>' % line + elif line.startswith('@@'): + hunk = '<span foreground="#FF8000">%s</span>' % line + else: + hunk += line + return hunk + +def hunk_unmarkup(text): + 'Format a diff hunk for display in a TreeView row without markup' + hunk = "" + lines = text.splitlines() + for line in lines: + line = gobject.markup_escape_text(hglib.toutf(line[:128])) + if line[-1] != '\n': + line += '\n' + hunk += line + return hunk    class GStatus(gdialog.GDialog):   """GTK+ based dialog for displaying repository status @@ -56,12 +86,16 @@
  def init(self):   gdialog.GDialog.init(self)   self.mode = 'status' + self.ready = True + self.last_file = None + self.filerowstart = {} + self.filechunks = {}     def auto_check(self):   if self.pats or self.opts.get('check'):   for entry in self.filemodel:   entry[FM_CHECKED] = True - self._update_check_count() + self.update_check_count()     def get_menu_info(self):   """Returns menu info in this order: @@ -72,49 +106,49 @@
  # merge   ((_('_difference'), self._diff_file),   (_('edit'), self._view_file), - (_('view other'), self._view_left_file), - (_('_revert'), self._revert_file), - (_('l_og'), self._log_file)), + (_('view other'), self.view_left_file), + (_('_revert'), self.revert_file), + (_('l_og'), self.log_file)),   # addrem   ((_('_difference'), self._diff_file),   (_('_view'), self._view_file), - (_('_revert'), self._revert_file), - (_('l_og'), self._log_file)), + (_('_revert'), self.revert_file), + (_('l_og'), self.log_file)),   # unknown   ((_('_view'), self._view_file), - (_('_delete'), self._delete_file), - (_('_add'), self._add_file), - (_('_guess rename'), self._guess_rename), - (_('_ignore'), self._ignore_file)), + (_('_delete'), self.delete_file), + (_('_add'), self.add_file), + (_('_guess rename'), self.guess_rename), + (_('_ignore'), self.ignore_file)),   # clean   ((_('_view'), self._view_file), - (_('re_move'), self._remove_file), - (_('re_name'), self._rename_file), - (_('_copy'), self._copy_file), - (_('l_og'), self._log_file)), + (_('re_move'), self.remove_file), + (_('re_name'), self.rename_file), + (_('_copy'), self.copy_file), + (_('l_og'), self.log_file)),   # ignored   ((_('_view'), self._view_file), - (_('_delete'), self._delete_file)), + (_('_delete'), self.delete_file)),   # deleted   ((_('_view'), self._view_file), - (_('_revert'), self._revert_file), - (_('re_move'), self._remove_file), - (_('l_og'), self._log_file)), + (_('_revert'), self.revert_file), + (_('re_move'), self.remove_file), + (_('l_og'), self.log_file)),   # unresolved   ((_('_difference'), self._diff_file),   (_('edit'), self._view_file), - (_('view other'), self._view_left_file), - (_('_revert'), self._revert_file), - (_('l_og'), self._log_file), - (_('resolve'), self._do_resolve), - (_('mark resolved'), self._mark_resolved)), + (_('view other'), self.view_left_file), + (_('_revert'), self.revert_file), + (_('l_og'), self.log_file), + (_('resolve'), self.do_resolve), + (_('mark resolved'), self.mark_resolved)),   # resolved   ((_('_difference'), self._diff_file),   (_('edit'), self._view_file), - (_('view other'), self._view_left_file), - (_('_revert'), self._revert_file), - (_('l_og'), self._log_file), - (_('mark unresolved'), self._unmark_resolved)), + (_('view other'), self.view_left_file), + (_('_revert'), self.revert_file), + (_('l_og'), self.log_file), + (_('mark unresolved'), self.unmark_resolved)),   )     ### End of overridable methods ### @@ -123,7 +157,7 @@
  ### Overrides of base class methods ###     def parse_opts(self): - self._ready = False + self.ready = False     # Determine which files to display   if self.test_opt('all'): @@ -161,35 +195,25 @@
    def get_tbbuttons(self):   tbuttons = [self.make_toolbutton(gtk.STOCK_REFRESH, _('Re_fresh'), - self._refresh_clicked, tip=_('refresh')), + self.refresh_clicked, tip=_('refresh')),   gtk.SeparatorToolItem()]     if self.count_revs() == 2:   tbuttons += [   self.make_toolbutton(gtk.STOCK_SAVE_AS, _('Save As'), - self._save_clicked, tip=_('Save selected changes'))] + self.save_clicked, tip=_('Save selected changes'))]   else:   tbuttons += [   self.make_toolbutton(gtk.STOCK_MEDIA_REWIND, _('Re_vert'), - self._revert_clicked, tip=_('revert')), + self.revert_clicked, tip=_('revert')),   self.make_toolbutton(gtk.STOCK_ADD, _('_Add'), - self._add_clicked, tip=_('add')), + self.add_clicked, tip=_('add')),   self.make_toolbutton(gtk.STOCK_JUMP_TO, _('Move'), - self._move_clicked, + self.move_clicked,   tip=_('move selected files to other directory')),   self.make_toolbutton(gtk.STOCK_DELETE, _('_Remove'), - self._remove_clicked, tip=_('remove')), + self.remove_clicked, tip=_('remove')),   gtk.SeparatorToolItem()] - - self.showdiff_toggle = gtk.ToggleToolButton(gtk.STOCK_JUSTIFY_FILL) - self.showdiff_toggle.set_use_underline(True) - self.showdiff_toggle.set_label(_('_Show Diff')) - self.showdiff_toggle.set_tooltip(self.tooltips, _('show diff pane')) - self.showdiff_toggle.set_active(False) - self._showdiff_toggled_id = self.showdiff_toggle.connect('toggled', - self._showdiff_toggled ) - tbuttons.append(self.showdiff_toggle) -   return tbuttons     @@ -217,8 +241,6 @@
  def get_body(self):   self.merging = len(self.repo.parents()) == 2   - self.connect('map-event', self._displayed) -   # TODO: should generate menus dynamically during right-click, currently   # there can be entires that are not always supported or relavant.   merge, addrem, unknown, clean, ignored, deleted, unresolved, resolved \ @@ -246,15 +268,15 @@
    # model stores the file list.   self.filemodel = gtk.ListStore(bool, str, str, str, str, bool) - self.filemodel.set_sort_func(1001, self._sort_by_stat) - self.filemodel.set_default_sort_func(self._sort_by_stat) + self.filemodel.set_sort_func(1001, self.sort_by_stat) + self.filemodel.set_default_sort_func(self.sort_by_stat)     self.filetree = gtk.TreeView(self.filemodel) - self.filetree.connect('button-press-event', self._tree_button_press) - self.filetree.connect('button-release-event', self._tree_button_release) - self.filetree.connect('popup-menu', self._tree_popup_menu) - self.filetree.connect('row-activated', self._tree_row_act) - self.filetree.connect('key-press-event', self._tree_key_press) + self.filetree.connect('button-press-event', self.tree_button_press) + self.filetree.connect('button-release-event', self.tree_button_release) + self.filetree.connect('popup-menu', self.tree_popup_menu) + self.filetree.connect('row-activated', self.tree_row_act) + self.filetree.connect('key-press-event', self.tree_key_press)   self.filetree.set_reorderable(False)   self.filetree.set_enable_search(True)   self.filetree.set_search_equal_func(self.search_filelist) @@ -273,7 +295,7 @@
  self.connect('thg-refresh', self.thgrefresh)     toggle_cell = gtk.CellRendererToggle() - toggle_cell.connect('toggled', self._select_toggle) + toggle_cell.connect('toggled', self.select_toggle)   toggle_cell.set_property('activatable', True)     path_cell = gtk.CellRendererText() @@ -288,11 +310,11 @@
  col0.add_attribute(toggle_cell, 'radio', FM_PARTIAL_SELECTED)   col0.set_resizable(False)   self.filetree.append_column(col0) - self.selcb = self._add_header_checkbox(col0, self._sel_clicked) + self.selcb = self.add_header_checkbox(col0, self.sel_clicked)     col1 = gtk.TreeViewColumn(_('st'), stat_cell)   col1.add_attribute(stat_cell, 'text', FM_STATUS) - col1.set_cell_data_func(stat_cell, self._text_color) + col1.set_cell_data_func(stat_cell, self.text_color)   col1.set_sort_column_id(1001)   col1.set_resizable(False)   self.filetree.append_column(col1) @@ -306,7 +328,7 @@
    col2 = gtk.TreeViewColumn(_('path'), path_cell)   col2.add_attribute(path_cell, 'text', FM_PATH_UTF8) - col2.set_cell_data_func(path_cell, self._text_color) + col2.set_cell_data_func(path_cell, self.text_color)   col2.set_sort_column_id(2)   col2.set_resizable(True)   self.filetree.append_column(col2) @@ -334,7 +356,7 @@
  self.merge_diff_text.modify_font(self.difffont)   self.filetree.get_selection().set_mode(gtk.SELECTION_SINGLE)   self.filetree.get_selection().connect('changed', - self.merge_sel_changed, False) + self.merge_sel_changed)   scroller.add(self.merge_diff_text)   diff_frame.add(scroller)   else: @@ -342,29 +364,33 @@
  sel = (os.name == 'nt') and 'CLIPBOARD' or 'PRIMARY'   self.clipboard = gtk.Clipboard(selection=sel)   - self.diff_model = gtk.ListStore(bool, str, str, str, bool, int, + self.diffmodel = gtk.ListStore( + bool, # DM_REJECTED + str, # DM_DISP_TEXT + bool, # DM_IS_HEADER + str, # DM_PATH + int, # DM_CHUNK_ID   pango.FontDescription)   - self.diff_tree = gtk.TreeView(self.diff_model) + difftree = gtk.TreeView(self.diffmodel)     # set CTRL-c accelerator for copy-clipboard   mod = gtklib.get_thg_modifier()   key, modifier = gtk.accelerator_parse(mod+'c') - self.diff_tree.add_accelerator('copy-clipboard', accelgroup, key, + difftree.add_accelerator('copy-clipboard', accelgroup, key,   modifier, gtk.ACCEL_VISIBLE) - self.diff_tree.connect('copy-clipboard', self.copy_to_clipboard) + difftree.connect('copy-clipboard', self.copy_to_clipboard)   - self.diff_tree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - self.diff_tree.set_headers_visible(False) - self.diff_tree.set_enable_search(False) - self.diff_tree.set_property('enable-grid-lines', True) - self.diff_tree.connect('row-activated', - self._diff_tree_row_act) + difftree.get_selection().set_mode(gtk.SELECTION_MULTIPLE) + difftree.set_headers_visible(False) + difftree.set_enable_search(False) + difftree.set_property('enable-grid-lines', True) + difftree.connect('row-activated', self.diff_tree_row_act)     cell = gtk.CellRendererText()   diffcol = gtk.TreeViewColumn('diff', cell)   diffcol.set_resizable(True) - diffcol.add_attribute(cell, 'markup', DM_DISPLAYED) + diffcol.add_attribute(cell, 'markup', DM_DISP_TEXT)     # differentiate header chunks   cell.set_property('cell-background', '#DDDDDD') @@ -381,11 +407,11 @@
  diffcol.add_attribute(cell, 'background-set', DM_REJECTED)   diffcol.add_attribute(cell, 'foreground-set', DM_REJECTED)   - self.diff_tree.append_column(diffcol) + difftree.append_column(diffcol)   self.filetree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)   self.filetree.get_selection().connect('changed', - self.tree_sel_changed, False) - scroller.add(self.diff_tree) + self.tree_sel_changed) + scroller.add(difftree)   diff_frame.add(scroller)     if self.diffbottom: @@ -395,12 +421,86 @@
    self._diffpane.pack1(tree_frame, True, False)   self._diffpane.pack2(diff_frame, True, True) - self._diffpane_moved_id = self._diffpane.connect('notify::position', - self._diffpane_moved)   self.filetree.set_headers_clickable(True)   gobject.idle_add(self.realize_status_settings)   return self._diffpane   + + def get_extras(self): + table = gtk.Table(rows=2, columns=3) + table.set_col_spacings(8) + + self._show_checks = {} + row, col = 0, 0 + # Tuple: (ctype, translated label) + checks = (('modified', _('modified')), + ('added', _('added')), + ('removed', _('removed'))) + if self.count_revs() <= 1: + checks += (('deleted', _('deleted')), + ('unknown', _('unknown')), + ('close', _('clean')), + ('ignored', _('ignored'))) + + for ctuple in checks: + check = gtk.CheckButton(ctuple[1]) + check.connect('toggled', self.show_toggle, ctuple[0]) + table.attach(check, col, col+1, row, row+1) + self._show_checks[ctuple[0]] = check + col += row + row = not row + + self.counter = gtk.Label('') + self.counter.set_alignment(1.0, 0.0) # right up + + hbox = gtk.HBox() + hbox.pack_start(table, expand=False) + hbox.pack_end(self.counter, expand=True, padding=2) + + return hbox + + def add_header_checkbox(self, col, post=None, pre=None, toggle=False): + def cbclick(hdr, cb): + state = cb.get_active() + if pre: + pre(state) + if toggle: + cb.set_active(not state) + if post: + post(not state) + + cb = gtk.CheckButton(col.get_title()) + cb.show() + col.set_widget(cb) + wgt = cb.get_parent() + while wgt: + if type(wgt) == gtk.Button: + wgt.connect('clicked', cbclick, cb) + return cb + wgt = wgt.get_parent() + return + + def update_check_count(self): + file_count = 0 + check_count = 0 + for row in self.filemodel: + file_count = file_count + 1 + if row[FM_CHECKED]: + check_count = check_count + 1 + self.counter.set_text(_('%d selected, %d total') % (check_count, + file_count)) + if self.selcb: + self.selcb.set_active(file_count and file_count == check_count) + + def prepare_display(self): + self.ready = True + # If the status load failed, no reason to continue + if not self.reload_status(): + raise util.Abort('could not load status') + + + ### End of overrides ### +   def realize_status_settings(self):   self._diffpane.set_position(self._setting_pos)   @@ -426,108 +526,25 @@
  w = self.get_focus()   w.emit('copy-clipboard')   return False + saves = {}   model, tpaths = treeview.get_selection().get_selected_rows() - cids = [ model[row][DM_CHUNK_ID] for row, in tpaths ] - headers = {} + for row, _ in tpaths: + wfile, cid = model[row][DM_PATH], model[row][DM_CHUNK_ID] + if wfile not in saves: + saves[wfile] = [cid] + else: + saves[wfile].append(cid)   fp = cStringIO.StringIO() - for cid in cids: - chunk = self._shelve_chunks[cid] - wfile = chunk.filename() - if not isinstance(chunk, hgshelve.header): - # Ensure each hunk has a file header - if wfile not in headers: - hrow = self._filechunks[wfile][0] - hcid = model[hrow][DM_CHUNK_ID] - self._shelve_chunks[hcid].write(fp) - headers[wfile] = cid - chunk.write(fp) + for wfile in saves.keys(): + chunks = self.filechunks[wfile] + chunks[0].write(fp) + for cid in saves[wfile]: + if cid != 0: + chunks[cid].write(fp)   fp.seek(0)   self.clipboard.set_text(fp.read())   - def get_extras(self): - table = gtk.Table(rows=2, columns=3) - table.set_col_spacings(8) - - self._show_checks = {} - row, col = 0, 0 - # Tuple: (ctype, translated label) - checks = (('modified', _('modified')), - ('added', _('added')), - ('removed', _('removed'))) - if self.count_revs() <= 1: - checks += (('deleted', _('deleted')), - ('unknown', _('unknown')), - ('close', _('clean')), - ('ignored', _('ignored'))) - - for ctuple in checks: - check = gtk.CheckButton(ctuple[1]) - check.connect('toggled', self._show_toggle, ctuple[0]) - table.attach(check, col, col+1, row, row+1) - self._show_checks[ctuple[0]] = check - col += row - row = not row - - self.counter = gtk.Label('') - self.counter.set_alignment(1.0, 0.0) # right up - - hbox = gtk.HBox() - hbox.pack_start(table, expand=False) - hbox.pack_end(self.counter, expand=True, padding=2) - - return hbox - - def _add_header_checkbox(self, col, post=None, pre=None, toggle=False): - def cbclick(hdr, cb): - state = cb.get_active() - if pre: - pre(state) - if toggle: - cb.set_active(not state) - if post: - post(not state) - - cb = gtk.CheckButton(col.get_title()) - cb.show() - col.set_widget(cb) - wgt = cb.get_parent() - while wgt: - if type(wgt) == gtk.Button: - wgt.connect('clicked', cbclick, cb) - return cb - wgt = wgt.get_parent() - return - - - def _update_check_count(self): - file_count = 0 - check_count = 0 - for row in self.filemodel: - file_count = file_count + 1 - if row[FM_CHECKED]: - check_count = check_count + 1 - self.counter.set_text(_('%d selected, %d total') % (check_count, - file_count)) - if self.selcb: - self.selcb.set_active(file_count and file_count == check_count) - - def prepare_display(self): - self._ready = True - self._last_file = None - self._shelve_chunks = [] - self._filechunks = {} - # If the status load failed, no reason to continue - if not self.reload_status(): - raise util.Abort('could not load status') - - - def _displayed(self, widget, event): - self._diffpane_moved(self._diffpane) - return False - - ### End of overrides ### - - def _do_reload_status(self): + def do_reload_status(self):   """Clear out the existing ListStore model and reload it from the   repository status. Also recheck and reselect files that remain   in the list. @@ -595,16 +612,15 @@
  if row[FM_PATH] in reselect:   selection.select_iter(row.iter)   selected = True -   if not selected:   selection.select_path((0,))   - files = [row[FM_PATH] for row in self.filemodel] - self._show_diff_hunks(files) -   # clear buffer after a merge commit - if not files and self.merging: - self.merge_diff_text.set_buffer(gtk.TextBuffer()) + if not len(self.filemodel): + if self.merging: + self.merge_diff_text.set_buffer(gtk.TextBuffer()) + else: + self.diffmodel.clear()     self.filetree.show()   if self.mode == 'commit': @@ -615,73 +631,92 @@
      def reload_status(self): - if not self._ready: return False - self._last_file = None - res, outtext = self._hg_call_wrapper('Status', self._do_reload_status) + if not self.ready: return False + self.last_file = None + res, outtext = self._hg_call_wrapper('Status', self.do_reload_status)   self.auto_check() - self._update_check_count() + self.update_check_count()   return res       def make_menu(self, entries):   menu = gtk.Menu()   for entry in entries: - menu.append(self._make_menuitem(entry[0], entry[1])) + menu.append(self.make_menuitem(entry[0], entry[1]))   menu.show_all()   return menu     - def _make_menuitem(self, label, handler): + def make_menuitem(self, label, handler):   menuitem = gtk.MenuItem(label, True) - menuitem.connect('activate', self._context_menu_act, handler) + menuitem.connect('activate', self.context_menu_act, handler)   menuitem.set_border_width(1)   return menuitem     - def _select_toggle(self, cellrenderer, path): - '''User manually toggled file status''' + def select_toggle(self, cellrenderer, path): + 'User manually toggled file status via checkbox'   self.filemodel[path][FM_CHECKED] = not self.filemodel[path][FM_CHECKED] - self._update_chunk_state(self.filemodel[path]) - self._update_check_count() + self.update_chunk_state(self.filemodel[path]) + self.update_check_count()   return True   - def _update_chunk_state(self, entry): - '''Update chunk toggle state to match file toggle state''' - wfile = util.pconvert(entry[FM_PATH]) - if wfile not in self._filechunks: return - selected = entry[FM_CHECKED] - for n in self._filechunks[wfile][1:]: - self.diff_model[n][DM_REJECTED] = not selected - self._update_diff_hunk(self.diff_model[n]) - entry[FM_PARTIAL_SELECTED] = False - self._update_diff_header(self.diff_model, wfile, selected) + def update_chunk_state(self, fileentry): + 'Update chunk toggle state to match file toggle state' + fileentry[FM_PARTIAL_SELECTED] = False + wfile = fileentry[FM_PATH] + selected = fileentry[FM_CHECKED] + chunks = self.filechunks[wfile] + for chunk in chunks: + chunk.active = selected + # this file's chunks may not be in diffmodel + if wfile not in self.filerowstart: + return + rowstart = self.filerowstart[wfile] + for n, chunk in enumerate(chunks): + if n == 0: + continue + self.diffmodel[rowstart+n][DM_REJECTED] = not selected + self.update_diff_hunk(self.diffmodel[rowstart+n]) + self.update_diff_header(self.diffmodel, wfile, selected)   - def _update_diff_hunk(self, row): - if row[DM_REJECTED]: + def update_diff_hunk(self, row): + 'Update the contents of a diff row based on its chunk state' + wfile = row[DM_PATH] + chunks = self.filechunks[wfile] + chunk = chunks[row[DM_CHUNK_ID]] + buf = cStringIO.StringIO() + chunk.pretty(buf) + buf.seek(0) + if chunk.active: + row[DM_REJECTED] = False + row[DM_FONT] = self.difffont + row[DM_DISP_TEXT] = hunk_markup(buf.read()) + else: + row[DM_REJECTED] = True   row[DM_FONT] = self.rejfont - row[DM_DISPLAYED] = row[DM_TEXT] - else: - row[DM_FONT] = self.difffont - row[DM_DISPLAYED] = row[DM_MARKUP] + row[DM_DISP_TEXT] = hunk_unmarkup(buf.read())   - def _update_diff_header(self, dmodel, wfile, selected): - fc = self._filechunks[wfile] - hc = fc[0] - lasthunk = len(fc)-1 - row = dmodel[hc] + def update_diff_header(self, dmodel, wfile, selected): + try: + hc = self.filerowstart[wfile] + chunks = self.filechunks[wfile] + except IndexError: + return + lasthunk = len(chunks)-1   sel = lambda x: x >= lasthunk or not dmodel[hc+x+1][DM_REJECTED] - newtext = self._shelve_chunks[row[DM_CHUNK_ID]].selpretty(sel) + newtext = chunks[0].selpretty(sel)   if not selected:   newtext = "<span foreground='#888888'>" + newtext + "</span>" - row[DM_DISPLAYED] = newtext + dmodel[hc][DM_DISP_TEXT] = newtext   - def _show_toggle(self, check, toggletype): + def show_toggle(self, check, toggletype):   self.opts[toggletype] = check.get_active()   self.reload_status()   return True     - def _sort_by_stat(self, model, iter1, iter2): + def sort_by_stat(self, model, iter1, iter2):   order = 'MAR!?IC'   lhs, rhs = (model.get_value(iter1, FM_STATUS),   model.get_value(iter2, FM_STATUS)) @@ -689,14 +724,14 @@
  # values to be None. When this happens, just return any value   # since the call is irrelevant and will be followed by another   # with the correct (non-None) value - if None in (lhs, rhs) : + if None in (lhs, rhs):   return 0     result = order.find(lhs) - order.find(rhs)   return min(max(result, -1), 1)     - def _text_color(self, column, text_renderer, model, row_iter): + def text_color(self, column, text_renderer, model, row_iter):   stat = model[row_iter][FM_STATUS]   if stat == 'M':   text_renderer.set_property('foreground', '#000090') @@ -716,24 +751,24 @@
  text_renderer.set_property('foreground', 'black')     - def _view_left_file(self, stat, wfile): + def view_left_file(self, stat, wfile):   return self._view_file(stat, wfile, True)     - def _remove_file(self, stat, wfile): - self._hg_remove([wfile]) + def remove_file(self, stat, wfile): + self.hg_remove([wfile])   return True     - def _rename_file(self, stat, wfile): + def rename_file(self, stat, wfile):   fdir, fname = os.path.split(wfile)   newfile = dialog.entry_dialog(self, _('Rename file to:'), True, fname)   if newfile and newfile != fname: - self._hg_move([wfile, os.path.join(fdir, newfile)]) + self.hg_move([wfile, os.path.join(fdir, newfile)])   return True     - def _copy_file(self, stat, wfile): + def copy_file(self, stat, wfile):   wfile = self.repo.wjoin(wfile)   fdir, fname = os.path.split(wfile)   dlg = gtk.FileChooserDialog(parent=self, @@ -750,11 +785,11 @@
  newfile = dlg.get_filename()   dlg.destroy()   if newfile != wfile: - self._hg_copy([wfile, newfile]) + self.hg_copy([wfile, newfile])   return True     - def _hg_remove(self, files): + def hg_remove(self, files):   wfiles = [self.repo.wjoin(x) for x in files]   if self.count_revs() > 1:   gdialog.Prompt(_('Nothing Removed'), @@ -773,7 +808,7 @@
  self.reload_status()     - def _hg_move(self, files): + def hg_move(self, files):   wfiles = [self.repo.wjoin(x) for x in files]   if self.count_revs() > 1:   gdialog.Prompt(_('Nothing Moved'), _('Move is not enabled when ' @@ -792,7 +827,7 @@
  self.reload_status()     - def _hg_copy(self, files): + def hg_copy(self, files):   wfiles = [self.repo.wjoin(x) for x in files]   if self.count_revs() > 1:   gdialog.Prompt(_('Nothing Copied'), _('Copy is not enabled when ' @@ -809,97 +844,178 @@
  shlib.update_thgstatus(self.ui, self.repo.root, wait=True)   self.reload_status()   - def merge_sel_changed(self, selection, force): - ''' Update the diff text with merge diff to both parents''' - def dohgdiff(): - difftext = [_('===== Diff to first parent =====\n')] + def merge_sel_changed(self, selection): + 'Selected row in file tree activated changed (merge mode)' + # Update the diff text with merge diff to both parents + model, paths = selection.get_selected_rows() + if not paths: + self.last_file = None + return + wfile = self.filemodel[paths[0]][FM_PATH] + if wfile == self.last_file: + return + self.last_file = wfile + difftext = [_('===== Diff to first parent =====\n')] + wfiles = [self.repo.wjoin(wfile)] + wctx = self.repo[None] + matcher = cmdutil.match(self.repo, wfiles, self.opts) + for s in patch.diff(self.repo, wctx.p1().node(), None, + match=matcher, opts=patch.diffopts(self.ui, self.opts)): + difftext.extend(s.splitlines(True)) + difftext.append(_('\n===== Diff to second parent =====\n')) + for s in patch.diff(self.repo, wctx.p2().node(), None, + match=matcher, opts=patch.diffopts(self.ui, self.opts)): + difftext.extend(s.splitlines(True)) + + buf = gtk.TextBuffer() + buf.create_tag('removed', foreground='#900000') + buf.create_tag('added', foreground='#006400') + buf.create_tag('position', foreground='#FF8000') + buf.create_tag('header', foreground='#000090') + + bufiter = buf.get_start_iter() + for line in difftext: + line = hglib.toutf(line) + if line.startswith('---') or line.startswith('+++'): + buf.insert_with_tags_by_name(bufiter, line, 'header') + elif line.startswith('-'): + line = hglib.diffexpand(line) + buf.insert_with_tags_by_name(bufiter, line, 'removed') + elif line.startswith('+'): + line = hglib.diffexpand(line) + buf.insert_with_tags_by_name(bufiter, line, 'added') + elif line.startswith('@@'): + buf.insert_with_tags_by_name(bufiter, line, 'position') + else: + line = hglib.diffexpand(line) + buf.insert(bufiter, line) + self.merge_diff_text.set_buffer(buf) + + + def tree_sel_changed(self, selection): + 'Selected row in file tree activated changed' + # Read this file's diffs into diff model + model, paths = selection.get_selected_rows() + if not paths: + self.last_file = None + return + wfile = self.filemodel[paths[0]][FM_PATH] + if wfile == self.last_file: + return + # TODO: this could be a for-loop + self.last_file = wfile + self.filerowstart = {} + self.diffmodel.clear() + self.append_diff_hunks(wfile) + + def read_file_chunks(self, wfile): + 'Get diffs of working file, parse into (c)hunks' + difftext = cStringIO.StringIO() + ctx = self.repo[self._node1] + fctx = ctx.filectx(wfile) + if fctx and fctx.size() > hglib.getmaxdiffsize(self.ui): + # Fake patch that displays size warning + lines = ['diff --git -r a/%s b/%s\n' % (wfile, wfile)] + lines.append('--- a/%s\n' % wfile) + lines.append('+++ b/%s\n' % wfile) + lines.append(_('File is larger than the specified max diff size\n')) + difftext.writelines(lines) + else:   wfiles = [self.repo.wjoin(wfile)] - wctx = self.repo[None]   matcher = cmdutil.match(self.repo, wfiles, self.opts) - for s in patch.diff(self.repo, wctx.p1().node(), None, - match=matcher, opts=patch.diffopts(self.ui, self.opts)): - difftext.extend(s.splitlines(True)) - difftext.append(_('\n===== Diff to second parent =====\n')) - for s in patch.diff(self.repo, wctx.p2().node(), None, - match=matcher, opts=patch.diffopts(self.ui, self.opts)): - difftext.extend(s.splitlines(True)) + diffopts = mdiff.diffopts(git=True, nodates=True) + for s in patch.diff(self.repo, self._node1, self._node2, + match=matcher, opts=diffopts): + difftext.writelines(s.splitlines(True)) + difftext.seek(0) + return hgshelve.parsepatch(difftext)   - buf = gtk.TextBuffer() - buf.create_tag('removed', foreground='#900000') - buf.create_tag('added', foreground='#006400') - buf.create_tag('position', foreground='#FF8000') - buf.create_tag('header', foreground='#000090') + def append_diff_hunks(self, wfile): + 'Append diff hunks of one file to the diffmodel' + chunks = self.read_file_chunks(wfile) + rows = [] + for n, chunk in enumerate(chunks): + chunk.active = True + if isinstance(chunk, hgshelve.header): + rows.append([False, '', True, wfile, n, self.headerfont]) + if chunk.special(): + chunks = chunks[:1] + break + else: + rows.append([False, '', False, wfile, n, self.difffont])   - bufiter = buf.get_start_iter() - for line in difftext: - line = hglib.toutf(line) - if line.startswith('---') or line.startswith('+++'): - buf.insert_with_tags_by_name(bufiter, line, 'header') - elif line.startswith('-'): - line = hglib.diffexpand(line) - buf.insert_with_tags_by_name(bufiter, line, 'removed') - elif line.startswith('+'): - line = hglib.diffexpand(line) - buf.insert_with_tags_by_name(bufiter, line, 'added') - elif line.startswith('@@'): - buf.insert_with_tags_by_name(bufiter, line, 'position') + + # recover old chunk selection/rejection states, match fromline + if wfile in self.filechunks: + ochunks = self.filechunks[wfile] + chunks[0].active = ochunks[0].active + next = 1 + for oc in ochunks[1:]: + for n in xrange(next, len(chunks)): + nc = chunks[n] + if oc.fromline == nc.fromline: + nc.active = oc.active + next = n+1 + break + elif nc.fromline > oc.fromline: + break + + self.filerowstart[wfile] = len(self.diffmodel) + self.filechunks[wfile] = chunks + + # Set row status based on chunk state + rej, nonrej = False, False + for n, row in enumerate(rows): + if row[DM_IS_HEADER]: + row[DM_REJECTED] = not chunks[n].active + else: + if chunks[n].active: + nonrej = True   else: - line = hglib.diffexpand(line) - buf.insert(bufiter, line) + rej = True + row[DM_REJECTED] = not chunks[n].active + self.update_diff_hunk(row) + self.diffmodel.append(row)   - self.merge_diff_text.set_buffer(buf) + newvalue = nonrej + partial = rej and nonrej + for fr in self.filemodel: + if fr[FM_PATH] == wfile: + break + if fr[FM_PARTIAL_SELECTED] != partial: + fr[FM_PARTIAL_SELECTED] = partial + if fr[FM_CHECKED] != newvalue: + fr[FM_CHECKED] = newvalue + self.update_check_count() + self.update_diff_header(self.diffmodel, wfile, newvalue)   - if self.showdiff_toggle.get_active(): - sel = self.filetree.get_selection().get_selected_rows()[1] - if not sel: - self._last_file = None - return False - wfile = self.filemodel[sel[0]][FM_PATH] - if force or wfile != self._last_file: - self._last_file = wfile - self._hg_call_wrapper('Diff', dohgdiff) - return False   - - def tree_sel_changed(self, selection, force): - if self.showdiff_toggle.get_active(): - sel = self.filetree.get_selection().get_selected_rows()[1] - if not sel: - self._last_file = None - return False - wfile = util.pconvert(self.filemodel[sel[0]][FM_PATH]) - if force or wfile != self._last_file: - self._last_file = wfile - if wfile in self._filechunks: - row = self._filechunks[wfile][0] - self.diff_tree.scroll_to_cell((row, ), None, True) - selection = self.diff_tree.get_selection() - selection.unselect_all() - selection.select_path((row,)) - return False - - def _diff_tree_row_act(self, dtree, path, column): + def diff_tree_row_act(self, dtree, path, column): + 'Row in diff tree (hunk) activated/toggled'   dmodel = dtree.get_model()   row = dmodel[path] - chunk = self._shelve_chunks[row[DM_CHUNK_ID]] - wfile = chunk.filename() - if wfile not in self._filechunks: - return + wfile = row[DM_PATH] + try: + startrow = self.filerowstart[wfile] + chunks = self.filechunks[wfile] + except IndexError: + pass + chunkrows = xrange(startrow+1, startrow+len(chunks))   for fr in self.filemodel: - if util.pconvert(fr[FM_PATH]) == wfile: + if fr[FM_PATH] == wfile:   break - fchunks = self._filechunks[wfile][1:]   if row[DM_IS_HEADER]: - for n in fchunks: - dmodel[n][DM_REJECTED] = fr[FM_CHECKED] - self._update_diff_hunk(dmodel[n]) + for chunk in chunks[1:]: + chunk.active = fr[FM_CHECKED] + self.update_diff_hunk(dmodel[startrow+n])   newvalue = not fr[FM_CHECKED]   partial = False   else: - row[DM_REJECTED] = not row[DM_REJECTED] - self._update_diff_hunk(row) - rej = [ n for n in fchunks if dmodel[n][DM_REJECTED] ] - nonrej = [ n for n in fchunks if not dmodel[n][DM_REJECTED] ] + chunk = chunks[row[DM_CHUNK_ID]] + chunk.active = not chunk.active + self.update_diff_hunk(row) + rej = [ n for n in chunkrows if dmodel[n][DM_REJECTED] ] + nonrej = [ n for n in chunkrows if not dmodel[n][DM_REJECTED] ]   newvalue = nonrej and True or False   partial = rej and nonrej and True or False   @@ -908,133 +1024,15 @@
  fr[FM_PARTIAL_SELECTED] = partial   if fr[FM_CHECKED] != newvalue:   fr[FM_CHECKED] = newvalue - self._update_check_count() - self._update_diff_header(dmodel, wfile, newvalue) + self.update_check_count() + self.update_diff_header(dmodel, wfile, newvalue)   - def _show_diff_hunks(self, files): - ''' Update the diff text ''' - def markup(chunk): - hunk = "" - chunk.seek(0) - lines = chunk.readlines() - lines[-1] = lines[-1].strip('\n\r') - for line in lines: - line = gobject.markup_escape_text(hglib.toutf(line[:128])) - if line[-1] != '\n': - line += '\n' - if line.startswith('---') or line.startswith('+++'): - hunk += '<span foreground="#000090">%s</span>' % line - elif line.startswith('-'): - hunk += '<span foreground="#900000">%s</span>' % line - elif line.startswith('+'): - hunk += '<span foreground="#006400">%s</span>' % line - elif line.startswith('@@'): - hunk = '<span foreground="#FF8000">%s</span>' % line - else: - hunk += line - return hunk   - def unmarkup(fp): - hunk = "" - fp.seek(0) - lines = fp.readlines() - lines[-1] = lines[-1].strip('\n\r') - for line in lines: - line = gobject.markup_escape_text(hglib.toutf(line[:128])) - if line[-1] != '\n': - line += '\n' - hunk += line - return hunk - - def dohgdiff(): - self.diff_model.clear() - difftext = cStringIO.StringIO() - try: - if len(files) != 0: - wfiles = [self.repo.wjoin(x) for x in files] - matcher = cmdutil.match(self.repo, wfiles, self.opts) - diffopts = mdiff.diffopts(git=True, nodates=True, - nobinary=True) - for s in patch.diff(self.repo, self._node1, self._node2, - match=matcher, opts=diffopts): - difftext.writelines(s.splitlines(True)) - difftext.seek(0) - - self._shelve_chunks = hgshelve.parsepatch(difftext) - self._filechunks = {} - skip = False - for n, chunk in enumerate(self._shelve_chunks): - if isinstance(chunk, hgshelve.header): - text = chunk.selpretty(lambda x: True) - for f in chunk.files(): - self._filechunks[f] = [len(self.diff_model)] - row = [False, text, text, text, - True, n, self.headerfont] - self.diff_model.append(row) - skip = chunk.special() - elif not skip: - fp = cStringIO.StringIO() - chunk.pretty(fp) - markedup = markup(fp) - text = unmarkup(fp) - f = chunk.filename() - self._filechunks[f].append(len(self.diff_model)) - row = [False, markedup, text, markedup, - False, n, self.difffont] - self.diff_model.append(row) - finally: - difftext.close() - - if self.merging: - return - self._hg_call_wrapper('Diff', dohgdiff) - - def _showdiff_toggled(self, togglebutton, data=None): - # prevent movement events while setting position - self._diffpane.handler_block(self._diffpane_moved_id) - - if togglebutton.get_active(): - if self.merging: - self.merge_sel_changed(self.filetree.get_selection(), True) - else: - self.tree_sel_changed(self.filetree.get_selection(), True) - self._diffpane.set_position(self._setting_lastpos) - else: - self._setting_lastpos = self._diffpane.get_position() - self._diffpane.set_position(64000) - - self._diffpane.handler_unblock(self._diffpane_moved_id) - return True - - - def _diffpane_moved(self, paned, data=None): - # prevent toggle events while setting toolbar state - self.showdiff_toggle.handler_block(self._showdiff_toggled_id) - if self.diffbottom: - sizemax = self._diffpane.get_allocation().height - else: - sizemax = self._diffpane.get_allocation().width - - if self.showdiff_toggle.get_active(): - if paned.get_position() >= sizemax - 55: - self.showdiff_toggle.set_active(False) - elif paned.get_position() < sizemax - 55: - self.showdiff_toggle.set_active(True) - selection = self.filetree.get_selection() - if self.merging: - self.merge_sel_changed(selection, True) - else: - self.tree_sel_changed(selection, True) - - self.showdiff_toggle.handler_unblock(self._showdiff_toggled_id) - return False - - - def _refresh_clicked(self, toolbutton, data=None): + def refresh_clicked(self, toolbutton, data=None):   self.reload_status()   return True   - def _save_clicked(self, toolbutton, data=None): + def save_clicked(self, toolbutton, data=None):   'Write selected diff hunks to a patch file'   revrange = self.opts.get('rev')[0]   filename = "%s.patch" % revrange.replace(':', '_to_') @@ -1045,40 +1043,45 @@
  if not result:   return   - cids = [] - dmodel = self.diff_model + buf = cStringIO.StringIO() + dmodel = self.diffmodel   for row in self.filemodel: - if row[FM_CHECKED]: - wfile = util.pconvert(row[FM_PATH]) - fc = self._filechunks[wfile] - cids.append(fc[0]) - cids += [dmodel[r][DM_CHUNK_ID] for r in fc[1:] - if not dmodel[r][DM_REJECTED]] + if not row[FM_CHECKED]: + continue + wfile = row[FM_PATH] + if wfile not in self.filechunks: + continue + chunks = self.filechunks[wfile] + for i, chunk in enumerate(chunks): + if i == 0: + chunk.write(buf) + elif not dmodel[start+i][DM_REJECTED]: + chunk.write(buf) + buf.seek(0)   try:   fp = open(result, "w") - for cid in cids: - self._shelve_chunks[cid].write(fp) + fp.write(buf.read())   except OSError:   pass   finally:   fp.close()   - def _revert_clicked(self, toolbutton, data=None): - revert_list = self._relevant_files('MAR!') + def revert_clicked(self, toolbutton, data=None): + revert_list = self.relevant_files('MAR!')   if len(revert_list) > 0: - self._hg_revert(revert_list) + self.hg_revert(revert_list)   else:   gdialog.Prompt(_('Nothing Reverted'),   _('No revertable files selected'), self).run()   return True     - def _revert_file(self, stat, wfile): - self._hg_revert([wfile]) + def revert_file(self, stat, wfile): + self.hg_revert([wfile])   return True     - def _log_file(self, stat, wfile): + def log_file(self, stat, wfile):   # Might want to include 'rev' here... trying without   from hggtk import history   dlg = history.GLog(self.ui, self.repo, self.cwd, [wfile], self.opts) @@ -1086,7 +1089,7 @@
  return True     - def _hg_revert(self, files): + def hg_revert(self, files):   wfiles = [self.repo.wjoin(x) for x in files]   if self.count_revs() > 1:   gdialog.Prompt(_('Nothing Reverted'), @@ -1130,22 +1133,22 @@
  shlib.shell_notify(wfiles)   self.reload_status()   - def _add_clicked(self, toolbutton, data=None): - add_list = self._relevant_files('?I') + def add_clicked(self, toolbutton, data=None): + add_list = self.relevant_files('?I')   if len(add_list) > 0: - self._hg_add(add_list) + self.hg_add(add_list)   else:   gdialog.Prompt(_('Nothing Added'),   _('No addable files selected'), self).run()   return True     - def _add_file(self, stat, wfile): - self._hg_add([wfile]) + def add_file(self, stat, wfile): + self.hg_add([wfile])   return True     - def _hg_add(self, files): + def hg_add(self, files):   wfiles = [self.repo.wjoin(x) for x in files]   # Create new opts, so nothing unintented gets through   addopts = self.merge_opts(commands.table['^add'][1], @@ -1158,20 +1161,20 @@
  shlib.shell_notify(wfiles)   self.reload_status()   - def _remove_clicked(self, toolbutton, data=None): - remove_list = self._relevant_files('C!') - delete_list = self._relevant_files('?I') + def remove_clicked(self, toolbutton, data=None): + remove_list = self.relevant_files('C!') + delete_list = self.relevant_files('?I')   if len(remove_list) > 0: - self._hg_remove(remove_list) + self.hg_remove(remove_list)   if len(delete_list) > 0: - self._delete_files(delete_list) + self.delete_files(delete_list)   if not remove_list and not delete_list:   gdialog.Prompt(_('Nothing Removed'),   _('No removable files selected'), self).run()   return True   - def _move_clicked(self, toolbutton, data=None): - move_list = self._relevant_files('C') + def move_clicked(self, toolbutton, data=None): + move_list = self.relevant_files('C')   if move_list:   # get destination directory to files into   dlg = gtk.FileChooserDialog(title=_('Move files to diretory...'), @@ -1196,18 +1199,18 @@
    # move the files to dest directory   move_list.append(hglib.fromutf(destdir)) - self._hg_move(move_list) + self.hg_move(move_list)   else:   gdialog.Prompt(_('Nothing Moved'), _('No movable files selected\n\n'   'Note: only clean files can be moved.'), self).run()   return True   - def _delete_file(self, stat, wfile): - self._delete_files([wfile]) + def delete_file(self, stat, wfile): + self.delete_files([wfile])   - def _delete_files(self, files): + def delete_files(self, files):   dlg = gdialog.Confirm(_('Confirm Delete Unrevisioned'), files, self) - if dlg.run() == gtk.RESPONSE_YES : + if dlg.run() == gtk.RESPONSE_YES:   errors = ''   for wfile in files:   try: @@ -1224,12 +1227,12 @@
  self.reload_status()   return True   - def _guess_rename(self, stat, wfile): + def guess_rename(self, stat, wfile):   dlg = guess.DetectRenameDialog()   dlg.show_all()   dlg.set_notify_func(self.ignoremask_updated)   - def _ignore_file(self, stat, wfile): + def ignore_file(self, stat, wfile):   dlg = hgignore.HgIgnoreDialog(self.repo.root, util.pconvert(wfile))   dlg.show_all()   dlg.set_notify_func(self.ignoremask_updated) @@ -1239,19 +1242,19 @@
  '''User has changed the ignore mask in hgignore dialog'''   self.reload_status()   - def _mark_resolved(self, stat, wfile): + def mark_resolved(self, stat, wfile):   ms = merge_.mergestate(self.repo)   ms.mark(util.pconvert(wfile), "r")   self.reload_status()     - def _unmark_resolved(self, stat, wfile): + def unmark_resolved(self, stat, wfile):   ms = merge_.mergestate(self.repo)   ms.mark(util.pconvert(wfile), "u")   self.reload_status()     - def _do_resolve(self, stat, wfile): + def do_resolve(self, stat, wfile):   ms = merge_.mergestate(self.repo)   wctx = self.repo[None]   mctx = wctx.parents()[-1] @@ -1259,27 +1262,27 @@
  self.reload_status()     - def _sel_clicked(self, state): - self._select_files(state) + def sel_clicked(self, state): + self.select_files(state)   return True     - def _select_files(self, state, ctype=None): + def select_files(self, state, ctype=None):   for entry in self.filemodel:   if ctype and not entry[FM_STATUS] in ctype:   continue   if entry[FM_CHECKED] != state:   entry[FM_CHECKED] = state - self._update_chunk_state(entry) - self._update_check_count() + self.update_chunk_state(entry) + self.update_check_count()     - def _relevant_files(self, stats): + def relevant_files(self, stats):   return [item[FM_PATH] for item in self.filemodel \   if item[FM_CHECKED] and item[FM_STATUS] in stats]     - def _context_menu_act(self, menuitem, handler): + def context_menu_act(self, menuitem, handler):   selection = self.filetree.get_selection()   assert(selection.count_selected_rows() == 1)   @@ -1289,7 +1292,7 @@
  return True     - def _tree_button_press(self, widget, event) : + def tree_button_press(self, widget, event):   # Set the flag to ignore the next activation when the   # shift/control keys are pressed. This avoids activations with   # multiple rows selected. @@ -1301,14 +1304,14 @@
  return False     - def _tree_button_release(self, widget, event) : + def tree_button_release(self, widget, event):   if event.button != 3:   return False   if not (event.state & (gtk.gdk.SHIFT_MASK | gtk.gdk.CONTROL_MASK)): - self._tree_popup_menu(widget, event.button, event.time) + self.tree_popup_menu(widget, event.button, event.time)   return False   - def _get_file_context_menu(self, rowdata): + def get_file_context_menu(self, rowdata):   st = rowdata[FM_STATUS]   ms = rowdata[FM_MERGE_STATUS]   if ms: @@ -1317,31 +1320,31 @@
  menu = self._menus[st]   return menu   - def _tree_popup_menu(self, widget, button=0, time=0) : + def tree_popup_menu(self, widget, button=0, time=0):   selection = self.filetree.get_selection()   if selection.count_selected_rows() != 1:   return False     model, tpaths = selection.get_selected_rows() - menu = self._get_file_context_menu(model[tpaths[0]]) + menu = self.get_file_context_menu(model[tpaths[0]])   menu.popup(None, None, None, button, time)   return True     - def _tree_key_press(self, tree, event): + def tree_key_press(self, tree, event):   if event.keyval == 32:   def toggler(model, path, bufiter):   model[path][FM_CHECKED] = not model[path][FM_CHECKED] - self._update_chunk_state(model[path]) + self.update_chunk_state(model[path])     selection = self.filetree.get_selection()   selection.selected_foreach(toggler) - self._update_check_count() + self.update_check_count()   return True   return False     - def _tree_row_act(self, tree, path, column) : + def tree_row_act(self, tree, path, column):   """Default action is the first entry in the context menu   """   # Ignore activations (like double click) on the first column, @@ -1355,7 +1358,7 @@
  return False     model, tpaths = selection.get_selected_rows() - menu = self._get_file_context_menu(model[tpaths[0]]) + menu = self.get_file_context_menu(model[tpaths[0]])   menu.get_children()[0].activate()   return True  
Show Entire File hggtk/​thgshelve.py Stacked
This file's diff was not loaded because this changeset is very large. Load changes