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]]
168 def from_stack(cls, prev, stack, message):
170 repo = stack.repository,
173 applied = list(stack.patchorder.applied),
174 unapplied = list(stack.patchorder.unapplied),
175 hidden = list(stack.patchorder.hidden),
176 patches = dict((pn, stack.patches.get(pn).commit)
177 for pn in stack.patchorder.all),
180 def __parse_metadata(repo, metadata):
181 """Parse a stack log metadata string."""
182 if not metadata.startswith('Version:'):
183 raise LogParseException('Malformed log metadata')
184 metadata = metadata.splitlines()
185 version_str = utils.strip_prefix('Version:', metadata.pop(0)).strip()
187 version = int(version_str)
189 raise LogParseException(
190 'Malformed version number: %r' % version_str)
192 raise LogException('Log is version %d, which is too old' % version)
194 raise LogException('Log is version %d, which is too new' % version)
196 for line in metadata:
197 if line.startswith(' '):
198 parsed[key].append(line.strip())
200 key, val = [x.strip() for x in line.split(':', 1)]
205 prev = parsed['Previous']
209 prev = repo.get_commit(prev)
210 head = repo.get_commit(parsed['Head'])
211 lists = { 'Applied': [], 'Unapplied': [], 'Hidden': [] }
213 for lst in lists.keys():
214 for entry in parsed[lst]:
215 pn, sha1 = [x.strip() for x in entry.split(':')]
216 lists[lst].append(pn)
217 patches[pn] = repo.get_commit(sha1)
218 return (prev, head, lists['Applied'], lists['Unapplied'],
219 lists['Hidden'], patches)
221 def from_commit(cls, repo, commit):
222 """Parse a (full or simplified) stack log commit."""
223 message = commit.data.message
225 perm, meta = commit.data.tree.data.entries['meta']
227 raise LogParseException('Not a stack log')
228 (prev, head, applied, unapplied, hidden, patches
229 ) = cls.__parse_metadata(repo, meta.data.str)
230 lg = cls(repo, prev, head, applied, unapplied, hidden, patches, message)
233 def __metadata_string(self):
234 e = StringIO.StringIO()
235 e.write('Version: 1\n')
236 if self.prev == None:
237 e.write('Previous: None\n')
239 e.write('Previous: %s\n' % self.prev.commit.sha1)
240 e.write('Head: %s\n' % self.head.sha1)
241 for lst, title in [(self.applied, 'Applied'),
242 (self.unapplied, 'Unapplied'),
243 (self.hidden, 'Hidden')]:
244 e.write('%s:\n' % title)
246 e.write(' %s: %s\n' % (pn, self.patches[pn].sha1))
249 """Return the set of parents this log entry needs in order to be a
250 descendant of all the commits it refers to."""
251 xp = set([self.head]) | set(self.patches[pn]
252 for pn in self.unapplied + self.hidden)
254 xp.add(self.patches[self.applied[-1]])
255 if self.prev != None:
256 xp.add(self.prev.commit)
257 xp -= set(self.prev.patches.values())
259 def __tree(self, metadata):
260 if self.prev == None:
262 return patch_file(self.__repo, c.data)
264 prev_top_tree = self.prev.commit.data.tree
265 perm, prev_patch_tree = prev_top_tree.data.entries['patches']
266 # Map from Commit object to patch_file() results taken
267 # from the previous log entry.
268 c2b = dict((self.prev.patches[pn], pf) for pn, pf
269 in prev_patch_tree.data.entries.iteritems())
273 r = patch_file(self.__repo, c.data)
275 patches = dict((pn, pf(c)) for pn, c in self.patches.iteritems())
276 return self.__repo.commit(git.TreeData({
277 'meta': self.__repo.commit(git.BlobData(metadata)),
278 'patches': self.__repo.commit(git.TreeData(patches)) }))
279 def write_commit(self):
280 metadata = self.__metadata_string()
281 tree = self.__tree(metadata)
282 self.__simplified = self.__repo.commit(git.CommitData(
283 tree = tree, message = self.message,
284 parents = [prev.simplified for prev in [self.prev]
286 parents = list(self.__parents())
287 while len(parents) >= self.__max_parents:
288 g = self.__repo.commit(git.CommitData(
289 tree = tree, parents = parents[-self.__max_parents:],
290 message = 'Stack log parent grouping'))
291 parents[-self.__max_parents:] = [g]
292 self.commit = self.__repo.commit(git.CommitData(
293 tree = tree, message = self.message,
294 parents = [self.simplified] + parents))
296 def get_log_entry(repo, ref, commit):
298 return LogEntry.from_commit(repo, commit)
299 except LogException, e:
300 raise LogException('While reading log from %s: %s' % (ref, e))
302 def same_state(log1, log2):
303 """Check whether two log entries describe the same current state."""
304 s = [[lg.head, lg.applied, lg.unapplied, lg.hidden, lg.patches]
305 for lg in [log1, log2]]
308 def log_entry(stack, msg):
309 """Write a new log entry for the stack."""
310 ref = log_ref(stack.name)
312 last_log_commit = stack.repository.refs.get(ref)
314 last_log_commit = None
317 last_log = get_log_entry(stack.repository, ref, last_log_commit)
320 new_log = LogEntry.from_stack(last_log, stack, msg)
321 except LogException, e:
322 out.warn(str(e), 'No log entry written.')
324 if last_log and same_state(last_log, new_log):
326 new_log.write_commit()
327 stack.repository.refs.set(ref, new_log.commit, msg)
329 class Fakestack(object):
330 """Imitates a real L{Stack<stgit.lib.stack.Stack>}, but with the
331 topmost patch popped."""
332 def __init__(self, stack):
333 appl = list(stack.patchorder.applied)
334 unappl = list(stack.patchorder.unapplied)
335 hidd = list(stack.patchorder.hidden)
336 class patchorder(object):
338 unapplied = [appl[-1]] + unappl
340 all = appl + unappl + hidd
341 self.patchorder = patchorder
342 class patches(object):
347 commit = stack.patches.get(pn).old_commit
350 return stack.patches.get(pn)
351 self.patches = patches
352 self.head = stack.head.data.parent
353 self.top = stack.top.data.parent
354 self.base = stack.base
355 self.name = stack.name
356 self.repository = stack.repository
357 def compat_log_entry(msg):
358 """Write a new log entry. (Convenience function intended for use by
359 code not yet converted to the new infrastructure.)"""
360 repo = default_repo()
362 stack = repo.get_stack(repo.current_branch_name)
363 except libstack.StackException, e:
364 out.warn(str(e), 'Could not write to stack log')
366 if repo.default_index.conflicts() and stack.patchorder.applied:
367 log_entry(Fakestack(stack), msg)
368 log_entry(stack, msg + ' (CONFLICT)')
370 log_entry(stack, msg)
372 def delete_log(repo, branch):
373 ref = log_ref(branch)
374 if repo.refs.exists(ref):
375 repo.refs.delete(ref)
377 def rename_log(repo, old_branch, new_branch, msg):
378 old_ref = log_ref(old_branch)
379 new_ref = log_ref(new_branch)
380 if repo.refs.exists(old_ref):
381 repo.refs.set(new_ref, repo.refs.get(old_ref), msg)
382 repo.refs.delete(old_ref)
384 def copy_log(repo, src_branch, dst_branch, msg):
385 src_ref = log_ref(src_branch)
386 dst_ref = log_ref(dst_branch)
387 if repo.refs.exists(src_ref):
388 repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)
391 return libstack.Repository.default()
393 def reset_stack(trans, iw, state, only_patches):
394 """Reset the stack to a given previous state. If C{only_patches} is
395 not empty, touch only patches whose names appear in it.
397 @param only_patches: Reset only these patches
398 @type only_patches: iterable"""
399 only_patches = set(only_patches)
402 return s & only_patches
405 patches_to_reset = mask(set(state.applied + state.unapplied + state.hidden))
406 existing_patches = set(trans.all_patches)
407 original_applied_order = list(trans.applied)
408 to_delete = mask(existing_patches - patches_to_reset)
410 # If we have to change the stack base, we need to pop all patches
412 if not only_patches and trans.base != state.base:
413 trans.pop_patches(lambda pn: True)
414 out.info('Setting stack base to %s' % state.base.sha1)
415 trans.base = state.base
417 # In one go, do all the popping we have to in order to pop the
418 # patches we're going to delete or modify.
420 if only_patches and not pn in only_patches:
424 if trans.patches[pn] != state.patches.get(pn, None):
427 trans.pop_patches(mod)
429 # Delete and modify/create patches. We've previously popped all
430 # patches that we touch in this step.
431 trans.delete_patches(lambda pn: pn in to_delete)
432 for pn in patches_to_reset:
433 if pn in existing_patches:
434 if trans.patches[pn] == state.patches[pn]:
437 out.info('Resetting %s' % pn)
439 if pn in state.hidden:
440 trans.hidden.append(pn)
442 trans.unapplied.append(pn)
443 out.info('Resurrecting %s' % pn)
444 trans.patches[pn] = state.patches[pn]
446 # Push/pop patches as necessary.
448 # Push all the patches that we've popped, if they still
450 pushable = set(trans.unapplied)
451 for pn in original_applied_order:
453 trans.push_patch(pn, iw)
455 # Recreate the exact order specified by the goal state.
456 trans.reorder_patches(state.applied, state.unapplied, state.hidden, iw)
458 def undo_state(stack, undo_steps):
459 """Find the log entry C{undo_steps} steps in the past. (Successive
460 undo operations are supposed to "add up", so if we find other undo
461 operations along the way we have to add those undo steps to
464 If C{undo_steps} is negative, redo instead of undo.
466 @return: The log entry that is the destination of the undo
468 @rtype: L{LogEntry}"""
469 ref = log_ref(stack.name)
471 commit = stack.repository.refs.get(ref)
473 raise LogException('Log is empty')
474 log = get_log_entry(stack.repository, ref, commit)
475 while undo_steps != 0:
476 msg = log.message.strip()
477 um = re.match(r'^undo\s+(\d+)$', msg)
480 undo_steps += int(um.group(1))
484 rm = re.match(r'^redo\s+(\d+)$', msg)
488 undo_steps -= int(rm.group(1))
490 raise LogException('No more redo information available')
492 raise LogException('Not enough undo information available')