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