Kiln » TortoiseHg » TortoiseHg
Clone URL:  
Pushed to one repository · View In Graph Contained in 2.1, 2.1.1, and 2.1.2

merge: refactor UI to no longer use a QStackedWidget

There are now four pages -

1 - Summary, same as before
2 - Merge, runs merge command and manages resolve tool
3 - Commit, entry for commit message, runs commit command
4 - Finished, shows merged changeset

A lot of "magical" behavior was removed, you can navigate backwards at any
time without risk of throwing away work (except the last page, you cannot go
back to the commit page).

I've added a couple of TODOs for allowing the process to be expedited for
experienced users (auto-continue from merge to commit, skip the finish page)

Changeset deb9afd9eccd

Parent d95a6e8bd22c

by Steve Borho

Changes to one file · Browse files at deb9afd9eccd Showing diff from parent d95a6e8bd22c Diff from another changeset...

 
1
2
3
 
4
5
6
7
8
9
10
11
 
12
13
14
 
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
 
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
 
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
 
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
 
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
 
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
 
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
 
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
 
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
 
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
 
1
2
3
4
5
6
7
8
 
 
 
 
9
10
11
12
 
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
 
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
 
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
 
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
 
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
 
197
198
199
 
 
 
200
201
202
 
 
203
204
205
206
 
 
 
 
207
208
209
210
 
