def in_svn(p, require_in_repo=False, prefix=""): """ Check if a given file/folder is being tracked by Subversion. Prior to SVN 1.6, we could "cheat" and look for the existence of ".svn" directories. With SVN 1.7 and beyond, WC-NG means only a single top-level ".svn" at the root of the working-copy. Use "svn status" to check the status of the file/folder. """ entries = svnclient.status(p, non_recursive=True) if not entries: return False d = entries[0] if require_in_repo and (d["status"] == "added" or d["revision"] is None): # If caller requires this path to be in the SVN repo, prevent returning True # for paths that are only locally-added. ret = False else: # Don't consider files tracked as deleted in the WC as under source-control. # Consider files which are locally added/copied as under source-control. ret = ( True if not (d["status"] == "deleted") and (d["type"] == "normal" or d["status"] == "added" or d["copied"] == "true") else False ) ui.status( prefix + ">> in_svn('%s', require_in_repo=%s) --> %s", p, str(require_in_repo), str(ret), level=ui.DEBUG, color="GREEN", ) return ret
def in_svn(p, require_in_repo=False, prefix=""): """ Check if a given file/folder is being tracked by Subversion. Prior to SVN 1.6, we could "cheat" and look for the existence of ".svn" directories. With SVN 1.7 and beyond, WC-NG means only a single top-level ".svn" at the root of the working-copy. Use "svn status" to check the status of the file/folder. """ entries = svnclient.status(p, non_recursive=True) if not entries: return False d = entries[0] if require_in_repo and (d['status'] == 'added' or d['revision'] is None): # If caller requires this path to be in the SVN repo, prevent returning True # for paths that are only locally-added. ret = False else: # Don't consider files tracked as deleted in the WC as under source-control. # Consider files which are locally added/copied as under source-control. ret = True if not (d['status'] == 'deleted') and ( d['type'] == 'normal' or d['status'] == 'added' or d['copied'] == 'true') else False ui.status(prefix + ">> in_svn('%s', require_in_repo=%s) --> %s", p, str(require_in_repo), str(ret), level=ui.DEBUG, color='GREEN') return ret
def _run_raw_command(cmd, args, fail_if_stderr=False, no_fail=False): cmd_string = "%s %s" % (cmd, " ".join(map(shell_quote, args))) color = 'BLUE_B' if cmd == 'svn' and args[0] in [ 'status', 'st', 'log', 'info', 'list', 'proplist', 'propget', 'update', 'up', 'cleanup', 'revert' ]: # Show status-only commands (commands which make no changes to WC) in dim-blue color = 'BLUE' ui.status("$ %s", cmd_string, level=ui.EXTRA, color=color) try: pipe = Popen([cmd] + args, executable=cmd, stdout=PIPE, stderr=PIPE) except OSError: etype, value = sys.exc_info()[:2] raise ExternalCommandFailed( "Failed running external program: %s\nError: %s" % (cmd_string, "".join(traceback.format_exception_only(etype, value)))) out, err = pipe.communicate() if "nothing changed" == out.strip(): # skip this error return out if (pipe.returncode != 0 or (fail_if_stderr and err.strip())) and not no_fail: raise ExternalCommandFailed( "External program failed (return code %d): %s\n%s\n%s" % (pipe.returncode, cmd_string, err, out)) return out
def _run_raw_shell_command(cmd, no_fail=False): ui.status("* %s", cmd, level=ui.EXTRA, color='BLUE') st, out = commands.getstatusoutput(cmd) if st != 0 and not no_fail: raise ExternalCommandFailed( "External program failed with non-zero return code (%d): %s\n%s" % (st, cmd, out)) return out
def _run_raw_command(cmd, args, fail_if_stderr=False, no_fail=False): cmd_string = "%s %s" % (cmd, " ".join(map(shell_quote, args))) color = 'BLUE_B' if cmd == 'svn' and args[0] in ['status', 'st', 'log', 'info', 'list', 'proplist', 'propget', 'update', 'up', 'cleanup', 'revert']: # Show status-only commands (commands which make no changes to WC) in dim-blue color = 'BLUE' ui.status("$ %s", cmd_string, level=ui.EXTRA, color=color) try: pipe = Popen([cmd] + args, executable=cmd, stdout=PIPE, stderr=PIPE) except OSError: etype, value = sys.exc_info()[:2] raise ExternalCommandFailed( "Failed running external program: %s\nError: %s" % (cmd_string, "".join(traceback.format_exception_only(etype, value)))) out, err = pipe.communicate() if "nothing changed" == out.strip(): # skip this error return out if (pipe.returncode != 0 or (fail_if_stderr and err.strip())) and not no_fail: raise ExternalCommandFailed( "External program failed (return code %d): %s\n%s\n%s" % (pipe.returncode, cmd_string, err, out)) return out
def real_main(args): global options url = args.pop(0) ui.status("url: %s", url, level=ui.DEBUG, color='GREEN') info = svnclient.info(url) repos_root = info['repos_url'] repos_path = url[len(repos_root):] ancestors = find_svn_ancestors(repos_root, repos_path, options.revision) if ancestors: max_len = 0 for idx in range(len(ancestors)): d = ancestors[idx] max_len = max(max_len, len(d['path'] + "@" + str(d['revision']))) for idx in range(len(ancestors)): d = ancestors[idx] ui.status("[%s] %s --> %s", len(ancestors) - idx - 1, str(d['path'] + "@" + str(d['revision'])).ljust(max_len), str(d['copyfrom_path'] + "@" + str(d['copyfrom_rev']))) else: ui.status("No ancestor-chain found: %s", repos_root + repos_path + "@" + str(options.revision)) return 0
def find_svn_ancestors(svn_repos_url, start_path, start_rev, stop_base_path=None, prefix=""): """ Given an initial starting path+rev, walk the SVN history backwards to inspect the ancestry of that path, optionally seeing if it traces back to stop_base_path. Build an array of copyfrom_path and copyfrom_revision pairs for each of the "svn copy"'s. If we find a copyfrom_path which stop_base_path is a substring match of (e.g. we crawled back to the initial branch-copy from trunk), then return the collection of ancestor paths. Otherwise, copyfrom_path has no ancestry compared to stop_base_path. This is useful when comparing "trunk" vs. "branch" paths, to handle cases where a file/folder was renamed in a branch and then that branch was merged back to trunk. 'svn_repos_url' is the full URL to the root of the SVN repository, e.g. 'file:///path/to/repo' 'start_path' is the path in the SVN repo to the source path to start checking ancestry at, e.g. '/branches/fix1/projectA/file1.txt'. 'start_rev' is the revision to start walking the history of start_path backwards from. 'stop_base_path' is the path in the SVN repo to stop tracing ancestry once we've reached, i.e. the target path we're trying to trace ancestry back to, e.g. '/trunk'. """ ui.status( prefix + ">> find_svn_ancestors: Start: (%s) start_path: %s stop_base_path: %s", svn_repos_url, start_path + "@" + str(start_rev), stop_base_path, level=ui.DEBUG, color='YELLOW') done = False no_ancestry = False cur_path = start_path cur_rev = start_rev first_iter_done = False ancestors = [] while not done: # Get the first "svn log" entry for cur_path (relative to @cur_rev) ui.status(prefix + ">> find_svn_ancestors: %s", svn_repos_url + cur_path + "@" + str(cur_rev), level=ui.DEBUG, color='YELLOW') log_entry = svnclient.get_first_svn_log_entry(svn_repos_url + cur_path, 1, cur_rev) if not log_entry: ui.status(prefix + ">> find_svn_ancestors: Done: no log_entry", level=ui.DEBUG, color='YELLOW') done = True break # If we found a copy-from case which matches our stop_base_path, we're done. # ...but only if we've at least tried to search for the first copy-from path. if stop_base_path is not None and first_iter_done and is_child_path( cur_path, stop_base_path): ui.status( prefix + ">> find_svn_ancestors: Done: Found is_child_path(cur_path, stop_base_path) and first_iter_done=True", level=ui.DEBUG, color='YELLOW') done = True break first_iter_done = True # Search for any actions on our target path (or parent paths). changed_paths_temp = [] for d in log_entry['changed_paths']: path = d['path'] if is_child_path(cur_path, path): changed_paths_temp.append({'path': path, 'data': d}) if not changed_paths_temp: # If no matches, then we've hit the end of the ancestry-chain. ui.status(prefix + ">> find_svn_ancestors: Done: No matching changed_paths", level=ui.DEBUG, color='YELLOW') done = True continue # Reverse-sort any matches, so that we start with the most-granular (deepest in the tree) path. changed_paths = sorted(changed_paths_temp, key=operator.itemgetter('path'), reverse=True) # Find the action for our cur_path in this revision. Use a loop to check in reverse order, # so that if the target file/folder is "M" but has a parent folder with an "A" copy-from # then we still correctly match the deepest copy-from. for v in changed_paths: d = v['data'] path = d['path'] # Check action-type for this file action = d['action'] if action not in svnclient.valid_svn_actions: raise UnsupportedSVNAction( "In SVN rev. %d: action '%s' not supported. Please report a bug!" % (log_entry['revision'], action)) ui.status( prefix + "> %s %s%s", action, path, (" (from %s)" % (d['copyfrom_path'] + "@" + str(d['copyfrom_revision']))) if d['copyfrom_path'] else "", level=ui.DEBUG, color='YELLOW') if action == 'D': # If file/folder was deleted, ancestry-chain stops here if stop_base_path: no_ancestry = True ui.status(prefix + ">> find_svn_ancestors: Done: deleted", level=ui.DEBUG, color='YELLOW') done = True break if action in 'RA': # If file/folder was added/replaced but not a copy, ancestry-chain stops here if not d['copyfrom_path']: if stop_base_path: no_ancestry = True ui.status( prefix + ">> find_svn_ancestors: Done: %s with no copyfrom_path", "Added" if action == "A" else "Replaced", level=ui.DEBUG, color='YELLOW') done = True break # Else, file/folder was added/replaced and is a copy, so add an entry to our ancestors list # and keep checking for ancestors ui.status( prefix + ">> find_svn_ancestors: Found copy-from (action=%s): %s --> %s", action, path, d['copyfrom_path'] + "@" + str(d['copyfrom_revision']), level=ui.DEBUG, color='YELLOW') ancestors.append({ 'path': cur_path, 'revision': log_entry['revision'], 'copyfrom_path': cur_path.replace(d['path'], d['copyfrom_path']), 'copyfrom_rev': d['copyfrom_revision'] }) cur_path = cur_path.replace(d['path'], d['copyfrom_path']) cur_rev = d['copyfrom_revision'] # Follow the copy and keep on searching break if stop_base_path and no_ancestry: # If we're tracing back ancestry to a specific target stop_base_path and # the ancestry-chain stopped before we reached stop_base_path, then return # nothing since there is no ancestry chaining back to that target. ancestors = [] if ancestors: if ui.get_level() >= ui.DEBUG: max_len = 0 for idx in range(len(ancestors)): d = ancestors[idx] max_len = max(max_len, len(d['path'] + "@" + str(d['revision']))) ui.status(prefix + ">> find_svn_ancestors: Found parent ancestors:", level=ui.DEBUG, color='YELLOW_B') for idx in range(len(ancestors)): d = ancestors[idx] ui.status( prefix + " [%s] %s --> %s", idx, str(d['path'] + "@" + str(d['revision'])).ljust(max_len), str(d['copyfrom_path'] + "@" + str(d['copyfrom_rev'])), level=ui.DEBUG, color='YELLOW') else: ui.status(prefix + ">> find_svn_ancestors: No ancestor-chain found: %s", svn_repos_url + start_path + "@" + str(start_rev), level=ui.DEBUG, color='YELLOW') return ancestors
def find_svn_ancestors(svn_repos_url, start_path, start_rev, stop_base_path=None, prefix=""): """ Given an initial starting path+rev, walk the SVN history backwards to inspect the ancestry of that path, optionally seeing if it traces back to stop_base_path. Build an array of copyfrom_path and copyfrom_revision pairs for each of the "svn copy"'s. If we find a copyfrom_path which stop_base_path is a substring match of (e.g. we crawled back to the initial branch-copy from trunk), then return the collection of ancestor paths. Otherwise, copyfrom_path has no ancestry compared to stop_base_path. This is useful when comparing "trunk" vs. "branch" paths, to handle cases where a file/folder was renamed in a branch and then that branch was merged back to trunk. 'svn_repos_url' is the full URL to the root of the SVN repository, e.g. 'file:///path/to/repo' 'start_path' is the path in the SVN repo to the source path to start checking ancestry at, e.g. '/branches/fix1/projectA/file1.txt'. 'start_rev' is the revision to start walking the history of start_path backwards from. 'stop_base_path' is the path in the SVN repo to stop tracing ancestry once we've reached, i.e. the target path we're trying to trace ancestry back to, e.g. '/trunk'. """ ui.status( prefix + ">> find_svn_ancestors: Start: (%s) start_path: %s stop_base_path: %s", svn_repos_url, start_path + "@" + str(start_rev), stop_base_path, level=ui.DEBUG, color="YELLOW", ) done = False no_ancestry = False cur_path = start_path cur_rev = start_rev first_iter_done = False ancestors = [] while not done: # Get the first "svn log" entry for cur_path (relative to @cur_rev) ui.status( prefix + ">> find_svn_ancestors: %s", svn_repos_url + cur_path + "@" + str(cur_rev), level=ui.DEBUG, color="YELLOW", ) log_entry = svnclient.get_first_svn_log_entry(svn_repos_url + cur_path, 1, cur_rev) if not log_entry: ui.status(prefix + ">> find_svn_ancestors: Done: no log_entry", level=ui.DEBUG, color="YELLOW") done = True break # If we found a copy-from case which matches our stop_base_path, we're done. # ...but only if we've at least tried to search for the first copy-from path. if stop_base_path is not None and first_iter_done and is_child_path(cur_path, stop_base_path): ui.status( prefix + ">> find_svn_ancestors: Done: Found is_child_path(cur_path, stop_base_path) and first_iter_done=True", level=ui.DEBUG, color="YELLOW", ) done = True break first_iter_done = True # Search for any actions on our target path (or parent paths). changed_paths_temp = [] for d in log_entry["changed_paths"]: path = d["path"] if is_child_path(cur_path, path): changed_paths_temp.append({"path": path, "data": d}) if not changed_paths_temp: # If no matches, then we've hit the end of the ancestry-chain. ui.status(prefix + ">> find_svn_ancestors: Done: No matching changed_paths", level=ui.DEBUG, color="YELLOW") done = True continue # Reverse-sort any matches, so that we start with the most-granular (deepest in the tree) path. changed_paths = sorted(changed_paths_temp, key=operator.itemgetter("path"), reverse=True) # Find the action for our cur_path in this revision. Use a loop to check in reverse order, # so that if the target file/folder is "M" but has a parent folder with an "A" copy-from # then we still correctly match the deepest copy-from. for v in changed_paths: d = v["data"] path = d["path"] # Check action-type for this file action = d["action"] if action not in svnclient.valid_svn_actions: raise UnsupportedSVNAction( "In SVN rev. %d: action '%s' not supported. Please report a bug!" % (log_entry["revision"], action) ) ui.status( prefix + "> %s %s%s", action, path, (" (from %s)" % (d["copyfrom_path"] + "@" + str(d["copyfrom_revision"]))) if d["copyfrom_path"] else "", level=ui.DEBUG, color="YELLOW", ) if action == "D": # If file/folder was deleted, ancestry-chain stops here if stop_base_path: no_ancestry = True ui.status(prefix + ">> find_svn_ancestors: Done: deleted", level=ui.DEBUG, color="YELLOW") done = True break if action in "RA": # If file/folder was added/replaced but not a copy, ancestry-chain stops here if not d["copyfrom_path"]: if stop_base_path: no_ancestry = True ui.status( prefix + ">> find_svn_ancestors: Done: %s with no copyfrom_path", "Added" if action == "A" else "Replaced", level=ui.DEBUG, color="YELLOW", ) done = True break # Else, file/folder was added/replaced and is a copy, so add an entry to our ancestors list # and keep checking for ancestors ui.status( prefix + ">> find_svn_ancestors: Found copy-from (action=%s): %s --> %s", action, path, d["copyfrom_path"] + "@" + str(d["copyfrom_revision"]), level=ui.DEBUG, color="YELLOW", ) ancestors.append( { "path": cur_path, "revision": log_entry["revision"], "copyfrom_path": cur_path.replace(d["path"], d["copyfrom_path"]), "copyfrom_rev": d["copyfrom_revision"], } ) cur_path = cur_path.replace(d["path"], d["copyfrom_path"]) cur_rev = d["copyfrom_revision"] # Follow the copy and keep on searching break if stop_base_path and no_ancestry: # If we're tracing back ancestry to a specific target stop_base_path and # the ancestry-chain stopped before we reached stop_base_path, then return # nothing since there is no ancestry chaining back to that target. ancestors = [] if ancestors: if ui.get_level() >= ui.DEBUG: max_len = 0 for idx in range(len(ancestors)): d = ancestors[idx] max_len = max(max_len, len(d["path"] + "@" + str(d["revision"]))) ui.status(prefix + ">> find_svn_ancestors: Found parent ancestors:", level=ui.DEBUG, color="YELLOW_B") for idx in range(len(ancestors)): d = ancestors[idx] ui.status( prefix + " [%s] %s --> %s", idx, str(d["path"] + "@" + str(d["revision"])).ljust(max_len), str(d["copyfrom_path"] + "@" + str(d["copyfrom_rev"])), level=ui.DEBUG, color="YELLOW", ) else: ui.status( prefix + ">> find_svn_ancestors: No ancestor-chain found: %s", svn_repos_url + start_path + "@" + str(start_rev), level=ui.DEBUG, color="YELLOW", ) return ancestors