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
22 from base64 import urlsafe_b64decode, urlsafe_b64encode
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(urlsafe_b64decode(branch_list.replace(".", "="))).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" % urlsafe_b64encode(bz2.compress("".join(map(lambda x:x+"\n", self.branch_list)))).replace("=", ".")
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
176 def is_tag_parent(self, path):
177 # ListBranchingScheme doesn't have tags
180 def is_branch_parent(self, path):
181 parts = path.strip("/").split("/")
182 for pattern in self.split_branch_list:
183 if len(parts) == len(pattern):
185 if self._pattern_cmp(parts, pattern[0:len(parts)]):
190 class NoBranchingScheme(ListBranchingScheme):
191 """Describes a scheme where there is just one branch, the
192 root of the repository."""
194 ListBranchingScheme.__init__(self, [""])
196 def is_branch(self, path):
197 """See BranchingScheme.is_branch()."""
198 return path.strip("/") == ""
200 def is_tag(self, path):
203 def unprefix(self, path):
204 """See BranchingScheme.unprefix()."""
205 return ("", path.strip("/"))
210 def is_branch_parent(self, path):
213 def is_tag_parent(self, path):
217 class TrunkBranchingScheme(ListBranchingScheme):
218 """Standard Subversion repository layout. Each project contains three
219 directories `trunk', `tags' and `branches'.
221 def __init__(self, level=0):
223 ListBranchingScheme.__init__(self,
224 ["*/" * level + "trunk",
225 "*/" * level + "branches/*",
226 "*/" * level + "tags/*"])
228 def is_branch(self, path):
229 """See BranchingScheme.is_branch()."""
230 parts = path.strip("/").split("/")
231 if len(parts) == self.level+1 and parts[self.level] == "trunk":
234 if len(parts) == self.level+2 and parts[self.level] == "branches":
239 def is_tag(self, path):
240 """See BranchingScheme.is_tag()."""
241 parts = path.strip("/").split("/")
242 if len(parts) == self.level+2 and \
243 (parts[self.level] == "tags"):
248 def unprefix(self, path):
249 """See BranchingScheme.unprefix()."""
250 parts = path.strip("/").split("/")
251 if len(parts) == 0 or self.level >= len(parts):
252 raise NotBranchError(path=path)
254 if parts[self.level] == "trunk" or parts[self.level] == "hooks":
255 return ("/".join(parts[0:self.level+1]).strip("/"),
256 "/".join(parts[self.level+1:]).strip("/"))
257 elif ((parts[self.level] == "tags" or parts[self.level] == "branches") and
258 len(parts) >= self.level+2):
259 return ("/".join(parts[0:self.level+2]).strip("/"),
260 "/".join(parts[self.level+2:]).strip("/"))
262 raise NotBranchError(path=path)
265 return "trunk%d" % self.level
267 def is_branch_parent(self, path):
268 parts = path.strip("/").split("/")
269 if len(parts) <= self.level:
271 return self.is_branch(path+"/trunk")
273 def is_tag_parent(self, path):
274 parts = path.strip("/").split("/")
275 return self.is_tag(path+"/aname")
279 class UnknownBranchingScheme(BzrError):
280 _fmt = "Branching scheme could not be found: %(name)s"
282 def __init__(self, name):
286 class SingleBranchingScheme(ListBranchingScheme):
287 """Recognizes just one directory in the repository as branch.
289 def __init__(self, path):
290 self.path = path.strip("/")
292 raise BzrError("NoBranchingScheme should be used")
293 ListBranchingScheme.__init__(self, [self.path])
295 def is_branch(self, path):
296 """See BranchingScheme.is_branch()."""
297 return self.path == path.strip("/")
299 def is_tag(self, path):
300 """See BranchingScheme.is_tag()."""
303 def unprefix(self, path):
304 """See BranchingScheme.unprefix()."""
305 path = path.strip("/")
306 if not path.startswith(self.path):
307 raise NotBranchError(path=path)
309 return (path[0:len(self.path)].strip("/"),
310 path[len(self.path):].strip("/"))
313 return "single-%s" % self.path
315 def is_branch_parent(self, path):
316 if not "/" in self.path:
318 return self.is_branch(path+"/"+self.path.split("/")[-1])
320 def is_tag_parent(self, path):
324 def _find_common_prefix(paths):
326 # Find a common prefix
327 parts = paths[0].split("/")
328 for i in range(len(parts)+1):
330 if j.split("/")[:i] != parts[:i]:
332 prefix = "/".join(parts[:i])
336 def find_commit_paths(changed_paths):
337 """Find the commit-paths used in a bunch of revisions.
339 :param changed_paths: List of changed_paths (dictionary with path -> action)
340 :return: List of potential commit paths.
342 for changes in changed_paths:
343 yield _find_common_prefix(changes.keys())
346 def guess_scheme_from_branch_path(relpath):
347 """Try to guess the branching scheme from a branch path.
349 :param relpath: Relative URL to a branch.
350 :return: New BranchingScheme instance.
352 parts = relpath.strip("/").split("/")
353 for i in range(0, len(parts)):
354 if parts[i] == "trunk" and i == len(parts)-1:
355 return TrunkBranchingScheme(level=i)
356 elif parts[i] in ("branches", "tags") and i == len(parts)-2:
357 return TrunkBranchingScheme(level=i)
360 return NoBranchingScheme()
361 return SingleBranchingScheme(relpath)
364 def guess_scheme_from_path(relpath):
365 """Try to guess the branching scheme from a path in the repository,
366 not necessarily a branch path.
368 :param relpath: Relative path in repository
369 :return: New BranchingScheme instance.
371 parts = relpath.strip("/").split("/")
372 for i in range(0, len(parts)):
373 if parts[i] == "trunk":
374 return TrunkBranchingScheme(level=i)
375 elif parts[i] in ("branches", "tags"):
376 return TrunkBranchingScheme(level=i)
378 return NoBranchingScheme()
381 def guess_scheme_from_history(changed_paths, last_revnum,
383 """Try to determine the best fitting branching scheme.
385 :param changed_paths: Iterator over (branch_path, changes, revnum)
386 as returned from LogWalker.follow_path().
387 :param last_revnum: Number of entries in changed_paths.
388 :param relpath: Branch path that should be accepted by the branching
390 :return: Branching scheme instance that matches best.
393 pb = ui.ui_factory.nested_progress_bar()
396 for (bp, revpaths, revnum) in changed_paths:
397 assert isinstance(revpaths, dict)
398 pb.update("analyzing repository layout", last_revnum-revnum,
400 for path in find_commit_paths([revpaths]):
401 scheme = guess_scheme_from_path(path)
402 if not potentials.has_key(str(scheme)):
403 potentials[str(scheme)] = 0
404 potentials[str(scheme)] += 1
405 scheme_cache[str(scheme)] = scheme
409 entries = potentials.items()
410 entries.sort(lambda (a, b), (c, d): d - b)
412 mutter('potential branching schemes: %r' % entries)
415 if len(entries) == 0:
416 return NoBranchingScheme()
417 return scheme_cache[entries[0][0]]
419 for (schemename, _) in entries:
420 scheme = scheme_cache[schemename]
421 if scheme.is_branch(relpath):
424 return guess_scheme_from_branch_path(relpath)
427 def scheme_from_branch_list(branch_list):
428 """Determine a branching scheme for a branch list.
430 :param branch_list: List of branch paths, may contain wildcards.
431 :return: New branching scheme.
433 if branch_list == ["."] or branch_list == []:
434 return NoBranchingScheme()
435 if branch_list == TrunkBranchingScheme(0).branch_list:
436 return TrunkBranchingScheme(0)
437 return ListBranchingScheme(branch_list)
440 help_schemes = """Subversion Branching Schemes
442 Subversion is basically a versioned file system. It does not have
443 any notion of branches and what is a branch in Subversion is therefor
446 In order for Bazaar to access a Subversion repository it has to know
447 what paths to consider branches. It does this by using so-called branching
448 schemes. When you connect to a repository for the first time, Bazaar
449 will try to determine the branching scheme to use using some simple
450 heuristics. It is always possible to change the branching scheme it should
453 There are some conventions in use in Subversion for repository layouts.
454 The most common one is probably the trunk/branches/tags
455 layout, where the repository contains a "trunk" directory with the main
456 development branch, other branches in a "branches" directory and tags as
457 subdirectories of a "tags" directory. This branching scheme is named
460 Another option is simply having just one branch at the root of the repository.
461 This scheme is called "none" by Bazaar.
463 The branching scheme bzr-svn should use for a repository can be set in the
464 configuration file ~/.bazaar/subversion.conf.