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 _patch_ref_prefix(stack_name
):
22 return _patch_ref(stack_name
, '')
26 """Keeps track of patch order, and which patches are applied.
28 Works with patch names, not actual patches.
32 def __init__(self
, state
):
33 self
._applied
= tuple(state
.applied
)
34 self
._unapplied
= tuple(state
.unapplied
)
35 self
._hidden
= tuple(state
.hidden
)
43 return self
._unapplied
51 return self
.applied
+ self
.unapplied
+ self
.hidden
54 def all_visible(self
):
55 return self
.applied
+ self
.unapplied
57 def set_order(self
, applied
, unapplied
, hidden
):
58 self
._applied
= tuple(applied
)
59 self
._unapplied
= tuple(unapplied
)
60 self
._hidden
= tuple(hidden
)
62 def rename_patch(self
, old_name
, new_name
):
63 for attr
in ['_applied', '_unapplied', '_hidden']:
64 patch_list
= list(getattr(self
, attr
))
66 index
= patch_list
.index(old_name
)
70 patch_list
[index
] = new_name
71 setattr(self
, attr
, tuple(patch_list
))
74 raise AssertionError('"%s" not found in patchorder' % old_name
)
78 """Interface for managing patch references."""
80 def __init__(self
, stack
, state
):
83 # Ensure patch refs in repository match those from stack state.
84 repository
= stack
.repository
85 patch_ref_prefix
= _patch_ref_prefix(stack
.name
)
87 state_patch_ref_map
= {
88 _patch_ref(stack
.name
, pn
): commit
for pn
, commit
in state
.patches
.items()
91 state_patch_refs
= set(state_patch_ref_map
)
93 ref
for ref
in repository
.refs
if ref
.startswith(patch_ref_prefix
)
96 delete_patch_refs
= repo_patch_refs
- state_patch_refs
97 create_patch_refs
= state_patch_refs
- repo_patch_refs
100 for ref
in state_patch_refs
- create_patch_refs
101 if state_patch_ref_map
[ref
].sha1
!= repository
.refs
.get(ref
).sha1
104 if create_patch_refs
or update_patch_refs
or delete_patch_refs
:
105 repository
.refs
.batch_update(
106 msg
='restore from stack state',
107 create
=[(ref
, state_patch_ref_map
[ref
]) for ref
in create_patch_refs
],
108 update
=[(ref
, state_patch_ref_map
[ref
]) for ref
in update_patch_refs
],
109 delete
=delete_patch_refs
,
112 def _patch_ref(self
, name
):
113 return _patch_ref(self
._stack
.name
, name
)
115 def __contains__(self
, name
):
116 return self
._stack
.repository
.refs
.exists(self
._patch
_ref
(name
))
118 def __getitem__(self
, name
):
119 return self
._stack
.repository
.refs
.get(self
._patch
_ref
(name
))
122 patch_ref_prefix
= _patch_ref_prefix(self
._stack
.name
)
123 for ref
in self
._stack
.repository
.refs
:
124 if ref
.startswith(patch_ref_prefix
):
125 yield ref
[len(patch_ref_prefix
) :]
127 def name_from_sha1(self
, partial_sha1
):
129 if self
[pn
].sha1
.startswith(partial_sha1
.lower()):
134 def is_name_valid(self
, name
):
136 # TODO slashes in patch names could be made to be okay
138 ref
= _patch_ref(self
._stack
.name
, name
)
139 p
= self
._stack
.repository
.run(['git', 'check-ref-format', ref
])
140 p
.returns([0, 1]).discard_stderr().discard_output()
141 return p
.exitcode
== 0
143 def new(self
, name
, commit
, msg
):
144 assert name
not in self
145 assert self
.is_name_valid(name
)
146 self
._stack
.repository
.refs
.set(self
._patch
_ref
(name
), commit
, msg
)
148 def update(self
, name
, commit
, msg
):
149 old_sha1
= self
[name
].sha1
150 if old_sha1
!= commit
.sha1
:
151 self
._stack
.repository
.refs
.set(self
._patch
_ref
(name
), commit
, msg
)
152 self
._stack
.repository
.copy_notes(old_sha1
, commit
.sha1
)
154 def rename(self
, old_name
, new_name
, msg
):
155 commit
= self
[old_name
]
156 self
._stack
.repository
.refs
.delete(self
._patch
_ref
(old_name
))
157 self
._stack
.repository
.refs
.set(self
._patch
_ref
(new_name
), commit
, msg
)
159 def delete(self
, name
):
160 self
._stack
.repository
.refs
.delete(self
._patch
_ref
(name
))
162 def make_name(self
, raw
, unique
=True, lower
=True, allow
=(), disallow
=()):
163 """Make a unique and valid patch name from provided raw name.
165 The raw name may come from a filename, commit message, or email subject line.
167 The generated patch name will meet the rules of `git check-ref-format` along
168 with some additional StGit patch name rules.
171 default_name
= 'patch'
173 for line
in raw
.split('\n'):
184 for part
in line
.split('/'):
186 part
= re
.sub(r
'\.lock$', '', part
) # Disallowed in Git refs
187 part
= re
.sub(r
'^\.+|\.+$', '', part
) # Cannot start or end with '.'
188 part
= re
.sub(r
'\.+', '.', part
) # No consecutive '.'
189 part
= re
.sub(r
'[^\w.]+', '-', part
) # Non-word and whitespace to dashes
190 part
= re
.sub(r
'-+', '-', part
) # Squash consecutive dashes
191 part
= re
.sub(r
'^-+|-+$', '', part
) # Remove leading and trailing dashes
196 long_name
= '/'.join(parts
)
198 # TODO: slashes could be allowed in the future.
199 long_name
= long_name
.replace('/', '-')
202 long_name
= default_name
204 assert self
.is_name_valid(long_name
)
206 name_len
= config
.getint('stgit.namelength')
208 words
= long_name
.split('-')
209 short_name
= words
[0]
210 for word
in words
[1:]:
211 new_name
= '%s-%s' % (short_name
, word
)
212 if name_len
<= 0 or len(new_name
) <= name_len
:
213 short_name
= new_name
216 assert self
.is_name_valid(short_name
)
221 unique_name
= short_name
222 while unique_name
not in allow
and (
223 unique_name
in self
or unique_name
in disallow
225 m
= re
.match(r
'(.*?)(-)?(\d+)$', unique_name
)
227 base
, sep
, n_str
= m
.groups()
230 unique_name
= '%s%s%d' % (base
, sep
, n
)
232 unique_name
= '%s%d' % (base
, n
)
234 unique_name
= '%s-1' % unique_name
236 assert self
.is_name_valid(unique_name
)
241 """Represents a StGit stack.
243 A StGit stack is a Git branch with extra metadata for patch stack state.
247 def __init__(self
, repository
, name
):
248 super().__init
__(repository
, name
)
249 if not stackupgrade
.update_to_current_format_version(repository
, name
):
250 raise StackException('%s: branch not initialized' % name
)
251 state
= log
.get_stack_state(self
.repository
, self
.state_ref
)
252 self
.patchorder
= PatchOrder(state
)
253 self
.patches
= Patches(self
, state
)
257 if self
.patchorder
.applied
:
258 return self
.patches
[self
.patchorder
.applied
[0]].data
.parent
264 """Commit of the topmost patch, or the stack base if no patches are applied."""
265 if self
.patchorder
.applied
:
266 return self
.patches
[self
.patchorder
.applied
[-1]]
268 # When no patches are applied, base == head.
271 def set_parents(self
, remote
, branch
):
273 self
.set_parent_remote(remote
)
275 self
.set_parent_branch(branch
)
276 config
.set('branch.%s.stgit.parentbranch' % self
.name
, branch
)
280 return config
.getbool('branch.%s.stgit.protect' % self
.name
)
283 def protected(self
, protect
):
284 protect_key
= 'branch.%s.stgit.protect' % self
.name
286 config
.set(protect_key
, 'true')
288 config
.unset(protect_key
)
292 return _stack_state_ref(self
.name
)
295 assert not self
.protected
, 'attempt to delete protected stack'
296 for pn
in self
.patchorder
.all
:
297 self
.patches
.delete(pn
)
298 self
.repository
.refs
.delete(self
.state_ref
)
299 config
.remove_section('branch.%s.stgit' % self
.name
)
301 def clear_log(self
, msg
='clear log'):
302 stack_state
= log
.StackState
.from_stack(prev
=None, stack
=self
)
303 state_commit
= stack_state
.commit_state(self
.repository
, msg
)
304 self
.repository
.refs
.set(self
.state_ref
, state_commit
, msg
=msg
)
306 def rename(self
, new_name
):
308 patch_names
= self
.patchorder
.all
309 super().rename(new_name
)
311 for pn
in patch_names
:
312 renames
.append((_patch_ref(old_name
, pn
), _patch_ref(new_name
, pn
)))
313 renames
.append((_stack_state_ref(old_name
), _stack_state_ref(new_name
)))
315 self
.repository
.refs
.rename('rename %s to %s' % (old_name
, new_name
), *renames
)
317 config
.rename_section(
318 'branch.%s.stgit' % old_name
,
319 'branch.%s.stgit' % new_name
,
322 def rename_patch(self
, old_name
, new_name
, msg
='rename'):
323 if new_name
== old_name
:
324 raise StackException('New patch name same as old: "%s"' % new_name
)
325 elif new_name
in self
.patches
:
326 raise StackException('Patch already exists: "%s"' % new_name
)
327 elif not self
.patches
.is_name_valid(new_name
):
328 raise StackException('Invalid patch name: "%s"' % new_name
)
329 elif old_name
not in self
.patches
:
330 raise StackException('Unknown patch name: "%s"' % old_name
)
331 self
.patchorder
.rename_patch(old_name
, new_name
)
332 self
.patches
.rename(old_name
, new_name
, msg
)
334 def clone(self
, clone_name
, msg
):
340 parent_remote
=self
.parent_remote
,
341 parent_branch
=self
.name
,
344 for pn
in self
.patchorder
.all_visible
:
345 clone
.patches
.new(pn
, self
.patches
[pn
], 'clone from %s' % self
.name
)
347 clone
.patchorder
.set_order(
349 unapplied
=self
.patchorder
.all_visible
,
353 prefix
= 'branch.%s.' % self
.name
354 clone_prefix
= 'branch.%s.' % clone_name
355 for k
, v
in list(config
.getstartswith(prefix
)):
356 clone_key
= k
.replace(prefix
, clone_prefix
, 1)
357 config
.set(clone_key
, v
)
359 self
.repository
.refs
.set(
361 self
.repository
.refs
.get(self
.state_ref
),
368 def initialise(cls
, repository
, name
=None, msg
='initialise', switch_to
=False):
369 """Initialise a Git branch to handle patch stack.
371 :param repository: :class:`Repository` where the :class:`Stack` will be created
372 :param name: the name of the :class:`Stack`
376 name
= repository
.current_branch_name
377 # make sure that the corresponding Git branch exists
378 branch
= Branch(repository
, name
)
380 stack_state_ref
= _stack_state_ref(name
)
381 if repository
.refs
.exists(stack_state_ref
):
382 raise StackException('%s: stack already initialized' % name
)
387 stack_state
= log
.StackState
.new_empty(branch
.head
)
388 state_commit
= stack_state
.commit_state(repository
, msg
)
389 repository
.refs
.set(stack_state_ref
, state_commit
, msg
)
391 return repository
.get_stack(name
)
404 """Create and initialise a Git branch returning the :class:`Stack` object.
406 :param repository: :class:`Repository` where the :class:`Stack` will be created
407 :param name: name of the :class:`Stack`
408 :param msg: message to use in newly created log
409 :param create_at: Git id used as the base for the newly created Git branch
410 :param parent_remote: name of the parent remote Git branch
411 :param parent_branch: name of the parent Git branch
414 branch
= Branch
.create(repository
, name
, create_at
=create_at
)
416 stack
= cls
.initialise(repository
, name
, msg
, switch_to
=switch_to
)
417 except (BranchException
, StackException
):
420 stack
.set_parents(parent_remote
, parent_branch
)
424 class StackRepository(Repository
):
425 """A Git :class:`Repository` with some added StGit-specific operations."""
427 def __init__(self
, directory
):
428 super().__init
__(directory
)
429 self
._stacks
= {} # name -> Stack
432 def current_stack(self
):
433 return self
.get_stack()
435 def get_stack(self
, name
=None):
437 name
= self
.current_branch_name
438 if name
not in self
._stacks
:
439 self
._stacks
[name
] = Stack(self
, name
)
440 return self
._stacks
[name
]