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)
157 return self.patches[self.applied[0]].data.parent
161 def from_stack(cls, prev, stack, message):
163 repo = stack.repository,
166 applied = list(stack.patchorder.applied),
167 unapplied = list(stack.patchorder.unapplied),
168 hidden = list(stack.patchorder.hidden),
169 patches = dict((pn, stack.patches.get(pn).commit)
170 for pn in stack.patchorder.all),
173 def __parse_metadata(repo, metadata):
174 """Parse a stack log metadata string."""
175 if not metadata.startswith('Version:'):
176 raise LogParseException('Malformed log metadata')
177 metadata = metadata.splitlines()
178 version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
180 version = int(version_str)
182 raise LogParseException(
183 'Malformed version number: %r' % version_str)
185 raise LogException('Log is version %d, which is too old' % version)
187 raise LogException('Log is version %d, which is too new' % version)
189 for line in metadata:
190 if line.startswith(' '):
191 parsed[key].append(line.strip())
193 key, val = [x.strip() for x in line.split(':', 1)]
198 prev = parsed['Previous']
202 prev = repo.get_commit(prev)
203 head = repo.get_commit(parsed['Head'])
204 lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
206 for lst in lists.keys():
207 for entry in parsed[lst]:
208 pn, sha1 = [x.strip() for x in entry.split(':')]
209 lists[lst].append(pn)
210 patches[pn] = repo.get_commit(sha1)
211 return (prev, head, lists['Applied'], lists['Unapplied'],
212 lists['Hidden'], patches)
214 def from_commit(cls, repo, commit):
215 """Parse a (full or simplified) stack log commit."""
216 message = commit.data.message
218 perm, meta = commit.data.tree.data.entries['meta']
220 raise LogParseException('Not a stack log')
221 (prev, head, applied, unapplied, hidden, patches
222 ) = cls.__parse_metadata(repo, meta.data.str)
223 lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
226 def __metadata_string(self):
227 e = StringIO.StringIO()
228 e.write('Version: 1\n')
229 if self.prev == None:
230 e.write('Previous: None\n')
232 e.write('Previous: %s\n' % self.prev.commit.sha1)
233 e.write('Head: %s\n' % self.head.sha1)
234 for lst, title in [(self.applied, 'Applied'),
235 (self.unapplied, 'Unapplied'),
236 (self.hidden, 'Hidden')]:
237 e.write('%s:\n' % title)
239 e.write(' %s: %s\n' % (pn, self.patches[pn].sha1))
242 """Return the set of parents this log entry needs in order to be a
243 descendant of all the commits it refers to."""
244 xp = set([self.head]) | set(self.patches[pn]
245 for pn in self.unapplied + self.hidden)
247 xp.add(self.patches[self.applied[-1]])
248 if self.prev != None:
249 xp.add(self.prev.commit)
250 xp -= set(self.prev.patches.values())
252 def __tree(self, metadata):
253 if self.prev == None:
255 return patch_file(self.__repo, c.data)
257 prev_top_tree = self.prev.commit.data.tree
258 perm, prev_patch_tree = prev_top_tree.data.entries['patches']
259 # Map from Commit object to patch_file() results taken
260 # from the previous log entry.
261 c2b = dict((self.prev.patches[pn], pf) for pn, pf
262 in prev_patch_tree.data.entries.iteritems())
266 r = patch_file(self.__repo, c.data)
268 patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
269 return self.__repo.commit(git.TreeData({
270 'meta': self.__repo.commit(git.BlobData(metadata)),
271 'patches': self.__repo.commit(git.TreeData(patches)) }))
272 def write_commit(self):
273 metadata = self.__metadata_string()
274 tree = self.__tree(metadata)
275 self.__simplified = self.__repo.commit(git.CommitData(
276 tree = tree, message = self.message,
277 parents = [prev.simplified for prev in [self.prev]
279 parents = list(self.__parents())
280 while len(parents) >= self.__max_parents:
281 g = self.__repo.commit(git.CommitData(
282 tree = tree, parents = parents[-self.__max_parents:],
283 message = 'Stack log parent grouping'))
284 parents[-self.__max_parents:] = [g]
285 self.commit = self.__repo.commit(git.CommitData(
286 tree = tree, message = self.message,
287 parents = [self.simplified] + parents))
289 def get_log_entry(repo, ref):
291 return LogEntry.from_commit(repo, repo.rev_parse(ref))
292 except LogException, e:
293 raise LogException('While reading log from %s: %s' % (ref, e))
295 def log_entry(stack, msg):
296 """Write a new log entry for the stack."""
297 ref = log_ref(stack.name)
299 last_log = stack.repository.refs.get(ref)
303 new_log = LogEntry.from_stack(last_log, stack, msg)
304 except LogException, e:
305 out.warn(str(e), 'No log entry written.')
307 new_log.write_commit()
308 stack.repository.refs.set(ref, new_log.commit, msg)
310 def compat_log_entry(msg):
311 """Write a new log entry. (Convenience function intended for use by
312 code not yet converted to the new infrastructure.)"""
313 repo = default_repo()
315 stack = repo.get_stack(repo.current_branch_name)
316 except libstack.StackException, e:
317 out.warn(str(e), 'Could not write to stack log')
319 log_entry(stack, msg)
321 def delete_log(repo, branch):
322 ref = log_ref(branch)
323 if repo.refs.exists(ref):
324 repo.refs.delete(ref)
326 def rename_log(repo, old_branch, new_branch, msg):
327 old_ref = log_ref(old_branch)
328 new_ref = log_ref(new_branch)
329 if repo.refs.exists(old_ref):
330 repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
331 repo.refs.delete(old_ref)
333 def copy_log(repo, src_branch, dst_branch, msg):
334 src_ref = log_ref(src_branch)
335 dst_ref = log_ref(dst_branch)
336 if repo.refs.exists(src_ref):
337 repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
340 return libstack.Repository.default()