1. Martin Rieser
  2. hgcr-gui

Commits

Martin Rieser  committed 54c11dc Merge

Merge

  • Participants
  • Parent commits ef781c3, 1a961a2
  • Branches default

Comments (0)

Files changed (1)

File hgcr-gui-qt.py

View file
-#!/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]')
-}
+#!/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
+            selectedRowIndes = index.row()
+        if(i==1):
+            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()
+        if i==1 and action == editAction:
+            # get file path item
+            file_item = self._qui.list_of_files.item(selectedRowIndes, self.FM_PATH)
+            self.editFileDlg(file_item)
+        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)
+        self.editFileDlg(file_item)
+       
+    
+    def editFileDlg(self,file_item):
+        
+         # 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]')
+}