chiark / gitweb /
Add stack creation and initialisation support to lib.Stack
[stgit] / stgit / lib / stack.py
CommitLineData
652b2e67
KH
1"""A Python class hierarchy wrapping the StGit on-disk metadata."""
2
cbe4567e
KH
3import os.path
4from stgit import exception, utils
f5c820a8 5from stgit.lib import git, stackupgrade
21b42305
CM
6from stgit.config import config
7
8class StackException(exception.StgException):
9 """Exception raised by L{stack} objects."""
cbe4567e
KH
10
11class Patch(object):
652b2e67
KH
12 """Represents an StGit patch. This class is mainly concerned with
13 reading and writing the on-disk representation of a patch."""
cbe4567e
KH
14 def __init__(self, stack, name):
15 self.__stack = stack
16 self.__name = name
17 name = property(lambda self: self.__name)
5a8e991e 18 @property
cbe4567e
KH
19 def __ref(self):
20 return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
21 @property
5a8e991e
KH
22 def __log_ref(self):
23 return self.__ref + '.log'
24 @property
cbe4567e 25 def commit(self):
5a8e991e
KH
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 = []
f5f22afe 43 cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
5a8e991e
KH
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)
cbe4567e 72 def set_commit(self, commit, msg):
5a8e991e
KH
73 self.__write_compat_files(commit, msg)
74 self.__stack.repository.refs.set(self.__ref, commit, msg)
cbe4567e 75 def delete(self):
5a8e991e
KH
76 self.__delete_compat_files()
77 self.__stack.repository.refs.delete(self.__ref)
cbe4567e
KH
78 def is_applied(self):
79 return self.name in self.__stack.patchorder.applied
80 def is_empty(self):
dcd32afa 81 return self.commit.data.is_nochange()
cbe4567e
KH
82
83class PatchOrder(object):
84 """Keeps track of patch order, and which patches are applied.
85 Works with patch names, not actual patches."""
cbe4567e
KH
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))
448e5d9d
CM
107 hidden = property(lambda self: self.__get_list('hidden'),
108 lambda self, val: self.__set_list('hidden', val))
d454cc06
CM
109 all = property(lambda self: self.applied + self.unapplied + self.hidden)
110 all_visible = property(lambda self: self.applied + self.unapplied)
cbe4567e 111
21b42305
CM
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
cbe4567e 120class Patches(object):
652b2e67
KH
121 """Creates L{Patch} objects. Makes sure there is only one such object
122 per patch."""
cbe4567e
KH
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
04c77bc5 145class Stack(git.Branch):
652b2e67
KH
146 """Represents an StGit stack (that is, a git branch with some extra
147 metadata)."""
04c77bc5
CM
148 __repo_subdir = 'patches'
149
cbe4567e 150 def __init__(self, repository, name):
04c77bc5 151 git.Branch.__init__(self, repository, name)
cbe4567e
KH
152 self.__patchorder = PatchOrder(self)
153 self.__patches = Patches(self)
317b386f 154 if not stackupgrade.update_to_current_format_version(repository, name):
21b42305 155 raise StackException('%s: branch not initialized' % name)
cbe4567e
KH
156 patchorder = property(lambda self: self.__patchorder)
157 patches = property(lambda self: self.__patches)
158 @property
159 def directory(self):
04c77bc5 160 return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
cbe4567e
KH
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
dcd32afa
KH
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
cbe4567e 172
21b42305
CM
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
cbe4567e 222class Repository(git.Repository):
652b2e67
KH
223 """A git L{Repository<git.Repository>} with some added StGit-specific
224 operations."""
cbe4567e
KH
225 def __init__(self, *args, **kwargs):
226 git.Repository.__init__(self, *args, **kwargs)
227 self.__stacks = {} # name -> Stack
228 @property
cbe4567e 229 def current_stack(self):
13b26f1a
CM
230 return self.get_stack()
231 def get_stack(self, name = None):
232 if not name:
04c77bc5 233 name = self.current_branch_name
cbe4567e
KH
234 if not name in self.__stacks:
235 self.__stacks[name] = Stack(self, name)
236 return self.__stacks[name]