Source

hgcr-gui / hgcr-gui-qt.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
#!/usr/bin/env python

# Code Review extension for TortoiseHg
#
# Copyright 2009  Boris Glimcher <glimchb@gmail.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

'''CodeReview management tool

##
#Overview:
##

# This extension helps you to manage reviews for the code in your project inside the mercurial repository.
# One can add files to review, remove them and notify the reviewer that files are ready for review.
# The reviewer can mark the reviewed code as 'completed' and return the message to the developer.
# The project manager can check the review status - which files are reviewed and which are not yet.
# The extension will automatically spot the files that were changed since their last review and notify about that. 
# This extension uses GUI from TortoiseHg but also implements command line interface.
# Code review database is stored in a .code-review file in your repository root directory as a map of file to revision when review was done.

##
#Usage:
##

hg cr [OPTIONS] [FILES]

Code Review Plugin (requires Mercurial 1.8 and TortoiseHg 2.0)

options:

 -c --complete  Mark CR as complete
 -a --add       Add files to CR list
 -r --remove    Remove files from CR list
 -l --list      Print files in CR list
 -d --developer Change the file developer
 -w --reviewer  Change the file reviewer
 -m --mail      Send mail
 
use "hg -v help cr" to show global options

Note: To start GUI don't give any options.

##
#More Details
##

# I've implemented the review around files and not changesets, because at the end,
  I want to be able to tell for the specific project if all the files went through
  the code review process or not - the project status.
# Suppose you have some project that you are in charge of and many developers write
  code for it, and there is a group of reviewers that review the developers' code.
# It is very difficult to keep track of changes developers do, but simple to find
  out what files have already been reviewed (by reviewers) and what were not.
# Using this extension, a Developer can mark his files (when the development 
  process is finished) as "Ready for review" and send notice to reviewer.
# Later, a Reviewer will pick up the changeset (because changesets are stored in the code
  review database) and perform code review (put notes inside the developer's code).
# Afterwards, the Reviewer will mark the files as "Review Completed" and return the 
  notice to the developer.
# The project manager can follow what is going on with their project at any time.

''' 

from __future__ import with_statement

import os
import re
import sys
import tempfile


from tortoisehg.util import hglib
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt import qtlib, cmdui, i18n, hgemail

from PyQt4.QtCore import *
from PyQt4.QtGui import *

keep = i18n.keepgettext()

from mercurial import error, extensions, ui, util, node, hg, mail, scmutil
from tortoisehg.util import hglib, paths
from tortoisehg.hgqt.hgcr_ui import Ui_CodeREviewDialog


