1 """Functions for managing stack state log.
3 A stack log is a Git branch. Each commit contains the complete state (metadata) of the
4 stack at the moment it was written; the most recent commit has the most recent state.
6 For a branch `foo`, the stack state log is stored in the ref `refs/stacks/foo`.
8 Each log entry makes sure to have proper references to everything it needs to make it
9 safe against garbage collection. The stack state log can even be pulled from one
10 repository to another.
15 Version 0 was an experimental version of the stack log format; it is no longer
24 The commit message is mostly for human consumption; in most cases it is just a subject
25 line: the StGit subcommand name and possibly some important command-line flag.
27 An exception to this is commits for ``undo`` and ``redo``. Their subject line is "undo
28 `n`" and "redo `n`"; the positive integer `n` indicates how many steps were undone or
34 * One blob, ``meta``, that contains the stack state:
38 Future versions of StGit might change the log format; when this is done, this
39 version number will be incremented.
41 * ``Previous: <sha1 or None>``
43 The commit of the previous log entry, or None if this is the first entry.
47 The current branch head.
51 Marks the start of the list of applied patches. Each patch is listed in order, one
52 per line. First one or more spaces, then the patch name, then a colon, space, then
57 Same as ``Applied:``, but for the unapplied patches.
61 Same as ``Applied:``, but for the hidden patches.
63 * One subtree, ``patches``, that contains one blob per patch::
65 Bottom: <sha1 of patch's bottom tree>
66 Top: <sha1 of patch's top tree>
67 Author: <author name and e-mail>
68 Date: <patch timestamp>
75 * The first parent is the *simplified log*, described below.
77 * The rest of the parents are just there to make sure that all the commits referred to
78 in the log entry--patches, branch head, previous log entry--are ancestors of the log
79 commit. This is necessary to make the log safe with regard to garbage collection and
85 The simplified log is exactly like the full log, except that its only parent is the
86 (simplified) previous log entry, if any. It's purpose is mainly ease of visualization
87 in, for example, ``git log --graph`` or ``gitk``.
92 The metadata in `refs/heads/<branch>.stgit` branch is the same as format version 1.
94 Format version 4 indicates that, unlike previous format versions used by older versions
95 of StGit, the stack log state is *only* contained in the stack log branch and *not* as
96 files in the .git/patches directory.
101 Stack metadata resides in `refs/stacks/<branch>` where the `stack.json` metadata file
102 is JSON format instead of the previous custom format.
109 from stgit
.exception
import StgException
110 from stgit
.lib
.git
import BlobData
, CommitData
, TreeData
111 from stgit
.lib
.stackupgrade
import FORMAT_VERSION
112 from stgit
.out
import out
115 class LogException(StgException
):
119 class LogParseException(LogException
):
138 self
.applied
= applied
139 self
.unapplied
= unapplied
141 self
.patches
= patches
145 def simplified_parent(self
):
146 return self
.commit
.data
.parents
[0]
148 def get_prev_state(self
, repo
):
149 if self
.prev
is None:
152 return self
.from_commit(repo
, self
.prev
)
157 return self
.patches
[self
.applied
[0]].data
.parent
164 return self
.patches
[self
.applied
[-1]]
169 def all_patches(self
):
170 return self
.applied
+ self
.unapplied
+ self
.hidden
173 def new_empty(cls
, head
):
184 def from_stack(cls
, prev
, stack
):
188 applied
=list(stack
.patchorder
.applied
),
189 unapplied
=list(stack
.patchorder
.unapplied
),
190 hidden
=list(stack
.patchorder
.hidden
),
191 patches
={pn
: stack
.patches
[pn
] for pn
in stack
.patchorder
.all
},
195 def from_commit(cls
, repo
, commit
):
196 """Parse a (full or simplified) stack log commit."""
198 perm
, stack_json_blob
= commit
.data
.tree
.data
['stack.json']
200 raise LogParseException('Not a stack log')
203 stack_json
= json
.loads(stack_json_blob
.data
.bytes
.decode('utf-8'))
204 except json
.JSONDecodeError
as e
:
205 raise LogParseException(str(e
))
207 version
= stack_json
.get('version')
210 raise LogException('Missing stack metadata version')
211 elif version
< FORMAT_VERSION
:
212 raise LogException('Log is version %d, which is too old' % version
)
213 elif version
> FORMAT_VERSION
:
214 raise LogException('Log is version %d, which is too new' % version
)
217 pn
: repo
.get_commit(patch_info
['oid'])
218 for pn
, patch_info
in stack_json
['patches'].items()
221 if stack_json
['prev'] is None:
224 prev
= repo
.get_commit(stack_json
['prev'])
228 repo
.get_commit(stack_json
['head']),
229 stack_json
['applied'],
230 stack_json
['unapplied'],
231 stack_json
['hidden'],
236 def _parents(self
, prev_state
):
237 """Parents this entry needs to be a descendant of all commits it refers to."""
238 xp
= {self
.head
, self
.top
}
239 xp |
= {self
.patches
[pn
] for pn
in self
.unapplied
}
240 xp |
= {self
.patches
[pn
] for pn
in self
.hidden
}
241 if prev_state
is not None:
242 xp
.add(prev_state
.commit
)
243 xp
-= set(prev_state
.patches
.values())
246 def _stack_json_blob(self
, repo
, prev_state
):
248 version
=FORMAT_VERSION
,
249 prev
=None if prev_state
is None else prev_state
.commit
.sha1
,
251 applied
=self
.applied
,
252 unapplied
=self
.unapplied
,
254 patches
={pn
: dict(oid
=patch
.sha1
) for pn
, patch
in self
.patches
.items()},
256 blob
= json
.dumps(stack_json
, indent
=2).encode('utf-8')
257 return repo
.commit(BlobData(blob
))
259 def _patch_blob(self
, repo
, pn
, commit
, prev_state
):
260 if prev_state
is not None:
261 perm
, prev_patch_tree
= prev_state
.commit
.data
.tree
.data
['patches']
262 if pn
in prev_patch_tree
.data
and commit
== prev_state
.patches
[pn
]:
263 return prev_patch_tree
.data
[pn
]
265 patch_meta
= '\n'.join(
267 'Bottom: %s' % commit
.data
.parent
.data
.tree
.sha1
,
268 'Top: %s' % commit
.data
.tree
.sha1
,
269 'Author: %s' % commit
.data
.author
.name_email
,
270 'Date: %s' % commit
.data
.author
.date
,
272 commit
.data
.message_str
,
275 return repo
.commit(BlobData(patch_meta
.encode('utf-8')))
277 def _patches_tree(self
, repo
, prev_state
):
281 pn
: self
._patch
_blob
(repo
, pn
, commit
, prev_state
)
282 for pn
, commit
in self
.patches
.items()
287 def _tree(self
, repo
, prev_state
):
291 'stack.json': self
._stack
_json
_blob
(repo
, prev_state
),
292 'patches': self
._patches
_tree
(repo
, prev_state
),
297 def commit_state(self
, repo
, message
):
298 """Commit stack state to stack metadata branch."""
299 prev_state
= self
.get_prev_state(repo
)
300 tree
= self
._tree
(repo
, prev_state
)
301 simplified_parent
= repo
.commit(
305 parents
=[] if prev_state
is None else [prev_state
.simplified_parent
],
308 parents
= list(self
._parents
(prev_state
))
309 while len(parents
) >= self
._max
_parents
:
313 parents
=parents
[-self
._max
_parents
:],
314 message
='Stack log parent grouping',
317 parents
[-self
._max
_parents
:] = [g
]
318 self
.commit
= repo
.commit(
322 parents
=[simplified_parent
] + parents
,
327 def same_state(self
, other
):
328 """Check whether two stack state entries describe the same stack state."""
330 self
.head
== other
.head
331 and self
.applied
== other
.applied
332 and self
.unapplied
== other
.unapplied
333 and self
.hidden
== other
.hidden
334 and self
.patches
== other
.patches
338 def get_stack_state(repo
, ref
, commit
=None):
340 commit
= repo
.refs
.get(ref
) # May raise KeyError
342 return StackState
.from_commit(repo
, commit
)
343 except LogException
as e
:
344 raise LogException('While reading log from %s: %s' % (ref
, e
))
347 def log_stack_state(stack
, msg
):
348 """Write a new metadata entry for the stack."""
350 prev_state
= get_stack_state(stack
.repository
, stack
.state_ref
)
355 new_state
= StackState
.from_stack(prev_state
.commit
, stack
)
356 except LogException
as e
:
357 out
.warn(str(e
), 'No log entry written.')
359 if not prev_state
or not new_state
.same_state(prev_state
):
360 state_commit
= new_state
.commit_state(stack
.repository
, msg
)
361 stack
.repository
.refs
.set(stack
.state_ref
, state_commit
, msg
)
364 def reset_stack(trans
, iw
, state
):
365 """Reset the stack to a given previous state."""
366 for pn
in trans
.all_patches
:
367 trans
.patches
[pn
] = None
368 for pn
in state
.all_patches
:
369 trans
.patches
[pn
] = state
.patches
[pn
]
370 trans
.applied
= state
.applied
371 trans
.unapplied
= state
.unapplied
372 trans
.hidden
= state
.hidden
373 trans
.base
= state
.base
374 trans
.head
= state
.head
377 def reset_stack_partially(trans
, iw
, state
, only_patches
):
378 """Reset the stack to a previous state, but only for the given patches.
380 :param only_patches: Touch only these patches
381 :type only_patches: iterable
384 only_patches
= set(only_patches
)
385 patches_to_reset
= set(state
.all_patches
) & only_patches
386 existing_patches
= set(trans
.all_patches
)
387 original_applied_order
= list(trans
.applied
)
388 to_delete
= (existing_patches
- patches_to_reset
) & only_patches
390 # In one go, do all the popping we have to in order to pop the
391 # patches we're going to delete or modify.
393 if pn
not in only_patches
:
397 if trans
.patches
[pn
] != state
.patches
.get(pn
, None):
401 trans
.pop_patches(mod
)
403 # Delete and modify/create patches. We've previously popped all
404 # patches that we touch in this step.
405 trans
.delete_patches(lambda pn
: pn
in to_delete
)
406 for pn
in patches_to_reset
:
407 if pn
in existing_patches
:
408 if trans
.patches
[pn
] == state
.patches
[pn
]:
411 out
.info('Resetting %s' % pn
)
413 if pn
in state
.hidden
:
414 trans
.hidden
.append(pn
)
416 trans
.unapplied
.append(pn
)
417 out
.info('Resurrecting %s' % pn
)
418 trans
.patches
[pn
] = state
.patches
[pn
]
420 # Push all the patches that we've popped, if they still
422 pushable
= set(trans
.unapplied
+ trans
.hidden
)
423 for pn
in original_applied_order
:
425 trans
.push_patch(pn
, iw
)
428 def undo_state(stack
, undo_steps
):
429 """Find the stack state ``undo_steps`` steps in the past.
431 Successive undo operations are supposed to "add up", so if we find other undo
432 operations along the way we have to add those undo steps to ``undo_steps``.
434 If ``undo_steps`` is negative, redo instead of undo.
436 :returns: stack state that is the destination of the undo operation
437 :rtype: :class:`StackState`
441 state
= get_stack_state(stack
.repository
, stack
.state_ref
)
443 raise LogException('Log is empty')
444 while undo_steps
!= 0:
445 msg
= state
.commit
.data
.message_str
.strip()
446 um
= re
.match(r
'^undo\s+(\d+)$', msg
)
449 undo_steps
+= int(um
.group(1))
453 rm
= re
.match(r
'^redo\s+(\d+)$', msg
)
457 undo_steps
-= int(rm
.group(1))
459 raise LogException('No more redo information available')
460 prev_state
= state
.get_prev_state(stack
.repository
)
461 if prev_state
is None:
462 raise LogException('Not enough undo information available')
467 def log_external_mods(stack
):
469 state
= get_stack_state(stack
.repository
, stack
.state_ref
)
472 log_stack_state(stack
, 'start of log')
475 # Something's wrong with the log, so don't bother.
477 if state
.head
== stack
.head
:
478 # No external modifications.
482 'external modifications\n\n'
483 'Modifications by tools other than StGit (e.g. git).\n',