chiark / gitweb /
47679b6057226f73f29221c1e2c9719b8b210302
[stgit] / stgit / lib / stack.py
1 """A Python class hierarchy wrapping the StGit on-disk metadata."""
2
3 import os.path
4 from stgit import exception, utils
5 from stgit.lib import git, stackupgrade
6 from stgit.config import config
7
8 class StackException(exception.StgException):
9     """Exception raised by L{stack} objects."""
10
11 class Patch(object):
12     """Represents an StGit patch. This class is mainly concerned with
13     reading and writing the on-disk representation of a patch."""
14     def __init__(self, stack, name):
15         self.__stack = stack
16         self.__name = name
17     name = property(lambda self: self.__name)
18     @property
19     def __ref(self):
20         return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
21     @property
22     def __log_ref(self):
23         return self.__ref + '.log'
24     @property
25     def commit(self):
26         return self.__stack.repository.refs.get(self.__ref)
27     @property
28     def old_commit(self):
29         """Return the previous commit for this patch."""
30         fn = os.path.join(self.__compat_dir, 'top.old')
31         if not os.path.isfile(fn):
32             return None
33         return self.__stack.repository.get_commit(utils.read_string(fn))
34     @property
35     def __compat_dir(self):
36         return os.path.join(self.__stack.directory, 'patches', self.__name)
37     def __write_compat_files(self, new_commit, msg):
38         """Write files used by the old infrastructure."""
39         def write(name, val, multiline = False):
40             fn = os.path.join(self.__compat_dir, name)
41             if val:
42                 utils.write_string(fn, val, multiline)
43             elif os.path.isfile(fn):
44                 os.remove(fn)
45         def write_patchlog():
46             try:
47                 old_log = [self.__stack.repository.refs.get(self.__log_ref)]
48             except KeyError:
49                 old_log = []
50             cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
51                                 message = '%s\t%s' % (msg, new_commit.sha1))
52             c = self.__stack.repository.commit(cd)
53             self.__stack.repository.refs.set(self.__log_ref, c, msg)
54             return c
55         d = new_commit.data
56         write('authname', d.author.name)
57         write('authemail', d.author.email)
58         write('authdate', d.author.date)
59         write('commname', d.committer.name)
60         write('commemail', d.committer.email)
61         write('description', d.message)
62         write('log', write_patchlog().sha1)
63         write('top', new_commit.sha1)
64         write('bottom', d.parent.sha1)
65         try:
66             old_top_sha1 = self.commit.sha1
67             old_bottom_sha1 = self.commit.data.parent.sha1
68         except KeyError:
69             old_top_sha1 = None
70             old_bottom_sha1 = None
71         write('top.old', old_top_sha1)
72         write('bottom.old', old_bottom_sha1)
73     def __delete_compat_files(self):
74         if os.path.isdir(self.__compat_dir):
75             for f in os.listdir(self.__compat_dir):
76                 os.remove(os.path.join(self.__compat_dir, f))
77             os.rmdir(self.__compat_dir)
78         try:
79             # this compatibility log ref might not exist
80             self.__stack.repository.refs.delete(self.__log_ref)
81         except KeyError:
82             pass
83     def set_commit(self, commit, msg):
84         self.__write_compat_files(commit, msg)
85         self.__stack.repository.refs.set(self.__ref, commit, msg)
86     def delete(self):
87         self.__delete_compat_files()
88         self.__stack.repository.refs.delete(self.__ref)
89     def is_applied(self):
90         return self.name in self.__stack.patchorder.applied
91     def is_empty(self):
92         return self.commit.data.is_nochange()
93     def files(self):
94         """Return the set of files this patch touches."""
95         fs = set()
96         for (_, _, _, _, _, oldname, newname
97              ) in self.__stack.repository.diff_tree_files(
98             self.commit.data.tree, self.commit.data.parent.data.tree):
99             fs.add(oldname)
100             fs.add(newname)
101         return fs
102
103 class PatchOrder(object):
104     """Keeps track of patch order, and which patches are applied.
105     Works with patch names, not actual patches."""
106     def __init__(self, stack):
107         self.__stack = stack
108         self.__lists = {}
109     def __read_file(self, fn):
110         return tuple(utils.read_strings(
111             os.path.join(self.__stack.directory, fn)))
112     def __write_file(self, fn, val):
113         utils.write_strings(os.path.join(self.__stack.directory, fn), val)
114     def __get_list(self, name):
115         if not name in self.__lists:
116             self.__lists[name] = self.__read_file(name)
117         return self.__lists[name]
118     def __set_list(self, name, val):
119         val = tuple(val)
120         if val != self.__lists.get(name, None):
121             self.__lists[name] = val
122             self.__write_file(name, val)
123     applied = property(lambda self: self.__get_list('applied'),
124                        lambda self, val: self.__set_list('applied', val))
125     unapplied = property(lambda self: self.__get_list('unapplied'),
126                          lambda self, val: self.__set_list('unapplied', val))
127     hidden = property(lambda self: self.__get_list('hidden'),
128                       lambda self, val: self.__set_list('hidden', val))
129     all = property(lambda self: self.applied + self.unapplied + self.hidden)
130     all_visible = property(lambda self: self.applied + self.unapplied)
131
132     @staticmethod
133     def create(stackdir):
134         """Create the PatchOrder specific files
135         """
136         utils.create_empty_file(os.path.join(stackdir, 'applied'))
137         utils.create_empty_file(os.path.join(stackdir, 'unapplied'))
138         utils.create_empty_file(os.path.join(stackdir, 'hidden'))
139
140 class Patches(object):
141     """Creates L{Patch} objects. Makes sure there is only one such object
142     per patch."""
143     def __init__(self, stack):
144         self.__stack = stack
145         def create_patch(name):
146             p = Patch(self.__stack, name)
147             p.commit # raise exception if the patch doesn't exist
148             return p
149         self.__patches = git.ObjectCache(create_patch) # name -> Patch
150     def exists(self, name):
151         try:
152             self.get(name)
153             return True
154         except KeyError:
155             return False
156     def get(self, name):
157         return self.__patches[name]
158     def new(self, name, commit, msg):
159         assert not name in self.__patches
160         p = Patch(self.__stack, name)
161         p.set_commit(commit, msg)
162         self.__patches[name] = p
163         return p
164
165 class Stack(git.Branch):
166     """Represents an StGit stack (that is, a git branch with some extra
167     metadata)."""
168     __repo_subdir = 'patches'
169
170     def __init__(self, repository, name):
171         git.Branch.__init__(self, repository, name)
172         self.__patchorder = PatchOrder(self)
173         self.__patches = Patches(self)
174         if not stackupgrade.update_to_current_format_version(repository, name):
175             raise StackException('%s: branch not initialized' % name)
176     patchorder = property(lambda self: self.__patchorder)
177     patches = property(lambda self: self.__patches)
178     @property
179     def directory(self):
180         return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
181     @property
182     def base(self):
183         if self.patchorder.applied:
184             return self.patches.get(self.patchorder.applied[0]
185                                     ).commit.data.parent
186         else:
187             return self.head
188     @property
189     def top(self):
190         """Commit of the topmost patch, or the stack base if no patches are
191         applied."""
192         if self.patchorder.applied:
193             return self.patches.get(self.patchorder.applied[-1]).commit
194         else:
195             # When no patches are applied, base == head.
196             return self.head
197     def head_top_equal(self):
198         if not self.patchorder.applied:
199             return True
200         return self.head == self.patches.get(self.patchorder.applied[-1]).commit
201
202     def set_parents(self, remote, branch):
203         if remote:
204             self.set_parent_remote(remote)
205         if branch:
206             self.set_parent_branch(branch)
207
208     @classmethod
209     def initialise(cls, repository, name = None):
210         """Initialise a Git branch to handle patch series.
211
212         @param repository: The L{Repository} where the L{Stack} will be created
213         @param name: The name of the L{Stack}
214         """
215         if not name:
216             name = repository.current_branch_name
217         # make sure that the corresponding Git branch exists
218         git.Branch(repository, name)
219
220         dir = os.path.join(repository.directory, cls.__repo_subdir, name)
221         compat_dir = os.path.join(dir, 'patches')
222         if os.path.exists(dir):
223             raise StackException('%s: branch already initialized' % name)
224
225         # create the stack directory and files
226         utils.create_dirs(dir)
227         utils.create_dirs(compat_dir)
228         PatchOrder.create(dir)
229         config.set(stackupgrade.format_version_key(name),
230                    str(stackupgrade.FORMAT_VERSION))
231
232         return repository.get_stack(name)
233
234     @classmethod
235     def create(cls, repository, name,
236                create_at = None, parent_remote = None, parent_branch = None):
237         """Create and initialise a Git branch returning the L{Stack} object.
238
239         @param repository: The L{Repository} where the L{Stack} will be created
240         @param name: The name of the L{Stack}
241         @param create_at: The Git id used as the base for the newly created
242             Git branch
243         @param parent_remote: The name of the remote Git branch
244         @param parent_branch: The name of the parent Git branch
245         """
246         git.Branch.create(repository, name, create_at = create_at)
247         stack = cls.initialise(repository, name)
248         stack.set_parents(parent_remote, parent_branch)
249         return stack
250
251 class Repository(git.Repository):
252     """A git L{Repository<git.Repository>} with some added StGit-specific
253     operations."""
254     def __init__(self, *args, **kwargs):
255         git.Repository.__init__(self, *args, **kwargs)
256         self.__stacks = {} # name -> Stack
257     @property
258     def current_stack(self):
259         return self.get_stack()
260     def get_stack(self, name = None):
261         if not name:
262             name = self.current_branch_name
263         if not name in self.__stacks:
264             self.__stacks[name] = Stack(self, name)
265         return self.__stacks[name]