################################################################################                             
class CodeReview(object):

    # database
    DB_FILE = '.code-review'

    def __init__(self, ui, repo, rev):
        self.ui = ui 
        self.repo = repo
        self.db_path = self.repo.wjoin(self.DB_FILE)        
                
    def _status(self, base, *files, **opts):
        # NOTE: files must have absolute paths
        node1, node2 = None, None
        if base:
            node1 = self.repo.lookup(base)
        # Prefix all files with repo's location
        files = [self.repo.wjoin(f) for f in files]
        return self.repo.status(node1=node1, node2=node2, match=scmutil.match(self.repo[None], files), **opts)

    def revno(self, rev):
        '''Return short revision id'''
        ctx = self.repo.changectx(rev) # get by revision id
        num = ctx.rev() # numeric id (not SHA-1)
        if num < 0:
            return 'null'
        return num
        
    def _find_user(self):
        user_name = (self.ui.config('email', 'from') or
                     self.ui.config('ui', 'username') or
                     os.environ.get('username') or # for windows users
                     os.environ.get('user', '?')) # for linux users
        return user_name
    
    def get_email_detail(self, files):
        db = self._open_db()
        send_to = []

        # only checked files
        recivers = {}
        for filename in files:
            
            if filename not in db:
                self.ui.warn('%s is not reviewed!\n' % (filename,))
                continue
                
            round, revision, developer, reviewer = db[filename]
            if self._find_user().lower() == developer.lower():
                send_to = reviewer
                subject = 'ready for your CR'
            elif self._find_user().lower() == reviewer.lower():
                send_to = developer
                subject = 'that completed my CR round'
            else:
                self.ui.warn('you are not developer nor reviewer - you cant send emails\n')
                continue
                        
            if (send_to, subject) not in recivers:
                recivers[send_to, subject] = []
            recivers[send_to, subject].append(filename)
            
        return recivers
            
    def _send_mail(self, sender, reciver, subject):
        mail.sendmail(self.ui, sender, reciver, subject)
            
    def _open_db(self):
        '''Open Database'''
        if not os.path.exists(self.db_path):
            # create the file if it does not exist
            open(self.db_path, 'wt').close()
        # parse database file
        db = [line.rstrip('\n') for line in open(self.db_path, 'rt')]
        db = [line.split('#', 1)[0] for line in db] # remove comments (from # to EOL)
        db = [line.strip() for line in db] # remove whitespace from both sides of the line

        regex = re.compile('\|')
        db = [regex.split(line) for line in db if line]
        # make dictionary from database file
        try:
            db = dict((f, (round, rev, developer, reviewer)) for (rev, round, f, developer, reviewer) in db)
        except ValueError:
            self.ui.warn('Error in Database file %s! Fixing...\n' % self.db_path)
            try:
                db = dict((f, (round, rev, 'developer', 'reviewer')) for (rev, round, f) in db)
            except ValueError:
                self.ui.warn('I cant fix %s! Truncating...\n' % self.db_path)
                db = dict()
        return db

    def _save_db(self, db):
        ''' Save Database file'''
        db = sorted(db.iteritems())
        db = ''.join(['%s|%s|%s|%s|%s\n' % (rev, round, f, developer, reviewer) for (f, (round, rev, developer, reviewer)) in db])
        with open(self.db_path, 'wt') as f:
            f.write(db)

    def done_files(self, files, rev=None):
        '''Mark files as code-review completed'''
        
        number_success_completed = 0
        
        if not files:
            self.ui.warn('No files were selected for COMPLETE command\n')
            return number_success_completed
        
        # There must be one ONLY parent for working directory (cannot CR during merge)
        parent, = self.repo[rev].parents()
        parent_rev = node.hex(parent.node())
        num = self.revno(parent_rev)
        self.ui.note('current revision: %s (#%s)\n' % (parent_rev, num))
        
        # open db
        db = self._open_db()
        
        # only checked files
        for filename in files:
            
            if filename not in db:
                self.ui.warn('%s is not reviewed!\n' % (filename,))
                continue
                
            if any(self._status(None, filename)):
                self.ui.warn('%s was modified since last commit!\n' % (filename,))
                continue
            
            round, old_rev, developer, reviewer = db[filename]
            db[filename] = (int(round) + 1, parent_rev, developer, reviewer)
            self.ui.status('%s review is done at #%s\n' % (filename, num))
            number_success_completed += 1

        # save db
        self._save_db(db)
        
        return number_success_completed

    def change_developer_reviewer(self, files, new_text, fieldname):
        
        if fieldname not in ('developer', 'reviewer'):
            self.ui.warn('%s should be developer or reviewer!\n' % (fieldname,))
            return

        if '|' in new_text:
            self.ui.warn('symbol | is special and cant be inside %s name!\n' % (fieldname,))
            return
        
        # open db
        db = self._open_db()

        # only checked files
        for filename in files:

            if filename not in db:
                self.ui.warn('%s is not reviewed!\n' % (filename,))
                continue
                
            round, revision, developer, reviewer = db[filename]
            previous = eval(fieldname)
            exec("%s = '%s'" % (fieldname, new_text))
            db[filename] = (int(round), revision, developer, reviewer)
            self.ui.status('%s %s has changed from %s to %s\n' % (filename, fieldname, previous, eval(fieldname)))
            
        self._save_db(db)
        
    def remove_files(self, files):
        '''REmove files from code-review'''
        
        number_success_removed = 0
        
        if not files:
            self.ui.warn('No files were selected for REMOVE\n')
            return number_success_removed
        
        # open db
        db = self._open_db()
        
        for filename in files:

            if filename not in db:
                self.ui.warn('%s is not reviewed!\n' % (filename,))
                continue
                
            self.ui.status('%s removed from review list\n' % (filename,))
            db.pop(filename)
            number_success_removed += 1
            
        # save db
        self._save_db(db)
        
        return number_success_removed

    def add_files(self, files):
        '''Add files to code review'''
        
        number_success_added = 0
        
        if not files:
            self.ui.warn('No files were selected for ADD\n')
            return number_success_added
            
        # open db
        db = self._open_db()

        for filename in files:
        
            root = QDir.fromNativeSeparators(self.repo.root)
            filename = QDir.fromNativeSeparators(filename)
            
            if not QDir(root).isRelativePath(filename):
            
              if not filename.startsWith(root, Qt.CaseInsensitive):
                  self.ui.warn('%s is not under source control!\n' % (filename,))
                  continue
                  
              filename = QDir(root).relativeFilePath(filename)
            
            if filename in db:
                self.ui.warn('%s is already in the review list!\n' % (filename,))
                continue
                
            if os.path.isdir(filename):
                self.ui.warn('cannot review directory \'%s\' !\n' % (filename,))
                continue
                
            if '|' in filename:
                self.ui.warn('file path cannot contain | \'%s\' !\n' % (filename,))
                continue
            
            # mark as not-review-yet
            reviewer = '?' #if reviewer is empty the database can not function
            developer = self._find_user()
            default_round = 0
            db[filename] = (default_round, node.hex(node.nullid), developer, reviewer)
            self.ui.status('%s added to review list\n' % (filename,))
            number_success_added += 1

        # save db
        self._save_db(db)
        
        return number_success_added

    def list_files(self):
        '''List files that are managed in code-review'''
        # make db
        db = self._open_db()
        files = sorted(db.keys())
        check = True
        
        result = []
        for f in files:
            round, base, developer, reviewer = db.get(f)
            if not base: # file is not CRed
                self.ui.warn('%s is not reviewed!\n' % (f,))
                continue
                
            # run "hg status" to find out if `f` was changed since its last CR
            res = self._status(base, f, clean=True)
            (modified, added, removed, deleted, unknown, ignored, clean) = res
            base = self.revno(base)
            result.append((clean, base, round, f, developer, reviewer))
        return result
        
        
