1 """A Python class hierarchy wrapping the StGit on-disk metadata."""
4 from stgit
.config
import config
5 from stgit
.exception
import StackException
6 from stgit
.lib
import log
, stackupgrade
7 from stgit
.lib
.git
import Repository
8 from stgit
.lib
.git
.branch
import Branch
, BranchException
11 def _stack_state_ref(stack_name
):
12 """Reference to stack state metadata. A.k.a. the stack's "log"."""
13 return 'refs/heads/%s.stgit' % (stack_name
,)
16 def _patch_ref(stack_name
, patch_name
):
17 """Reference to a named patch's commit."""
18 return 'refs/patches/%s/%s' % (stack_name
, patch_name
)
21 def _patches_ref_prefix(stack_name
):
22 return _patch_ref(stack_name
, '')
26 """Represents an StGit patch."""
28 def __init__(self
, stack
, name
):
34 return _patch_ref(self
._stack
.name
, self
.name
)
38 return self
._stack
.repository
.refs
.get(self
._ref
)
40 def set_commit(self
, commit
, msg
):
42 old_sha1
= self
.commit
.sha1
45 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
46 if old_sha1
and old_sha1
!= commit
.sha1
:
47 self
._stack
.repository
.copy_notes(old_sha1
, commit
.sha1
)
49 def set_name(self
, name
, msg
):
51 self
._stack
.repository
.refs
.delete(self
._ref
)
53 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
56 return self
.commit
.data
.is_nochange()
59 """Return the set of files this patch touches."""
61 for dt
in self
._stack
.repository
.diff_tree_files(
62 self
.commit
.data
.parent
.data
.tree
,
63 self
.commit
.data
.tree
,
65 _
, _
, _
, _
, _
, oldname
, newname
= dt
72 """Keeps track of patch order, and which patches are applied.
74 Works with patch names, not actual patches.
78 def __init__(self
, state
):
79 self
._applied
= tuple(state
.applied
)
80 self
._unapplied
= tuple(state
.unapplied
)
81 self
._hidden
= tuple(state
.hidden
)
89 return self
._unapplied
97 return self
.applied
+ self
.unapplied
+ self
.hidden
100 def all_visible(self
):
101 return self
.applied
+ self
.unapplied
103 def set_order(self
, applied
, unapplied
, hidden
):
104 self
._applied
= tuple(applied
)
105 self
._unapplied
= tuple(unapplied
)
106 self
._hidden
= tuple(hidden
)
108 def rename_patch(self
, old_name
, new_name
):
109 for attr
in ['_applied', '_unapplied', '_hidden']:
110 patch_list
= list(getattr(self
, attr
))
112 index
= patch_list
.index(old_name
)
116 patch_list
[index
] = new_name
117 setattr(self
, attr
, tuple(patch_list
))
120 raise AssertionError('"%s" not found in patchorder' % old_name
)
124 """Manage the set of :class:`Patch` objects.
126 Ensures a single :class:`Patch` instance per patch.
130 def __init__(self
, stack
, state
):
132 self
._patches
= {pn
: Patch(stack
, pn
) for pn
in state
.patches
}
134 # Ensure patch refs in repository match those from stack state.
135 repository
= stack
.repository
136 patch_ref_prefix
= _patches_ref_prefix(stack
.name
)
138 state_patch_ref_map
= {
139 _patch_ref(stack
.name
, pn
): commit
for pn
, commit
in state
.patches
.items()
142 state_patch_refs
= set(state_patch_ref_map
)
144 ref
for ref
in repository
.refs
if ref
.startswith(patch_ref_prefix
)
147 delete_patch_refs
= repo_patch_refs
- state_patch_refs
148 create_patch_refs
= state_patch_refs
- repo_patch_refs
149 update_patch_refs
= {
151 for ref
in state_patch_refs
- create_patch_refs
152 if state_patch_ref_map
[ref
].sha1
!= repository
.refs
.get(ref
).sha1
155 if create_patch_refs
or update_patch_refs
or delete_patch_refs
:
156 repository
.refs
.batch_update(
157 msg
='restore from stack state',
158 create
=[(ref
, state_patch_ref_map
[ref
]) for ref
in create_patch_refs
],
159 update
=[(ref
, state_patch_ref_map
[ref
]) for ref
in update_patch_refs
],
160 delete
=delete_patch_refs
,
163 def exists(self
, name
):
171 return self
._patches
[name
]
173 def name_from_sha1(self
, partial_sha1
):
174 for pn
, patch
in self
._patches
.items():
175 if patch
.commit
.sha1
.startswith(partial_sha1
.lower()):
180 def is_name_valid(self
, name
):
182 # TODO slashes in patch names could be made to be okay
184 ref
= _patch_ref(self
._stack
.name
, name
)
185 p
= self
._stack
.repository
.run(['git', 'check-ref-format', ref
])
186 p
.returns([0, 1]).discard_stderr().discard_output()
187 return p
.exitcode
== 0
189 def new(self
, name
, commit
, msg
):
190 assert name
not in self
._patches
191 assert self
.is_name_valid(name
)
192 p
= Patch(self
._stack
, name
)
193 p
.set_commit(commit
, msg
)
194 self
._patches
[name
] = p
197 def update(self
, name
, commit
, msg
):
198 self
._patches
[name
].set_commit(commit
, msg
)
200 def rename(self
, old_name
, new_name
, msg
):
201 patch
= self
._patches
[old_name
]
202 patch
.set_name(new_name
, msg
)
203 self
._patches
[new_name
] = patch
204 del self
._patches
[old_name
]
206 def delete(self
, name
):
207 patch
= self
._patches
.pop(name
)
208 self
._stack
.repository
.refs
.delete(patch
._ref
)
210 def make_name(self
, raw
, unique
=True, lower
=True, allow
=(), disallow
=()):
211 """Make a unique and valid patch name from provided raw name.
213 The raw name may come from a filename, commit message, or email subject line.
215 The generated patch name will meet the rules of `git check-ref-format` along
216 with some additional StGit patch name rules.
219 default_name
= 'patch'
221 for line
in raw
.split('\n'):
232 for part
in line
.split('/'):
234 part
= re
.sub(r
'\.lock$', '', part
) # Disallowed in Git refs
235 part
= re
.sub(r
'^\.+|\.+$', '', part
) # Cannot start or end with '.'
236 part
= re
.sub(r
'\.+', '.', part
) # No consecutive '.'
237 part
= re
.sub(r
'[^\w.]+', '-', part
) # Non-word and whitespace to dashes
238 part
= re
.sub(r
'-+', '-', part
) # Squash consecutive dashes
239 part
= re
.sub(r
'^-+|-+$', '', part
) # Remove leading and trailing dashes
244 long_name
= '/'.join(parts
)
246 # TODO: slashes could be allowed in the future.
247 long_name
= long_name
.replace('/', '-')
250 long_name
= default_name
252 assert self
.is_name_valid(long_name
)
254 name_len
= config
.getint('stgit.namelength')
256 words
= long_name
.split('-')
257 short_name
= words
[0]
258 for word
in words
[1:]:
259 new_name
= '%s-%s' % (short_name
, word
)
260 if name_len
<= 0 or len(new_name
) <= name_len
:
261 short_name
= new_name
264 assert self
.is_name_valid(short_name
)
269 unique_name
= short_name
270 while unique_name
not in allow
and (
271 self
.exists(unique_name
) or unique_name
in disallow
273 m
= re
.match(r
'(.*?)(-)?(\d+)$', unique_name
)
275 base
, sep
, n_str
= m
.groups()
278 unique_name
= '%s%s%d' % (base
, sep
, n
)
280 unique_name
= '%s%d' % (base
, n
)
282 unique_name
= '%s-1' % unique_name
284 assert self
.is_name_valid(unique_name
)
289 """Represents a StGit stack.
291 A StGit stack is a Git branch with extra metadata for patch stack state.
295 def __init__(self
, repository
, name
):
296 super().__init
__(repository
, name
)
297 if not stackupgrade
.update_to_current_format_version(repository
, name
):
298 raise StackException('%s: branch not initialized' % name
)
299 state
= log
.get_stack_state(self
.repository
, self
.state_ref
)
300 self
.patchorder
= PatchOrder(state
)
301 self
.patches
= Patches(self
, state
)
305 if self
.patchorder
.applied
:
306 return self
.patches
.get(self
.patchorder
.applied
[0]).commit
.data
.parent
312 """Commit of the topmost patch, or the stack base if no patches are applied."""
313 if self
.patchorder
.applied
:
314 return self
.patches
.get(self
.patchorder
.applied
[-1]).commit
316 # When no patches are applied, base == head.
319 def head_top_equal(self
):
320 if not self
.patchorder
.applied
:
322 top
= self
.patches
.get(self
.patchorder
.applied
[-1]).commit
323 return self
.head
== top
325 def set_parents(self
, remote
, branch
):
327 self
.set_parent_remote(remote
)
329 self
.set_parent_branch(branch
)
330 config
.set('branch.%s.stgit.parentbranch' % self
.name
, branch
)
334 return config
.getbool('branch.%s.stgit.protect' % self
.name
)
337 def protected(self
, protect
):
338 protect_key
= 'branch.%s.stgit.protect' % self
.name
340 config
.set(protect_key
, 'true')
342 config
.unset(protect_key
)
346 return _stack_state_ref(self
.name
)
349 assert not self
.protected
, 'attempt to delete protected stack'
350 for pn
in self
.patchorder
.all
:
351 self
.patches
.delete(pn
)
352 self
.repository
.refs
.delete(self
.state_ref
)
353 config
.remove_section('branch.%s.stgit' % self
.name
)
355 def clear_log(self
, msg
='clear log'):
356 stack_state
= log
.StackState
.from_stack(prev
=None, stack
=self
)
357 state_commit
= stack_state
.commit_state(self
.repository
, msg
)
358 self
.repository
.refs
.set(self
.state_ref
, state_commit
, msg
=msg
)
360 def rename(self
, new_name
):
362 patch_names
= self
.patchorder
.all
363 super().rename(new_name
)
365 for pn
in patch_names
:
366 renames
.append((_patch_ref(old_name
, pn
), _patch_ref(new_name
, pn
)))
367 renames
.append((_stack_state_ref(old_name
), _stack_state_ref(new_name
)))
369 self
.repository
.refs
.rename('rename %s to %s' % (old_name
, new_name
), *renames
)
371 config
.rename_section(
372 'branch.%s.stgit' % old_name
,
373 'branch.%s.stgit' % new_name
,
376 def rename_patch(self
, old_name
, new_name
, msg
='rename'):
377 if new_name
== old_name
:
378 raise StackException('New patch name same as old: "%s"' % new_name
)
379 elif self
.patches
.exists(new_name
):
380 raise StackException('Patch already exists: "%s"' % new_name
)
381 elif not self
.patches
.is_name_valid(new_name
):
382 raise StackException('Invalid patch name: "%s"' % new_name
)
383 elif not self
.patches
.exists(old_name
):
384 raise StackException('Unknown patch name: "%s"' % old_name
)
385 self
.patchorder
.rename_patch(old_name
, new_name
)
386 self
.patches
.rename(old_name
, new_name
, msg
)
388 def clone(self
, clone_name
, msg
):
394 parent_remote
=self
.parent_remote
,
395 parent_branch
=self
.name
,
398 for pn
in self
.patchorder
.all_visible
:
399 patch
= self
.patches
.get(pn
)
400 clone
.patches
.new(pn
, patch
.commit
, 'clone from %s' % self
.name
)
402 clone
.patchorder
.set_order(
404 unapplied
=self
.patchorder
.all_visible
,
408 prefix
= 'branch.%s.' % self
.name
409 clone_prefix
= 'branch.%s.' % clone_name
410 for k
, v
in list(config
.getstartswith(prefix
)):
411 clone_key
= k
.replace(prefix
, clone_prefix
, 1)
412 config
.set(clone_key
, v
)
414 self
.repository
.refs
.set(
416 self
.repository
.refs
.get(self
.state_ref
),
423 def initialise(cls
, repository
, name
=None, msg
='initialise', switch_to
=False):
424 """Initialise a Git branch to handle patch stack.
426 :param repository: :class:`Repository` where the :class:`Stack` will be created
427 :param name: the name of the :class:`Stack`
431 name
= repository
.current_branch_name
432 # make sure that the corresponding Git branch exists
433 branch
= Branch(repository
, name
)
435 stack_state_ref
= _stack_state_ref(name
)
436 if repository
.refs
.exists(stack_state_ref
):
437 raise StackException('%s: stack already initialized' % name
)
442 stack_state
= log
.StackState
.new_empty(branch
.head
)
443 state_commit
= stack_state
.commit_state(repository
, msg
)
444 repository
.refs
.set(stack_state_ref
, state_commit
, msg
)
446 return repository
.get_stack(name
)
459 """Create and initialise a Git branch returning the :class:`Stack` object.
461 :param repository: :class:`Repository` where the :class:`Stack` will be created
462 :param name: name of the :class:`Stack`
463 :param msg: message to use in newly created log
464 :param create_at: Git id used as the base for the newly created Git branch
465 :param parent_remote: name of the parent remote Git branch
466 :param parent_branch: name of the parent Git branch
469 branch
= Branch
.create(repository
, name
, create_at
=create_at
)
471 stack
= cls
.initialise(repository
, name
, msg
, switch_to
=switch_to
)
472 except (BranchException
, StackException
):
475 stack
.set_parents(parent_remote
, parent_branch
)
479 class StackRepository(Repository
):
480 """A Git :class:`Repository` with some added StGit-specific operations."""
482 def __init__(self
, directory
):
483 super().__init
__(directory
)
484 self
._stacks
= {} # name -> Stack
487 def current_stack(self
):
488 return self
.get_stack()
490 def get_stack(self
, name
=None):
492 name
= self
.current_branch_name
493 if name
not in self
._stacks
:
494 self
._stacks
[name
] = Stack(self
, name
)
495 return self
._stacks
[name
]