More intelligent empty directory removal (Tom Widmer)
[jelmer/python-fastimport.git] / branch_updater.py
1 # Copyright (C) 2009 Canonical Ltd
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
17 """An object that updates a bunch of branches based on data imported."""
18
19 from operator import itemgetter
20
21 from bzrlib import bzrdir, errors, osutils
22 from bzrlib.trace import error, note
23
24 import branch_mapper
25 import helpers
26
27
28 class BranchUpdater(object):
29
30     def __init__(self, repo, branch, cache_mgr, heads_by_ref, last_ref, tags):
31         """Create an object responsible for updating branches.
32
33         :param heads_by_ref: a dictionary where
34           names are git-style references like refs/heads/master;
35           values are one item lists of commits marks.
36         """
37         self.repo = repo
38         self.branch = branch
39         self.cache_mgr = cache_mgr
40         self.heads_by_ref = heads_by_ref
41         self.last_ref = last_ref
42         self.tags = tags
43         self.name_mapper = branch_mapper.BranchMapper()
44         self._branch_format = \
45             helpers.best_format_for_objects_in_a_repository(repo)
46
47     def update(self):
48         """Update the Bazaar branches and tips matching the heads.
49
50         If the repository is shared, this routine creates branches
51         as required. If it isn't, warnings are produced about the
52         lost of information.
53
54         :return: updated, lost_heads where
55           updated = the list of branches updated ('trunk' is first)
56           lost_heads = a list of (bazaar-name,revision) for branches that
57             would have been created had the repository been shared
58         """
59         updated = []
60         branch_tips, lost_heads = self._get_matching_branches()
61         for br, tip in branch_tips:
62             if self._update_branch(br, tip):
63                 updated.append(br)
64         return updated, lost_heads
65
66     def _get_matching_branches(self):
67         """Get the Bazaar branches.
68
69         :return: branch_tips, lost_heads where
70           branch_tips = a list of (branch,tip) tuples for branches. The
71             first tip is the 'trunk'.
72           lost_heads = a list of (bazaar-name,revision) for branches that
73             would have been created had the repository been shared and
74             everything succeeded
75         """
76         branch_tips = []
77         lost_heads = []
78         ref_names = self.heads_by_ref.keys()
79         if self.branch is not None:
80             trunk = self.select_trunk(ref_names)
81             default_tip = self.heads_by_ref[trunk][0]
82             branch_tips.append((self.branch, default_tip))
83             ref_names.remove(trunk)
84
85         # Convert the reference names into Bazaar speak. If we haven't
86         # already put the 'trunk' first, do it now.
87         git_to_bzr_map = self.name_mapper.git_to_bzr(ref_names)
88         if ref_names and self.branch is None:
89             trunk = self.select_trunk(ref_names)
90             git_bzr_items = [(trunk, git_to_bzr_map[trunk])]
91             del git_to_bzr_map[trunk]
92         else:
93             git_bzr_items = []
94         git_bzr_items.extend(sorted(git_to_bzr_map.items(), key=itemgetter(1)))
95
96         # Policy for locating branches
97         def dir_under_current(name, ref_name):
98             # Using the Bazaar name, get a directory under the current one
99             repo_base = self.repo.bzrdir.transport.base
100             return osutils.pathjoin(repo_base, "..", name)
101         def dir_sister_branch(name, ref_name):
102             # Using the Bazaar name, get a sister directory to the branch
103             return osutils.pathjoin(self.branch.base, "..", name)
104         if self.branch is not None:
105             dir_policy = dir_sister_branch
106         else:
107             dir_policy = dir_under_current
108
109         # Create/track missing branches
110         shared_repo = self.repo.is_shared()
111         for ref_name, name in git_bzr_items:
112             tip = self.heads_by_ref[ref_name][0]
113             if shared_repo:
114                 location = dir_policy(name, ref_name)
115                 try:
116                     br = self.make_branch(location)
117                     branch_tips.append((br,tip))
118                     continue
119                 except errors.BzrError, ex:
120                     error("ERROR: failed to create branch %s: %s",
121                         location, ex)
122             lost_head = self.cache_mgr.revision_ids[tip]
123             lost_info = (name, lost_head)
124             lost_heads.append(lost_info)
125         return branch_tips, lost_heads
126
127     def select_trunk(self, ref_names):
128         """Given a set of ref names, choose one as the trunk."""
129         for candidate in ['refs/heads/master']:
130             if candidate in ref_names:
131                 return candidate
132         # Use the last reference in the import stream
133         return self.last_ref
134
135     def make_branch(self, location):
136         """Make a branch in the repository if not already there."""
137         try:
138             return bzrdir.BzrDir.open(location).open_branch()
139         except errors.NotBranchError, ex:
140             return bzrdir.BzrDir.create_branch_convenience(location,
141                 format=self._branch_format)
142
143     def _update_branch(self, br, last_mark):
144         """Update a branch with last revision and tag information.
145         
146         :return: whether the branch was changed or not
147         """
148         last_rev_id = self.cache_mgr.revision_ids[last_mark]
149         revs = list(self.repo.iter_reverse_revision_history(last_rev_id))
150         revno = len(revs)
151         existing_revno, existing_last_rev_id = br.last_revision_info()
152         changed = False
153         if revno != existing_revno or last_rev_id != existing_last_rev_id:
154             br.set_last_revision_info(revno, last_rev_id)
155             changed = True
156         # apply tags known in this branch
157         my_tags = {}
158         if self.tags:
159             for tag,rev in self.tags.items():
160                 if rev in revs:
161                     my_tags[tag] = rev
162             if my_tags:
163                 br.tags._set_tag_dict(my_tags)
164                 changed = True
165         if changed:
166             tagno = len(my_tags)
167             note("\t branch %s now has %d %s and %d %s", br.nick,
168                 revno, helpers.single_plural(revno, "revision", "revisions"),
169                 tagno, helpers.single_plural(tagno, "tag", "tags"))
170         return changed