Add news entry for FAQ.
[jelmer/subvertpy.git] / scheme.py
1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
2
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.
7
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.
12
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."""
17
18 from bzrlib import ui
19 from bzrlib.errors import NotBranchError, BzrError
20 from bzrlib.trace import mutter
21
22 import base64
23 import bz2
24
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.
29     """
30     def __init__(self):
31         pass
32
33     def is_branch(self, path):
34         """Check whether a location refers to a branch.
35         
36         :param path: Path to check.
37         """
38         raise NotImplementedError
39
40     def unprefix(self, path):
41         """Split up a Subversion path into a branch-path and inside-branch path.
42
43         :param path: Path to split up.
44         :return: Tuple with branch-path and inside-branch path.
45         """
46         raise NotImplementedError
47
48     @staticmethod
49     def find_scheme(name):
50         """Find a branching scheme by name.
51
52         :param name: Name of branching scheme.
53         :return: Branching scheme instance.
54         """
55         if name.startswith("trunk"):
56             if name == "trunk":
57                 return TrunkBranchingScheme()
58             try:
59                 return TrunkBranchingScheme(level=int(name[len("trunk"):]))
60             except ValueError:
61                 raise UnknownBranchingScheme(name)
62
63         if name == "none":
64             return NoBranchingScheme()
65
66         if name.startswith("single-"):
67             return SingleBranchingScheme(name[len("single-"):])
68
69         if name.startswith("list-"):
70             return ListBranchingScheme(name[len("list-"):])
71
72         raise UnknownBranchingScheme(name)
73
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.
77         
78         :param path: path to check
79         :returns: boolean
80         """
81         raise NotImplementedError
82
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.
86         
87         :param path: path to check
88         :returns: boolean
89         """
90         raise NotImplementedError
91
92     def is_tag(self, path):
93         """Check whether the specified path is a tag 
94         according to this branching scheme.
95
96         :param path: path to check
97         :return: boolean
98         """
99         raise NotImplementedError
100
101     def to_lines(self):
102         """Generate a list of lines for this branching scheme.
103
104         :return: List of lines representing the data in this branching 
105             scheme.
106         """
107         raise NotImplementedError(self.to_lines)
108
109
110 def parse_list_scheme_text(text):
111     """Parse a text containing the branches for a ListBranchingScheme.
112
113     :param text: Text.
114     :return: List of branch paths.
115     """
116     branches = []
117     for line in text.splitlines():
118         if line.startswith("#"):
119             continue
120         branches.append(line.strip("/"))
121     return branches
122
123
124 class ListBranchingScheme(BranchingScheme):
125     """Branching scheme that keeps a list of branch paths, including 
126     wildcards."""
127     def __init__(self, branch_list):
128         """Create new ListBranchingScheme instance.
129
130         :param branch_list: List of know branch locations.
131         """
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]
136
137     def __str__(self):
138         return "list-%s" % base64.b64encode(bz2.compress("".join(map(lambda x:x+"\n", self.branch_list))))
139
140     def is_tag(self, path):
141         """See BranchingScheme.is_tag()."""
142         return False
143
144     @staticmethod
145     def _pattern_cmp(parts, pattern):
146         if len(parts) != len(pattern):
147             return False
148         for (p, q) in zip(pattern, parts):
149             if p != q and p != "*":
150                 return False
151         return True
152
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):
158                 return True
159         return False
160
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)
169
170     def __eq__(self, other):
171         return self.branch_list == other.branch_list
172
173     def to_lines(self):
174         return self.branch_list
175
176
177 class NoBranchingScheme(ListBranchingScheme):
178     """Describes a scheme where there is just one branch, the 
179     root of the repository."""
180     def __init__(self):
181         ListBranchingScheme.__init__(self, [""])
182
183     def is_branch(self, path):
184         """See BranchingScheme.is_branch()."""
185         return path.strip("/") == ""
186
187     def is_tag(self, path):
188         return False
189
190     def unprefix(self, path):
191         """See BranchingScheme.unprefix()."""
192         return ("", path.strip("/"))
193
194     def __str__(self):
195         return "none"
196
197     def is_branch_parent(self, path):
198         return False
199
200     def is_tag_parent(self, path):
201         return False
202
203
204 class TrunkBranchingScheme(ListBranchingScheme):
205     """Standard Subversion repository layout. Each project contains three 
206     directories `trunk', `tags' and `branches'. 
207     """
208     def __init__(self, level=0):
209         self.level = level
210         ListBranchingScheme.__init__(self,
211             ["*/" * level + "trunk",
212              "*/" * level + "branches/*",
213              "*/" * level + "tags/*"])
214
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":
219             return True
220
221         if len(parts) == self.level+2 and parts[self.level] == "branches":
222             return True
223
224         return False
225
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"):
231             return True
232
233         return False
234
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)
240
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("/"))
248         else:
249             raise NotBranchError(path=path)
250
251     def __str__(self):
252         return "trunk%d" % self.level
253
254     def is_branch_parent(self, path):
255         parts = path.strip("/").split("/")
256         if len(parts) <= self.level:
257             return True
258         return self.is_branch(path+"/trunk")
259
260     def is_tag_parent(self, path):
261         parts = path.strip("/").split("/")
262         return self.is_tag(path+"/aname")
263
264
265
266 class UnknownBranchingScheme(BzrError):
267     _fmt = "Branching scheme could not be found: %(name)s"
268
269     def __init__(self, name):
270         self.name = name
271
272
273 class SingleBranchingScheme(ListBranchingScheme):
274     """Recognizes just one directory in the repository as branch.
275     """
276     def __init__(self, path):
277         self.path = path.strip("/")
278         if self.path == "":
279             raise BzrError("NoBranchingScheme should be used")
280         ListBranchingScheme.__init__(self, [self.path])
281
282     def is_branch(self, path):
283         """See BranchingScheme.is_branch()."""
284         return self.path == path.strip("/")
285
286     def is_tag(self, path):
287         """See BranchingScheme.is_tag()."""
288         return False
289
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)
295
296         return (path[0:len(self.path)].strip("/"), 
297                 path[len(self.path):].strip("/"))
298
299     def __str__(self):
300         return "single-%s" % self.path
301
302     def is_branch_parent(self, path):
303         if not "/" in self.path:
304             return False
305         return self.is_branch(path+"/"+self.path.split("/")[-1])
306
307     def is_tag_parent(self, path):
308         return False
309
310
311 def _find_common_prefix(paths):
312     prefix = ""
313     # Find a common prefix
314     parts = paths[0].split("/")
315     for i in range(len(parts)+1):
316         for j in paths:
317             if j.split("/")[:i] != parts[:i]:
318                 return prefix
319         prefix = "/".join(parts[:i])
320     return prefix
321
322
323 def find_commit_paths(changed_paths):
324     """Find the commit-paths used in a bunch of revisions.
325
326     :param changed_paths: List of changed_paths (dictionary with path -> action)
327     :return: List of potential commit paths.
328     """
329     for changes in changed_paths:
330         yield _find_common_prefix(changes.keys())
331
332
333 def guess_scheme_from_branch_path(relpath):
334     """Try to guess the branching scheme from a branch path.
335
336     :param relpath: Relative URL to a branch.
337     :return: New BranchingScheme instance.
338     """
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)
345
346     if parts == [""]:
347         return NoBranchingScheme()
348     return SingleBranchingScheme(relpath)
349
350
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.
354
355     :param relpath: Relative path in repository
356     :return: New BranchingScheme instance.
357     """
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)
364
365     return NoBranchingScheme()
366
367
368 def guess_scheme_from_history(changed_paths, last_revnum, 
369                               relpath=None):
370     """Try to determine the best fitting branching scheme.
371
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 
376                     scheme as a branch.
377     :return: Branching scheme instance that matches best.
378     """
379     potentials = {}
380     pb = ui.ui_factory.nested_progress_bar()
381     scheme_cache = {}
382     try:
383         for (bp, revpaths, revnum) in changed_paths:
384             assert isinstance(revpaths, dict)
385             pb.update("analyzing repository layout", last_revnum-revnum, 
386                       last_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
393     finally:
394         pb.finished()
395     
396     entries = potentials.items()
397     entries.sort(lambda (a, b), (c, d): d - b)
398
399     mutter('potential branching schemes: %r' % entries)
400
401     if relpath is None:
402         if len(entries) == 0:
403             return NoBranchingScheme()
404         return scheme_cache[entries[0][0]]
405
406     for (schemename, _) in entries:
407         scheme = scheme_cache[schemename]
408         if scheme.is_branch(relpath):
409             return scheme
410
411     return guess_scheme_from_branch_path(relpath)
412
413
414 def scheme_from_branch_list(branch_list):
415     """Determine a branching scheme for a branch list.
416
417     :param branch_list: List of branch paths, may contain wildcards.
418     :return: New branching scheme.
419     """
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) 
425
426
427 help_schemes = """Subversion Branching Schemes
428
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
431 up to the user. 
432
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 
438 use later.
439
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 
445 "trunk" in Bazaar.
446
447 Another option is simply having just one branch at the root of the repository. 
448 This scheme is called "none" by Bazaar.
449
450 The branching scheme bzr-svn should use for a repository can be set in the 
451 configuration file ~/.bazaar/subversion.conf.
452 """