chiark / gitweb /
Create a git.Branch class as ancestor of stack.Stack
[stgit] / stgit / lib / stack.py
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
7 class Patch(object):
8     """Represents an StGit patch. This class is mainly concerned with
9     reading and writing the on-disk representation of a patch."""
10     def __init__(self, stack, name):
11         self.__stack = stack
12         self.__name = name
13     name = property(lambda self: self.__name)
14     @property
15     def __ref(self):
16         return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)
17     @property
18     def __log_ref(self):
19         return self.__ref + '.log'
20     @property
21     def commit(self):
22         return self.__stack.repository.refs.get(self.__ref)
23     @property
24     def __compat_dir(self):
25         return os.path.join(self.__stack.directory, 'patches', self.__name)
26     def __write_compat_files(self, new_commit, msg):
27         """Write files used by the old infrastructure."""
28         def write(name, val, multiline = False):
29             fn = os.path.join(self.__compat_dir, name)
30             if val:
31                 utils.write_string(fn, val, multiline)
32             elif os.path.isfile(fn):
33                 os.remove(fn)
34         def write_patchlog():
35             try:
36                 old_log = [self.__stack.repository.refs.get(self.__log_ref)]
37             except KeyError:
38                 old_log = []
39             cd = git.CommitData(tree = new_commit.data.tree, parents = old_log,
40                                 message = '%s\t%s' % (msg, new_commit.sha1))
41             c = self.__stack.repository.commit(cd)
42             self.__stack.repository.refs.set(self.__log_ref, c, msg)
43             return c
44         d = new_commit.data
45         write('authname', d.author.name)
46         write('authemail', d.author.email)
47         write('authdate', d.author.date)
48         write('commname', d.committer.name)
49         write('commemail', d.committer.email)
50         write('description', d.message)
51         write('log', write_patchlog().sha1)
52         write('top', new_commit.sha1)
53         write('bottom', d.parent.sha1)
54         try:
55             old_top_sha1 = self.commit.sha1
56             old_bottom_sha1 = self.commit.data.parent.sha1
57         except KeyError:
58             old_top_sha1 = None
59             old_bottom_sha1 = None
60         write('top.old', old_top_sha1)
61         write('bottom.old', old_bottom_sha1)
62     def __delete_compat_files(self):
63         if os.path.isdir(self.__compat_dir):
64             for f in os.listdir(self.__compat_dir):
65                 os.remove(os.path.join(self.__compat_dir, f))
66             os.rmdir(self.__compat_dir)
67         self.__stack.repository.refs.delete(self.__log_ref)
68     def set_commit(self, commit, msg):
69         self.__write_compat_files(commit, msg)
70         self.__stack.repository.refs.set(self.__ref, commit, msg)
71     def delete(self):
72         self.__delete_compat_files()
73         self.__stack.repository.refs.delete(self.__ref)
74     def is_applied(self):
75         return self.name in self.__stack.patchorder.applied
76     def is_empty(self):
77         return self.commit.data.is_nochange()
78
79 class PatchOrder(object):
80     """Keeps track of patch order, and which patches are applied.
81     Works with patch names, not actual patches."""
82     def __init__(self, stack):
83         self.__stack = stack
84         self.__lists = {}
85     def __read_file(self, fn):
86         return tuple(utils.read_strings(
87             os.path.join(self.__stack.directory, fn)))
88     def __write_file(self, fn, val):
89         utils.write_strings(os.path.join(self.__stack.directory, fn), val)
90     def __get_list(self, name):
91         if not name in self.__lists:
92             self.__lists[name] = self.__read_file(name)
93         return self.__lists[name]
94     def __set_list(self, name, val):
95         val = tuple(val)
96         if val != self.__lists.get(name, None):
97             self.__lists[name] = val
98             self.__write_file(name, val)
99     applied = property(lambda self: self.__get_list('applied'),
100                        lambda self, val: self.__set_list('applied', val))
101     unapplied = property(lambda self: self.__get_list('unapplied'),
102                          lambda self, val: self.__set_list('unapplied', val))
103     hidden = property(lambda self: self.__get_list('hidden'),
104                       lambda self, val: self.__set_list('hidden', val))
105     all = property(lambda self: self.applied + self.unapplied + self.hidden)
106     all_visible = property(lambda self: self.applied + self.unapplied)
107
108 class Patches(object):
109     """Creates L{Patch} objects. Makes sure there is only one such object
110     per patch."""
111     def __init__(self, stack):
112         self.__stack = stack
113         def create_patch(name):
114             p = Patch(self.__stack, name)
115             p.commit # raise exception if the patch doesn't exist
116             return p
117         self.__patches = git.ObjectCache(create_patch) # name -> Patch
118     def exists(self, name):
119         try:
120             self.get(name)
121             return True
122         except KeyError:
123             return False
124     def get(self, name):
125         return self.__patches[name]
126     def new(self, name, commit, msg):
127         assert not name in self.__patches
128         p = Patch(self.__stack, name)
129         p.set_commit(commit, msg)
130         self.__patches[name] = p
131         return p
132
133 class Stack(git.Branch):
134     """Represents an StGit stack (that is, a git branch with some extra
135     metadata)."""
136     __repo_subdir = 'patches'
137
138     def __init__(self, repository, name):
139         git.Branch.__init__(self, repository, name)
140         self.__patchorder = PatchOrder(self)
141         self.__patches = Patches(self)
142         if not stackupgrade.update_to_current_format_version(repository, name):
143             raise exception.StgException('%s: branch not initialized' % name)
144     patchorder = property(lambda self: self.__patchorder)
145     patches = property(lambda self: self.__patches)
146     @property
147     def directory(self):
148         return os.path.join(self.repository.directory, self.__repo_subdir, self.name)
149     @property
150     def base(self):
151         if self.patchorder.applied:
152             return self.patches.get(self.patchorder.applied[0]
153                                     ).commit.data.parent
154         else:
155             return self.head
156     def head_top_equal(self):
157         if not self.patchorder.applied:
158             return True
159         return self.head == self.patches.get(self.patchorder.applied[-1]).commit
160
161 class Repository(git.Repository):
162     """A git L{Repository<git.Repository>} with some added StGit-specific
163     operations."""
164     def __init__(self, *args, **kwargs):
165         git.Repository.__init__(self, *args, **kwargs)
166         self.__stacks = {} # name -> Stack
167     @property
168     def current_stack(self):
169         return self.get_stack()
170     def get_stack(self, name = None):
171         if not name:
172             name = self.current_branch_name
173         if not name in self.__stacks:
174             self.__stacks[name] = Stack(self, name)
175         return self.__stacks[name]