| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2016 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 | |
|---|
| 12 | import hashlib |
|---|
| 13 | import os |
|---|
| 14 | from trac.versioncontrol.api import NoSuchNode, RepositoryManager |
|---|
| 15 | from .model import ReviewFileModel |
|---|
| 16 | |
|---|
| 17 | __author__ = 'Cinc' |
|---|
| 18 | __license__ = "BSD" |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | def hash_from_file_node(node): |
|---|
| 22 | content = node.get_content() |
|---|
| 23 | blocksize = 4096 |
|---|
| 24 | hasher = hashlib.sha256() |
|---|
| 25 | |
|---|
| 26 | buf = content.read(blocksize) |
|---|
| 27 | while len(buf) > 0: |
|---|
| 28 | hasher.update(buf) |
|---|
| 29 | buf = content.read(blocksize) |
|---|
| 30 | return hasher.hexdigest() |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | def repo_path_exists(env, path, reponame=''): |
|---|
| 34 | repos = RepositoryManager(env).get_repository(reponame) |
|---|
| 35 | if not repos: |
|---|
| 36 | return False |
|---|
| 37 | |
|---|
| 38 | rev = repos.youngest_rev |
|---|
| 39 | try: |
|---|
| 40 | node = repos.get_node(path, rev) |
|---|
| 41 | return node.isdir |
|---|
| 42 | except NoSuchNode as e: |
|---|
| 43 | return False |
|---|
| 44 | |
|---|
| 45 | |
|---|
| 46 | def get_node(repos, path, rev): |
|---|
| 47 | try: |
|---|
| 48 | return repos.get_node(path, rev) |
|---|
| 49 | except NoSuchNode as e: |
|---|
| 50 | return None |
|---|
| 51 | |
|---|
| 52 | from svn_externals import parse_externals |
|---|
| 53 | |
|---|
| 54 | |
|---|
| 55 | def get_nodes_for_dir(env, repodict, dir_node, fnodes, ignore_ext, follow_ext): |
|---|
| 56 | """Get file nodes recursively for a given directory node. |
|---|
| 57 | |
|---|
| 58 | :param env: Trac environment object |
|---|
| 59 | :param repodict: dict holding info about known repositories |
|---|
| 60 | :param dir_node: a trac directory node |
|---|
| 61 | :param fnodes: list of file info nodes. Info for found files will be appended. |
|---|
| 62 | :param ignore_ext: list of file extensions to be ignored |
|---|
| 63 | :param follow_ext: if True follow externals to folders in the same or another repository |
|---|
| 64 | |
|---|
| 65 | :return errors: list of errors. Empty list if no errors occurred. |
|---|
| 66 | """ |
|---|
| 67 | errors = [] |
|---|
| 68 | for node in dir_node.get_entries(): |
|---|
| 69 | if node.isdir: |
|---|
| 70 | errors += get_nodes_for_dir(env, repodict, node, fnodes, ignore_ext, follow_ext) |
|---|
| 71 | if follow_ext: |
|---|
| 72 | props = node.get_properties() |
|---|
| 73 | try: |
|---|
| 74 | for external in parse_externals(props['svn:externals']): |
|---|
| 75 | try: |
|---|
| 76 | |
|---|
| 77 | # list of lists. First item is len of common prefix, second is repository object |
|---|
| 78 | len_common = [] |
|---|
| 79 | for key, val in repodict.iteritems(): |
|---|
| 80 | try: |
|---|
| 81 | len_common.append([len(os.path.commonprefix([external['url'], val['url']])), |
|---|
| 82 | val['repo']]) |
|---|
| 83 | except KeyError: |
|---|
| 84 | pass |
|---|
| 85 | len_common.sort(reverse=True) |
|---|
| 86 | # First item in list is repo holding the external path because it has the longest |
|---|
| 87 | # common prefix |
|---|
| 88 | repos = len_common[0][1] |
|---|
| 89 | repo_path = repodict[repos.reponame]['prefix'] + '/' + \ |
|---|
| 90 | external['url'][len_common[0][0]:].lstrip('/') |
|---|
| 91 | ext_node = get_node(repos, repo_path, external['rev']) |
|---|
| 92 | if ext_node: |
|---|
| 93 | errors += get_nodes_for_dir(env, repodict, ext_node, fnodes, ignore_ext, follow_ext) |
|---|
| 94 | else: |
|---|
| 95 | txt = "No node for external path '%s' in repository '%s'. " \ |
|---|
| 96 | "External: '%s %s' was ignored for directory '%s'." \ |
|---|
| 97 | % (repo_path, repos.reponame, external['url'], external['dir'], node.name) |
|---|
| 98 | env.log.warning(txt) |
|---|
| 99 | errors.append(txt) |
|---|
| 100 | except KeyError: # Missing data in dictionary e.g. we try to use aan unnamed repository |
|---|
| 101 | txt = "External: '%s %s' was ignored for directory '%s'." %\ |
|---|
| 102 | (external['url'], external['dir'], node.name) |
|---|
| 103 | env.log.warning(txt) |
|---|
| 104 | errors.append(txt) |
|---|
| 105 | except KeyError: # property has no svn:externals |
|---|
| 106 | pass |
|---|
| 107 | else: |
|---|
| 108 | if os.path.splitext(node.path)[1].lower() not in ignore_ext: |
|---|
| 109 | fnodes.append({ |
|---|
| 110 | 'path': node.path, |
|---|
| 111 | 'rev': node.rev, |
|---|
| 112 | 'change_rev':node.created_rev, |
|---|
| 113 | 'hash': hash_from_file_node(node) |
|---|
| 114 | }) |
|---|
| 115 | return errors |
|---|
| 116 | |
|---|
| 117 | |
|---|
| 118 | def file_data_from_repo(node, keyword_substitution=False): |
|---|
| 119 | |
|---|
| 120 | dat = u'' |
|---|
| 121 | if keyword_substitution: |
|---|
| 122 | content = node.get_processed_content() |
|---|
| 123 | else: |
|---|
| 124 | content = node.get_content() |
|---|
| 125 | res = content.read() |
|---|
| 126 | while res: |
|---|
| 127 | dat += res.decode('utf-8') # We assume 'utf-8' here. In fact it may be anything. |
|---|
| 128 | res = content.read() |
|---|
| 129 | return dat.splitlines() |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | def get_repository_dict(env): |
|---|
| 133 | """Get a dict with information about all repositories. |
|---|
| 134 | |
|---|
| 135 | :param env: Trac environment object |
|---|
| 136 | :return: dict with key = reponame, value = dict with information about repository. |
|---|
| 137 | |
|---|
| 138 | The information about a repository is queried using ''get_all_repositories'' from |
|---|
| 139 | RepositoryManager. |
|---|
| 140 | - For any real repository (that means not an alias) the Repository object |
|---|
| 141 | is inserted into the dictionary using the key 'repo'. |
|---|
| 142 | - For any real repository (that means not an alias) a prefix is calculated from the url info |
|---|
| 143 | and inserted using the key 'prefix'. This prefix is used to build paths into the repository. |
|---|
| 144 | |
|---|
| 145 | """ |
|---|
| 146 | repoman = RepositoryManager(env) |
|---|
| 147 | |
|---|
| 148 | repolist = repoman.get_all_repositories() # repolist is a dict with key = reponame, val = dict |
|---|
| 149 | for repo in repoman.get_real_repositories(): |
|---|
| 150 | repolist[repo.reponame]['repo'] = repoman.get_repository(repo.reponame) |
|---|
| 151 | # We need the last part of the path later when following externals |
|---|
| 152 | try: |
|---|
| 153 | repolist[repo.reponame]['prefix'] = '/' + os.path.basename(repolist[repo.reponame]['url'].rstrip('/')) |
|---|
| 154 | except KeyError: |
|---|
| 155 | repolist[repo.reponame]['prefix'] = '' |
|---|
| 156 | return repolist |
|---|
| 157 | |
|---|
| 158 | |
|---|
| 159 | def insert_project_files(self, src_path, project, ignore_ext, follow_ext=False, rev=None, reponame=''): |
|---|
| 160 | """Add project files to the database. |
|---|
| 161 | |
|---|
| 162 | :param self: Trac component object |
|---|
| 163 | :param src_path |
|---|
| 164 | """ |
|---|
| 165 | repolist = get_repository_dict(self.env) |
|---|
| 166 | try: |
|---|
| 167 | repos = repolist[reponame]['repo'] |
|---|
| 168 | except KeyError: |
|---|
| 169 | return |
|---|
| 170 | |
|---|
| 171 | if not repos: |
|---|
| 172 | return |
|---|
| 173 | |
|---|
| 174 | if rev: |
|---|
| 175 | rev = repos.normalize_rev(rev) |
|---|
| 176 | rev_or_latest = rev or repos.youngest_rev |
|---|
| 177 | |
|---|
| 178 | root_node = get_node(repos, src_path, rev_or_latest) |
|---|
| 179 | |
|---|
| 180 | fnodes = [] |
|---|
| 181 | if root_node.isdir: |
|---|
| 182 | errors = get_nodes_for_dir(self.env, repolist, root_node, fnodes, ignore_ext, follow_ext) |
|---|
| 183 | else: |
|---|
| 184 | errors = [] |
|---|
| 185 | |
|---|
| 186 | ReviewFileModel.delete_files_by_project_name(self.env, project) |
|---|
| 187 | @self.env.with_transaction() |
|---|
| 188 | def insert_data(db): |
|---|
| 189 | cursor = db.cursor() |
|---|
| 190 | for item in fnodes: |
|---|
| 191 | cursor.execute("INSERT INTO peerreviewfile" |
|---|
| 192 | "(review_id,path,line_start,line_end,repo,revision, changerevision,hash,project)" |
|---|
| 193 | "VALUES (0, %s, 0, 0, %s, %s, %s, %s, %s)", |
|---|
| 194 | (item['path'], reponame, item['rev'], item['change_rev'], item['hash'], project)) |
|---|
| 195 | |
|---|
| 196 | return errors, len(fnodes) |
|---|