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 | |
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 | |
83 | class 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 | 120 | class 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 | 145 | class 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 | 222 | class 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] |