Convert "stg refresh" to the new infrastructure
[stgit] / stgit / lib / stack.py
CommitLineData
652b2e67
KH
1"""A Python class hierarchy wrapping the StGit on-disk metadata."""
2
cbe4567e
KH
3import os.path
4from stgit import exception, utils
f5c820a8 5from stgit.lib import git, stackupgrade
21b42305
CM
6from stgit.config import config
7
8class StackException(exception.StgException):
9 """Exception raised by L{stack} objects."""
cbe4567e
KH
10
11class Patch(object):
652b2e67
KH
12 """Represents an StGit patch. This class is mainly concerned with
13 reading and writing the on-disk representation of a patch."""
cbe4567e
KH
14 def __init__(self, stack, name):
15 self.__stack = stack
16 self.__name = name
17 name = property(lambda self: self.__name)
5a8e991e 18 @property
cbe4567e
KH
19 def __ref(self):
20 return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
21 @property
5a8e991e
KH
22 def __log_ref(self):
23 return self.__ref + '.log'
24 @property
cbe4567e 25 def commit(self):
5a8e991e
KH
26 return self.__stack.repository.refs.get(self.__ref)
27 @property
d6a698bb
KH
28 def old_commit(self):
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):
32 return None
33 return self.__stack.repository.get_commit(utils.read_string(fn))
34 @property
5a8e991e
KH
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)
41 if val:
42 utils.write_string(fn, val, multiline)
43 elif os.path.isfile(fn):
44 os.remove(fn)
45 def write_patchlog():
46 try:
47 old_log = [self.__stack.repository.refs.get(self.__log_ref)]
48 except KeyError:
49 old_log = []
f5f22afe 50 cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
5a8e991e
KH
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)
54 return c
55 d = new_commit.data
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)
65 try:
66 old_top_sha1 = self.commit.sha1
67 old_bottom_sha1 = self.commit.data.parent.sha1
68 except KeyError:
69 old_top_sha1 = None
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)
d6de0462
CM
78 try:
79 # this compatibility log ref might not exist
80 self.__stack.repository.refs.delete(self.__log_ref)
81 except KeyError:
82 pass
cbe4567e 83 def set_commit(self, commit, msg):
5a8e991e
KH
84 self.__write_compat_files(commit, msg)
85 self.__stack.repository.refs.set(self.__ref, commit, msg)
cbe4567e 86 def delete(self):
5a8e991e
KH
87 self.__delete_compat_files()
88 self.__stack.repository.refs.delete(self.__ref)
cbe4567e
KH
89 def is_applied(self):
90 return self.name in self.__stack.patchorder.applied
91 def is_empty(self):
dcd32afa 92 return self.commit.data.is_nochange()
85aaed81
KH
93 def files(self):
94 """Return the set of files this patch touches."""
95 fs = set()
96 for (_, _, _, _, _, oldname, newname
97 ) in self.__stack.repository.diff_tree_files(
98 self.commit.data.tree, self.commit.data.parent.data.tree):
99 fs.add(oldname)
100 fs.add(newname)
101 return fs
cbe4567e
KH
102
103class PatchOrder(object):
104 """Keeps track of patch order, and which patches are applied.
105 Works with patch names, not actual patches."""
cbe4567e
KH
106 def __init__(self, stack):
107 self.__stack = stack
108 self.__lists = {}
109 def __read_file(self, fn):
110 return tuple(utils.read_strings(
111 os.path.join(self.__stack.directory, fn)))
112 def __write_file(self, fn, val):
113 utils.write_strings(os.path.join(self.__stack.directory, fn), val)
114 def __get_list(self, name):
115 if not name in self.__lists:
116 self.__lists[name] = self.__read_file(name)
117 return self.__lists[name]
118 def __set_list(self, name, val):
119 val = tuple(val)
120 if val != self.__lists.get(name, None):
121 self.__lists[name] = val
122 self.__write_file(name, val)
123 applied = property(lambda self: self.__get_list('applied'),
124 lambda self, val: self.__set_list('applied', val))
125 unapplied = property(lambda self: self.__get_list('unapplied'),
126 lambda self, val: self.__set_list('unapplied', val))
448e5d9d
CM
127 hidden = property(lambda self: self.__get_list('hidden'),
128 lambda self, val: self.__set_list('hidden', val))
d454cc06
CM
129 all = property(lambda self: self.applied + self.unapplied + self.hidden)
130 all_visible = property(lambda self: self.applied + self.unapplied)
cbe4567e 131
21b42305
CM
132 @staticmethod
133 def create(stackdir):
134 """Create the PatchOrder specific files
135 """
136 utils.create_empty_file(os.path.join(stackdir, 'applied'))
137 utils.create_empty_file(os.path.join(stackdir, 'unapplied'))
138 utils.create_empty_file(os.path.join(stackdir, 'hidden'))
139
cbe4567e 140class Patches(object):
652b2e67
KH
141 """Creates L{Patch} objects. Makes sure there is only one such object
142 per patch."""
cbe4567e
KH
143 def __init__(self, stack):
144 self.__stack = stack
145 def create_patch(name):
146 p = Patch(self.__stack, name)
147 p.commit # raise exception if the patch doesn't exist
148 return p
149 self.__patches = git.ObjectCache(create_patch) # name -> Patch
150 def exists(self, name):
151 try:
152 self.get(name)
153 return True
154 except KeyError:
155 return False
156 def get(self, name):
157 return self.__patches[name]
158 def new(self, name, commit, msg):
159 assert not name in self.__patches
160 p = Patch(self.__stack, name)
161 p.set_commit(commit, msg)
162 self.__patches[name] = p
163 return p
164
04c77bc5 165class Stack(git.Branch):
652b2e67
KH
166 """Represents an StGit stack (that is, a git branch with some extra
167 metadata)."""
04c77bc5
CM
168 __repo_subdir = 'patches'
169
cbe4567e 170 def __init__(self, repository, name):
04c77bc5 171 git.Branch.__init__(self, repository, name)
cbe4567e
KH
172 self.__patchorder = PatchOrder(self)
173 self.__patches = Patches(self)
317b386f 174 if not stackupgrade.update_to_current_format_version(repository, name):
21b42305 175 raise StackException('%s: branch not initialized' % name)
cbe4567e
KH
176 patchorder = property(lambda self: self.__patchorder)
177 patches = property(lambda self: self.__patches)
178 @property
179 def directory(self):
04c77bc5 180 return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
cbe4567e
KH
181 @property
182 def base(self):
183 if self.patchorder.applied:
184 return self.patches.get(self.patchorder.applied[0]
185 ).commit.data.parent
186 else:
187 return self.head
117ed129
KH
188 @property
189 def top(self):
190 """Commit of the topmost patch, or the stack base if no patches are
191 applied."""
192 if self.patchorder.applied:
193 return self.patches.get(self.patchorder.applied[-1]).commit
194 else:
195 # When no patches are applied, base == head.
196 return self.head
dcd32afa
KH
197 def head_top_equal(self):
198 if not self.patchorder.applied:
199 return True
200 return self.head == self.patches.get(self.patchorder.applied[-1]).commit
cbe4567e 201
21b42305
CM
202 def set_parents(self, remote, branch):
203 if remote:
204 self.set_parent_remote(remote)
205 if branch:
206 self.set_parent_branch(branch)
207
208 @classmethod
209 def initialise(cls, repository, name = None):
210 """Initialise a Git branch to handle patch series.
211
212 @param repository: The L{Repository} where the L{Stack} will be created
213 @param name: The name of the L{Stack}
214 """
215 if not name:
216 name = repository.current_branch_name
217 # make sure that the corresponding Git branch exists
218 git.Branch(repository, name)
219
220 dir = os.path.join(repository.directory, cls.__repo_subdir, name)
221 compat_dir = os.path.join(dir, 'patches')
222 if os.path.exists(dir):
223 raise StackException('%s: branch already initialized' % name)
224
225 # create the stack directory and files
226 utils.create_dirs(dir)
227 utils.create_dirs(compat_dir)
228 PatchOrder.create(dir)
229 config.set(stackupgrade.format_version_key(name),
230 str(stackupgrade.FORMAT_VERSION))
231
232 return repository.get_stack(name)
233
234 @classmethod
235 def create(cls, repository, name,
236 create_at = None, parent_remote = None, parent_branch = None):
237 """Create and initialise a Git branch returning the L{Stack} object.
238
239 @param repository: The L{Repository} where the L{Stack} will be created
240 @param name: The name of the L{Stack}
241 @param create_at: The Git id used as the base for the newly created
242 Git branch
243 @param parent_remote: The name of the remote Git branch
244 @param parent_branch: The name of the parent Git branch
245 """
246 git.Branch.create(repository, name, create_at = create_at)
247 stack = cls.initialise(repository, name)
248 stack.set_parents(parent_remote, parent_branch)
249 return stack
250
cbe4567e 251class Repository(git.Repository):
652b2e67
KH
252 """A git L{Repository<git.Repository>} with some added StGit-specific
253 operations."""
cbe4567e
KH
254 def __init__(self, *args, **kwargs):
255 git.Repository.__init__(self, *args, **kwargs)
256 self.__stacks = {} # name -> Stack
257 @property
cbe4567e 258 def current_stack(self):
13b26f1a
CM
259 return self.get_stack()
260 def get_stack(self, name = None):
261 if not name:
04c77bc5 262 name = self.current_branch_name
cbe4567e
KH
263 if not name in self.__stacks:
264 self.__stacks[name] = Stack(self, name)
265 return self.__stacks[name]