################################################################################
'''
TODO:
  1. fix CRs and TODOs (i.e. optimisation notes in refresh)
  2. fix ui (printing to console) or qDebug()
'''
class CodeREviewDialog(QDialog):

    # file model row enumerations
    FM_STATUS = 0
    FM_REVISION = 1
    FM_ROUND = 2
    FM_DEVELOPER = 3
    FM_REVIEWER = 4
    FM_PATH = 5

    VIEW_ALL = 0
    VIEW_MY_FILES = 1
    VIEW_FILES_FOR_ME = 2
    VIEW_UNKNOWN = 3
    VIEW = VIEW_ALL
    
    def __init__(self, ui, repo, tag='', rev='tip', parent=None, opts={}):
        super(CodeREviewDialog, self).__init__(parent)
        self.setWindowFlags(Qt.Window)

        self.repo = repo
        self.ui = ui
        self.rev = repo[rev].rev()
        
        self._last_been_dir = self.repo.root
        
        self._qui = Ui_CodeREviewDialog()
        self._qui.setupUi(self)

        # window
        self.setWindowTitle(_('CodeReview - %s') % repo.displayname)
        # TODO: choose appropriate icon
        self.setWindowIcon(qtlib.geticon('hg-tag'))

        # icons
        self._qui.refresh_button.setIcon(qtlib.geticon("refresh_overlays"))
        self._qui.add_button.setIcon(qtlib.geticon("fileadd"))
        self._qui.remove_button.setIcon(qtlib.geticon("filedelete"))
        self._qui.done_button.setIcon(qtlib.geticon("settings_user"))
        self._qui.commit_button.setIcon(qtlib.geticon("menucommit"))
        self._qui.email_button.setIcon(qtlib.geticon("menulog"))
        self._qui.history_button.setIcon(qtlib.geticon("menurevisiongraph"))
        
        # create codereview extension
        self.code_review = CodeReview(self.ui, self.repo, self.rev) 
        
        self.refresh()
        
        # colums size adjustment, do this only once after init, so individual adjusments does not get lost.
        self._qui.list_of_files.resizeColumnsToContents () #status
        
		#disable table grid
        self._qui.list_of_files.setShowGrid(False)
         
		#setup context (right click) menu
        self._qui.list_of_files.setContextMenuPolicy(Qt.CustomContextMenu)
        self._qui.list_of_files.customContextMenuRequested.connect(self.onContextMenuTriggered)
        
        self.adjustSize()
    ################################################################################

    ## actions ##
    def onContextMenuTriggered(self,position):
        menu = QMenu()
        doneAction = menu.addAction(qtlib.geticon("settings_user"),_('Done'))
        #commitAction = menu.addAction(qtlib.geticon("menucommit"),_('Commit'))
        emailAction  = menu.addAction(qtlib.geticon("menulog"),_('Email'))
        removeAction = menu.addAction(qtlib.geticon("filedelete"),_('Remove'))
        
        #add action, only possible if only one file is selected
        #TODO There must be a better way to find out, that only one line is selected
        i =0
        selectionModel = self._qui.list_of_files.selectionModel()
        for index in selectionModel.selectedRows():
            i = i+1
        if(i==1):
            #TODO editAction = menu.addAction(qtlib.geticon("filemodify"),_('Edit'))
            historyAction = menu.addAction(qtlib.geticon("menurevisiongraph"),_('History'))
        
        action = menu.exec_(self._qui.list_of_files.mapToGlobal(position+ QPoint(+30, +15)))
        
        #deside what should be done, depending on the selected menu item
        if action == doneAction:
            self.onDoneButtonClicked()
        #if action == commitAction:
        #    self.onCommitButtonClicked()
        if action == emailAction:
            self.onEmailButtonClicked()
        if action == removeAction:
            self.onRemoveButtonClicked()
        #TODO add edit file function call
        #if i==1 and action == editAction:
        #    self.onTableDoubleClick(QModelIndex)
        if i==1 and action == historyAction:
            self.onHistoryButtonClicked()
    
    @pyqtSlot()
    def onRefreshButtonClicked(self):
        # refresh
        self.refresh()

    @pyqtSlot()
    def onAddButtonClicked(self):

        if self.check_settings():
            files = QFileDialog.getOpenFileNames(self, _('Choose files to add for CR...'), self._last_been_dir)

            if not files:
              response = QMessageBox.warning(self, _('Warning'), _('No files were selected for ADD command'))
              return

            # add files to CR manager
            number_success_added = self.code_review.add_files([str(f) for f in files])
            
            # refresh
            self.refresh()
            
            # update last been directory
            if len(files) > 0:
              self._last_been_dir = os.path.dirname(str(files[0]))
            
    @pyqtSlot()
    def onRemoveButtonClicked(self):
        ''' remove files from code review manager'''
        
        # get only selected files
        files = []
        selectionModel = self._qui.list_of_files.selectionModel()
        for index in selectionModel.selectedRows():
            path = self._qui.list_of_files.item(index.row(), self.FM_PATH)
            files.append(str(path.text()))

        if not files:
          response = QMessageBox.warning(self, _('Warning'), _('No files were selected for REMOVE command'))
          return
        
        # remove these files from CR list
        number_success_removed = self.code_review.remove_files(files)

        # refresh
        self.refresh()        
            
    @pyqtSlot()
    def onDoneButtonClicked(self):
        ''' Mark selected files as Done'''
        
        # get only selected files
        files = []
        message = QString(_('are you sure you want to commit and mark as completed these files?\n'))
        selectionModel = self._qui.list_of_files.selectionModel()
        for index in selectionModel.selectedRows():
            path = self._qui.list_of_files.item(index.row(), self.FM_PATH)
            files.append(str(path.text()))
            message += QString("FILE -- %1\n").arg(path.text())

        if not files:
          response = QMessageBox.warning(self, _('Warning'), _('No files were selected for COMPLETE command'))
          return

        #check if there are modified file to commit.
        reproStatus = self.repo.status()
        modifiedFiles = reproStatus[0]
        commitDlgRequrired = 0
        for i in files:
           if i in modifiedFiles:
              commitDlgRequrired = 1
        
        if (commitDlgRequrired):
           response = QMessageBox.question(self, _('Question'), _('There are modified files in your repository. Would you like to commit them?'),  QMessageBox.Yes, QMessageBox.No, QMessageBox.NoButton)
           if response == QMessageBox.Yes:
              self.show_CommitDlg()
           else:              
              return
        
        #check if there are still modified file to commit.
        reproStatus = self.repo.status()
        modifiedFiles = reproStatus[0]
        commitDlgRequrired = 0
        for i in files:
           if i in modifiedFiles:
              response = QMessageBox.warning(self, _('Warning'), _('Some of the selected files where not commited. So no file was marked as done.'))
              return
        
        number_success_completed = self.code_review.done_files(files, rev=None)

        #refresh the display to make green files
        self.refresh()        
            
    @pyqtSlot()
    def onCommitButtonClicked(self):
        self.show_CommitDlg()     
                    
    @pyqtSlot()
    def onEmailButtonClicked(self):

        if self.check_settings():

            # get only selected files
            files = []
            selectionModel = self._qui.list_of_files.selectionModel()
            for index in selectionModel.selectedRows():
                path = self._qui.list_of_files.item(index.row(), self.FM_PATH)
                files.append(str(path.text()))

            if not files:
              response = QMessageBox.warning(self, _('Warning'), _('No files were selected for EMAIL command'))
              return
                          
            recivers = self.code_review.get_email_detail(files)
            sender = self.code_review._find_user()
            defaultRepro = self.ui.config('paths', 'default')
            for x, list_of_files in recivers.iteritems():
                reciver, messageHeader = x
                subject = 'subject: files %s\n\n' % messageHeader
                if (defaultRepro):
                   subject += 'HG repository: %s\n\n' % defaultRepro
                subject += 'Files %s:\n' % messageHeader
                message = QString('are you sure you want to send mail to %1?\n').arg(reciver)
                for i in list_of_files:
                    subject += '%s\n' % i
                    message += QString("FILE -- %1\n").arg(i)

                subject += '\n\n'
                subject += _('This message was automaticaly generated by tortoiseHG code review extention: \'hgcr\'.')
                response = QMessageBox.question(self, _('send email'), message,  QMessageBox.Yes, QMessageBox.No, QMessageBox.NoButton)
                if response == QMessageBox.Yes:
                    self.code_review._send_mail(sender, reciver, subject)

    @pyqtSlot()
    def onHistoryButtonClicked(self):
        """Show mercurial file history of the current row"""

        row = self._qui.list_of_files.currentRow()

        if row < 0:
          response = QMessageBox.warning(self, _('Warning'), _('No files were selected for HISTORY command'))
          return
        
        file_path = str(self._qui.list_of_files.item(row, self.FM_PATH).text())

        self.ui.status('Viewing history on file: %s\n' % (file_path,))

        # refresh the display
        self.refresh()        

        from tortoisehg.hgqt import filedialogs
        dlg = filedialogs.FileLogDialog(self.repo, file_path, repoviewer=self.window())
        dlg.setWindowTitle(_('Hg file log viewer - %s') % hglib.tounicode(file_path))
        dlg.show()
        dlg.raise_()
        dlg.activateWindow() 

    @pyqtSlot()
    def onRadioButtonClicked(self, bool_state):
        """change a view of files to match radio choice"""
        
        if self._qui.all_files.isChecked():
            self.VIEW = self.VIEW_ALL
        elif self._qui.files_i_own.isChecked():
            self.VIEW = self.VIEW_MY_FILES
        elif self._qui.files_for_my_review.isChecked():
            self.VIEW = self.VIEW_FILES_FOR_ME
        elif self._qui.unknown_reviewer.isChecked():
            self.VIEW = self.VIEW_UNKNOWN

        # refresh the display
        self.refresh()        

    @pyqtSlot()
    def onTableEdit(self, row, column):
        """Function to handle user changes in cells of the files table.

        it's called automatically whenever signal "cellChanged(...)" is emitted.

        @param row index of a row that has changed
        @param column index of a column that has changed
        """
        
        if not self.relMembersLoaded:
            return
    
        item = self._qui.list_of_files.item(row, column)
        new_text = str(item.text())
        if new_text == '': new_text = '?' # if new_text is '' the database will corrupt
        
        file_item = self._qui.list_of_files.item(row, self.FM_PATH)
        if not file_item:
          return
          
        filename = str(file_item.text())
        
        if column == self.FM_DEVELOPER:
            name = "developer"
        elif column == self.FM_REVIEWER:
            name = "reviewer"
        else:
            response = QMessageBox.warning(self, _('Error'), _('Can change only Developer or Reviewer'))
            return

        self.code_review.change_developer_reviewer([filename], new_text, name) 
        #no display refresh, if a filtered view is active, the edited line disappears instantaneously. Thats a bad user experience. 

    @pyqtSlot()
    def onTableDoubleClick(self, QModelIndex):
        """Function to handle user double-clicks on the table cells.
        
        The action will be openning the corresponding file to clicked row with predefined editor.

        it's called automatically whenever signal "doubleClicked(...)" is emitted.

        @param Index of an item that has was double-clicked.
        """

        # get file path item
        file_item = self._qui.list_of_files.item(QModelIndex.row(), self.FM_PATH)
        
        # get absolute path
        file_path = self.repo.wjoin(str(file_item.text()))

        self.ui.status('editing file: %s\n' % (file_path,))

        from tortoisehg.hgqt import qtlib
        qtlib.editfiles(self.repo, [file_path], 1, parent=self) 
                        
    @pyqtSlot()
    def onTableSelection(self):
        ''' Updates status bar with how many selected files of all files in the table'''

        # get only selected files
        files = []
        selectionModel = self._qui.list_of_files.selectionModel()
        for index in selectionModel.selectedRows():
            path = self._qui.list_of_files.item(index.row(), self.FM_PATH)
            files.append(str(path.text()))

        self._qui.status.setText(_('%s selected, %s total' % (len(files), self._qui.list_of_files.rowCount()) ))
    
    ################################################################################
    def show_CommitDlg(self):
        ''' open a commit dialog'''
        from tortoisehg.hgqt import commit
        dlg = commit.CommitDialog(self.repo, [], {}, self)
        dlg.finished.connect(dlg.deleteLater)
        dlg.exec_()
        #refresh the display after commit
        self.refresh()   
        
    def check_settings(self):
        ''' Checks if email settings are configured and opens SettingsDialog if necessary'''

        if not self.code_review.ui.config('email', 'from'):
            message = _('please enter vaild information:\n\nFrom: your e-amil addres\nSMTP host: localhost\nSMTP port: 25\nProceed to userconfig?')
            response = QMessageBox.question(self, 'enter settings', message,  QMessageBox.Yes, QMessageBox.No, QMessageBox.NoButton)
            if response == QMessageBox.Yes:
                from tortoisehg.hgqt import settings
                if settings.SettingsDialog(parent=self, focus='email.from').exec_():
                    # not use repo.configChanged because it can clobber user input
                    # accidentally.
                    self.repo.invalidateui()  # force reloading config immediately                
        
        return bool(self.code_review.ui.config('email', 'from'))

    def refresh(self):
        ''' Re-Fills the file table with files from the database'''
        
        # Note: needed to avoid getting cellChanged() signals while constructing a table with setItem()
        self.relMembersLoaded = False
        self._qui.list_of_files.setSortingEnabled(False)
        
        # clear all rows
        self._qui.list_of_files.clearContents()
        self._qui.list_of_files.setRowCount(0)
        
        for clean, base, round, f, developer, reviewer in self.code_review.list_files():
            res = 'completed' if clean else 'changed '
            
            # select colors
            if res == 'changed ':
                color = Qt.red
            elif res == 'completed':
                color = Qt.darkGreen
            else:
                color = Qt.black

            if (self.VIEW == self.VIEW_MY_FILES and developer.lower() == self.code_review._find_user().lower()) \
            or (self.VIEW == self.VIEW_FILES_FOR_ME and reviewer.lower() == self.code_review._find_user().lower() and res == 'changed ') \
            or (self.VIEW == self.VIEW_UNKNOWN and reviewer == '?') \
            or (self.VIEW == self.VIEW_ALL):
            
                row = self._qui.list_of_files.rowCount()
                self._qui.list_of_files.insertRow(row)                

                item = QTableWidgetItem('%s' % res)
                item.setTextColor(color)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_STATUS, item)

                item = QTableWidgetItem('%s' % base)
                item.setTextColor(color)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_REVISION, item)
                
                item = QTableWidgetItem('%s' % round)
                item.setTextColor(color)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_ROUND, item)
                
                item = QTableWidgetItem('%s' % developer)
                item.setTextColor(color)
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_DEVELOPER, item)
                
                item = QTableWidgetItem('%s' % reviewer)
                item.setTextColor(color)
                item.setFlags(item.flags() | Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_REVIEWER, item)
                
                item = QTableWidgetItem('%s' % f)
                item.setTextColor(color)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self._qui.list_of_files.setItem(row, self.FM_PATH, item);
                
                #adjust row hieght
                self._qui.list_of_files.setRowHeight(row, 18)

        
        #order table by file name (colum = 5)
        self._qui.list_of_files.sortItems(5,Qt.AscendingOrder)
        self._qui.list_of_files.setSortingEnabled(True)
        self.relMembersLoaded = True
        self.onTableSelection()
            
