1 r"""This module contains functions and classes for manipulating
2 I{patch stack logs} (or just I{stack logs}).
4 A stack log is a git branch. Each commit contains the complete state
5 of the stack at the moment it was written; the most recent commit has
8 For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.
9 Each log entry makes sure to have proper references to everything it
10 needs, which means that it is safe against garbage collection -- you
11 can even pull it from one repository to another.
13 Stack log format (version 0)
14 ============================
16 Version 0 was an experimental version of the stack log format; it is
19 Stack log format (version 1)
20 ============================
25 The commit message is mostly for human consumption; in most cases it
26 is just a subject line: the stg subcommand name and possibly some
27 important command-line flag.
29 An exception to this is log commits for undo and redo. Their subject
30 line is "C{undo I{n}}" and "C{redo I{n}}"; the positive integer I{n}
31 says how many steps were undone or redone.
36 - One blob, C{meta}, that contains the log data:
40 where I{n} must be 1. (Future versions of StGit might change
41 the log format; when this is done, this version number will be
44 - C{Previous:} I{sha1 or C{None}}
46 The commit of the previous log entry, or C{None} if this is
51 The current branch head.
55 Marks the start of the list of applied patches. They are
56 listed in order, each on its own line: first one or more
57 spaces, then the patch name, then a colon, space, then the
62 Same as C{Applied:}, but for the unapplied patches.
66 Same as C{Applied:}, but for the hidden patches.
68 - One subtree, C{patches}, that contains one blob per patch::
70 Bottom: <sha1 of patch's bottom tree>
71 Top: <sha1 of patch's top tree>
72 Author: <author name and e-mail>
73 Date: <patch timestamp>
81 Following the message is a newline, three dashes, and another newline.
82 Then come, each on its own line,
87 - The first parent is the I{simplified log}, described below.
89 - The rest of the parents are just there to make sure that all the
90 commits referred to in the log entry -- patches, branch head,
91 previous log entry -- are ancestors of the log commit. (This is
92 necessary to make the log safe with regard to garbage collection
98 The simplified log is exactly like the full log, except that its only
99 parent is the (simplified) previous log entry, if any. It's purpose is
100 mainly ease of visualization."""
102 from stgit.lib import git, stack as libstack
103 from stgit import exception, utils
104 from stgit.out import out
107 class LogException(exception.StgException):
110 class LogParseException(LogException):
113 def patch_file(repo, cd):
114 return repo.commit(git.BlobData(''.join(s + '\n' for s in [
115 'Bottom: %s' % cd.parent.data.tree.sha1,
116 'Top: %s' % cd.tree.sha1,
117 'Author: %s' % cd.author.name_email,
118 'Date: %s' % cd.author.date,
124 repo.diff_tree(cd.parent.data.tree, cd.tree, ['-M']
128 return 'refs/heads/%s.stgit' % branch
130 class LogEntry(object):
131 __separator = '\n---\n'
133 def __init__(self, repo, prev, head, applied, unapplied, hidden,
137 self.__simplified = None
139 self.applied = applied
140 self.unapplied = unapplied
142 self.patches = patches
143 self.message = message
145 def simplified(self):
146 if not self.__simplified:
147 self.__simplified = self.commit.data.parents[0]
148 return self.__simplified
151 if self.__prev != None and not isinstance(self.__prev, LogEntry):
152 self.__prev = self.from_commit(self.__repo, self.__prev)
155 def from_stack(cls, prev, stack, message):
157 repo = stack.repository,
160 applied = list(stack.patchorder.applied),
161 unapplied = list(stack.patchorder.unapplied),
162 hidden = list(stack.patchorder.hidden),
163 patches = dict((pn, stack.patches.get(pn).commit)
164 for pn in stack.patchorder.all),
167 def __parse_metadata(repo, metadata):
168 """Parse a stack log metadata string."""
169 if not metadata.startswith('Version:'):
170 raise LogParseException('Malformed log metadata')
171 metadata = metadata.splitlines()
172 version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
174 version = int(version_str)
176 raise LogParseException(
177 'Malformed version number: %r' % version_str)
179 raise LogException('Log is version %d, which is too old' % version)
181 raise LogException('Log is version %d, which is too new' % version)
183 for line in metadata:
184 if line.startswith(' '):
185 parsed[key].append(line.strip())
187 key, val = [x.strip() for x in line.split(':', 1)]
192 prev = parsed['Previous']
196 prev = repo.get_commit(prev)
197 head = repo.get_commit(parsed['Head'])
198 lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
200 for lst in lists.keys():
201 for entry in parsed[lst]:
202 pn, sha1 = [x.strip() for x in entry.split(':')]
203 lists[lst].append(pn)
204 patches[pn] = repo.get_commit(sha1)
205 return (prev, head, lists['Applied'], lists['Unapplied'],
206 lists['Hidden'], patches)
208 def from_commit(cls, repo, commit):
209 """Parse a (full or simplified) stack log commit."""
210 message = commit.data.message
212 perm, meta = commit.data.tree.data.entries['meta']
214 raise LogParseException('Not a stack log')
215 (prev, head, applied, unapplied, hidden, patches
216 ) = cls.__parse_metadata(repo, meta.data.str)
217 lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
220 def __metadata_string(self):
221 e = StringIO.StringIO()
222 e.write('Version: 1\n')
223 if self.prev == None:
224 e.write('Previous: None\n')
226 e.write('Previous: %s\n' % self.prev.commit.sha1)
227 e.write('Head: %s\n' % self.head.sha1)
228 for lst, title in [(self.applied, 'Applied'),
229 (self.unapplied, 'Unapplied'),
230 (self.hidden, 'Hidden')]:
231 e.write('%s:\n' % title)
233 e.write(' %s: %s\n' % (pn, self.patches[pn].sha1))
236 """Return the set of parents this log entry needs in order to be a
237 descendant of all the commits it refers to."""
238 xp = set([self.head]) | set(self.patches[pn]
239 for pn in self.unapplied + self.hidden)
241 xp.add(self.patches[self.applied[-1]])
242 if self.prev != None:
243 xp.add(self.prev.commit)
244 xp -= set(self.prev.patches.values())
246 def __tree(self, metadata):
247 if self.prev == None:
249 return patch_file(self.__repo, c.data)
251 prev_top_tree = self.prev.commit.data.tree
252 perm, prev_patch_tree = prev_top_tree.data.entries['patches']
253 # Map from Commit object to patch_file() results taken
254 # from the previous log entry.
255 c2b = dict((self.prev.patches[pn], pf) for pn, pf
256 in prev_patch_tree.data.entries.iteritems())
260 r = patch_file(self.__repo, c.data)
262 patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
263 return self.__repo.commit(git.TreeData({
264 'meta': self.__repo.commit(git.BlobData(metadata)),
265 'patches': self.__repo.commit(git.TreeData(patches)) }))
266 def write_commit(self):
267 metadata = self.__metadata_string()
268 tree = self.__tree(metadata)
269 self.__simplified = self.__repo.commit(git.CommitData(
270 tree = tree, message = self.message,
271 parents = [prev.simplified for prev in [self.prev]
273 parents = list(self.__parents())
274 while len(parents) >= self.__max_parents:
275 g = self.__repo.commit(git.CommitData(
276 tree = tree, parents = parents[-self.__max_parents:],
277 message = 'Stack log parent grouping'))
278 parents[-self.__max_parents:] = [g]
279 self.commit = self.__repo.commit(git.CommitData(
280 tree = tree, message = self.message,
281 parents = [self.simplified] + parents))
283 def log_entry(stack, msg):
284 """Write a new log entry for the stack."""
285 ref = log_ref(stack.name)
287 last_log = stack.repository.refs.get(ref)
291 new_log = LogEntry.from_stack(last_log, stack, msg)
292 except LogException, e:
293 out.warn(str(e), 'No log entry written.')
295 new_log.write_commit()
296 stack.repository.refs.set(ref, new_log.commit, msg)
298 def compat_log_entry(msg):
299 """Write a new log entry. (Convenience function intended for use by
300 code not yet converted to the new infrastructure.)"""
301 repo = default_repo()
303 stack = repo.get_stack(repo.current_branch_name)
304 except libstack.StackException, e:
305 out.warn(str(e), 'Could not write to stack log')
307 log_entry(stack, msg)
309 def delete_log(repo, branch):
310 ref = log_ref(branch)
311 if repo.refs.exists(ref):
312 repo.refs.delete(ref)
314 def rename_log(repo, old_branch, new_branch, msg):
315 old_ref = log_ref(old_branch)
316 new_ref = log_ref(new_branch)
317 if repo.refs.exists(old_ref):
318 repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
319 repo.refs.delete(old_ref)
321 def copy_log(repo, src_branch, dst_branch, msg):
322 src_ref = log_ref(src_branch)
323 dst_ref = log_ref(dst_branch)
324 if repo.refs.exists(src_ref):
325 repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
328 return libstack.Repository.default()