1 """A Python class hierarchy wrapping the StGit on-disk metadata."""
4 from stgit import exception, utils
5 from stgit.lib import git, stackupgrade
6 from stgit.config import config
8 class StackException(exception.StgException):
9 """Exception raised by L{stack} objects."""
12 """Represents an StGit patch. This class is mainly concerned with
13 reading and writing the on-disk representation of a patch."""
14 def __init__(self, stack, name):
17 name = property(lambda self: self.__name)
20 return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
23 return self.__ref + '.log'
26 return self.__stack.repository.refs.get(self.__ref)
29 """Return the previous commit for this patch."""
30 fn = os.path.join(self.__compat_dir, 'top.old')
31 if not os.path.isfile(fn):
33 return self.__stack.repository.get_commit(utils.read_string(fn))
35 def __compat_dir(self):
36 return os.path.join(self.__stack.directory, 'patches', self.__name)
37 def __write_compat_files(self, new_commit, msg):
38 """Write files used by the old infrastructure."""
39 def write(name, val, multiline = False):
40 fn = os.path.join(self.__compat_dir, name)
42 utils.write_string(fn, val, multiline)
43 elif os.path.isfile(fn):
47 old_log = [self.__stack.repository.refs.get(self.__log_ref)]
50 cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
51 message = '%s\t%s' % (msg, new_commit.sha1))
52 c = self.__stack.repository.commit(cd)
53 self.__stack.repository.refs.set(self.__log_ref, c, msg)
56 write('authname', d.author.name)
57 write('authemail', d.author.email)
58 write('authdate', d.author.date)
59 write('commname', d.committer.name)
60 write('commemail', d.committer.email)
61 write('description', d.message)
62 write('log', write_patchlog().sha1)
63 write('top', new_commit.sha1)
64 write('bottom', d.parent.sha1)
66 old_top_sha1 = self.commit.sha1
67 old_bottom_sha1 = self.commit.data.parent.sha1
70 old_bottom_sha1 = None
71 write('top.old', old_top_sha1)
72 write('bottom.old', old_bottom_sha1)
73 def __delete_compat_files(self):
74 if os.path.isdir(self.__compat_dir):
75 for f in os.listdir(self.__compat_dir):
76 os.remove(os.path.join(self.__compat_dir, f))
77 os.rmdir(self.__compat_dir)
79 # this compatibility log ref might not exist
80 self.__stack.repository.refs.delete(self.__log_ref)
83 def set_commit(self, commit, msg):
84 self.__write_compat_files(commit, msg)
85 self.__stack.repository.refs.set(self.__ref, commit, msg)
87 self.__delete_compat_files()
88 self.__stack.repository.refs.delete(self.__ref)
90 return self.name in self.__stack.patchorder.applied
92 return self.commit.data.is_nochange()
94 class PatchOrder(object):
95 """Keeps track of patch order, and which patches are applied.
96 Works with patch names, not actual patches."""
97 def __init__(self, stack):
100 def __read_file(self, fn):
101 return tuple(utils.read_strings(
102 os.path.join(self.__stack.directory, fn)))
103 def __write_file(self, fn, val):
104 utils.write_strings(os.path.join(self.__stack.directory, fn), val)
105 def __get_list(self, name):
106 if not name in self.__lists:
107 self.__lists[name] = self.__read_file(name)
108 return self.__lists[name]
109 def __set_list(self, name, val):
111 if val != self.__lists.get(name, None):
112 self.__lists[name] = val
113 self.__write_file(name, val)
114 applied = property(lambda self: self.__get_list('applied'),
115 lambda self, val: self.__set_list('applied', val))
116 unapplied = property(lambda self: self.__get_list('unapplied'),
117 lambda self, val: self.__set_list('unapplied', val))
118 hidden = property(lambda self: self.__get_list('hidden'),
119 lambda self, val: self.__set_list('hidden', val))
120 all = property(lambda self: self.applied + self.unapplied + self.hidden)
121 all_visible = property(lambda self: self.applied + self.unapplied)
124 def create(stackdir):
125 """Create the PatchOrder specific files
127 utils.create_empty_file(os.path.join(stackdir, 'applied'))
128 utils.create_empty_file(os.path.join(stackdir, 'unapplied'))
129 utils.create_empty_file(os.path.join(stackdir, 'hidden'))
131 class Patches(object):
132 """Creates L{Patch} objects. Makes sure there is only one such object
134 def __init__(self, stack):
136 def create_patch(name):
137 p = Patch(self.__stack, name)
138 p.commit # raise exception if the patch doesn't exist
140 self.__patches = git.ObjectCache(create_patch) # name -> Patch
141 def exists(self, name):
148 return self.__patches[name]
149 def new(self, name, commit, msg):
150 assert not name in self.__patches
151 p = Patch(self.__stack, name)
152 p.set_commit(commit, msg)
153 self.__patches[name] = p
156 class Stack(git.Branch):
157 """Represents an StGit stack (that is, a git branch with some extra
159 __repo_subdir = 'patches'
161 def __init__(self, repository, name):
162 git.Branch.__init__(self, repository, name)
163 self.__patchorder = PatchOrder(self)
164 self.__patches = Patches(self)
165 if not stackupgrade.update_to_current_format_version(repository, name):
166 raise StackException('%s: branch not initialized' % name)
167 patchorder = property(lambda self: self.__patchorder)
168 patches = property(lambda self: self.__patches)
171 return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
174 if self.patchorder.applied:
175 return self.patches.get(self.patchorder.applied[0]
181 """Commit of the topmost patch, or the stack base if no patches are
183 if self.patchorder.applied:
184 return self.patches.get(self.patchorder.applied[-1]).commit
186 # When no patches are applied, base == head.
188 def head_top_equal(self):
189 if not self.patchorder.applied:
191 return self.head == self.patches.get(self.patchorder.applied[-1]).commit
193 def set_parents(self, remote, branch):
195 self.set_parent_remote(remote)
197 self.set_parent_branch(branch)
200 def initialise(cls, repository, name = None):
201 """Initialise a Git branch to handle patch series.
203 @param repository: The L{Repository} where the L{Stack} will be created
204 @param name: The name of the L{Stack}
207 name = repository.current_branch_name
208 # make sure that the corresponding Git branch exists
209 git.Branch(repository, name)
211 dir = os.path.join(repository.directory, cls.__repo_subdir, name)
212 compat_dir = os.path.join(dir, 'patches')
213 if os.path.exists(dir):
214 raise StackException('%s: branch already initialized' % name)
216 # create the stack directory and files
217 utils.create_dirs(dir)
218 utils.create_dirs(compat_dir)
219 PatchOrder.create(dir)
220 config.set(stackupgrade.format_version_key(name),
221 str(stackupgrade.FORMAT_VERSION))
223 return repository.get_stack(name)
226 def create(cls, repository, name,
227 create_at = None, parent_remote = None, parent_branch = None):
228 """Create and initialise a Git branch returning the L{Stack} object.
230 @param repository: The L{Repository} where the L{Stack} will be created
231 @param name: The name of the L{Stack}
232 @param create_at: The Git id used as the base for the newly created
234 @param parent_remote: The name of the remote Git branch
235 @param parent_branch: The name of the parent Git branch
237 git.Branch.create(repository, name, create_at = create_at)
238 stack = cls.initialise(repository, name)
239 stack.set_parents(parent_remote, parent_branch)
242 class Repository(git.Repository):
243 """A git L{Repository<git.Repository>} with some added StGit-specific
245 def __init__(self, *args, **kwargs):
246 git.Repository.__init__(self, *args, **kwargs)
247 self.__stacks = {} # name -> Stack
249 def current_stack(self):
250 return self.get_stack()
251 def get_stack(self, name = None):
253 name = self.current_branch_name
254 if not name in self.__stacks:
255 self.__stacks[name] = Stack(self, name)
256 return self.__stacks[name]