################################################################################
def main(ui, repo, *pats, **opts):
    """Code Review Plugin (requires Mercurial 1.8 and TortoiseHg 2.0)"""

    rev = opts.get('rev', None)
    code_review = CodeReview(ui, repo, rev)

    files = []    
    if pats:
        # Match all files using given patterns
        m = scmutil.match(repo[None], pats, opts)
        files = m.files()
        for f in files:
            ui.note('matched: %s\n' % (f,))
    
    if opts['list']:
        format = '%5s %10s %10s %15s %15s %15s\n'
        ui.status(format % ('round', 'revision', 'status', 'developer', 'reviewer', 'filename' ) )
        ui.status(format % ('-----', '--------', '------', '---------', '--------', '--------' ) )
        for clean, base, round, f, developer, reviewer in code_review.list_files():
            res = 'completed' if clean else 'changed'
            ui.status(format % (round, code_review.revno(base), res, developer, reviewer, f) )
        
    elif opts['complete']:
        code_review.done_files(files)
        
    elif opts['add']:
        code_review.add_files(files)
        
    elif opts['remove']:
        code_review.remove_files(files)
    
    elif opts['reviewer']:
        new_text = raw_input('enter new reviewer name: ')
        code_review.change_developer_reviewer(files, new_text, 'reviewer')
    
    elif opts['developer']:
        new_text = raw_input('enter new developer name: ')
        code_review.change_developer_reviewer(files, new_text, 'developer')
    
    elif opts['mail']:
        sender = code_review._find_user()
        recivers = code_review.get_email_detail(files)
        for x, list_of_files in recivers.iteritems():
            reciver, subject = x
            subject = 'subject: files %s\n\n' % subject
            subject += '\n'.join(list_of_files)
            code_review._send_mail(sender, reciver, subject)
        
    else:
    
        def run_with_gui(ui, *pats, **opts):
            kargs = {}
            tag = len(pats) > 0 and pats[0] or None
            if tag:
                kargs['tag'] = tag
            rev = opts.get('rev')
            if rev:
                kargs['rev'] = rev
            from tortoisehg.util import paths
            from tortoisehg.hgqt import thgrepo
            repo = thgrepo.repository(ui, path=paths.find_root())
            return CodeREviewDialog(ui, repo, opts=opts, **kargs)
            
        from tortoisehg.hgqt import run
        return run.qtrun(run_with_gui, ui, *pats, **opts)
        
        
cmdtable = {
    'cr':           (main,
                     [('c', 'complete', False, 'Mark CR as complete'),
                      ('a', 'add', False, 'Add files to CR list'),
                      ('r', 'remove', False, 'Remove files from CR list'),
                      ('l', 'list', False, 'Print files in CR list'),
                      ('d', 'developer', False, 'Change the file developer'),
                      ('w', 'reviewer', False, 'Change the file reviewer'),
                      ('m', 'mail', False, 'Send mail'),
                      ],
                     'hg cr [OPTIONS] [FILES]')
}