]> Tony Duckles's Git Repositories (git.nynim.org) - svn2svn.git/blob - svn2svn/run/common.py
Introduce 'svnancest' utility
[svn2svn.git] / svn2svn / run / common.py
1 from svn2svn import ui
2 from svn2svn import svnclient
3
4 import operator
5
6
7 def in_svn(p, require_in_repo=False, prefix=""):
8 """
9 Check if a given file/folder is being tracked by Subversion.
10 Prior to SVN 1.6, we could "cheat" and look for the existence of ".svn" directories.
11 With SVN 1.7 and beyond, WC-NG means only a single top-level ".svn" at the root of the working-copy.
12 Use "svn status" to check the status of the file/folder.
13 """
14 entries = svnclient.status(p, non_recursive=True)
15 if not entries:
16 return False
17 d = entries[0]
18 if require_in_repo and (d['status'] == 'added' or d['revision'] is None):
19 # If caller requires this path to be in the SVN repo, prevent returning True
20 # for paths that are only locally-added.
21 ret = False
22 else:
23 # Don't consider files tracked as deleted in the WC as under source-control.
24 # Consider files which are locally added/copied as under source-control.
25 ret = True if not (d['status'] == 'deleted') and (d['type'] == 'normal' or d['status'] == 'added' or d['copied'] == 'true') else False
26 ui.status(prefix + ">> in_svn('%s', require_in_repo=%s) --> %s", p, str(require_in_repo), str(ret), level=ui.DEBUG, color='GREEN')
27 return ret
28
29 def is_child_path(path, p_path):
30 return True if (path == p_path) or (path.startswith(p_path+"/")) else False
31
32 def join_path(base, child):
33 base.rstrip('/')
34 return base+"/"+child if child else base
35
36 def find_svn_ancestors(svn_repos_url, start_path, start_rev, stop_base_path=None, prefix=""):
37 """
38 Given an initial starting path+rev, walk the SVN history backwards to inspect the
39 ancestry of that path, optionally seeing if it traces back to stop_base_path.
40
41 Build an array of copyfrom_path and copyfrom_revision pairs for each of the "svn copy"'s.
42 If we find a copyfrom_path which stop_base_path is a substring match of (e.g. we crawled
43 back to the initial branch-copy from trunk), then return the collection of ancestor
44 paths. Otherwise, copyfrom_path has no ancestry compared to stop_base_path.
45
46 This is useful when comparing "trunk" vs. "branch" paths, to handle cases where a
47 file/folder was renamed in a branch and then that branch was merged back to trunk.
48
49 'svn_repos_url' is the full URL to the root of the SVN repository,
50 e.g. 'file:///path/to/repo'
51 'start_path' is the path in the SVN repo to the source path to start checking
52 ancestry at, e.g. '/branches/fix1/projectA/file1.txt'.
53 'start_rev' is the revision to start walking the history of start_path backwards from.
54 'stop_base_path' is the path in the SVN repo to stop tracing ancestry once we've reached,
55 i.e. the target path we're trying to trace ancestry back to, e.g. '/trunk'.
56 """
57 ui.status(prefix + ">> find_svn_ancestors: Start: (%s) start_path: %s stop_base_path: %s",
58 svn_repos_url, start_path+"@"+str(start_rev), stop_base_path, level=ui.DEBUG, color='YELLOW')
59 done = False
60 no_ancestry = False
61 cur_path = start_path
62 cur_rev = start_rev
63 first_iter_done = False
64 ancestors = []
65 while not done:
66 # Get the first "svn log" entry for cur_path (relative to @cur_rev)
67 ui.status(prefix + ">> find_svn_ancestors: %s", svn_repos_url+cur_path+"@"+str(cur_rev), level=ui.DEBUG, color='YELLOW')
68 log_entry = svnclient.get_first_svn_log_entry(svn_repos_url+cur_path, 1, cur_rev)
69 if not log_entry:
70 ui.status(prefix + ">> find_svn_ancestors: Done: no log_entry", level=ui.DEBUG, color='YELLOW')
71 done = True
72 break
73 # If we found a copy-from case which matches our stop_base_path, we're done.
74 # ...but only if we've at least tried to search for the first copy-from path.
75 if stop_base_path is not None and first_iter_done and is_child_path(cur_path, stop_base_path):
76 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')
77 done = True
78 break
79 first_iter_done = True
80 # Search for any actions on our target path (or parent paths).
81 changed_paths_temp = []
82 for d in log_entry['changed_paths']:
83 path = d['path']
84 if is_child_path(cur_path, path):
85 changed_paths_temp.append({'path': path, 'data': d})
86 if not changed_paths_temp:
87 # If no matches, then we've hit the end of the ancestry-chain.
88 ui.status(prefix + ">> find_svn_ancestors: Done: No matching changed_paths", level=ui.DEBUG, color='YELLOW')
89 done = True
90 continue
91 # Reverse-sort any matches, so that we start with the most-granular (deepest in the tree) path.
92 changed_paths = sorted(changed_paths_temp, key=operator.itemgetter('path'), reverse=True)
93 # Find the action for our cur_path in this revision. Use a loop to check in reverse order,
94 # so that if the target file/folder is "M" but has a parent folder with an "A" copy-from
95 # then we still correctly match the deepest copy-from.
96 for v in changed_paths:
97 d = v['data']
98 path = d['path']
99 # Check action-type for this file
100 action = d['action']
101 if action not in svnclient.valid_svn_actions:
102 raise UnsupportedSVNAction("In SVN rev. %d: action '%s' not supported. Please report a bug!"
103 % (log_entry['revision'], action))
104 ui.status(prefix + "> %s %s%s", action, path,
105 (" (from %s)" % (d['copyfrom_path']+"@"+str(d['copyfrom_revision']))) if d['copyfrom_path'] else "",
106 level=ui.DEBUG, color='YELLOW')
107 if action == 'D':
108 # If file/folder was deleted, ancestry-chain stops here
109 if stop_base_path:
110 no_ancestry = True
111 ui.status(prefix + ">> find_svn_ancestors: Done: deleted", level=ui.DEBUG, color='YELLOW')
112 done = True
113 break
114 if action in 'RA':
115 # If file/folder was added/replaced but not a copy, ancestry-chain stops here
116 if not d['copyfrom_path']:
117 if stop_base_path:
118 no_ancestry = True
119 ui.status(prefix + ">> find_svn_ancestors: Done: %s with no copyfrom_path",
120 "Added" if action == "A" else "Replaced",
121 level=ui.DEBUG, color='YELLOW')
122 done = True
123 break
124 # Else, file/folder was added/replaced and is a copy, so add an entry to our ancestors list
125 # and keep checking for ancestors
126 ui.status(prefix + ">> find_svn_ancestors: Found copy-from (action=%s): %s --> %s",
127 action, path, d['copyfrom_path']+"@"+str(d['copyfrom_revision']),
128 level=ui.DEBUG, color='YELLOW')
129 ancestors.append({'path': cur_path, 'revision': log_entry['revision'],
130 'copyfrom_path': cur_path.replace(d['path'], d['copyfrom_path']), 'copyfrom_rev': d['copyfrom_revision']})
131 cur_path = cur_path.replace(d['path'], d['copyfrom_path'])
132 cur_rev = d['copyfrom_revision']
133 # Follow the copy and keep on searching
134 break
135 if stop_base_path and no_ancestry:
136 # If we're tracing back ancestry to a specific target stop_base_path and
137 # the ancestry-chain stopped before we reached stop_base_path, then return
138 # nothing since there is no ancestry chaining back to that target.
139 ancestors = []
140 if ancestors:
141 if ui.get_level() >= ui.DEBUG:
142 max_len = 0
143 for idx in range(len(ancestors)):
144 d = ancestors[idx]
145 max_len = max(max_len, len(d['path']+"@"+str(d['revision'])))
146 ui.status(prefix + ">> find_svn_ancestors: Found parent ancestors:", level=ui.DEBUG, color='YELLOW_B')
147 for idx in range(len(ancestors)):
148 d = ancestors[idx]
149 ui.status(prefix + " [%s] %s --> %s", idx,
150 str(d['path']+"@"+str(d['revision'])).ljust(max_len),
151 str(d['copyfrom_path']+"@"+str(d['copyfrom_rev'])),
152 level=ui.DEBUG, color='YELLOW')
153 else:
154 ui.status(prefix + ">> find_svn_ancestors: No ancestor-chain found: %s",
155 svn_repos_url+start_path+"@"+str(start_rev), level=ui.DEBUG, color='YELLOW')
156 return ancestors