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