1 """A Python class hierarchy wrapping the StGit on-disk metadata."""
6 from stgit
import utils
7 from stgit
.compat
import fsencode_utf8
8 from stgit
.config
import config
9 from stgit
.exception
import StackException
10 from stgit
.lib
import log
, stackupgrade
11 from stgit
.lib
.git
import CommitData
, Repository
12 from stgit
.lib
.git
.branch
import Branch
, BranchException
13 from stgit
.lib
.objcache
import ObjectCache
16 def _stack_state_ref(stack_name
):
17 """Reference to stack state metadata. A.k.a. the stack's "log"."""
18 return 'refs/heads/%s.stgit' % (stack_name
,)
21 def _patch_ref(stack_name
, patch_name
):
22 """Reference to a named patch's commit."""
23 return 'refs/patches/%s/%s' % (stack_name
, patch_name
)
26 def _patch_log_ref(stack_name
, patch_name
):
27 """Reference to a named patch's log."""
28 return 'refs/patches/%s/%s.log' % (stack_name
, patch_name
)
32 """Represents an StGit patch. This class is mainly concerned with
33 reading and writing the on-disk representation of a patch."""
35 def __init__(self
, stack
, name
):
41 return _patch_ref(self
._stack
.name
, self
.name
)
45 return _patch_log_ref(self
._stack
.name
, self
.name
)
49 return self
._stack
.repository
.refs
.get(self
._ref
)
52 def _compat_dir(self
):
53 return os
.path
.join(self
._stack
.directory
, 'patches', self
.name
)
55 def _write_compat_files(self
, new_commit
, msg
):
56 """Write files used by the old infrastructure."""
58 def write(name
, val
, multiline
=False):
59 fn
= os
.path
.join(self
._compat
_dir
, name
)
60 fn
= fsencode_utf8(fn
)
62 utils
.write_string(fn
, val
, multiline
)
63 elif os
.path
.isfile(fn
):
68 old_log
= [self
._stack
.repository
.refs
.get(self
._log
_ref
)]
72 tree
=new_commit
.data
.tree
,
74 message
='%s\t%s' % (msg
, new_commit
.sha1
),
76 c
= self
._stack
.repository
.commit(cd
)
77 self
._stack
.repository
.refs
.set(self
._log
_ref
, c
, msg
)
81 write('authname', d
.author
.name
)
82 write('authemail', d
.author
.email
)
83 write('authdate', str(d
.author
.date
))
84 write('commname', d
.committer
.name
)
85 write('commemail', d
.committer
.email
)
86 write('description', d
.message_str
, multiline
=True)
87 write('log', write_patchlog().sha1
)
88 write('top', new_commit
.sha1
)
89 write('bottom', d
.parent
.sha1
)
91 old_top_sha1
= self
.commit
.sha1
92 old_bottom_sha1
= self
.commit
.data
.parent
.sha1
95 old_bottom_sha1
= None
96 write('top.old', old_top_sha1
)
97 write('bottom.old', old_bottom_sha1
)
99 def _delete_compat_files(self
):
100 if os
.path
.isdir(self
._compat
_dir
):
101 for f
in os
.listdir(self
._compat
_dir
):
102 os
.remove(os
.path
.join(self
._compat
_dir
, f
))
103 os
.rmdir(self
._compat
_dir
)
105 # this compatibility log ref might not exist
106 self
._stack
.repository
.refs
.delete(self
._log
_ref
)
110 def set_commit(self
, commit
, msg
):
112 old_sha1
= self
.commit
.sha1
115 self
._write
_compat
_files
(commit
, msg
)
116 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
117 if old_sha1
and old_sha1
!= commit
.sha1
:
118 self
._stack
.repository
.copy_notes(old_sha1
, commit
.sha1
)
120 def set_name(self
, name
, msg
):
124 self
._write
_compat
_files
(commit
, msg
)
125 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
128 self
._delete
_compat
_files
()
129 self
._stack
.repository
.refs
.delete(self
._ref
)
132 return self
.commit
.data
.is_nochange()
135 """Return the set of files this patch touches."""
137 for dt
in self
._stack
.repository
.diff_tree_files(
138 self
.commit
.data
.parent
.data
.tree
,
139 self
.commit
.data
.tree
,
141 _
, _
, _
, _
, _
, oldname
, newname
= dt
148 """Keeps track of patch order, and which patches are applied.
149 Works with patch names, not actual patches."""
151 def __init__(self
, stack
):
155 def _read_file(self
, fn
):
156 return tuple(utils
.read_strings(os
.path
.join(self
._stack
.directory
, fn
)))
158 def _write_file(self
, fn
, val
):
159 utils
.write_strings(os
.path
.join(self
._stack
.directory
, fn
), val
)
161 def _get_list(self
, name
):
162 if name
not in self
._lists
:
163 self
._lists
[name
] = self
._read
_file
(name
)
164 return self
._lists
[name
]
166 def _set_list(self
, name
, val
):
168 if val
!= self
._lists
.get(name
, None):
169 self
._lists
[name
] = val
170 self
._write
_file
(name
, val
)
174 return self
._get
_list
('applied')
178 return self
._get
_list
('unapplied')
182 return self
._get
_list
('hidden')
186 return self
.applied
+ self
.unapplied
+ self
.hidden
189 def all_visible(self
):
190 return self
.applied
+ self
.unapplied
192 def set_order(self
, applied
, unapplied
, hidden
):
193 self
._set
_list
('applied', applied
)
194 self
._set
_list
('unapplied', unapplied
)
195 self
._set
_list
('hidden', hidden
)
197 def rename_patch(self
, old_name
, new_name
):
198 for list_name
in ['applied', 'unapplied', 'hidden']:
199 patch_list
= list(self
._get
_list
(list_name
))
201 index
= patch_list
.index(old_name
)
205 patch_list
[index
] = new_name
206 self
._set
_list
(list_name
, patch_list
)
209 raise AssertionError('"%s" not found in patchorder' % old_name
)
212 def create(stackdir
):
213 """Create the PatchOrder specific files"""
214 utils
.create_empty_file(os
.path
.join(stackdir
, 'applied'))
215 utils
.create_empty_file(os
.path
.join(stackdir
, 'unapplied'))
216 utils
.create_empty_file(os
.path
.join(stackdir
, 'hidden'))
220 """Creates L{Patch} objects. Makes sure there is only one such object
223 def __init__(self
, stack
):
226 def create_patch(name
):
227 p
= Patch(self
._stack
, name
)
228 p
.commit
# raise exception if the patch doesn't exist
231 self
._patches
= ObjectCache(create_patch
) # name -> Patch
233 def exists(self
, name
):
241 return self
._patches
[name
]
243 def is_name_valid(self
, name
):
245 # TODO slashes in patch names could be made to be okay
247 ref
= _patch_ref(self
._stack
.name
, name
)
248 p
= self
._stack
.repository
.run(['git', 'check-ref-format', ref
])
249 p
.returns([0, 1]).discard_stderr().discard_output()
250 return p
.exitcode
== 0
252 def new(self
, name
, commit
, msg
):
253 assert name
not in self
._patches
254 assert self
.is_name_valid(name
)
255 p
= Patch(self
._stack
, name
)
256 p
.set_commit(commit
, msg
)
257 self
._patches
[name
] = p
262 """Represents an StGit stack (that is, a git branch with some extra
265 _repo_subdir
= 'patches'
267 def __init__(self
, repository
, name
):
268 Branch
.__init
__(self
, repository
, name
)
269 self
.patchorder
= PatchOrder(self
)
270 self
.patches
= Patches(self
)
271 if not stackupgrade
.update_to_current_format_version(repository
, name
):
272 raise StackException('%s: branch not initialized' % name
)
276 return os
.path
.join(self
.repository
.directory
, self
._repo
_subdir
, self
.name
)
280 if self
.patchorder
.applied
:
281 return self
.patches
.get(self
.patchorder
.applied
[0]).commit
.data
.parent
287 """Commit of the topmost patch, or the stack base if no patches are
289 if self
.patchorder
.applied
:
290 return self
.patches
.get(self
.patchorder
.applied
[-1]).commit
292 # When no patches are applied, base == head.
295 def head_top_equal(self
):
296 if not self
.patchorder
.applied
:
298 top
= self
.patches
.get(self
.patchorder
.applied
[-1]).commit
299 return self
.head
== top
301 def set_parents(self
, remote
, branch
):
303 self
.set_parent_remote(remote
)
305 self
.set_parent_branch(branch
)
306 config
.set('branch.%s.stgit.parentbranch' % self
.name
, branch
)
310 return config
.getbool('branch.%s.stgit.protect' % self
.name
)
313 def protected(self
, protect
):
314 protect_key
= 'branch.%s.stgit.protect' % self
.name
316 config
.set(protect_key
, 'true')
318 config
.unset(protect_key
)
322 return _stack_state_ref(self
.name
)
325 assert not self
.protected
, 'attempt to delete protected stack'
326 for pn
in self
.patchorder
.all
:
327 patch
= self
.patches
.get(pn
)
329 self
.repository
.refs
.delete(self
.state_ref
)
330 shutil
.rmtree(self
.directory
)
331 config
.remove_section('branch.%s.stgit' % self
.name
)
333 def clear_log(self
, msg
='clear log'):
334 new_stack_state
= log
.StackState
.from_stack(prev
=None, stack
=self
, message
=msg
)
335 new_stack_state
.write_commit()
336 self
.repository
.refs
.set(self
.state_ref
, new_stack_state
.commit
, msg
=msg
)
338 def rename(self
, new_name
):
340 patch_names
= self
.patchorder
.all
341 super(Stack
, self
).rename(new_name
)
343 for pn
in patch_names
:
344 renames
.append((_patch_ref(old_name
, pn
), _patch_ref(new_name
, pn
)))
345 renames
.append((_patch_log_ref(old_name
, pn
), _patch_log_ref(new_name
, pn
)))
346 renames
.append((_stack_state_ref(old_name
), _stack_state_ref(new_name
)))
348 self
.repository
.refs
.rename('rename %s to %s' % (old_name
, new_name
), *renames
)
350 config
.rename_section(
351 'branch.%s.stgit' % old_name
,
352 'branch.%s.stgit' % new_name
,
356 os
.path
.join(self
.repository
.directory
, self
._repo
_subdir
),
361 def rename_patch(self
, old_name
, new_name
, msg
='rename'):
362 if new_name
== old_name
:
363 raise StackException('New patch name same as old: "%s"' % new_name
)
364 elif self
.patches
.exists(new_name
):
365 raise StackException('Patch already exists: "%s"' % new_name
)
366 elif not self
.patches
.is_name_valid(new_name
):
367 raise StackException('Invalid patch name: "%s"' % new_name
)
368 elif not self
.patches
.exists(old_name
):
369 raise StackException('Unknown patch name: "%s"' % old_name
)
370 self
.patchorder
.rename_patch(old_name
, new_name
)
371 self
.patches
.get(old_name
).set_name(new_name
, msg
)
373 def clone(self
, clone_name
, msg
):
379 parent_remote
=self
.parent_remote
,
380 parent_branch
=self
.name
,
383 for pn
in self
.patchorder
.all_visible
:
384 patch
= self
.patches
.get(pn
)
385 clone
.patches
.new(pn
, patch
.commit
, 'clone from %s' % self
.name
)
387 clone
.patchorder
.set_order(
389 unapplied
=self
.patchorder
.all_visible
,
393 prefix
= 'branch.%s.' % self
.name
394 clone_prefix
= 'branch.%s.' % clone_name
395 for k
, v
in list(config
.getstartswith(prefix
)):
396 clone_key
= k
.replace(prefix
, clone_prefix
, 1)
397 config
.set(clone_key
, v
)
399 self
.repository
.refs
.set(
401 self
.repository
.refs
.get(self
.state_ref
),
408 def initialise(cls
, repository
, name
=None, msg
='initialise', switch_to
=False):
409 """Initialise a Git branch to handle patch series.
411 @param repository: The L{Repository} where the L{Stack} will be created
412 @param name: The name of the L{Stack}
415 name
= repository
.current_branch_name
416 # make sure that the corresponding Git branch exists
417 branch
= Branch(repository
, name
)
419 stack_state_ref
= _stack_state_ref(name
)
420 if repository
.refs
.exists(stack_state_ref
):
421 raise StackException('%s: stack already initialized' % name
)
423 dir = os
.path
.join(repository
.directory
, cls
._repo
_subdir
, name
)
424 if os
.path
.exists(dir):
425 raise StackException('%s: branch already initialized' % name
)
430 # create the stack directory and files
431 utils
.create_dirs(dir)
432 compat_dir
= os
.path
.join(dir, 'patches')
433 utils
.create_dirs(compat_dir
)
434 PatchOrder
.create(dir)
436 stackupgrade
.format_version_key(name
), str(stackupgrade
.FORMAT_VERSION
)
439 new_stack_state
= log
.StackState(
449 new_stack_state
.write_commit()
450 repository
.refs
.set(stack_state_ref
, new_stack_state
.commit
, msg
)
452 return repository
.get_stack(name
)
465 """Create and initialise a Git branch returning the L{Stack} object.
467 @param repository: The L{Repository} where the L{Stack} will be created
468 @param name: The name of the L{Stack}
469 @param msg: Message to use in newly created log
470 @param create_at: The Git id used as the base for the newly created Git branch
471 @param parent_remote: The name of the remote Git branch
472 @param parent_branch: The name of the parent Git branch
474 branch
= Branch
.create(repository
, name
, create_at
=create_at
)
476 stack
= cls
.initialise(repository
, name
, msg
, switch_to
=switch_to
)
477 except (BranchException
, StackException
):
480 stack
.set_parents(parent_remote
, parent_branch
)
484 class StackRepository(Repository
):
485 """A git L{Repository<Repository>} with some added StGit-specific
488 def __init__(self
, *args
, **kwargs
):
489 Repository
.__init
__(self
, *args
, **kwargs
)
490 self
._stacks
= {} # name -> Stack
493 def current_stack(self
):
494 return self
.get_stack()
496 def get_stack(self
, name
=None):
498 name
= self
.current_branch_name
499 if name
not in self
._stacks
:
500 self
._stacks
[name
] = Stack(self
, name
)
501 return self
._stacks
[name
]