1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 """Branching scheme implementations."""
19 from bzrlib.errors import NotBranchError, BzrError
20 from bzrlib.trace import mutter
25 class BranchingScheme:
26 """ Divides SVN repository data up into branches. Since there
27 is no proper way to do this, there are several subclasses of this class
28 each of which handles a particular convention that may be in use.
33 def is_branch(self, path):
34 """Check whether a location refers to a branch.
36 :param path: Path to check.
38 raise NotImplementedError
40 def unprefix(self, path):
41 """Split up a Subversion path into a branch-path and inside-branch path.
43 :param path: Path to split up.
44 :return: Tuple with branch-path and inside-branch path.
46 raise NotImplementedError
49 def find_scheme(name):
50 """Find a branching scheme by name.
52 :param name: Name of branching scheme.
53 :return: Branching scheme instance.
55 if name.startswith("trunk"):
57 return TrunkBranchingScheme()
59 return TrunkBranchingScheme(level=int(name[len("trunk"):]))
61 raise UnknownBranchingScheme(name)
64 return NoBranchingScheme()
66 if name.startswith("single-"):
67 return SingleBranchingScheme(name[len("single-"):])
69 if name.startswith("list-"):
70 return ListBranchingScheme(name[len("list-"):])
72 raise UnknownBranchingScheme(name)
74 def is_branch_parent(self, path):
75 """Check whether the specified path is the parent directory of branches.
76 The path may not be a branch itself.
78 :param path: path to check
81 raise NotImplementedError
83 def is_tag_parent(self, path):
84 """Check whether the specified path is the parent directory of tags.
85 The path may not be a tag itself.
87 :param path: path to check
90 raise NotImplementedError
92 def is_tag(self, path):
93 """Check whether the specified path is a tag
94 according to this branching scheme.
96 :param path: path to check
99 raise NotImplementedError
102 """Generate a list of lines for this branching scheme.
104 :return: List of lines representing the data in this branching
107 raise NotImplementedError(self.to_lines)
110 def parse_list_scheme_text(text):
111 """Parse a text containing the branches for a ListBranchingScheme.
114 :return: List of branch paths.
117 for line in text.splitlines():
118 if line.startswith("#"):
120 branches.append(line.strip("/"))
124 class ListBranchingScheme(BranchingScheme):
125 """Branching scheme that keeps a list of branch paths, including
127 def __init__(self, branch_list):
128 """Create new ListBranchingScheme instance.
130 :param branch_list: List of know branch locations.
132 if isinstance(branch_list, basestring):
133 branch_list = bz2.decompress(base64.b64decode(branch_list)).splitlines()
134 self.branch_list = [p.strip("/") for p in branch_list]
135 self.split_branch_list = [p.split("/") for p in self.branch_list]
138 return "list-%s" % base64.b64encode(bz2.compress("".join(map(lambda x:x+"\n", self.branch_list))))
140 def is_tag(self, path):
141 """See BranchingScheme.is_tag()."""
145 def _pattern_cmp(parts, pattern):
146 if len(parts) != len(pattern):
148 for (p, q) in zip(pattern, parts):
149 if p != q and p != "*":
153 def is_branch(self, path):
154 """See BranchingScheme.is_branch()."""
155 parts = path.strip("/").split("/")
156 for pattern in self.split_branch_list:
157 if self._pattern_cmp(parts, pattern):
161 def unprefix(self, path):
162 """See BranchingScheme.unprefix()."""
163 parts = path.strip("/").split("/")
164 for pattern in self.split_branch_list:
165 if self._pattern_cmp(parts[:len(pattern)], pattern):
166 return ("/".join(parts[:len(pattern)]),
167 "/".join(parts[len(pattern):]))
168 raise NotBranchError(path=path)
170 def __eq__(self, other):
171 return self.branch_list == other.branch_list
174 return self.branch_list
177 class NoBranchingScheme(ListBranchingScheme):
178 """Describes a scheme where there is just one branch, the
179 root of the repository."""
181 ListBranchingScheme.__init__(self, [""])
183 def is_branch(self, path):
184 """See BranchingScheme.is_branch()."""
185 return path.strip("/") == ""
187 def is_tag(self, path):
190 def unprefix(self, path):
191 """See BranchingScheme.unprefix()."""
192 return ("", path.strip("/"))
197 def is_branch_parent(self, path):
200 def is_tag_parent(self, path):
204 class TrunkBranchingScheme(ListBranchingScheme):
205 """Standard Subversion repository layout. Each project contains three
206 directories `trunk', `tags' and `branches'.
208 def __init__(self, level=0):
210 ListBranchingScheme.__init__(self,
211 ["*/" * level + "trunk",
212 "*/" * level + "branches/*",
213 "*/" * level + "tags/*"])
215 def is_branch(self, path):
216 """See BranchingScheme.is_branch()."""
217 parts = path.strip("/").split("/")
218 if len(parts) == self.level+1 and parts[self.level] == "trunk":
221 if len(parts) == self.level+2 and parts[self.level] == "branches":
226 def is_tag(self, path):
227 """See BranchingScheme.is_tag()."""
228 parts = path.strip("/").split("/")
229 if len(parts) == self.level+2 and \
230 (parts[self.level] == "tags"):
235 def unprefix(self, path):
236 """See BranchingScheme.unprefix()."""
237 parts = path.strip("/").split("/")
238 if len(parts) == 0 or self.level >= len(parts):
239 raise NotBranchError(path=path)
241 if parts[self.level] == "trunk" or parts[self.level] == "hooks":
242 return ("/".join(parts[0:self.level+1]).strip("/"),
243 "/".join(parts[self.level+1:]).strip("/"))
244 elif ((parts[self.level] == "tags" or parts[self.level] == "branches") and
245 len(parts) >= self.level+2):
246 return ("/".join(parts[0:self.level+2]).strip("/"),
247 "/".join(parts[self.level+2:]).strip("/"))
249 raise NotBranchError(path=path)
252 return "trunk%d" % self.level
254 def is_branch_parent(self, path):
255 parts = path.strip("/").split("/")
256 if len(parts) <= self.level:
258 return self.is_branch(path+"/trunk")
260 def is_tag_parent(self, path):
261 parts = path.strip("/").split("/")
262 return self.is_tag(path+"/aname")
266 class UnknownBranchingScheme(BzrError):
267 _fmt = "Branching scheme could not be found: %(name)s"
269 def __init__(self, name):
273 class SingleBranchingScheme(ListBranchingScheme):
274 """Recognizes just one directory in the repository as branch.
276 def __init__(self, path):
277 self.path = path.strip("/")
279 raise BzrError("NoBranchingScheme should be used")
280 ListBranchingScheme.__init__(self, [self.path])
282 def is_branch(self, path):
283 """See BranchingScheme.is_branch()."""
284 return self.path == path.strip("/")
286 def is_tag(self, path):
287 """See BranchingScheme.is_tag()."""
290 def unprefix(self, path):
291 """See BranchingScheme.unprefix()."""
292 path = path.strip("/")
293 if not path.startswith(self.path):
294 raise NotBranchError(path=path)
296 return (path[0:len(self.path)].strip("/"),
297 path[len(self.path):].strip("/"))
300 return "single-%s" % self.path
302 def is_branch_parent(self, path):
303 if not "/" in self.path:
305 return self.is_branch(path+"/"+self.path.split("/")[-1])
307 def is_tag_parent(self, path):
311 def _find_common_prefix(paths):
313 # Find a common prefix
314 parts = paths[0].split("/")
315 for i in range(len(parts)+1):
317 if j.split("/")[:i] != parts[:i]:
319 prefix = "/".join(parts[:i])
323 def find_commit_paths(changed_paths):
324 """Find the commit-paths used in a bunch of revisions.
326 :param changed_paths: List of changed_paths (dictionary with path -> action)
327 :return: List of potential commit paths.
329 for changes in changed_paths:
330 yield _find_common_prefix(changes.keys())
333 def guess_scheme_from_branch_path(relpath):
334 """Try to guess the branching scheme from a branch path.
336 :param relpath: Relative URL to a branch.
337 :return: New BranchingScheme instance.
339 parts = relpath.strip("/").split("/")
340 for i in range(0, len(parts)):
341 if parts[i] == "trunk" and i == len(parts)-1:
342 return TrunkBranchingScheme(level=i)
343 elif parts[i] in ("branches", "tags") and i == len(parts)-2:
344 return TrunkBranchingScheme(level=i)
347 return NoBranchingScheme()
348 return SingleBranchingScheme(relpath)
351 def guess_scheme_from_path(relpath):
352 """Try to guess the branching scheme from a path in the repository,
353 not necessarily a branch path.
355 :param relpath: Relative path in repository
356 :return: New BranchingScheme instance.
358 parts = relpath.strip("/").split("/")
359 for i in range(0, len(parts)):
360 if parts[i] == "trunk":
361 return TrunkBranchingScheme(level=i)
362 elif parts[i] in ("branches", "tags"):
363 return TrunkBranchingScheme(level=i)
365 return NoBranchingScheme()
368 def guess_scheme_from_history(changed_paths, last_revnum,
370 """Try to determine the best fitting branching scheme.
372 :param changed_paths: Iterator over (branch_path, changes, revnum)
373 as returned from LogWalker.follow_path().
374 :param last_revnum: Number of entries in changed_paths.
375 :param relpath: Branch path that should be accepted by the branching
377 :return: Branching scheme instance that matches best.
380 pb = ui.ui_factory.nested_progress_bar()
383 for (bp, revpaths, revnum) in changed_paths:
384 assert isinstance(revpaths, dict)
385 pb.update("analyzing repository layout", last_revnum-revnum,
387 for path in find_commit_paths([revpaths]):
388 scheme = guess_scheme_from_path(path)
389 if not potentials.has_key(str(scheme)):
390 potentials[str(scheme)] = 0
391 potentials[str(scheme)] += 1
392 scheme_cache[str(scheme)] = scheme
396 entries = potentials.items()
397 entries.sort(lambda (a, b), (c, d): d - b)
399 mutter('potential branching schemes: %r' % entries)
402 if len(entries) == 0:
403 return NoBranchingScheme()
404 return scheme_cache[entries[0][0]]
406 for (schemename, _) in entries:
407 scheme = scheme_cache[schemename]
408 if scheme.is_branch(relpath):
411 return guess_scheme_from_branch_path(relpath)
414 def scheme_from_branch_list(branch_list):
415 """Determine a branching scheme for a branch list.
417 :param branch_list: List of branch paths, may contain wildcards.
418 :return: New branching scheme.
420 if branch_list == ["."] or branch_list == []:
421 return NoBranchingScheme()
422 if branch_list == TrunkBranchingScheme(0).branch_list:
423 return TrunkBranchingScheme(0)
424 return ListBranchingScheme(branch_list)
427 help_schemes = """Subversion Branching Schemes
429 Subversion is basically a versioned file system. It does not have
430 any notion of branches and what is a branch in Subversion is therefor
433 In order for Bazaar to access a Subversion repository it has to know
434 what paths to consider branches. It does this by using so-called branching
435 schemes. When you connect to a repository for the first time, Bazaar
436 will try to determine the branching scheme to use using some simple
437 heuristics. It is always possible to change the branching scheme it should
440 There are some conventions in use in Subversion for repository layouts.
441 The most common one is probably the trunk/branches/tags
442 layout, where the repository contains a "trunk" directory with the main
443 development branch, other branches in a "branches" directory and tags as
444 subdirectories of a "tags" directory. This branching scheme is named
447 Another option is simply having just one branch at the root of the repository.
448 This scheme is called "none" by Bazaar.
450 The branching scheme bzr-svn should use for a repository can be set in the
451 configuration file ~/.bazaar/subversion.conf.