source: codereviewerplugin/0.12/coderev/util/reviewer.py

Last change on this file was 14033, checked in by Ryan J Ollos, 9 years ago

Changed license to 3-Clause BSD with permission of author. Refs #11832.

  • Property svn:executable set to *
File size: 6.5 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2012 Rob Guttman <guttman@alum.mit.edu>
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
8#
9
10import re
11import os
12import json
13import sqlite3
14from subprocess import Popen, STDOUT, PIPE
15
16from coderev.model import CodeReview
17from trac.env import Environment
18from trac.ticket.model import Ticket
19from trac.resource import ResourceNotFound
20
21class Reviewer(object):
22    """Returns the latest changeset in a given repo whose Trac tickets have
23    been fully reviewed.  Works in conjunction with the Trac CodeReviewer
24    plugin and its database tables."""
25
26    def __init__(self, trac_env, repo_dir, target_ref, data_file, verbose=True):
27        self.env = Environment(trac_env)
28        self.repo_dir = repo_dir.rstrip('/')
29        self.reponame = os.path.basename(self.repo_dir).lower()
30        self.target_ref = target_ref
31        self.data_file = data_file
32        self.verbose = verbose
33
34    def get_next_changeset(self, save=True):
35        """Return the next reviewed changeset and save it as the current
36        changeset when save is True."""
37        next = self.get_current_changeset()
38        for changeset in self.get_changesets():
39            if self.verbose:
40                print '.',
41            if not self.is_complete(changeset):
42                return self.set_current_changeset(next, save)
43            next = changeset
44        return self.set_current_changeset(next, save)
45
46    def get_blocking_changeset(self, changesets=None):
47        """Return the next blocking changeset."""
48        if changesets is None:
49            changesets = self.get_changesets()
50        for changeset in changesets:
51            if not self.is_complete(changeset):
52                return changeset
53        return None
54
55    def get_blocked_tickets(self):
56        """Return all tickets of blocking changesets in order of them
57        getting unblocked."""
58        tickets = []
59        tickets_visited = set(['']) # merge changesets have empty ticket values
60
61        # restrict changesets to only those not completed
62        changesets = self.get_changesets()
63        blocking_changeset = self.get_blocking_changeset(changesets)
64        if blocking_changeset is None:
65            return []
66        changesets = changesets[changesets.index(blocking_changeset):]
67
68        # only consider changesets that come after the blocking changeset
69        review = self.get_review(blocking_changeset)
70        blocking_when = review.changeset_when
71
72        # find blocked tickets
73        for changeset in changesets:
74            review = self.get_review(changeset)
75            for ticket in review.tickets:
76                if ticket in tickets_visited:
77                    continue
78                tickets_visited.add(ticket)
79
80                def get_first_remaining_changeset():
81                    for review in self.get_reviews(ticket):
82                        if review.changeset in changesets and \
83                           review.changeset_when >= blocking_when:
84                            return review # changeset exists on path
85                    raise ResourceNotFound("Not found for #%s" % ticket)
86
87                # the ticket's oldest *remaining* changeset determines blockage
88                # i.e., if current is already past a changeset, ignore it
89                try:
90                    first = get_first_remaining_changeset()
91                    tkt = Ticket(self.env, ticket)
92                    tkt.first_changeset = first.changeset
93                    tkt.first_changeset_when = first.changeset_when
94                    tickets.append( tkt )
95                except ResourceNotFound:
96                    pass # e.g., incorrect ticket reference
97        return sorted(tickets, key=lambda t: t.first_changeset_when)
98
99    def get_changesets(self):
100        """Extract changesets in order from current to target ref."""
101        current_ref = self.get_current_changeset()
102        review = self.get_review(current_ref)
103        when = int(review.changeset_when / CodeReview.EPOCH_MULTIPLIER)
104        cmds = ['cd %s' % self.repo_dir,
105                'git rev-list --reverse --since=%s HEAD' % when]
106        changesets = self._execute(' && '.join(cmds)).splitlines()
107        if self.verbose:
108            print "\n%d changesets from current %s to target %s" % \
109                    (len(changesets),current_ref,self.target_ref)
110        if current_ref not in changesets:
111            changesets.insert(0,current_ref)
112        return changesets
113
114    def get_reviews(self, ticket):
115        return CodeReview.get_reviews(self.env, ticket)
116
117    def get_review(self, changeset):
118        return CodeReview(self.env, self.reponame, changeset)
119
120    def is_complete(self, changeset):
121        """Returns True if all of the given changeset's tickets are complete.
122        Complete means that the ticket has no pending reviews and the last
123        review has passed, -AND- the ticket's completeness criteria (if any)
124        is satisfied."""
125        review = self.get_review(changeset)
126        for ticket in review.tickets:
127            reason = review.is_incomplete(ticket)
128            if reason:
129                if self.verbose:
130                    print '\n' + reason
131                return False
132        return True
133
134    def _execute(self, cmd):
135        p = Popen(cmd, shell=True, stderr=STDOUT, stdout=PIPE)
136        out = p.communicate()[0]
137        if p.returncode != 0:
138            raise Exception('cmd: %s\n%s' % (cmd,out))
139        return out
140
141    def get_current_changeset(self):
142        data = self._get_data()
143        return data['current']
144
145    def set_current_changeset(self, changeset, save=True):
146        if save:
147            data = self._get_data()
148            if data['current'] != changeset:
149                data['current'] = changeset
150                self._set_data(data)
151                if self.verbose:
152                    print "setting current changeset to %s" % changeset
153            elif self.verbose:
154                print "current changeset already is %s" % changeset
155        return changeset
156
157    def _get_data(self):
158        if os.path.exists(self.data_file):
159            data = json.loads(open(self.data_file,'r').read())
160        else:
161            # grab the latest rev as the changeset
162            cmds = ['cd %s' % self.repo_dir,
163                    'git log -1 --pretty="format:%H"']
164            changeset = self._execute(' && '.join(cmds))
165            data = {'current': changeset}
166            self._set_data(data)
167        return data
168
169    def _set_data(self, data):
170        f = open(self.data_file,'w')
171        f.write(json.dumps(data))
172        f.close()
Note: See TracBrowser for help on using the repository browser.