1 2from hgsvn.errors import ( 3 EmptySVNLog 4 , ExternalCommandFailed 5 , RunCommandError 6) 7from hgsvn import ui, shell 8from .shell import once_or_more, run_command 9from .ui import encodes 10 11import sys 12import os 13import time 14import calendar 15import operator 16import re 17import functools 18from six import string_types, binary_type, text_type, b 19from six.moves import urllib 20import traceback 21import codecs 22 23try: 24 from xml.etree import cElementTree as ET 25except ImportError: 26 try: 27 from xml.etree import ElementTree as ET 28 except ImportError: 29 try: 30 import cElementTree as ET 31 except ImportError: 32 from elementtree import ElementTree as ET 33 34 35svn_log_args = ['log', '--xml', '-v'] 36svn_info_args = ['info', '--xml'] 37svn_checkout_args = ['checkout', '-q'] 38svn_status_args = ['status', '--xml', '--ignore-externals'] 39 40_identity_table = "".join(map(chr, range(256))) 41_forbidden_xml_chars = "".join( 42 set( map(chr, range(32)) ) - set('\x09\x0A\x0D') 43) 44if sys.version_info[0] >= 3: 45 _forbidden_xml_bytes = b"".join( map(bytes, set( range(32) ) - set([ 9, 0x0A, 0x0D])) ) 46 _forbidden_xml_trans = b"".maketrans(_forbidden_xml_bytes, b"".ljust(len(_forbidden_xml_bytes), b' ' ) ) 47 _forbidden_xml_strans = "".maketrans(_forbidden_xml_chars, "".ljust(len(_forbidden_xml_chars), ' ' ) ) 48else: 49 _forbidden_xml_bytes = binary_type(_forbidden_xml_chars) #b"".join( set( range(32) ) - set([ 9, 0x0A, 0x0D]) ) 50 import string 51 _forbidden_xml_trans = string.maketrans(_forbidden_xml_bytes, b"".ljust(len(_forbidden_xml_bytes), b' ' ) ) 52 _forbidden_xml_strans = _forbidden_xml_trans 53 54def run_svn(args=None, bulk_args=None, fail_if_stderr=False, 55 mask_atsign=False): 56 """ 57 Run an SVN command, returns the (bytes) output. 58 """ 59 if mask_atsign: 60 # The @ sign in Subversion revers to a pegged revision number. 61 # SVN treats files with @ in the filename a bit special. 62 # See: http://stackoverflow.com/questions/1985203 63 for idx in range(len(args)): 64 if "@" in args[idx] and args[idx][0] not in ("-", '"'): 65 args[idx] = "%s@" % args[idx] 66 if bulk_args: 67 for idx in range(len(bulk_args)): 68 if ("@" in bulk_args[idx] 69 and bulk_args[idx][0] not in ("-", '"')): 70 bulk_args[idx] = "%s@" % bulk_args[idx] 71 72 if sys.version_info[0] < 3: 73 def prepare_args(args, target_encode): 74 return encodes(args, target_encode) 75 else: 76 def prepare_args(args, target_encode): 77 return args 78 79 def decode_output(output, enc): 80 # some hg cmds, for example "log" return output with mixed encoded lines, therefore decode them 81 # line by line independently 82 #outlines = output #.splitlines(True) 83 outlines = text_type(output, encoding=enc, errors = 'strict') #.splitlines(True) #'utf-8' 84 return outlines 85 86 enc = shell.locale_encoding 87 88 res = run_command("svn", args=prepare_args(args, enc), bulk_args=prepare_args(bulk_args, enc), fail_if_stderr=fail_if_stderr) 89 90 return res #decode_output(res, enc) 91 92def svn_decode( textline ): 93 loccodec = codecs.lookup(shell.locale_encoding) 94 ucodec = codecs.lookup('utf-8') 95 # some hg cmds, for example "log" return output with mixed encoded lines, therefore decode them 96 # line by line independently 97 lines = textline.splitlines(True) 98 outlines=[] 99 for line in lines: 100 #locale_encoding or 'UTF-8' 101 try: 102 uline, ulen = loccodec.decode(line, errors='strict') 103 except : 104 uline, ulen = ucodec.decode(line, errors='strict') 105 outlines.append(uline) 106 107 return "\n".join(outlines) 108 109 110def strip_forbidden_xml_chars(xml_string): 111 """ 112 Given an XML string, strips forbidden characters as per the XML spec. 113 (these are all control characters except 0x9, 0xA and 0xD). 114 """ 115 #filter(lambda x: x not in _forbidden_xml_chars, xml_string) 116 # portale python 2-3 version of same 117 118 #filt = lambda y, x: y+chr(x) if (chr(x) not in _forbidden_xml_chars) else y 119 #return functools.reduce(filt, xml_string, "") 120 if isinstance(xml_string, binary_type): #text_type 121 out = xml_string.translate(_forbidden_xml_trans) 122 else: 123 out = xml_string.translate(_forbidden_xml_strans) 124 ui.status("xml filtered is:%s"%out, level = ui.PARSE) 125 return out 126 127 128def svn_date_to_timestamp(svn_date): 129 """ 130 Parse an SVN date as read from the XML output and return the corresponding 131 timestamp. 132 """ 133 # Strip microseconds and timezone (always UTC, hopefully) 134 # XXX there are various ISO datetime parsing routines out there, 135 # cf. http://seehuhn.de/comp/pdate 136 date = svn_date.split('.', 2)[0] 137 time_tuple = time.strptime(date, "%Y-%m-%dT%H:%M:%S") 138 return calendar.timegm(time_tuple) 139 140def parse_svn_info_xml(xml_string): 141 """ 142 Parse the XML output from an "svn info" command and extract useful information 143 as a dict. 144 """ 145 d = {} 146 xml_string = strip_forbidden_xml_chars(xml_string) 147 tree = ET.fromstring(xml_string) 148 entry = tree.find('.//entry') 149 d['url'] = urllib.parse.unquote(entry.find('url').text) #.decode('utf8') 150 d['revision'] = int(entry.get('revision')) 151 d['repos_url'] = urllib.parse.unquote(tree.find('.//repository/root').text) #.decode('utf8') 152 d['last_changed_rev'] = int(tree.find('.//commit').get('revision')) 153 author_element = tree.find('.//commit/author') 154 if author_element is not None: 155 d['last_changed_author'] = author_element.text 156 d['last_changed_date'] = svn_date_to_timestamp(tree.find('.//commit/date').text) 157 return d 158 159class svnlog_merge_entry(): 160 revno = -1 161 entry = {} 162 163def parse_svn_log_xml_entry(entry): 164 d = {} 165 d['revision'] = int(entry.get('revision')) 166 # Some revisions don't have authors, most notably the first revision 167 # in a repository. 168 # logentry nodes targeting directories protected by path-based 169 # authentication have no child nodes at all. We return an entry 170 # in that case. Anyway, as it has no path entries, no further 171 # processing will be made. 172 author = entry.find('author') 173 date = entry.find('date') 174 msg = entry.find('msg') 175 src_paths = entry.find('paths') 176 # Issue 64 - modified to prevent crashes on svn log entries with "No author" 177 d['author'] = author is not None and author.text or "No author" 178 if date is not None: 179 d['date'] = svn_date_to_timestamp(date.text) 180 else: 181 d['date'] = None 182 d['message'] = msg is not None and msg.text or "" 183 paths = d['changed_paths'] = [] 184 if not (src_paths is None): 185 for path in src_paths.findall('.//path'): 186 copyfrom_rev = path.get('copyfrom-rev') 187 if copyfrom_rev: 188 copyfrom_rev = int(copyfrom_rev) 189 paths.append({ 190 'path': path.text, 191 'action': path.get('action'), 192 'copyfrom_path': path.get('copyfrom-path'), 193 'copyfrom_revision': copyfrom_rev, 194 }) 195 196 ui.status("svnlog entry: rev=%d "%(d['revision']), level=ui.PARSE) 197 merges = list(); 198 for rev in entry.findall('logentry'): 199 revno = int(rev.get('revision')) 200 if revno == d['revision']: continue 201 revdef = svnlog_merge_entry() 202 revdef.revno = revno 203 ui.status("svnlog r%d merge r%d"%(d['revision'], revno), level=ui.DEBUG) 204 revdef.entry = parse_svn_log_xml_entry(rev) 205 merges.append(revdef) 206 break 207 if len(merges) <= 0: 208 merges = None 209 d['merges'] = merges 210 return d 211 212svnlog_revseq_no = 0 213svnlog_revseq_up = 1 214svnlog_revseq_down = -1 215 216def svnlog_revseq(revstart, revend): 217 if (revstart > revend): 218 return svnlog_revseq_down 219 elif (revstart < revend): 220 return svnlog_revseq_up 221 return svnlog_revseq_no 222 223def parse_svn_log_xml(xml_string, strict_log_seq = True): 224 """ 225 Parse the XML output from an "svn log" command and extract useful information 226 as a list of dicts (one per log changeset). 227 228 strict_log_seq - no - no resisions order stricts 229 - up - revisions must flow incrising order 230 - down - revisions must flow decrising order 231 """ 232 l = [] 233 xml_string = strip_forbidden_xml_chars(xml_string) 234 ui.status("parse_svn_log_xml: filtered xml:%s"%(xml_string), level=ui.PARSE) 235 tree = ET.fromstring(xml_string) 236 last_rev = -1 237 use_strict = 0; 238 entry = tree.find('logentry') 239 240 while not(entry is None): 241 d = parse_svn_log_xml_entry(entry); 242 now_rev = int(d['revision']) 243 if (strict_log_seq): 244 if use_strict == 0: 245 if last_rev >= 0: 246 use_strict = (now_rev - last_rev) 247 else: 248 isup = (use_strict > 0) and (last_rev < now_rev) 249 isdn = (use_strict < 0) and (last_rev > now_rev) 250 if not (isup or isdn): 251 ui.status("svn log broken revisions sequence: %d after %d"%(d['revision'], last_rev), level=ui.WARNING); 252 if ui.is_debug(): 253 ui.status("xml dump:\n %s"%xml_string, level = ui.DEBUG, truncate = False) 254 break 255 l.append(d) 256 last_rev = now_rev 257 tree.remove(entry) 258 entry = tree.find('logentry') 259 return l 260 261def parse_svn_status_xml_entry(tree, base_dir=None, ignore_externals=False): 262 isSVNPre180 = False; 263 l = [] 264 for entry in tree.findall('.//entry'): 265 d = {} 266 path = entry.get('path') 267 if path is None: continue 268 if base_dir is not None: 269 ui.status("svn status path:%s by entry:%s"%(base_dir, path), level=ui.PARSE) 270 if not isSVNPre180: 271 if os.path.normcase(path).startswith(base_dir): 272 ui.status("svn status check version: pre 1.8.0 ", level=ui.PARSE) 273 isSVNPre180 = True 274 path = path[len(base_dir):].lstrip('/\\') 275 else: 276 ui.status("svn status check version: looks > 1.8.0 ", level=ui.PARSE) 277 base_dir = None 278 else: 279 assert os.path.normcase(path).startswith(base_dir) 280 path = path[len(base_dir):].lstrip('/\\') 281 d['path'] = path 282 wc_status = entry.find('wc-status') 283 if wc_status is None: 284 continue 285 if wc_status.get('item') == 'external': 286 if ignore_externals: 287 continue 288 d['type'] = 'external' 289 elif wc_status.get('item') == 'replaced': 290 d['type'] = 'normal' 291 elif wc_status.get('revision') is not None: 292 d['type'] = 'normal' 293 else: 294 d['type'] = 'unversioned' 295 if wc_status.get('tree-conflicted') is None: 296 d['status'] = wc_status.get('item') 297 else: 298 d['status'] = 'conflicted' 299 d['props'] = wc_status.get('props') 300 if d is not None: 301 l.append(d) 302 ui.status("svn status have:%s"%d, level=ui.PARSE) 303 return l 304 305def parse_svn_status_xml(xml_string, base_dir=None, ignore_externals=False): 306 """ 307 Parse the XML output from an "svn status" command and extract useful info 308 as a list of dicts (one per status entry). 309 """ 310 if base_dir: 311 base_dir = os.path.normcase(base_dir) 312 l = [] 313 xml_string = strip_forbidden_xml_chars(xml_string) 314 tree = ET.fromstring(xml_string) 315 for target in tree.findall('.//target'): 316 path = target.get('path') 317 if path is not None: 318 if base_dir is not None: 319 ui.status("svn status target path:%s of base %s"%(path, base_dir), level=ui.PARSE) 320 path = os.path.normcase(path) 321 assert path.startswith(base_dir) 322 #path = path[len(base_dir):].lstrip('/\\') 323 ui.status("svn status target subpath as:\'%s\'"%(path), level=ui.PARSE) 324 if len(path) == 0: 325 path = None 326 else: 327 path=base_dir 328 subl = parse_svn_status_xml_entry(target, path, ignore_externals) 329 if subl is not None: 330 l.extend(subl) 331 tree.remove(target) 332 333 subl = parse_svn_status_xml_entry(tree, base_dir, ignore_externals) 334 if subl is not None: 335 l.extend(subl) 336 337 return l 338 339def get_svn_info(svn_url_or_wc, rev_number=None): 340 """ 341 Get SVN information for the given URL or working copy, with an optionally 342 specified revision number. 343 Returns a dict as created by parse_svn_info_xml(). 344 """ 345 if rev_number is not None: 346 args = ['-r', rev_number] 347 else: 348 args = [] 349 xml_string = run_svn(svn_info_args + args + [svn_url_or_wc], 350 fail_if_stderr=True) 351 return parse_svn_info_xml(xml_string) 352 353def svn_checkout(svn_url, checkout_dir, rev_number=None): 354 """ 355 Checkout the given URL at an optional revision number. 356 """ 357 args = [] 358 if rev_number is not None: 359 args += ['-r', rev_number] 360 args += [svn_url, checkout_dir] 361 return run_svn(svn_checkout_args + args) 362 363def run_svn_log_restricted_merges(svn_url, rev_start, rev_end, limit, stop_on_copy=False): 364 """ 365 Fetch up to 'limit' SVN log entries between the given revisions. 366 """ 367 if stop_on_copy: 368 args = ['--stop-on-copy'] 369 else: 370 args = [] 371 reslog = None 372 args += ['-r','%s:%s' % (rev_start, rev_end), '--limit', limit, svn_url] 373 xml_string = run_svn(svn_log_args + args) 374 reslog = parse_svn_log_xml(xml_string) 375 for element in (reslog): 376 rev = element['revision'] 377 args = ['-r','%s' % rev, '-g', svn_url] 378 xml_string = run_svn(svn_log_args + args) 379 emerges = parse_svn_log_xml(xml_string) #, strict_log_seq = False 380 if len(emerges) != 1: 381 raise 382 element['merges'] = emerges[0]['merges'] 383 return reslog 384 385def run_svn_log(svn_url, rev_start, rev_end, limit, stop_on_copy=False): 386 """ 387 Fetch up to 'limit' SVN log entries between the given revisions. 388 """ 389 if stop_on_copy: 390 args = ['--stop-on-copy'] 391 else: 392 args = [] 393 reslog = None 394 try: 395 args += ['-r','%s:%s' % (rev_start, rev_end), '-g', '--limit', limit, svn_url] 396 xml_string = run_svn(svn_log_args + args) 397 reslog = parse_svn_log_xml(xml_string) 398 except (RunCommandError) as e: 399 if e.err_msg.find("truncated HTTP response body") <= 0: 400 ui.status("svn log failed:%s" % e.msg(noout=True) , level= ui.ERROR) 401 raise 402 ui.status("svn log failed, try to fetch in safer way", level= ui.VERBOSE) 403 reslog = run_svn_log_restricted_merges(svn_url, rev_start, rev_end, limit, stop_on_copy) 404 return reslog 405 406def get_svn_status(svn_wc, quiet=False): 407 """ 408 Get SVN status information about the given working copy. 409 """ 410 # Ensure proper stripping by canonicalizing the path 411 svn_wc = os.path.abspath(svn_wc) 412 args = [svn_wc] 413 if quiet: 414 args += ['-q'] 415 else: 416 args += ['-v'] 417 xml_string = run_svn(svn_status_args + args) 418 return parse_svn_status_xml(xml_string, svn_wc, ignore_externals=True) 419 420def svn_is_clean(svn_wc): 421 svn_wc = os.path.abspath(svn_wc) 422 changes = run_svn(['st', svn_wc ,'-q']) 423 changes = changes.strip() 424 ui.status("svn status is %s changes", len(changes), level=ui.VERBOSE); 425 return (len(changes) <= 0) 426 427def get_svn_versioned_files(svn_wc): 428 """ 429 Get the list of versioned files in the SVN working copy. 430 """ 431 contents = [] 432 for e in get_svn_status(svn_wc): 433 if e['path'] and e['type'] == 'normal': 434 contents.append(e['path']) 435 return contents 436 437 438def get_one_svn_log_entry(svn_url, rev_start, rev_end, stop_on_copy=False): 439 """ 440 Get the first SVN log entry in the requested revision range. 441 """ 442 entries = run_svn_log(svn_url, rev_start, rev_end, 1, stop_on_copy) 443 if entries: 444 return entries[0] 445 raise EmptySVNLog("No SVN log for %s between revisions %s and %s" % 446 (svn_url, rev_start, rev_end)) 447 448 449def get_first_svn_log_entry(svn_url, rev_start, rev_end): 450 """ 451 Get the first log entry after (or at) the given revision number in an SVN branch. 452 By default the revision number is set to 0, which will give you the log 453 entry corresponding to the branch creaction. 454 455 NOTE: to know whether the branch creation corresponds to an SVN import or 456 a copy from another branch, inspect elements of the 'changed_paths' entry 457 in the returned dictionary. 458 """ 459 return get_one_svn_log_entry(svn_url, rev_start, rev_end, stop_on_copy=True) 460 461def get_last_svn_log_entry(svn_url, rev_start, rev_end): 462 """ 463 Get the last log entry before (or at) the given revision number in an SVN branch. 464 By default the revision number is set to HEAD, which will give you the log 465 entry corresponding to the latest commit in branch. 466 """ 467 return get_one_svn_log_entry(svn_url, rev_end, rev_start, stop_on_copy=True) 468 469 470log_duration_threshold = 10.0 471log_min_chunk_length = 10 472 473def iter_svn_log_entries(svn_url, first_rev, last_rev, retry): 474 """ 475 Iterate over SVN log entries between first_rev and last_rev. 476 477 This function features chunked log fetching so that it isn't too nasty 478 to the SVN server if many entries are requested. 479 """ 480 cur_rev = first_rev 481 chunk_length = log_min_chunk_length 482 first_run = True 483 while last_rev == "HEAD" or cur_rev <= last_rev: 484 start_t = time.time() 485 stop_rev = min(last_rev, cur_rev + chunk_length) 486 ui.status("Fetching %s SVN log entries starting from revision %d...", 487 chunk_length, cur_rev, level=ui.VERBOSE) 488 entries = once_or_more("Fetching SVN log", retry, run_svn_log, svn_url, 489 cur_rev, last_rev, chunk_length) 490 duration = time.time() - start_t 491 if not first_run: 492 # skip first revision on subsequent runs, as it is overlapped 493 entries.pop(0) 494 first_run = False 495 if not entries: 496 break 497 for e in entries: 498 if e['revision'] > last_rev: 499 break 500 yield e 501 if e['revision'] >= last_rev: 502 break 503 cur_rev = e['revision'] 504 # Adapt chunk length based on measured request duration 505 if duration < log_duration_threshold: 506 chunk_length = int(chunk_length * 2.0) 507 elif duration > log_duration_threshold * 2: 508 chunk_length = max(log_min_chunk_length, int(chunk_length / 2.0)) 509 510 511_svn_client_version = None 512 513def get_svn_client_version(): 514 """Returns the SVN client version as a tuple. 515 516 The returned tuple only contains numbers, non-digits in version string are 517 silently ignored. 518 """ 519 global _svn_client_version 520 if _svn_client_version is None: 521 raw = run_svn(['--version', '-q']).strip() 522 _svn_client_version = tuple(map(int, [x for x in raw.split(b'.') 523 if x.isdigit()])) 524 return _svn_client_version 525 526def svn_cleanup(): 527 args = ["cleanup"] 528 return run_svn(args) 529 530def svn_resolve_all(): 531 args = ["resolve","-R","--non-interactive","--accept=working","."] 532 return run_svn(args) 533 534def svn_revert_all(): 535 args = ["revert","-R","--non-interactive","."] 536 try: 537 run_svn(args) 538 except (ExternalCommandFailed) as e: 539 if str(e).find("Run 'svn cleanup'") <= 0: 540 raise 541 svn_cleanup() 542 run_svn(args) 543 return 544 545def svn_switch_to(svn_base, rev_number = None, clean = False, ignore_ancetry=False, propagate_rev = False): 546 # propagate_rev - provides smart handle of situation when rev_number is origin of desired branch base, and branch is pure, 547 # so that we can take 548 args = ["switch","--non-interactive","--force"] 549 if ignore_ancetry: 550 args += ["--ignore-ancestry"] 551 if rev_number is not None: 552 svn_base += '@%s'%rev_number 553 args += [svn_base] 554 ui.status("switching to %s" % svn_base, level=ui.DEBUG) 555 if clean: 556 # switching is some like merge - on modified working copy try merges changes and got conflicts 557 # therefor revert all before switch for prepare one 558 svn_revert_all() 559 try: 560 run_svn(args) 561 except (ExternalCommandFailed) as e: 562 ui.status("switch failed with %s"%e, level=ui.ERROR) 563 delpath = re.search(r"Can't remove directory '(?P<delpath>\S+)'", str(e)) 564 nopath = re.search(r" path not found'(?P<nopath>\S+)'", str(e)) 565 if not (delpath is None): 566 #when switchwith externals svn can confuse on removing absent directories 567 if str(e).find("Removed external") <= 0: 568 raise 569 failepath = nestpath.group("delpath") 570 ui.status("try switch again after remove absent dir %s" % failepath, level=ui.VERBOSE) 571 run_svn(args) 572 elif propagate_rev and (not (nopath is None)): 573 ui.status("try to propagate rev_number to branch base if can", level=ui.DEBUG) 574 branch_info = svn_branch_revset() 575 (rev_base, rev_origin, rev_head) = branch_info.eval_path(svn_base) 576 if (rev_base == rev_head) and (rev_origin == rev_number): 577 basehgid = get_hg_cset_of_svn(rev_base) 578 if (len(basehgid) == 0): 579 ui.status('branch svn:%s@%s used as candidate for its origin %s'%(svn_base, rev_base, rev_origin)); 580 origin_hgid = get_hg_cset_of_svn(rev_number) 581 hg_rev_tag_by_svn(origin_hgid, rev_base) 582 svn_switch_to(svn_base, rev_base, clean, ignore_ancetry, False) 583 else: 584 raise 585 except Exception as e: 586 ui.status("err uncknown:%s"%str(e), level = ui.ERROR) 587 raise e 588 589class svn_branch_revset(object): 590 #this class describes svn-branch 591 headrev = None 592 baserev = None 593 source = None 594 srcrev = None 595 path = None 596 597 def __repr__(self): 598 return "svn branch: head:%s base:%s path:%s sourced from %s@%s"%(self.headrev, self.baserev, self.path, self.source, self.srcrev) 599 600 def eval_path(self, svn_base): 601 self.path = svn_base 602 self.source = None 603 try: 604 base = get_first_svn_log_entry(svn_base, 0, 'HEAD') 605 except (RunCommandError) as e: 606 if e.err_have("File not found") or e.err_have("path not found"): 607 ui.status("svn looks have no branch %s" % svn_base) # , level= ui.ERROR 608 base = None 609 else: 610 raise 611 if base is None: 612 self.baserev = None 613 self.headrev = None 614 return self.baserev, self.source, self.headrev 615 head = get_last_svn_log_entry(svn_base, 0, 'HEAD') 616 self.baserev = base['revision'] 617 self.headrev = head['revision'] 618 paths = base['changed_paths'] 619 path = None 620 if len(paths) > 0: 621 for path in paths: 622 if (svn_base.endswith(path['path'])): 623 self.source = path['copyfrom_path'] 624 self.srcrev = path['copyfrom_revision'] 625 break 626 else: 627 ui.status("cant determine branch %s origin source"%svn_base) 628 return self.baserev, self.source, self.headrev 629 630 #true if branch is just a plain copy on another without any evolution 631 def is_clean(self): 632 if (self.baserev != self.headrev): 633 return False 634 if (self.source is None): 635 return False 636 return True 637 638def svn_branch_empty(svn_base): 639 entryes = run_svn_log(svn_base, 0, 'HEAD', 2, stop_on_copy=True) 640 if len(entryes) > 1: 641 return False 642 if len(entryes) == 0: 643 return True 644 base = entryes[0] 645 # the 1st revision can only have 1 path entry - add branch folder 646 if (len(base['changed_paths']) > 1): 647 return False 648 if not(base['merges'] is None): 649 return False 650 return True 651 652def svn_get_prop(prop_name, path, svn_base = '.'): 653 try: 654 report = run_svn(['propget', os.path.join(svn_base, prop_name)], mask_atsign=True) 655 except (RunCommandError) as e: 656 report = "" 657 return report 658