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, commit):
291 return LogEntry.from_commit(repo, commit)
292 except LogException, e:
293 raise LogException('While reading log from %s: %s' % (ref, e))
295 def same_state(log1, log2):
296 """Check whether two log entries describe the same current state."""
297 s = [[lg.head, lg.applied, lg.unapplied, lg.hidden, lg.patches]
298 for lg in [log1, log2]]
301 def log_entry(stack, msg):
302 """Write a new log entry for the stack."""
303 ref = log_ref(stack.name)
305 last_log_commit = stack.repository.refs.get(ref)
307 last_log_commit = None
310 last_log = get_log_entry(stack.repository, ref, last_log_commit)
313 new_log = LogEntry.from_stack(last_log, stack, msg)
314 except LogException, e:
315 out.warn(str(e), 'No log entry written.')
317 if last_log and same_state(last_log, new_log):
319 new_log.write_commit()
320 stack.repository.refs.set(ref, new_log.commit, msg)
322 class Fakestack(object):
323 """Imitates a real L{Stack<stgit.lib.stack.Stack>}, but with the
324 topmost patch popped."""
325 def __init__(self, stack):
326 appl = list(stack.patchorder.applied)
327 unappl = list(stack.patchorder.unapplied)
328 hidd = list(stack.patchorder.hidden)
329 class patchorder(object):
331 unapplied = [appl[-1]] + unappl
333 all = appl + unappl + hidd
334 self.patchorder = patchorder
335 class patches(object):
340 commit = stack.patches.get(pn).old_commit
343 return stack.patches.get(pn)
344 self.patches = patches
345 self.head = stack.head.data.parent
346 self.top = stack.top.data.parent
347 self.base = stack.base
348 self.name = stack.name
349 self.repository = stack.repository
350 def compat_log_entry(msg):
351 """Write a new log entry. (Convenience function intended for use by
352 code not yet converted to the new infrastructure.)"""
353 repo = default_repo()
355 stack = repo.get_stack(repo.current_branch_name)
356 except libstack.StackException, e:
357 out.warn(str(e), 'Could not write to stack log')
359 if repo.default_index.conflicts() and stack.patchorder.applied:
360 log_entry(Fakestack(stack), msg)
361 log_entry(stack, msg + ' (CONFLICT)')
363 log_entry(stack, msg)
365 def delete_log(repo, branch):
366 ref = log_ref(branch)
367 if repo.refs.exists(ref):
368 repo.refs.delete(ref)
370 def rename_log(repo, old_branch, new_branch, msg):
371 old_ref = log_ref(old_branch)
372 new_ref = log_ref(new_branch)
373 if repo.refs.exists(old_ref):
374 repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
375 repo.refs.delete(old_ref)
377 def copy_log(repo, src_branch, dst_branch, msg):
378 src_ref = log_ref(src_branch)
379 dst_ref = log_ref(dst_branch)
380 if repo.refs.exists(src_ref):
381 repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
384 return libstack.Repository.default()