Commit | Line | Data |
---|---|---|
652b2e67 KH |
1 | """A Python class hierarchy wrapping the StGit on-disk metadata.""" |
2 | ||
cbe4567e KH |
3 | import os.path |
4 | from stgit import exception, utils | |
f5c820a8 | 5 | from stgit.lib import git, stackupgrade |
21b42305 CM |
6 | from stgit.config import config |
7 | ||
8 | class StackException(exception.StgException): | |
9 | """Exception raised by L{stack} objects.""" | |
cbe4567e KH |
10 | |
11 | class 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 | |
d6a698bb KH |
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 | |
5a8e991e KH |
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 = [] | |
f5f22afe | 50 | cd = git.CommitData(tree = new_commit.data.tree, parents = old_log, |
5a8e991e KH |
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) | |
d6de0462 CM |
78 | try: |
79 | # this compatibility log ref might not exist | |
80 | self.__stack.repository.refs.delete(self.__log_ref) | |
81 | except KeyError: | |
82 | pass | |
cbe4567e | 83 | def set_commit(self, commit, msg): |
5a8e991e KH |
84 | self.__write_compat_files(commit, msg) |
85 | self.__stack.repository.refs.set(self.__ref, commit, msg) | |
cbe4567e | 86 | def delete(self): |
5a8e991e KH |
87 | self.__delete_compat_files() |
88 | self.__stack.repository.refs.delete(self.__ref) | |
cbe4567e KH |
89 | def is_applied(self): |
90 | return self.name in self.__stack.patchorder.applied | |
91 | def is_empty(self): | |
dcd32afa | 92 | return self.commit.data.is_nochange() |
85aaed81 KH |
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 | |
cbe4567e KH |
102 | |
103 | class PatchOrder(object): | |
104 | """Keeps track of patch order, and which patches are applied. | |
105 | Works with patch names, not actual patches.""" | |
cbe4567e KH |
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)) | |
448e5d9d CM |
127 | hidden = property(lambda self: self.__get_list('hidden'), |
128 | lambda self, val: self.__set_list('hidden', val)) | |
d454cc06 CM |
129 | all = property(lambda self: self.applied + self.unapplied + self.hidden) |
130 | all_visible = property(lambda self: self.applied + self.unapplied) | |
cbe4567e | 131 | |
21b42305 CM |
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 | ||
cbe4567e | 140 | class Patches(object): |
652b2e67 KH |
141 | """Creates L{Patch} objects. Makes sure there is only one such object |
142 | per patch.""" | |
cbe4567e KH |
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 | ||
04c77bc5 | 165 | class Stack(git.Branch): |
652b2e67 KH |
166 | """Represents an StGit stack (that is, a git branch with some extra |
167 | metadata).""" | |
04c77bc5 CM |
168 | __repo_subdir = 'patches' |
169 | ||
cbe4567e | 170 | def __init__(self, repository, name): |
04c77bc5 | 171 | git.Branch.__init__(self, repository, name) |
cbe4567e KH |
172 | self.__patchorder = PatchOrder(self) |
173 | self.__patches = Patches(self) | |
317b386f | 174 | if not stackupgrade.update_to_current_format_version(repository, name): |
21b42305 | 175 | raise StackException('%s: branch not initialized' % name) |
cbe4567e KH |
176 | patchorder = property(lambda self: self.__patchorder) |
177 | patches = property(lambda self: self.__patches) | |
178 | @property | |
179 | def directory(self): | |
04c77bc5 | 180 | return os.path.join(self.repository.directory, self.__repo_subdir, self.name) |
cbe4567e KH |
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 | |
117ed129 KH |
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 | |
dcd32afa KH |
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 | |
cbe4567e | 201 | |
21b42305 CM |
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 | ||
cbe4567e | 251 | class Repository(git.Repository): |
652b2e67 KH |
252 | """A git L{Repository<git.Repository>} with some added StGit-specific |
253 | operations.""" | |
cbe4567e KH |
254 | def __init__(self, *args, **kwargs): |
255 | git.Repository.__init__(self, *args, **kwargs) | |
256 | self.__stacks = {} # name -> Stack | |
257 | @property | |
cbe4567e | 258 | def current_stack(self): |
13b26f1a CM |
259 | return self.get_stack() |
260 | def get_stack(self, name = None): | |
261 | if not name: | |
04c77bc5 | 262 | name = self.current_branch_name |
cbe4567e KH |
263 | if not name in self.__stacks: |
264 | self.__stacks[name] = Stack(self, name) | |
265 | return self.__stacks[name] |