| 1 | """A Python class hierarchy wrapping the StGit on-disk metadata.""" |
| 2 | |
| 3 | import os.path |
| 4 | from stgit import exception, utils |
| 5 | from stgit.lib import git, stackupgrade |
| 6 | from stgit.config import config |
| 7 | |
| 8 | class StackException(exception.StgException): |
| 9 | """Exception raised by L{stack} objects.""" |
| 10 | |
| 11 | class Patch(object): |
| 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): |
| 15 | self.__stack = stack |
| 16 | self.__name = name |
| 17 | name = property(lambda self: self.__name) |
| 18 | @property |
| 19 | def __ref(self): |
| 20 | return 'refs/patches/%s/%s' % (self.__stack.name, self.__name) |
| 21 | @property |
| 22 | def __log_ref(self): |
| 23 | return self.__ref + '.log' |
| 24 | @property |
| 25 | def commit(self): |
| 26 | return self.__stack.repository.refs.get(self.__ref) |
| 27 | @property |
| 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 |
| 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 = [] |
| 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) |
| 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) |
| 78 | try: |
| 79 | # this compatibility log ref might not exist |
| 80 | self.__stack.repository.refs.delete(self.__log_ref) |
| 81 | except KeyError: |
| 82 | pass |
| 83 | def set_commit(self, commit, msg): |
| 84 | self.__write_compat_files(commit, msg) |
| 85 | self.__stack.repository.refs.set(self.__ref, commit, msg) |
| 86 | def delete(self): |
| 87 | self.__delete_compat_files() |
| 88 | self.__stack.repository.refs.delete(self.__ref) |
| 89 | def is_applied(self): |
| 90 | return self.name in self.__stack.patchorder.applied |
| 91 | def is_empty(self): |
| 92 | return self.commit.data.is_nochange() |
| 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 |
| 102 | |
| 103 | class PatchOrder(object): |
| 104 | """Keeps track of patch order, and which patches are applied. |
| 105 | Works with patch names, not actual patches.""" |
| 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)) |
| 127 | hidden = property(lambda self: self.__get_list('hidden'), |
| 128 | lambda self, val: self.__set_list('hidden', val)) |
| 129 | all = property(lambda self: self.applied + self.unapplied + self.hidden) |
| 130 | all_visible = property(lambda self: self.applied + self.unapplied) |
| 131 | |
| 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 | |
| 140 | class Patches(object): |
| 141 | """Creates L{Patch} objects. Makes sure there is only one such object |
| 142 | per patch.""" |
| 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 | |
| 165 | class Stack(git.Branch): |
| 166 | """Represents an StGit stack (that is, a git branch with some extra |
| 167 | metadata).""" |
| 168 | __repo_subdir = 'patches' |
| 169 | |
| 170 | def __init__(self, repository, name): |
| 171 | git.Branch.__init__(self, repository, name) |
| 172 | self.__patchorder = PatchOrder(self) |
| 173 | self.__patches = Patches(self) |
| 174 | if not stackupgrade.update_to_current_format_version(repository, name): |
| 175 | raise StackException('%s: branch not initialized' % name) |
| 176 | patchorder = property(lambda self: self.__patchorder) |
| 177 | patches = property(lambda self: self.__patches) |
| 178 | @property |
| 179 | def directory(self): |
| 180 | return os.path.join(self.repository.directory, self.__repo_subdir, self.name) |
| 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 |
| 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 |
| 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 |
| 201 | |
| 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 | |
| 251 | class Repository(git.Repository): |
| 252 | """A git L{Repository<git.Repository>} with some added StGit-specific |
| 253 | operations.""" |
| 254 | def __init__(self, *args, **kwargs): |
| 255 | git.Repository.__init__(self, *args, **kwargs) |
| 256 | self.__stacks = {} # name -> Stack |
| 257 | @property |
| 258 | def current_stack(self): |
| 259 | return self.get_stack() |
| 260 | def get_stack(self, name = None): |
| 261 | if not name: |
| 262 | name = self.current_branch_name |
| 263 | if not name in self.__stacks: |
| 264 | self.__stacks[name] = Stack(self, name) |
| 265 | return self.__stacks[name] |