source: peerreviewplugin/trunk/codereview/repo.py

Last change on this file was 18284, checked in by Cinc-th, 2 years ago

PeerReviewPlugin: allow to review added, copied and moved files in changesets. Up to now you had to visit the review pages for such files, see also [18271] which introduced an explanatory message for those files.

Refs #14007

File size: 9.9 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2016-2021 Cinc
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING.txt, which
7# you should have received as part of this distribution.
8#
9# Author: Cinc
10#
11import hashlib
12import os
13import posixpath
14from trac.versioncontrol.api import Node, NoSuchNode, RepositoryManager
15from .model import ReviewFileModel
16from .util import to_db_path
17
18
19__author__ = 'Cinc'
20__license__ = "BSD"
21
22
23def hash_from_file_node(node):
24    if node.kind != Node.FILE:
25        raise ValueError('Node is a directory. Must be a file.')
26
27    content = node.get_content()
28    blocksize = 4096
29    hasher = hashlib.sha256()
30
31    buf = content.read(blocksize)
32    while len(buf) > 0:
33        hasher.update(buf)
34        buf = content.read(blocksize)
35    return hasher.hexdigest()
36
37
38def repo_path_exists(env, path, reponame=''):
39    repos = RepositoryManager(env).get_repository(reponame)
40    if not repos:
41        return False
42
43    rev = repos.youngest_rev
44    try:
45        node = repos.get_node(path, rev)
46        return node.isdir
47    except NoSuchNode as e:
48        return False
49
50
51def get_node(repos, path, rev):
52    # TODO: from trac.versioncontrol.web_ui.util import get_existing_node is probably a better
53    #       way to do it...
54    rev = repos.normalize_rev(rev)
55    try:
56        return repos.get_node(path, rev)
57    except NoSuchNode:
58        return None
59
60from .svn_externals import parse_externals
61
62
63def get_nodes_for_dir(self, repodict, dir_node, fnodes, ignore_ext, incl_ext, excl_path, follow_ext, repo_name=''):
64    """Get file nodes recursively for a given directory node.
65
66    :param env: Trac environment object
67    :param repodict: dict holding info about known repositories
68    :param dir_node: a trac directory node
69    :param fnodes: list of file info nodes. Info for found files will be appended. Note that the path
70                   in the info dict is with leading '/'.
71    :param ignore_ext: list of file extensions to be ignored
72    :param follow_ext: if True follow externals to folders in the same or another repository
73
74    :return errors: list of errors. Empty list if no errors occurred.
75    """
76    if not self._externals_map:
77        for dummykey, value in self.external_map_section.options():
78            value = value.split()
79            if len(value) != 3:
80                self.log.warn("peerreview:repomap entry %s doesn't contain "
81                              "a space-separated list of three items, "
82                              "skipping.", dummykey)
83                continue
84            key, value, repo = value
85            self._externals_map[key] = [value, repo]
86
87    env = self.env
88    errors = []
89    for node in dir_node.get_entries():
90        if node.isdir:
91            errors += get_nodes_for_dir(self, repodict, node, fnodes, ignore_ext, incl_ext, excl_path, follow_ext,
92                                        repo_name)
93            if follow_ext:
94                props = node.get_properties()
95                try:
96                    for external in parse_externals(props['svn:externals']):
97                        try:
98                            # Create a valid paths from externals information. The path may point to virtual
99                            # repositories.
100                            base_url = external['url']
101                            while base_url:
102                                if base_url in self._externals_map or base_url == u'/':
103                                    break
104                                base_url, pref = posixpath.split(base_url)
105                            # base_url is the path head of the external
106                            ext_info = self._externals_map.get(base_url)
107                            file_path = repos = reponame = None
108                            if ext_info:
109                                file_path, reponame = ext_info
110                                file_path = file_path + external['url'][len(base_url):]
111                                repos = repodict[reponame]['repo']
112
113                            if file_path:
114                                rev = external['rev']
115                                if rev:
116                                    rev = repos.normalize_rev(rev)
117                                rev_or_latest = rev or repos.youngest_rev
118                                ext_node = get_node(repos, file_path, rev_or_latest)
119                            else:
120                                ext_node = None
121
122                            if ext_node and ext_node.isdir:
123                                errors += get_nodes_for_dir(self, repodict, ext_node, fnodes, ignore_ext, incl_ext,
124                                                            excl_path, follow_ext, reponame)
125                            else:
126                                txt = "No node for external path '%s' in repository '%s'. " \
127                                      "External: '%s %s' was ignored for directory '%s'." \
128                                      % (file_path, reponame, external['url'], external['dir'], node.name)
129                                env.log.warning(txt)
130                                errors.append(txt)
131                        except KeyError:  # Missing data in dictionary e.g. we try to use an unnamed repository
132                            txt = "External: '%s %s' was ignored for directory '%s'." %\
133                                  (external['url'], external['dir'], node.name)
134                            env.log.warning(txt)
135                            errors.append(txt)
136                except KeyError:  # property has no svn:externals
137                    pass
138        else:
139            for p in excl_path:
140                if to_db_path(node.path).startswith(p):
141                    break
142            else:
143                if incl_ext:
144                    if os.path.splitext(node.path)[1].lower() in incl_ext:
145                        fnodes.append({
146                            'path': to_db_path(node.path),
147                            'rev': node.rev,
148                            'change_rev':node.created_rev,
149                            'hash': hash_from_file_node(node),
150                            'reponame': repo_name
151                        })
152                else:
153                    if os.path.splitext(node.path)[1].lower() not in ignore_ext:
154                        fnodes.append({
155                            'path': to_db_path(node.path),
156                            'rev': node.rev,
157                            'change_rev':node.created_rev,
158                            'hash': hash_from_file_node(node),
159                            'reponame': repo_name
160                        })
161    return errors
162
163
164def file_lines_from_node(node, keyword_substitution=False):
165    """Get a list of lines for the text file represented by the given node.
166
167    Note that it is assumed that the text is utf-8.
168    End separators are stripped of.
169    """
170    if not node or node.isdir:
171        return []
172
173    dat = u''
174    if keyword_substitution:
175        content = node.get_processed_content()
176    else:
177        content = node.get_content()
178    res = content.read()
179    while res:
180        dat += res.decode('utf-8')  # We assume 'utf-8' here. In fact it may be anything.
181        res = content.read()
182    return dat.splitlines()
183
184
185def get_repository_dict(env):
186    """Get a dict with information about all repositories.
187
188    :param env: Trac environment object
189    :return: dict with key = reponame, value = dict with information about repository.
190
191    The information about a repository is queried using ''get_all_repositories'' from
192    RepositoryManager.
193    - For any real repository (that means not an alias) the Repository object
194      is inserted into the dictionary using the key 'repo'.
195    - For any real repository (that means not an alias) a prefix is calculated from the url info
196      and inserted using the key 'prefix'. This prefix is used to build paths into the repository.
197
198    """
199    repoman = RepositoryManager(env)
200
201    repolist = repoman.get_all_repositories()  # repolist is a dict with key = reponame, val = dict
202    for repo in repoman.get_real_repositories():
203        repolist[repo.reponame]['repo'] = repoman.get_repository(repo.reponame)
204        # We need the last part of the path later when following externals
205        try:
206            repolist[repo.reponame]['prefix'] = '/' + os.path.basename(repolist[repo.reponame]['url'].rstrip('/'))
207        except KeyError:
208            repolist[repo.reponame]['prefix'] = ''
209    return repolist
210
211
212def insert_project_files(self, src_path, project, ignore_ext, incl_ext, excl_path,
213                         follow_ext=False, rev=None, repo_name=''):
214    """Add project files to the database.
215
216    :param self: Trac component object
217    :param src_path
218    """
219    repolist = get_repository_dict(self.env)
220    try:
221        repos = repolist[repo_name]['repo']
222    except KeyError:
223        return
224
225    if not repos:
226        return
227
228    if rev:
229        rev = repos.normalize_rev(rev)
230    rev_or_latest = rev or repos.youngest_rev
231
232    root_node = get_node(repos, src_path, rev_or_latest)
233
234    fnodes = []
235    if root_node.isdir:
236        errors = get_nodes_for_dir(self, repolist, root_node, fnodes, ignore_ext, incl_ext, excl_path, follow_ext,
237                                   repo_name)
238    else:
239        errors = []
240
241    ReviewFileModel.delete_files_by_project_name(self.env, project)
242    with self.env.db_transaction as db:
243        cursor = db.cursor()
244        for item in fnodes:
245            cursor.execute("INSERT INTO peerreviewfile"
246                           "(review_id,path,line_start,line_end,repo,revision, changerevision,hash,project)"
247                           "VALUES (0, %s, 0, 0, %s, %s, %s, %s, %s)",
248                           (item['path'], item['reponame'], item['rev'], item['change_rev'], item['hash'], project))
249
250    return errors, len(fnodes)
Note: See TracBrowser for help on using the repository browser.