| 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 | # |
|---|
| 11 | import hashlib |
|---|
| 12 | import os |
|---|
| 13 | import posixpath |
|---|
| 14 | from trac.versioncontrol.api import Node, NoSuchNode, RepositoryManager |
|---|
| 15 | from .model import ReviewFileModel |
|---|
| 16 | from .util import to_db_path |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | __author__ = 'Cinc' |
|---|
| 20 | __license__ = "BSD" |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | def 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 | |
|---|
| 38 | def 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 | |
|---|
| 51 | def 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 | |
|---|
| 60 | from .svn_externals import parse_externals |
|---|
| 61 | |
|---|
| 62 | |
|---|
| 63 | def 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 | |
|---|
| 164 | def 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 | |
|---|
| 185 | def 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 | |
|---|
| 212 | def 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) |
|---|