211
212
213
214
 
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
 
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
 
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
 
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
@@ -1,14 +1,12 @@
 # merge.py - Merge dialog for TortoiseHg  #  # Copyright 2010 Yuki KODAMA <endflow.net@gmail.com> +# Copyright 2011 Steve Borho <steve@borho.org>  #  # This software may be used and distributed according to the terms of the  # GNU General Public License version 2, incorporated herein by reference.   -from PyQt4.QtCore import * -from PyQt4.QtGui import * - -from mercurial import error +from mercurial import hg, error  from mercurial import merge as mergemod    from tortoisehg.util import hglib @@ -16,34 +14,40 @@
 from tortoisehg.hgqt import qtlib, csinfo, i18n, cmdui, status, commit, resolve  from tortoisehg.hgqt import qscilib, thgrepo   +from PyQt4.QtCore import * +from PyQt4.QtGui import * +  keep = i18n.keepgettext()   -MERGE_PAGE = 0 -COMMIT_PAGE = 1 -RESULT_PAGE = 2 +SUMMARY_PAGE = 0 +MERGE_PAGE = 1 +COMMIT_PAGE = 2 +RESULT_PAGE = 3 + +MARGINS = (8, 0, 0, 0)    class MergeDialog(QWizard):   - def __init__(self, other, repo, parent): + def __init__(self, otherrev, repo, parent):   super(MergeDialog, self).__init__(parent)   f = self.windowFlags()   self.setWindowFlags(f & ~Qt.WindowContextHelpButtonHint)     self.repo = repo - self.other = str(other) - self.local = str(self.repo['.'].rev()) + self.otherrev = str(otherrev) + self.localrev = str(self.repo['.'].rev())     self.setWindowTitle(_('Merge - %s') % self.repo.displayname)   self.setWindowIcon(qtlib.geticon('hg-merge')) - self.setMinimumSize(600, 528) - self.setOption(QWizard.DisabledBackButtonOnLastPage, True)   self.setOption(QWizard.HelpButtonOnRight, False) - self.setDefaultProperty('QComboBox', 'currentText', 'editTextChanged()') + self.setOption(QWizard.IndependentPages, True)     # set pages - self.setPage(MERGE_PAGE, MergePage(self)) - self.setPage(COMMIT_PAGE, CommitPage(self)) - self.setPage(RESULT_PAGE, ResultPage(self)) + self.setPage(SUMMARY_PAGE, SummaryPage(repo, self)) + self.setPage(MERGE_PAGE, MergePage(repo, self)) + self.setPage(COMMIT_PAGE, CommitPage(repo, self)) + self.setPage(RESULT_PAGE, ResultPage(repo, self)) + self.currentIdChanged.connect(self.pageChanged)     repo.repositoryChanged.connect(self.repositoryChanged)   repo.configChanged.connect(self.configChanged) @@ -54,42 +58,27 @@
  def configChanged(self):   self.currentPage().configChanged()   + def pageChanged(self, id): + if id != -1: + self.currentPage().currentPage() +   def reject(self): - page = self.currentPage() - if hasattr(page, 'need_cleanup') and page.need_cleanup(): - main = _('Do you want to exit?') - text = _('To finish merging, you need to commit ' - 'the working directory.') - labels = ((QMessageBox.Yes, _('&Exit')), - (QMessageBox.No, _('Cancel'))) - if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, - labels=labels, parent=self): - return - page.reject() - super(MergeDialog, self).reject() + if self.currentPage().canExit(): + super(MergeDialog, self).reject()   -MAIN_PANE = 0 -PERFORM_PANE = 1    class BasePage(QWizardPage): + def __init__(self, repo, parent): + super(BasePage, self).__init__(parent) + self.repo = repo   - def __init__(self, parent=None): - super(BasePage, self).__init__(parent) - self.nextEnabled = True - self.done = False + def validatePage(self): + 'user pressed NEXT button, can we proceed?' + return True   - def switch_pane(self, pane): - self.setup_buttons(pane) - self.layout().setCurrentIndex(pane) - if pane == MAIN_PANE: - self.ready() - elif pane == PERFORM_PANE: - self.cmd.core.clearOutput() - self.perform() - else: - raise 'unknown pane: %s' % pane - - ### Override Method ### + def isComplete(self): + 'should NEXT button be sensitive?' + return True     def repositoryChanged(self):   'repository has detected a change to changelog or parents' @@ -99,125 +88,39 @@
  'repository has detected a change to config files'   pass   - def reject(self): + def currentPage(self):   pass   + def canExit(self): + if len(self.repo.parents()) == 2: + main = _('Do you want to exit?') + text = _('To finish merging, you need to commit ' + 'the working directory.') + labels = ((QMessageBox.Yes, _('&Exit')), + (QMessageBox.No, _('Cancel'))) + if not qtlib.QuestionMsgBox(_('Confirm Exit'), main, text, + labels=labels, parent=self): + return False + return True + +class SummaryPage(BasePage): + + def __init__(self, repo, parent): + super(SummaryPage, self).__init__(repo, parent) + self.clean = None + self.th = None + + ### Override Methods ### +   def initializePage(self):   if self.layout():   return + self.setTitle(_('Preparation')) + self.setSubTitle(_('Verify merge targets and ensure your working ' + 'directory is clean.')) + self.setLayout(QVBoxLayout())   - stack = QStackedLayout() - self.setLayout(stack) - - def wrap(layout): - widget = QWidget() - widget.setLayout(layout) - return widget - - # main pane - fpane = self.get_pane() - num = stack.addWidget(wrap(fpane)) - assert num == MAIN_PANE - - # perform pane - ppane = QVBoxLayout() - ppane.addSpacing(4) - self.cmd = cmdui.Widget(True, True, self) - self.cmd.setShowOutput(True) - self.cmd.commandFinished.connect(self.command_finished) - self.cmd.commandCanceling.connect(self.command_canceling) - ppane.addWidget(self.cmd) - num = stack.addWidget(wrap(ppane)) - assert num == PERFORM_PANE - - def setVisible(self, visible): - super(BasePage, self).setVisible(visible) - if visible: - self.switch_pane(MAIN_PANE) - - def validatePage(self): - #When the user first clicks on the "Next" button ("Merge"/"Commit") - #After any validation via overloading validatePage(), - #we switch to the perform pane - if self.layout().currentIndex() == MAIN_PANE: - self.switch_pane(PERFORM_PANE) - return False - - #When the perform pane is done, it'll call this again - return self.can_continue() - - ### Method to be overridden ### - - def get_pane(self): - return QVBoxLayout() - - def get_perform_label(self): - return None - - def setup_buttons(self, pane): - if pane == MAIN_PANE: - label = self.get_perform_label() - if label: - self.wizard().setButtonText(QWizard.NextButton, label); - self.nextEnabled = True - else: - self.nextEnabled = False - self.wizard().setOption(QWizard.HaveHelpButton, False) - self.wizard().setOption(QWizard.HaveCustomButton1, False) - self.wizard().setOption(QWizard.NoCancelButton, False) - elif pane == PERFORM_PANE: - button = QPushButton(_('Cancel')) - self.wizard().setButton(QWizard.CustomButton1, button) - self.wizard().setOption(QWizard.HaveCustomButton1, True) - button.clicked.connect(self.cancel_clicked) - self.nextEnabled = False - self.wizard().setOption(QWizard.NoCancelButton, True) - else: - raise 'unknown pane: %s' % pane - - def ready(self): - pass - - def perform(self): - pass - - def cancel(self): - self.cmd.cancel() - - def can_continue(self): - return self.done - - def need_cleanup(self): - return False - - ### Signal Handlers ### - - def cancel_clicked(self): - self.cancel() - - def command_finished(self, ret): - pass - - def command_canceling(self): - pass - -MARGINS = (8, 0, 0, 0) - -class MergePage(BasePage): - - def __init__(self, parent=None): - super(MergePage, self).__init__(parent) - - self.clean = None - self.undo = False - self.th = None - - ### Override Methods ### - - def get_pane(self): - repo = self.wizard().repo - box = QVBoxLayout() - + repo = self.repo   contents = ('ishead',) + csinfo.PANEL_DEFAULT   style = csinfo.panelstyle(contents=contents)   def markup_func(widget, item, value): @@ -230,36 +133,31 @@
    ## merge target   other_sep = qtlib.LabeledSeparator(_('Merge from (other revision)')) - box.addWidget(other_sep) + self.layout().addWidget(other_sep)   try: - other_info = create(self.wizard().other) - other_info.setContentsMargins(5, 0, 0, 0) - box.addWidget(other_info) - self.other_info = other_info + otherCsInfo = create(self.wizard().otherrev)   except error.RepoLookupError:   qtlib.InfoMsgBox(_('Unable to merge'),   _('Merge revision not specified or not found'))   QTimer.singleShot(0, self.wizard().close) + self.layout().addWidget(otherCsInfo) + self.otherCsInfo = otherCsInfo     ## current revision - box.addSpacing(6)   local_sep = qtlib.LabeledSeparator(_('Merge to (working directory)')) - box.addWidget(local_sep) - local_info = create(self.wizard().local) - local_info.setContentsMargins(5, 0, 0, 0) - box.addWidget(local_info) - self.local_info = local_info + self.layout().addWidget(local_sep) + localCsInfo = create(self.wizard().localrev) + self.layout().addWidget(localCsInfo) + self.localCsInfo = localCsInfo     ## working directory status - box.addSpacing(6)   wd_sep = qtlib.LabeledSeparator(_('Working directory status')) - box.addWidget(wd_sep) + self.layout().addWidget(wd_sep)     self.groups = qtlib.WidgetGroups()     wdbox = QHBoxLayout() - wdbox.setContentsMargins(*MARGINS) - box.addLayout(wdbox) + self.layout().addLayout(wdbox)   self.wd_status = qtlib.StatusLabel()   self.wd_status.set_status(_('Checking...'))   wdbox.addWidget(self.wd_status) @@ -268,34 +166,28 @@
  wd_prog.setTextVisible(False)   self.groups.add(wd_prog, 'prog')   wdbox.addWidget(wd_prog, 1) - wd_detail = QLabel(_('<a href="view">View changes...</a>')) - wd_detail.linkActivated.connect(self.link_activated) - self.groups.add(wd_detail, 'detail') - wdbox.addWidget(wd_detail) - wdbox.addSpacing(4)     wd_merged = QLabel(_('The working directory is already <b>merged</b>. '   '<a href="skip"><b>Continue</b></a> or '   '<a href="discard"><b>discard</b></a> existing '   'merge.')) - wd_merged.setContentsMargins(*MARGINS) - wd_merged.linkActivated.connect(self.link_activated) + wd_merged.linkActivated.connect(self.onLinkActivated) + wd_merged.setWordWrap(True)   self.groups.add(wd_merged, 'merged') - box.addWidget(wd_merged) + self.layout().addWidget(wd_merged)     text = _('Before merging, you must <a href="commit"><b>commit</b></a>, '   '<a href="shelve"><b>shelve</b></a> to patch, '   'or <a href="discard"><b>discard</b></a> changes.')   wd_text = QLabel(text) - wd_text.setContentsMargins(*MARGINS) - wd_text.linkActivated.connect(self.link_activated) + wd_text.setWordWrap(True) + wd_text.linkActivated.connect(self.onLinkActivated)   self.wd_text = wd_text   self.groups.add(wd_text, 'dirty') - box.addWidget(wd_text) + self.layout().addWidget(wd_text)     wdbox = QHBoxLayout() - wdbox.setContentsMargins(*MARGINS) - box.addLayout(wdbox) + self.layout().addLayout(wdbox)   wd_alt = QLabel(_('Or use:'))   self.groups.add(wd_alt, 'dirty')   wdbox.addWidget(wd_alt) @@ -305,25 +197,18 @@
  self.registerField('force', force_chk)   self.groups.add(force_chk, 'dirty')   wdbox.addWidget(force_chk) - wdbox.addStretch(0) - - box.addSpacing(6)     ### options   expander = qtlib.ExpanderLabel(_('Options'), False) - expander.expanded.connect(self.show_options) - box.addWidget(expander) + expander.expanded.connect(self.toggleShowOptions) + self.layout().addWidget(expander)   self.expander = expander   - optbox = QVBoxLayout() - optbox.setSpacing(6) - box.addLayout(optbox) -   ### discard option   discard_chk = QCheckBox(_('Discard all changes from merge target '   '(other) revision'))   self.registerField('discard', discard_chk) - optbox.addWidget(discard_chk) + self.layout().addWidget(discard_chk)   self.discard_chk = discard_chk     ## auto-resolve @@ -332,129 +217,95 @@
  autoresolve_chk.setChecked(   repo.ui.configbool('tortoisehg', 'autoresolve', False))   self.registerField('autoresolve', autoresolve_chk) - optbox.addWidget(autoresolve_chk) + self.layout().addWidget(autoresolve_chk)   self.autoresolve_chk = autoresolve_chk   - box.addStretch(0) - - return box - - def get_perform_label(self): - return _('&Merge') - - def ready(self): - self.done = False   self.setTitle(_('Merge another revision to the working directory'))   self.groups.set_visible(False, 'dirty')   self.groups.set_visible(False, 'merged') - self.groups.set_visible(False, 'detail') - self.show_options(self.expander.is_expanded()) - self.check_status() + self.toggleShowOptions(self.expander.is_expanded())   - if self.undo: - self.link_activated('discard:noconfirm') - self.undo = False + def isComplete(self): + 'should Next button be sensitive?' + return self.clean or self.field('force').toBool()     def validatePage(self): - #If we haven't already done the action, pop up a confirmation for - #dummy merge. - if not self.done and self.field('discard').toBool(): + 'validate that we can continue with the merge' + if self.field('discard').toBool():   labels = [(QMessageBox.Yes, _('&Discard')),   (QMessageBox.No, _('Cancel'))]   if not qtlib.QuestionMsgBox(_('Confirm Discard Changes'),   _('The changes from revision %s and all unmerged parents '   'will be discarded.\n\n'   'Are you sure this is what you want to do?') - % (self.other_info.get_data('revid')), + % (self.otherCsInfo.get_data('revid')),   labels=labels, parent=self):   return False + return super(SummaryPage, self).validatePage();   - return super(MergePage, self).validatePage(); + ## custom methods ##   - def perform(self): - self.setTitle(_('Merging...')) - self.setSubTitle(_('All conflicting files will be marked unresolved.')) - - if self.field('discard').toBool(): - # '.' is safer than self.localrev, in case the user has - # pulled a fast one on us and updated from the CLI - cmdline = ['--repository', self.wizard().repo.root, - 'debugsetparents', '.', self.wizard().other] - else: - cmdline = ['--repository', self.wizard().repo.root, 'merge'] - if self.field('force').toBool(): - cmdline.append('--force') - tool = self.field('autoresolve').toBool() and 'merge' or 'fail' - cmdline += ['--tool=internal:' + tool] - cmdline.append(self.wizard().other) - self.cmd.run(cmdline) - - def cancel(self): - main = _('Cancel merge and discard changes?') - # Does this restart "resolved" files too? - text = _('Discard local changes and restart merge?') - labels = ((QMessageBox.Yes, _('&Discard')), - (QMessageBox.No, _('Cancel'))) - if qtlib.QuestionMsgBox(_('Confirm Clean Up'), main, text, - labels=labels, parent=self): - o = self.cmd.outputLog - o.appendLog(_('Canceling merge...\n'), 'control') - o.appendLog(_('(Please close any running merge tools)\n'), 'control') - self.cmd.cancel() - - def isComplete(self): - if not self.nextEnabled: - return False - if self.clean: - return True - return self.field('force').toBool() - - ### Signal Handlers ### - - def command_finished(self, ret): - repo = self.wizard().repo - if ret in (0, 1): - repo.incrementBusyCount() - repo.decrementBusyCount() - self.done = True - self.wizard().next() - else: - qtlib.InfoMsgBox(_('Merge failed'), _('Returning to first page')) - self.link_activated('discard:noconfirm') - self.switch_pane(MAIN_PANE) + def toggleShowOptions(self, visible): + self.discard_chk.setShown(visible) + self.autoresolve_chk.setShown(visible)     def repositoryChanged(self):   'repository has detected a change to changelog or parents' - pctx = self.wizard().repo['.'] - self.local_info.update(pctx) - self.wizard().local = str(pctx.rev()) + pctx = self.repo['.'] + self.localCsInfo.update(pctx) + self.wizard().localrev = str(pctx.rev())   - def reject(self): - if self.th is not None and not self.th.isFinished(): + def canExit(self): + 'can merge tool be closed?' + if self.th is not None and self.th.isRunning():   self.th.cancel()   self.th.wait() + return True   - def show_options(self, visible): - self.discard_chk.setShown(visible) - self.autoresolve_chk.setShown(visible) + def currentPage(self): + self.refresh()   - def command_canceling(self): - self.wizard().button(QWizard.CustomButton1).setDisabled(True) + def refresh(self): + if self.th is None: + self.th = CheckThread(self.repo, self) + self.th.finished.connect(self.threadFinished) + if self.th.isRunning(): + return + self.groups.set_visible(True, 'prog') + self.th.start()   - def link_activated(self, cmd): - cmd = str(cmd) - repo = self.wizard().repo + def threadFinished(self): + self.groups.set_visible(False, 'prog') + if self.th.canceled: + return + dirty, parents = self.th.results + self.clean = not dirty + if dirty: + self.groups.set_visible(parents == 2, 'merged') + self.groups.set_visible(parents == 1, 'dirty') + self.wd_status.set_status(_('<b>Uncommitted local changes ' + 'are detected</b>'), 'thg-warning') + else: + self.groups.set_visible(False, 'dirty') + self.groups.set_visible(False, 'merged') + self.wd_status.set_status(_('Clean'), True) + self.completeChanged.emit() + + @pyqtSlot(QString) + def onLinkActivated(self, cmd): + cmd = hglib.fromunicode(cmd) + repo = self.repo   if cmd == 'commit':   dlg = commit.CommitDialog([], dict(root=repo.root), self)   dlg.finished.connect(dlg.deleteLater)   dlg.exec_() - self.check_status() + self.refresh()   elif cmd == 'shelve':   from tortoisehg.hgqt import shelve   dlg = shelve.ShelveDialog(repo, self.wizard())   dlg.finished.connect(dlg.deleteLater)   dlg.exec_() - self.check_status() + self.refresh()   elif cmd.startswith('discard'):   if cmd != 'discard:noconfirm':   labels = [(QMessageBox.Yes, _('&Discard')), @@ -465,108 +316,112 @@
  return   def finished(ret):   repo.decrementBusyCount() - if ret == 0: - self.check_status() + self.refresh()   cmdline = ['update', '--clean', '--repository', repo.root,   '--rev', '.']   self.runner = cmdui.Runner(True, self)   self.runner.commandFinished.connect(finished)   repo.incrementBusyCount()   self.runner.run(cmdline) - elif cmd.startswith('rename:'): - patch = cmd[7:] - name, ok = QInputDialog.getText(self, _('Rename Patch'), - _('Input a new patch name:'), text=patch) - if not ok or name == patch: - return - oldpatch = hglib.fromunicode(patch) - newpatch = hglib.fromunicode(name) - def finished(ret): - repo.decrementBusyCount() - if ret == 0: - text = _('The patch <b>%(old)s</b> is renamed to <b>' - '%(new)s</b>. <a href="rename:%(new)s"><b>' - 'Rename</b></a> again?') - self.wd_text.setText(text % dict(old=patch, new=name)) - self.runner = cmdui.Runner(True, self) - self.runner.commandFinished.connect(finished) - repo.incrementBusyCount() - self.runner.run(['qrename', '--repository', repo.root, - oldpatch, newpatch])   elif cmd == 'view':   dlg = status.StatusDialog([], {}, repo.root, self)   dlg.exec_() - self.check_status() + self.refresh()   elif cmd == 'skip': - self.done = True   self.wizard().next()   else: - raise 'unknown command: %s' % str(cmd) + raise 'unknown command: %s' % cmd   - ### Private Methods ###   - def check_status(self, callback=None): - repo = self.wizard().repo - class CheckThread(QThread): - def __init__(self, parent): - QThread.__init__(self, parent) - self.results = (False, 1) - self.canceled = False +class MergePage(BasePage): + # TODO: add a checkbox on this page to automatically continue   - def run(self): - unresolved = False - for root, path, status in thgrepo.recursiveMergeStatus(repo): - if self.canceled: - return - if status == 'u': - unresolved = True - break - wctx = repo[None] - dirty = bool(wctx.dirty()) or unresolved - self.results = (dirty, len(wctx.parents())) + def __init__(self, repo, parent): + super(MergePage, self).__init__(repo, parent) + self.mergecomplete = False   - def cancel(self): - self.canceled = True + self.setTitle(_('Merging...')) + self.setSubTitle(_('All conflicting files will be marked unresolved.')) + self.setLayout(QVBoxLayout())   - def completed(): - if self.th.canceled: - return - self.th.wait() - dirty, parents = self.th.results - self.clean = not dirty - self.groups.set_visible(False, 'prog') - self.groups.set_visible(dirty, 'detail') - if dirty: - self.groups.set_visible(parents == 2, 'merged') - self.groups.set_visible(parents == 1, 'dirty') - self.wd_status.set_status(_('<b>Uncommitted local changes ' - 'are detected</b>'), 'thg-warning') + self.cmd = cmdui.Widget(True, False, self) + self.cmd.commandFinished.connect(self.onCommandFinished) + self.cmd.setShowOutput(True) + self.layout().addWidget(self.cmd) + + self.reslabel = QLabel() + self.reslabel.linkActivated.connect(self.onLinkActivated) + self.layout().addWidget(self.reslabel) + + def currentPage(self): + if self.field('discard').toBool(): + # '.' is safer than self.localrev, in case the user has + # pulled a fast one on us and updated from the CLI + cmdline = ['--repository', self.repo.root, 'debugsetparents', + '.', self.wizard().otherrev] + else: + cmdline = ['--repository', self.repo.root, 'merge'] + if self.field('force').toBool(): + cmdline.append('--force') + tool = self.field('autoresolve').toBool() and 'merge' or 'fail' + cmdline += ['--tool=internal:' + tool] + cmdline.append(self.wizard().otherrev) + + if len(self.repo.parents()) == 1: + self.repo.incrementBusyCount() + self.cmd.core.clearOutput() + self.cmd.run(cmdline) + else: + self.mergecomplete = True + self.completeChanged.emit() + + def isComplete(self): + 'should Next button be sensitive?' + if not self.mergecomplete: + return False + count = 0 + for root, path, status in thgrepo.recursiveMergeStatus(self.repo): + if status == 'u': + count += 1 + if count: + if self.field('autoresolve').toBool(): + # if autoresolve is enabled, we know these were real conflicts + self.reslabel.setText(_('%d files have <b>merge conflicts</b> ' + 'that must be <a href="resolve">' + '<b>resolved</b></a>') % count)   else: - self.groups.set_visible(False, 'dirty') - self.groups.set_visible(False, 'merged') - self.wd_status.set_status(_('Clean'), True) + # else give a calmer indication of conflicts + self.reslabel.setText(_('%d files were modified on both ' + 'branches and must be <a href="resolve">' + '<b>resolved</b></a>') % count) + return False + else: + self.reslabel.setText(_('No merge conflicts, ready to commit')) + return True + + def onCommandFinished(self, ret): + self.repo.decrementBusyCount() + if ret in (0, 1): + self.mergecomplete = True + self.completeChanged.emit() + + @pyqtSlot(QString) + def onLinkActivated(self, cmd): + if cmd == 'resolve': + dlg = resolve.ResolveDialog(self.repo, self) + dlg.finished.connect(dlg.deleteLater) + dlg.exec_()   self.completeChanged.emit() - if callable(callback): - callback() - self.th = CheckThread(self) - self.th.finished.connect(completed) - self.th.start()      class CommitPage(BasePage):   - def __init__(self, parent=None): - super(CommitPage, self).__init__(parent) + def __init__(self, repo, parent): + super(CommitPage, self).__init__(repo, parent)   - ### Override Methods ### - - def get_pane(self): - repo = self.wizard().repo - box = QVBoxLayout() - - self.reslabel = QLabel() - self.reslabel.linkActivated.connect(self.link_activated) - box.addWidget(self.reslabel) + self.setTitle(_('Commit merge results')) + self.setLayout(QVBoxLayout()) + self.setCommitPage(True)     # csinfo   def label_func(widget, item, ctx): @@ -614,28 +469,29 @@
  style = csinfo.panelstyle(contents=contents, margin=6)     # merged files - box.addSpacing(12)   rev_sep = qtlib.LabeledSeparator(_('Working Directory (merged)')) - box.addWidget(rev_sep) - rev_info = csinfo.create(repo, None, style, custom=custom, - withupdate=True) - page = self.wizard().page(MERGE_PAGE) - rev_info.linkActivated.connect(page.link_activated) - box.addWidget(rev_info) + self.layout().addWidget(rev_sep) + mergeCsInfo = csinfo.create(repo, None, style, custom=custom, + withupdate=True) + mergeCsInfo.linkActivated.connect(self.onLinkActivated) + self.layout().addWidget(mergeCsInfo)     # commit message area   msg_sep = qtlib.LabeledSeparator(_('Commit message')) - box.addWidget(msg_sep) - msg_text = commit.MessageEntry(self) - msg_text.installEventFilter(qscilib.KeyPressInterceptor(self)) - msg_text.refresh(repo) - msg_text.loadSettings(QSettings(), 'merge/message') - engmsg = repo.ui.configbool('tortoisehg', 'engmsg', False) - msgset = keep._('Merge ') - msg_text.setText(engmsg and msgset['id'] or msgset['str']) - msg_text.textChanged.connect(self.completeChanged) - self.msg_text = msg_text - box.addWidget(msg_text) + self.layout().addWidget(msg_sep) + msgEntry = commit.MessageEntry(self) + msgEntry.installEventFilter(qscilib.KeyPressInterceptor(self)) + msgEntry.refresh(repo) + msgEntry.loadSettings(QSettings(), 'merge/message') + + msgEntry.textChanged.connect(self.completeChanged) + self.layout().addWidget(msgEntry) + self.msgEntry = msgEntry + + self.cmd = cmdui.Widget(True, False, self) + self.cmd.commandFinished.connect(self.onCommandFinished) + self.cmd.setShowOutput(False) + self.layout().addWidget(self.cmd)     def tryperform():   if self.isComplete(): @@ -644,121 +500,90 @@
  actionEnter.setShortcuts([Qt.CTRL+Qt.Key_Return, Qt.CTRL+Qt.Key_Enter])   actionEnter.triggered.connect(tryperform)   self.addAction(actionEnter) - return box   - def link_activated(self, cmd): - if cmd == 'resolve': - dlg = resolve.ResolveDialog(self.wizard().repo, self) - dlg.finished.connect(dlg.deleteLater) + def currentPage(self): + # TODO: add other branch name, when appropriate + engmsg = self.repo.ui.configbool('tortoisehg', 'engmsg', False) + msgset = keep._('Merge ') + self.msgEntry.setText(engmsg and msgset['id'] or msgset['str']) + self.msgEntry.moveCursorToEnd() + + @pyqtSlot(QString) + def onLinkActivated(self, cmd): + if cmd == 'view': + dlg = status.StatusDialog([], {}, self.repo.root, self)   dlg.exec_() - self.completeChanged.emit() + self.refresh()   - def get_perform_label(self): - return _('&Commit') + def isComplete(self): + return len(self.repo.parents()) == 2 and len(self.msgEntry.text()) > 0   - def setup_buttons(self, pane): - super(CommitPage, self).setup_buttons(pane) + def validatePage(self): + if len(self.repo.parents()) == 1: + # commit succeeded, repositoryChanged() called wizard().next() + return True + if self.cmd.core.running(): + return False   - if pane == MAIN_PANE: - undo = QPushButton(_('Undo')) - undo.clicked.connect(self.wizard().back) - self.wizard().setButton(QWizard.HelpButton, undo) - self.wizard().setOption(QWizard.HaveHelpButton, True) - elif pane == PERFORM_PANE: - self.wizard().setOption(QWizard.HaveHelpButton, False) - else: - raise 'unknown pane: %s' % pane - - def ready(self): - self.setTitle(_('Commit merged files')) - self.msg_text.moveCursorToEnd() - - def perform(self):   self.setTitle(_('Committing...'))   self.setSubTitle(_('Please wait while committing merged files.'))   - # merges must be committed without specifying file list - message = hglib.fromunicode(self.msg_text.text()) + message = hglib.fromunicode(self.msgEntry.text())   cmdline = ['commit', '--verbose', '--message', message, - '--repository', self.wizard().repo.root] - self.wizard().repo.incrementBusyCount() + '--repository', self.repo.root] + self.repo.incrementBusyCount() + self.cmd.setShowOutput(True)   self.cmd.run(cmdline) - - def isComplete(self): - if not self.nextEnabled: - return False - repo = self.wizard().repo - for root, path, status in thgrepo.recursiveMergeStatus(repo): - if status == 'u': - self.reslabel.setText(_('There were <b>merge conflicts</b> ' - 'that must be <a href="resolve">' - '<b>resolved</b></a>')) - return False - else: - self.reslabel.setText(_('No merge conflicts, ready to commit')) - return len(self.msg_text.text()) > 0 - - def need_cleanup(self): - return len(self.wizard().repo.parents()) == 2 + self.msgEntry.saveSettings(QSettings(), 'merge/message') + return False     def repositoryChanged(self):   'repository has detected a change to changelog or parents' - if self.done: - return - if len(self.wizard().repo.parents()) == 1: - self.wizard().restart() + if len(self.repo.parents()) == 1: + self.wizard().next()   - ### Private Method ### + def onCommandFinished(self, ret): + self.repo.decrementBusyCount() + self.completeChanged.emit()   - def undo(self): - page = self.wizard().page(MERGE_PAGE) - page.undo = True +class ResultPage(BasePage): + # TODO: Add a checkbox on this page to make it optional   - ### Signal Handlers ### + def __init__(self, repo, parent): + super(ResultPage, self).__init__(repo, parent) + self.setTitle(_('Finished')) + self.setFinalPage(True)   - def command_finished(self, ret): - if ret == 0: - self.done = True - self.wizard().repo.decrementBusyCount() - self.msg_text.saveSettings(QSettings(), 'merge/message') - self.wizard().next() - else: - self.wizard().repo.decrementBusyCount() + self.setLayout(QVBoxLayout()) + merge_sep = qtlib.LabeledSeparator(_('Merge changeset')) + self.layout().addWidget(merge_sep) + merge_info = csinfo.create(self.repo, 'tip', withupdate=True) + self.layout().addWidget(merge_info) + self.layout().addStretch(1)   - def command_canceling(self): - page = self.wizard().page(MERGE_PAGE) - page.undo = True   -class ResultPage(QWizardPage): +class CheckThread(QThread): + def __init__(self, repo, parent): + QThread.__init__(self, parent) + self.repo = hg.repository(repo.ui, repo.root) + self.results = (False, 1) + self.canceled = False   - def __init__(self, parent=None): - super(ResultPage, self).__init__(parent) + def run(self): + self.repo.dirstate.invalidate() + unresolved = False + for root, path, status in thgrepo.recursiveMergeStatus(self.repo): + if self.canceled: + return + if status == 'u': + unresolved = True + break + wctx = self.repo[None] + dirty = bool(wctx.dirty()) or unresolved + self.results = (dirty, len(wctx.parents()))   - self.setTitle(_('Finished')) - - ### Override Method ### - - def reject(self): - pass - - def repositoryChanged(self): - 'repository has detected a change to changelog or parents' - pass - - def initializePage(self): - box = QVBoxLayout() - self.setLayout(box) - - # merge changeset - merge_sep = qtlib.LabeledSeparator(_('Merge changeset')) - box.addWidget(merge_sep) - merge_info = csinfo.create(self.wizard().repo, 'tip', withupdate=True) - box.addWidget(merge_info) - box.addStretch(0) - - self.wizard().setOption(QWizard.HaveHelpButton, False) - self.wizard().setOption(QWizard.NoCancelButton, True) - self.wizard().setOption(QWizard.HaveCustomButton1, False) + def cancel(self): + self.canceled = True    def run(ui, *pats, **opts):   from tortoisehg.util import paths