1#!/usr/bin/env python 2# 3# ==================================================================== 4# Licensed to the Apache Software Foundation (ASF) under one 5# or more contributor license agreements. See the NOTICE file 6# distributed with this work for additional information 7# regarding copyright ownership. The ASF licenses this file 8# to you under the Apache License, Version 2.0 (the 9# "License"); you may not use this file except in compliance 10# with the License. You may obtain a copy of the License at 11# 12# http://www.apache.org/licenses/LICENSE-2.0 13# 14# Unless required by applicable law or agreed to in writing, 15# software distributed under the License is distributed on an 16# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17# KIND, either express or implied. See the License for the 18# specific language governing permissions and limitations 19# under the License. 20# ==================================================================== 21import svn 22import sys 23import os 24import getopt 25import hashlib 26import pickle 27import getpass 28from svn import client, core, ra, wc 29 30## This script first fetches the mergeinfo of the working copy and tries 31## to fetch the location segments for the source paths in the respective 32## revisions present in the mergeinfo. With the obtained location segments 33## result, it creates a new mergeinfo. The depth is infinity by default. 34## This script would stop proceeding if there are any local modifications in the 35## working copy. 36 37try: 38 my_getopt = getopt.gnu_getopt 39except AttributeError: 40 my_getopt = getopt.getopt 41mergeinfo = {} 42 43def usage(): 44 sys.stderr.write(""" Usage: %s WCPATH [OPTION] 45 46Analyze the mergeinfo property of the given WCPATH. 47Look for the existence of merge_source's locations at their recorded 48merge ranges. If non-existent merge source is found fix the mergeinfo. 49 50Valid Options: 51 -f [--fix] : set the svn:mergeinfo property. Not committing the changes. 52 -h [--help] : display the usage 53 54""" % os.path.basename(sys.argv[0]) ) 55 56 57## 58# This function would 'svn propset' the new mergeinfo to the working copy 59## 60def set_new_mergeinfo(wcpath, newmergeinfo, ctx): 61 client.propset3("svn:mergeinfo", newmergeinfo, wcpath, core.svn_depth_empty, 62 0, core.SVN_INVALID_REVNUM, None, None, ctx) 63 64 65## 66# Returns the md5 hash of the file 67## 68def md5_of_file(f, block_size = 2*20): 69 md5 = hashlib.md5() 70 while True: 71 data = f.read(block_size) 72 if not data: 73 break 74 md5.update(data) 75 return md5.digest() 76 77 78 79def hasher(hash_file, newmergeinfo_file): 80 new_mergeinfo = core.svn_mergeinfo_to_string(mergeinfo) 81 with open(newmergeinfo_file, "a") as buffer_file: 82 pickle.dump(new_mergeinfo, buffer_file) 83 buffer_file.close() 84 85 with open(newmergeinfo_file, "rb") as buffer_file: 86 hash_of_buffer_file = md5_of_file(buffer_file) 87 buffer_file.close() 88 89 with open(hash_file, "w") as hash_file: 90 pickle.dump(hash_of_buffer_file, hash_file) 91 hash_file.close() 92 93 94def location_segment_callback(segment, pool): 95 if segment.path is not None: 96 source_path = '/' + segment.path 97 path_ranges = mergeinfo.get(source_path, []) 98 range = svn.core.svn_merge_range_t() 99 range.start = segment.range_start - 1 100 range.end = segment.range_end 101 range.inheritable = 1 102 path_ranges.append(range) 103 mergeinfo[source_path] = path_ranges 104 105## 106# This function does the authentication in an interactive way 107## 108def prompt_func_ssl_unknown_cert(realm, failures, cert_info, may_save, pool): 109 print("The certificate details are as follows:") 110 print("--------------------------------------") 111 print("Issuer : " + str(cert_info.issuer_dname)) 112 print("Hostname : " + str(cert_info.hostname)) 113 print("ValidFrom : " + str(cert_info.valid_from)) 114 print("ValidUpto : " + str(cert_info.valid_until)) 115 print("Fingerprint: " + str(cert_info.fingerprint)) 116 print("") 117 ssl_trust = core.svn_auth_cred_ssl_server_trust_t() 118 if may_save: 119 choice = raw_input( "accept (t)temporarily (p)permanently: ") 120 else: 121 choice = raw_input( "(r)Reject or accept (t)temporarily: ") 122 if choice[0] == "t" or choice[0] == "T": 123 ssl_trust.may_save = False 124 ssl_trust.accepted_failures = failures 125 elif choice[0] == "p" or choice[0] == "P": 126 ssl_trust.may_save = True 127 ssl_trust.accepted_failures = failures 128 else: 129 ssl_trust = None 130 return ssl_trust 131 132def prompt_func_simple_prompt(realm, username, may_save, pool): 133 username = raw_input("username: ") 134 password = getpass.getpass(prompt="password: ") 135 simple_cred = core.svn_auth_cred_simple_t() 136 simple_cred.username = username 137 simple_cred.password = password 138 simple_cred.may_save = False 139 return simple_cred 140 141## 142# This function tries to authenticate(if needed) and fetch the 143# location segments for the available mergeinfo and create a new 144# mergeinfo dictionary 145## 146def get_new_location_segments(parsed_original_mergeinfo, repo_root, 147 wcpath, ctx): 148 149 for path in parsed_original_mergeinfo: 150 full_url = repo_root + path 151 ra_callbacks = ra.callbacks_t() 152 ra_callbacks.auth_baton = core.svn_auth_open([ 153 core.svn_auth_get_ssl_server_trust_file_provider(), 154 core.svn_auth_get_simple_prompt_provider(prompt_func_simple_prompt, 2), 155 core.svn_auth_get_ssl_server_trust_prompt_provider(prompt_func_ssl_unknown_cert), 156 svn.client.get_simple_provider(), 157 svn.client.get_username_provider() 158 ]) 159 try: 160 ctx.config = core.svn_config_get_config(None) 161 ra_session = ra.open(full_url, ra_callbacks, None, ctx.config) 162 163 for revision_range in parsed_original_mergeinfo[path]: 164 try: 165 ra.get_location_segments(ra_session, "", revision_range.end, 166 revision_range.end, revision_range.start + 1, location_segment_callback) 167 except svn.core.SubversionException: 168 sys.stderr.write(" Could not find location segments for %s \n" % path) 169 except Exception as e: 170 sys.stderr.write("") 171 172 173def sanitize_mergeinfo(parsed_original_mergeinfo, repo_root, wcpath, 174 ctx, hash_file, newmergeinfo_file, temp_pool): 175 full_mergeinfo = {} 176 for entry in parsed_original_mergeinfo: 177 get_new_location_segments(parsed_original_mergeinfo[entry], repo_root, wcpath, ctx) 178 full_mergeinfo.update(parsed_original_mergeinfo[entry]) 179 180 hasher(hash_file, newmergeinfo_file) 181 diff_mergeinfo = core.svn_mergeinfo_diff(full_mergeinfo, 182 mergeinfo, 1, temp_pool) 183 #There should be no mergeinfo added by our population. There should only 184 #be deletion of mergeinfo. so take it from diff_mergeinfo[0] 185 print("The bogus mergeinfo summary:") 186 bogus_mergeinfo_deleted = diff_mergeinfo[0] 187 for bogus_mergeinfo_path in bogus_mergeinfo_deleted: 188 sys.stdout.write(bogus_mergeinfo_path + ": ") 189 for revision_range in bogus_mergeinfo_deleted[bogus_mergeinfo_path]: 190 sys.stdout.write(str(revision_range.start + 1) + "-" + str(revision_range.end) + ",") 191 print("") 192 193## 194# This function tries to 'propset the new mergeinfo into the working copy. 195# It reads the new mergeinfo from the .newmergeinfo file and verifies its 196# hash against the hash in the .hashfile 197## 198def fix_sanitized_mergeinfo(parsed_original_mergeinfo, repo_root, wcpath, 199 ctx, hash_file, newmergeinfo_file, temp_pool): 200 has_local_modification = check_local_modifications(wcpath, temp_pool) 201 old_hash = '' 202 new_hash = '' 203 try: 204 with open(hash_file, "r") as f: 205 old_hash = pickle.load(f) 206 f.close 207 except IOError as e: 208 get_new_location_segments(parsed_original_mergeinfo, repo_root, wcpath, ctx) 209 hasher(hash_file, newmergeinfo_file) 210 try: 211 with open(hash_file, "r") as f: 212 old_hash = pickle.load(f) 213 f.close 214 except IOError: 215 hasher(hash_file, newmergeinfo_file) 216 try: 217 with open(newmergeinfo_file, "r") as f: 218 new_hash = md5_of_file(f) 219 f.close 220 except IOError as e: 221 if not mergeinfo: 222 get_new_location_segments(parsed_original_mergeinfo, repo_root, wcpath, ctx) 223 hasher(hash_file, newmergeinfo_file) 224 with open(newmergeinfo_file, "r") as f: 225 new_hash = md5_of_file(f) 226 f.close 227 if old_hash == new_hash: 228 with open(newmergeinfo_file, "r") as f: 229 newmergeinfo = pickle.load(f) 230 f.close 231 set_new_mergeinfo(wcpath, newmergeinfo, ctx) 232 if os.path.exists(newmergeinfo_file): 233 os.remove(newmergeinfo_file) 234 os.remove(hash_file) 235 else: 236 print("The hashes are not matching. Probable chance of unwanted tweaking in the mergeinfo") 237 238 239## 240# This function checks the working copy for any local modifications 241## 242def check_local_modifications(wcpath, temp_pool): 243 has_local_mod = wc.svn_wc_revision_status(wcpath, None, 0, None, temp_pool) 244 if has_local_mod.modified: 245 print("""The working copy has local modifications. Please revert them or clean 246the working copy before running the script.""") 247 sys.exit(1) 248 249def get_original_mergeinfo(wcpath, revision, depth, ctx, temp_pool): 250 propget_list = client.svn_client_propget3("svn:mergeinfo", wcpath, 251 revision, revision, depth, None, 252 ctx, temp_pool) 253 254 pathwise_mergeinfo = "" 255 pathwise_mergeinfo_list = [] 256 mergeinfo_catalog = propget_list[0] 257 mergeinfo_catalog_dict = {} 258 for entry in mergeinfo_catalog: 259 mergeinfo_catalog_dict[entry] = core.svn_mergeinfo_parse(mergeinfo_catalog[entry], temp_pool) 260 return mergeinfo_catalog_dict 261 262 263def main(): 264 try: 265 opts, args = my_getopt(sys.argv[1:], "h?f", ["help", "fix"]) 266 except Exception as e: 267 sys.stderr.write(""" Improperly used """) 268 sys.exit(1) 269 270 if len(args) == 1: 271 wcpath = args[0] 272 wcpath = os.path.abspath(wcpath) 273 else: 274 usage() 275 sys.exit(1) 276 277 fix = 0 278 current_path = os.getcwd() 279 hash_file = os.path.join(current_path, ".hashfile") 280 newmergeinfo_file = os.path.join(current_path, ".newmergeinfo") 281 282 temp_pool = core.svn_pool_create() 283 ctx = client.svn_client_create_context(temp_pool) 284 depth = core.svn_depth_infinity 285 revision = core.svn_opt_revision_t() 286 revision.kind = core.svn_opt_revision_unspecified 287 288 for opt, values in opts: 289 if opt == "--help" or opt in ("-h", "-?"): 290 usage() 291 elif opt == "--fix" or opt == "-f": 292 fix = 1 293 294 # Check for any local modifications in the working copy 295 check_local_modifications(wcpath, temp_pool) 296 297 parsed_original_mergeinfo = get_original_mergeinfo(wcpath, revision, 298 depth, ctx, temp_pool) 299 300 repo_root = client.svn_client_root_url_from_path(wcpath, ctx, temp_pool) 301 302 core.svn_config_ensure(None) 303 304 if fix == 0: 305 sanitize_mergeinfo(parsed_original_mergeinfo, repo_root, wcpath, ctx, 306 hash_file, newmergeinfo_file, temp_pool) 307 if fix == 1: 308 fix_sanitized_mergeinfo(parsed_original_mergeinfo, repo_root, wcpath, 309 ctx, hash_file, newmergeinfo_file, temp_pool) 310 311 312if __name__ == "__main__": 313 try: 314 main() 315 except KeyboardInterrupt: 316 print("") 317 sys.stderr.write("The script is interrupted and stopped manually.") 318 print("") 319 320