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 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
17 """Represents an StGit patch. This class is mainly concerned with
18 reading and writing the on-disk representation of a patch."""
20 def __init__(self
, stack
, name
):
26 return 'refs/patches/%s/%s' % (self
._stack
.name
, self
.name
)
30 return self
._ref
+ '.log'
34 return self
._stack
.repository
.refs
.get(self
._ref
)
37 def _compat_dir(self
):
38 return os
.path
.join(self
._stack
.directory
, 'patches', self
.name
)
40 def _write_compat_files(self
, new_commit
, msg
):
41 """Write files used by the old infrastructure."""
43 def write(name
, val
, multiline
=False):
44 fn
= os
.path
.join(self
._compat
_dir
, name
)
45 fn
= fsencode_utf8(fn
)
47 utils
.write_string(fn
, val
, multiline
)
48 elif os
.path
.isfile(fn
):
53 old_log
= [self
._stack
.repository
.refs
.get(self
._log
_ref
)]
57 tree
=new_commit
.data
.tree
,
59 message
='%s\t%s' % (msg
, new_commit
.sha1
),
61 c
= self
._stack
.repository
.commit(cd
)
62 self
._stack
.repository
.refs
.set(self
._log
_ref
, c
, msg
)
66 write('authname', d
.author
.name
)
67 write('authemail', d
.author
.email
)
68 write('authdate', str(d
.author
.date
))
69 write('commname', d
.committer
.name
)
70 write('commemail', d
.committer
.email
)
71 write('description', d
.message_str
, multiline
=True)
72 write('log', write_patchlog().sha1
)
73 write('top', new_commit
.sha1
)
74 write('bottom', d
.parent
.sha1
)
76 old_top_sha1
= self
.commit
.sha1
77 old_bottom_sha1
= self
.commit
.data
.parent
.sha1
80 old_bottom_sha1
= None
81 write('top.old', old_top_sha1
)
82 write('bottom.old', old_bottom_sha1
)
84 def _delete_compat_files(self
):
85 if os
.path
.isdir(self
._compat
_dir
):
86 for f
in os
.listdir(self
._compat
_dir
):
87 os
.remove(os
.path
.join(self
._compat
_dir
, f
))
88 os
.rmdir(self
._compat
_dir
)
90 # this compatibility log ref might not exist
91 self
._stack
.repository
.refs
.delete(self
._log
_ref
)
95 def set_commit(self
, commit
, msg
):
97 old_sha1
= self
.commit
.sha1
100 self
._write
_compat
_files
(commit
, msg
)
101 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
102 if old_sha1
and old_sha1
!= commit
.sha1
:
103 self
._stack
.repository
.copy_notes(old_sha1
, commit
.sha1
)
105 def set_name(self
, name
, msg
):
109 self
._write
_compat
_files
(commit
, msg
)
110 self
._stack
.repository
.refs
.set(self
._ref
, commit
, msg
)
113 self
._delete
_compat
_files
()
114 self
._stack
.repository
.refs
.delete(self
._ref
)
117 return self
.commit
.data
.is_nochange()
120 """Return the set of files this patch touches."""
122 for dt
in self
._stack
.repository
.diff_tree_files(
123 self
.commit
.data
.parent
.data
.tree
,
124 self
.commit
.data
.tree
,
126 _
, _
, _
, _
, _
, oldname
, newname
= dt
133 """Keeps track of patch order, and which patches are applied.
134 Works with patch names, not actual patches."""
136 def __init__(self
, stack
):
140 def _read_file(self
, fn
):
141 return tuple(utils
.read_strings(os
.path
.join(self
._stack
.directory
, fn
)))
143 def _write_file(self
, fn
, val
):
144 utils
.write_strings(os
.path
.join(self
._stack
.directory
, fn
), val
)
146 def _get_list(self
, name
):
147 if name
not in self
._lists
:
148 self
._lists
[name
] = self
._read
_file
(name
)
149 return self
._lists
[name
]
151 def _set_list(self
, name
, val
):
153 if val
!= self
._lists
.get(name
, None):
154 self
._lists
[name
] = val
155 self
._write
_file
(name
, val
)
159 return self
._get
_list
('applied')
163 return self
._get
_list
('unapplied')
167 return self
._get
_list
('hidden')
171 return self
.applied
+ self
.unapplied
+ self
.hidden
174 def all_visible(self
):
175 return self
.applied
+ self
.unapplied
177 def set_order(self
, applied
, unapplied
, hidden
):
178 self
._set
_list
('applied', applied
)
179 self
._set
_list
('unapplied', unapplied
)
180 self
._set
_list
('hidden', hidden
)
182 def rename_patch(self
, old_name
, new_name
):
183 for list_name
in ['applied', 'unapplied', 'hidden']:
184 patch_list
= list(self
._get
_list
(list_name
))
186 index
= patch_list
.index(old_name
)
190 patch_list
[index
] = new_name
191 self
._set
_list
(list_name
, patch_list
)
194 raise AssertionError('"%s" not found in patchorder' % old_name
)
197 def create(stackdir
):
198 """Create the PatchOrder specific files"""
199 utils
.create_empty_file(os
.path
.join(stackdir
, 'applied'))
200 utils
.create_empty_file(os
.path
.join(stackdir
, 'unapplied'))
201 utils
.create_empty_file(os
.path
.join(stackdir
, 'hidden'))
205 """Creates L{Patch} objects. Makes sure there is only one such object
208 def __init__(self
, stack
):
211 def create_patch(name
):
212 p
= Patch(self
._stack
, name
)
213 p
.commit
# raise exception if the patch doesn't exist
216 self
._patches
= ObjectCache(create_patch
) # name -> Patch
218 def exists(self
, name
):
226 return self
._patches
[name
]
228 def is_name_valid(self
, name
):
230 # TODO slashes in patch names could be made to be okay
232 ref_name
= 'refs/patches/%s/%s' % (self
._stack
.name
, name
)
233 p
= self
._stack
.repository
.run(['git', 'check-ref-format', ref_name
])
234 p
.returns([0, 1]).discard_stderr().discard_output()
235 return p
.exitcode
== 0
237 def new(self
, name
, commit
, msg
):
238 assert name
not in self
._patches
239 assert self
.is_name_valid(name
)
240 p
= Patch(self
._stack
, name
)
241 p
.set_commit(commit
, msg
)
242 self
._patches
[name
] = p
247 """Represents an StGit stack (that is, a git branch with some extra
250 _repo_subdir
= 'patches'
252 def __init__(self
, repository
, name
):
253 Branch
.__init
__(self
, repository
, name
)
254 self
.patchorder
= PatchOrder(self
)
255 self
.patches
= Patches(self
)
256 if not stackupgrade
.update_to_current_format_version(repository
, name
):
257 raise StackException('%s: branch not initialized' % name
)
261 return os
.path
.join(self
.repository
.directory
, self
._repo
_subdir
, self
.name
)
265 if self
.patchorder
.applied
:
266 return self
.patches
.get(self
.patchorder
.applied
[0]).commit
.data
.parent
272 """Commit of the topmost patch, or the stack base if no patches are
274 if self
.patchorder
.applied
:
275 return self
.patches
.get(self
.patchorder
.applied
[-1]).commit
277 # When no patches are applied, base == head.
280 def head_top_equal(self
):
281 if not self
.patchorder
.applied
:
283 top
= self
.patches
.get(self
.patchorder
.applied
[-1]).commit
284 return self
.head
== top
286 def set_parents(self
, remote
, branch
):
288 self
.set_parent_remote(remote
)
290 self
.set_parent_branch(branch
)
291 config
.set('branch.%s.stgit.parentbranch' % self
.name
, branch
)
295 return config
.getbool('branch.%s.stgit.protect' % self
.name
)
298 def protected(self
, protect
):
299 protect_key
= 'branch.%s.stgit.protect' % self
.name
301 config
.set(protect_key
, 'true')
303 config
.unset(protect_key
)
306 assert not self
.protected
, 'attempt to delete protected stack'
307 for pn
in self
.patchorder
.all
:
308 patch
= self
.patches
.get(pn
)
310 shutil
.rmtree(self
.directory
)
311 config
.remove_section('branch.%s.stgit' % self
.name
)
313 def rename(self
, name
):
315 patch_names
= self
.patchorder
.all
316 super(Stack
, self
).rename(name
)
317 old_ref_root
= 'refs/patches/%s' % old_name
318 new_ref_root
= 'refs/patches/%s' % name
321 for pn
in patch_names
:
322 old_ref
= '%s/%s' % (old_ref_root
, pn
)
323 new_ref
= '%s/%s' % (new_ref_root
, pn
)
324 old_log_ref
= old_ref
+ '.log'
325 new_log_ref
= new_ref
+ '.log'
326 patch_commit_id
= self
.repository
.refs
.get(old_ref
).sha1
327 log_commit_id
= self
.repository
.refs
.get(old_log_ref
).sha1
329 ref_updates
+= 'update %s %s %s\n' % (new_ref
, patch_commit_id
, empty_id
)
330 ref_updates
+= 'update %s %s %s\n' % (new_log_ref
, log_commit_id
, empty_id
)
331 ref_updates
+= 'delete %s %s\n' % (old_ref
, patch_commit_id
)
332 ref_updates
+= 'delete %s %s\n' % (old_log_ref
, log_commit_id
)
333 self
.repository
.run(['git', 'update-ref', '--stdin']).raw_input(
337 config
.rename_section('branch.%s.stgit' % old_name
, 'branch.%s.stgit' % name
)
340 os
.path
.join(self
.repository
.directory
, self
._repo
_subdir
), old_name
, name
343 def rename_patch(self
, old_name
, new_name
, msg
='rename'):
344 if new_name
== old_name
:
345 raise StackException('New patch name same as old: "%s"' % new_name
)
346 elif self
.patches
.exists(new_name
):
347 raise StackException('Patch already exists: "%s"' % new_name
)
348 elif not self
.patches
.is_name_valid(new_name
):
349 raise StackException('Invalid patch name: "%s"' % new_name
)
350 elif not self
.patches
.exists(old_name
):
351 raise StackException('Unknown patch name: "%s"' % old_name
)
352 self
.patchorder
.rename_patch(old_name
, new_name
)
353 self
.patches
.get(old_name
).set_name(new_name
, msg
)
356 def initialise(cls
, repository
, name
=None, switch_to
=False):
357 """Initialise a Git branch to handle patch series.
359 @param repository: The L{Repository} where the L{Stack} will be created
360 @param name: The name of the L{Stack}
363 name
= repository
.current_branch_name
364 # make sure that the corresponding Git branch exists
365 branch
= Branch(repository
, name
)
367 dir = os
.path
.join(repository
.directory
, cls
._repo
_subdir
, name
)
368 if os
.path
.exists(dir):
369 raise StackException('%s: branch already initialized' % name
)
374 # create the stack directory and files
375 utils
.create_dirs(dir)
376 compat_dir
= os
.path
.join(dir, 'patches')
377 utils
.create_dirs(compat_dir
)
378 PatchOrder
.create(dir)
380 stackupgrade
.format_version_key(name
), str(stackupgrade
.FORMAT_VERSION
)
383 return repository
.get_stack(name
)
395 """Create and initialise a Git branch returning the L{Stack} object.
397 @param repository: The L{Repository} where the L{Stack} will be created
398 @param name: The name of the L{Stack}
399 @param create_at: The Git id used as the base for the newly created
401 @param parent_remote: The name of the remote Git branch
402 @param parent_branch: The name of the parent Git branch
404 branch
= Branch
.create(repository
, name
, create_at
=create_at
)
406 stack
= cls
.initialise(repository
, name
, switch_to
=switch_to
)
407 except (BranchException
, StackException
):
410 stack
.set_parents(parent_remote
, parent_branch
)
414 class StackRepository(Repository
):
415 """A git L{Repository<Repository>} with some added StGit-specific
418 def __init__(self
, *args
, **kwargs
):
419 Repository
.__init
__(self
, *args
, **kwargs
)
420 self
._stacks
= {} # name -> Stack
423 def current_stack(self
):
424 return self
.get_stack()
426 def get_stack(self
, name
=None):
428 name
= self
.current_branch_name
429 if name
not in self
._stacks
:
430 self
._stacks
[name
] = Stack(self
, name
)
431 return self
._stacks
[name
]