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."""
103 from stgit.lib import git, stack as libstack
104 from stgit import exception, utils
105 from stgit.out import out
108 class LogException(exception.StgException):
111 class LogParseException(LogException):
114 def patch_file(repo, cd):
115 return repo.commit(git.BlobData(''.join(s + '\n' for s in [
116 'Bottom: %s' % cd.parent.data.tree.sha1,
117 'Top: %s' % cd.tree.sha1,
118 'Author: %s' % cd.author.name_email,
119 'Date: %s' % cd.author.date,
125 repo.diff_tree(cd.parent.data.tree, cd.tree, ['-M']
129 return 'refs/heads/%s.stgit' % branch
131 class LogEntry(object):
132 __separator = '\n---\n'
134 def __init__(self, repo, prev, head, applied, unapplied, hidden,
138 self.__simplified = None
140 self.applied = applied
141 self.unapplied = unapplied
143 self.patches = patches
144 self.message = message
146 def simplified(self):
147 if not self.__simplified:
148 self.__simplified = self.commit.data.parents[0]
149 return self.__simplified
152 if self.__prev != None and not isinstance(self.__prev, LogEntry):
153 self.__prev = self.from_commit(self.__repo, self.__prev)
158 return self.patches[self.applied[0]].data.parent
164 return self.patches[self.applied[-1]]
167 all_patches = property(lambda self: (self.applied + self.unapplied
170 def from_stack(cls, prev, stack, message):
172 repo = stack.repository,
175 applied = list(stack.patchorder.applied),
176 unapplied = list(stack.patchorder.unapplied),
177 hidden = list(stack.patchorder.hidden),
178 patches = dict((pn, stack.patches.get(pn).commit)
179 for pn in stack.patchorder.all),
182 def __parse_metadata(repo, metadata):
183 """Parse a stack log metadata string."""
184 if not metadata.startswith('Version:'):
185 raise LogParseException('Malformed log metadata')
186 metadata = metadata.splitlines()
187 version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
189 version = int(version_str)
191 raise LogParseException(
192 'Malformed version number: %r' % version_str)
194 raise LogException('Log is version %d, which is too old' % version)
196 raise LogException('Log is version %d, which is too new' % version)
198 for line in metadata:
199 if line.startswith(' '):
200 parsed[key].append(line.strip())
202 key, val = [x.strip() for x in line.split(':', 1)]
207 prev = parsed['Previous']
211 prev = repo.get_commit(prev)
212 head = repo.get_commit(parsed['Head'])
213 lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
215 for lst in lists.keys():
216 for entry in parsed[lst]:
217 pn, sha1 = [x.strip() for x in entry.split(':')]
218 lists[lst].append(pn)
219 patches[pn] = repo.get_commit(sha1)
220 return (prev, head, lists['Applied'], lists['Unapplied'],
221 lists['Hidden'], patches)
223 def from_commit(cls, repo, commit):
224 """Parse a (full or simplified) stack log commit."""
225 message = commit.data.message
227 perm, meta = commit.data.tree.data.entries['meta']
229 raise LogParseException('Not a stack log')
230 (prev, head, applied, unapplied, hidden, patches
231 ) = cls.__parse_metadata(repo, meta.data.str)
232 lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
235 def __metadata_string(self):
236 e = StringIO.StringIO()
237 e.write('Version: 1\n')
238 if self.prev == None:
239 e.write('Previous: None\n')
241 e.write('Previous: %s\n' % self.prev.commit.sha1)
242 e.write('Head: %s\n' % self.head.sha1)
243 for lst, title in [(self.applied, 'Applied'),
244 (self.unapplied, 'Unapplied'),
245 (self.hidden, 'Hidden')]:
246 e.write('%s:\n' % title)
248 e.write(' %s: %s\n' % (pn, self.patches[pn].sha1))
251 """Return the set of parents this log entry needs in order to be a
252 descendant of all the commits it refers to."""
253 xp = set([self.head]) | set(self.patches[pn]
254 for pn in self.unapplied + self.hidden)
256 xp.add(self.patches[self.applied[-1]])
257 if self.prev != None:
258 xp.add(self.prev.commit)
259 xp -= set(self.prev.patches.values())
261 def __tree(self, metadata):
262 if self.prev == None:
264 return patch_file(self.__repo, c.data)
266 prev_top_tree = self.prev.commit.data.tree
267 perm, prev_patch_tree = prev_top_tree.data.entries['patches']
268 # Map from Commit object to patch_file() results taken
269 # from the previous log entry.
270 c2b = dict((self.prev.patches[pn], pf) for pn, pf
271 in prev_patch_tree.data.entries.iteritems())
275 r = patch_file(self.__repo, c.data)
277 patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
278 return self.__repo.commit(git.TreeData({
279 'meta': self.__repo.commit(git.BlobData(metadata)),
280 'patches': self.__repo.commit(git.TreeData(patches)) }))
281 def write_commit(self):
282 metadata = self.__metadata_string()
283 tree = self.__tree(metadata)
284 self.__simplified = self.__repo.commit(git.CommitData(
285 tree = tree, message = self.message,
286 parents = [prev.simplified for prev in [self.prev]
288 parents = list(self.__parents())
289 while len(parents) >= self.__max_parents:
290 g = self.__repo.commit(git.CommitData(
291 tree = tree, parents = parents[-self.__max_parents:],
292 message = 'Stack log parent grouping'))
293 parents[-self.__max_parents:] = [g]
294 self.commit = self.__repo.commit(git.CommitData(
295 tree = tree, message = self.message,
296 parents = [self.simplified] + parents))
298 def get_log_entry(repo, ref, commit):
300 return LogEntry.from_commit(repo, commit)
301 except LogException, e:
302 raise LogException('While reading log from %s: %s' % (ref, e))
304 def same_state(log1, log2):
305 """Check whether two log entries describe the same current state."""
306 s = [[lg.head, lg.applied, lg.unapplied, lg.hidden, lg.patches]
307 for lg in [log1, log2]]
310 def log_entry(stack, msg):
311 """Write a new log entry for the stack."""
312 ref = log_ref(stack.name)
314 last_log_commit = stack.repository.refs.get(ref)
316 last_log_commit = None
319 last_log = get_log_entry(stack.repository, ref, last_log_commit)
322 new_log = LogEntry.from_stack(last_log, stack, msg)
323 except LogException, e:
324 out.warn(str(e), 'No log entry written.')
326 if last_log and same_state(last_log, new_log):
328 new_log.write_commit()
329 stack.repository.refs.set(ref, new_log.commit, msg)
331 class Fakestack(object):
332 """Imitates a real L{Stack<stgit.lib.stack.Stack>}, but with the
333 topmost patch popped."""
334 def __init__(self, stack):
335 appl = list(stack.patchorder.applied)
336 unappl = list(stack.patchorder.unapplied)
337 hidd = list(stack.patchorder.hidden)
338 class patchorder(object):
340 unapplied = [appl[-1]] + unappl
342 all = appl + unappl + hidd
343 self.patchorder = patchorder
344 class patches(object):
349 commit = stack.patches.get(pn).old_commit
352 return stack.patches.get(pn)
353 self.patches = patches
354 self.head = stack.head.data.parent
355 self.top = stack.top.data.parent
356 self.base = stack.base
357 self.name = stack.name
358 self.repository = stack.repository
359 def compat_log_entry(msg):
360 """Write a new log entry. (Convenience function intended for use by
361 code not yet converted to the new infrastructure.)"""
362 repo = default_repo()
364 stack = repo.get_stack(repo.current_branch_name)
365 except libstack.StackException, e:
366 out.warn(str(e), 'Could not write to stack log')
368 if repo.default_index.conflicts() and stack.patchorder.applied:
369 log_entry(Fakestack(stack), msg)
370 log_entry(stack, msg + ' (CONFLICT)')
372 log_entry(stack, msg)
374 def delete_log(repo, branch):
375 ref = log_ref(branch)
376 if repo.refs.exists(ref):
377 repo.refs.delete(ref)
379 def rename_log(repo, old_branch, new_branch, msg):
380 old_ref = log_ref(old_branch)
381 new_ref = log_ref(new_branch)
382 if repo.refs.exists(old_ref):
383 repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
384 repo.refs.delete(old_ref)
386 def copy_log(repo, src_branch, dst_branch, msg):
387 src_ref = log_ref(src_branch)
388 dst_ref = log_ref(dst_branch)
389 if repo.refs.exists(src_ref):
390 repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
393 return libstack.Repository.default()
395 def reset_stack(trans, iw, state):
396 """Reset the stack to a given previous state."""
397 for pn in trans.all_patches:
398 trans.patches[pn] = None
399 for pn in state.all_patches:
400 trans.patches[pn] = state.patches[pn]
401 trans.applied = state.applied
402 trans.unapplied = state.unapplied
403 trans.hidden = state.hidden
404 trans.base = state.base
405 trans.head = state.head
407 def reset_stack_partially(trans, iw, state, only_patches):
408 """Reset the stack to a given previous state -- but only the given
409 patches, not anything else.
411 @param only_patches: Touch only these patches
412 @type only_patches: iterable"""
413 only_patches = set(only_patches)
414 patches_to_reset = set(state.all_patches) & only_patches
415 existing_patches = set(trans.all_patches)
416 original_applied_order = list(trans.applied)
417 to_delete = (existing_patches - patches_to_reset) & only_patches
419 # In one go, do all the popping we have to in order to pop the
420 # patches we're going to delete or modify.
422 if not pn in only_patches:
426 if trans.patches[pn] != state.patches.get(pn, None):
429 trans.pop_patches(mod)
431 # Delete and modify/create patches. We've previously popped all
432 # patches that we touch in this step.
433 trans.delete_patches(lambda pn: pn in to_delete)
434 for pn in patches_to_reset:
435 if pn in existing_patches:
436 if trans.patches[pn] == state.patches[pn]:
439 out.info('Resetting %s' % pn)
441 if pn in state.hidden:
442 trans.hidden.append(pn)
444 trans.unapplied.append(pn)
445 out.info('Resurrecting %s' % pn)
446 trans.patches[pn] = state.patches[pn]
448 # Push all the patches that we've popped, if they still
450 pushable = set(trans.unapplied + trans.hidden)
451 for pn in original_applied_order:
453 trans.push_patch(pn, iw)
455 def undo_state(stack, undo_steps):
456 """Find the log entry C{undo_steps} steps in the past. (Successive
457 undo operations are supposed to "add up", so if we find other undo
458 operations along the way we have to add those undo steps to
461 If C{undo_steps} is negative, redo instead of undo.
463 @return: The log entry that is the destination of the undo
465 @rtype: L{LogEntry}"""
466 ref = log_ref(stack.name)
468 commit = stack.repository.refs.get(ref)
470 raise LogException('Log is empty')
471 log = get_log_entry(stack.repository, ref, commit)
472 while undo_steps != 0:
473 msg = log.message.strip()
474 um = re.match(r'^undo\s+(\d+)$', msg)
477 undo_steps += int(um.group(1))
481 rm = re.match(r'^redo\s+(\d+)$', msg)
485 undo_steps -= int(rm.group(1))
487 raise LogException('No more redo information available')
489 raise LogException('Not enough undo information available')
493 def log_external_mods(stack):
494 ref = log_ref(stack.name)
496 log_commit = stack.repository.refs.get(ref)
499 log_entry(stack, 'start of log')
502 log = get_log_entry(stack.repository, ref, log_commit)
504 # Something's wrong with the log, so don't bother.
506 if log.head == stack.head:
507 # No external modifications.
509 log_entry(stack, '\n'.join([
510 'external modifications', '',
511 'Modifications by tools other than StGit (e.g. git).']))
513 def compat_log_external_mods():
515 repo = default_repo()
516 except git.RepositoryException:
517 # No repository, so we couldn't log even if we wanted to.
520 stack = repo.get_stack(repo.current_branch_name)
521 except exception.StgException:
522 # Stack doesn't exist, so we can't log.
524 log_external_mods(stack)