dag: call wait() when the subprocess finishes
[git-cola.git] / cola / dag / model.py
blob1ca1fadf760827addfff36be4a370cd20d730aab
1 import subprocess
3 from cola import core
4 from cola import utils
5 from cola.git import git
6 from cola.observable import Observable
8 # put summary at the end b/c it can contain
9 # any number of funky characters, including the separator
10 logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%ad%x01%ae%x01%s'
11 logsep = chr(0x01)
14 class CommitFactory(object):
15 root_generation = 0
16 commits = {}
18 @classmethod
19 def reset(cls):
20 cls.commits.clear()
21 cls.root_generation = 0
23 @classmethod
24 def new(cls, sha1=None, log_entry=None):
25 if not sha1 and log_entry:
26 sha1 = log_entry[:40]
27 try:
28 commit = cls.commits[sha1]
29 if log_entry and not commit.parsed:
30 commit.parse(log_entry)
31 cls.root_generation = max(commit.generation,
32 cls.root_generation)
33 except KeyError:
34 commit = Commit(sha1=sha1,
35 log_entry=log_entry)
36 if not log_entry:
37 cls.root_generation += 1
38 commit.generation = max(commit.generation,
39 cls.root_generation)
40 cls.commits[sha1] = commit
41 return commit
44 class DAG(Observable):
45 ref_updated = 'ref_updated'
46 count_updated = 'count_updated'
48 def __init__(self, ref, count):
49 Observable.__init__(self)
50 self.ref = ref
51 self.count = count
52 self.overrides = {}
54 def set_ref(self, ref):
55 changed = ref != self.ref
56 if changed:
57 self.ref = ref
58 self.notify_observers(self.ref_updated)
59 return changed
61 def set_count(self, count):
62 changed = count != self.count
63 if changed:
64 self.count = count
65 self.notify_observers(self.count_updated)
66 return changed
68 def set_arguments(self, args):
69 if args is None:
70 return
71 if self.set_count(args.count):
72 self.overrides['count'] = args.count
74 if args.args:
75 ref = subprocess.list2cmdline(map(core.decode, args.args))
76 if self.set_ref(ref):
77 self.overrides['ref'] = ref
79 def overridden(self, opt):
80 return opt in self.overrides
82 def paths(self):
83 all_refs = utils.shell_split(self.ref)
84 if '--' in all_refs:
85 all_refs = all_refs[all_refs.index('--'):]
87 return [p for p in all_refs if p and core.exists(p)]
90 class Commit(object):
91 root_generation = 0
93 __slots__ = ('sha1',
94 'summary',
95 'parents',
96 'children',
97 'tags',
98 'author',
99 'authdate',
100 'email',
101 'generation',
102 'parsed')
103 def __init__(self, sha1=None, log_entry=None):
104 self.sha1 = sha1
105 self.summary = None
106 self.parents = []
107 self.children = []
108 self.tags = set()
109 self.email = None
110 self.author = None
111 self.authdate = None
112 self.parsed = False
113 self.generation = CommitFactory.root_generation
114 if log_entry:
115 self.parse(log_entry)
117 def parse(self, log_entry, sep=logsep):
118 self.sha1 = log_entry[:40]
119 (parents, tags, author, authdate, email, summary) = \
120 log_entry[41:].split(sep, 6)
122 self.summary = summary and summary or ''
123 self.author = author and author or ''
124 self.authdate = authdate or ''
125 self.email = email and email or ''
127 if parents:
128 generation = None
129 for parent_sha1 in parents.split(' '):
130 parent = CommitFactory.new(sha1=parent_sha1)
131 parent.children.append(self)
132 if generation is None:
133 generation = parent.generation+1
134 self.parents.append(parent)
135 generation = max(parent.generation+1, generation)
136 self.generation = generation
138 if tags:
139 for tag in tags[2:-1].split(', '):
140 if tag.startswith('tag: '):
141 tag = tag[5:] # tag: refs/
142 elif tag.startswith('refs/remotes/'):
143 tag = tag[13:] # refs/remotes/
144 elif tag.startswith('refs/heads/'):
145 tag = tag[11:] # refs/heads/
146 if tag.endswith('/HEAD'):
147 continue
148 self.tags.add(tag)
150 self.parsed = True
151 return self
153 def __str__(self):
154 return self.sha1
156 def __repr__(self):
157 return ("{\n"
158 " sha1: " + self.sha1 + "\n"
159 " summary: " + self.summary + "\n"
160 " author: " + self.author + "\n"
161 " authdate: " + self.authdate + "\n"
162 " parents: [" + ', '.join([p.sha1 for p in self.parents]) + "]\n"
163 " tags: [" + ', '.join(self.tags) + "]\n"
164 "}")
166 def is_fork(self):
167 ''' Returns True if the node is a fork'''
168 return len(self.children) > 1
170 def is_merge(self):
171 ''' Returns True if the node is a fork'''
172 return len(self.parents) > 1
175 class RepoReader(object):
177 def __init__(self, dag, git=git):
178 self.dag = dag
179 self.git = git
180 self._proc = None
181 self._objects = {}
182 self._cmd = ['git', 'log',
183 '--topo-order',
184 '--reverse',
185 '--pretty='+logfmt]
186 self._cached = False
187 """Indicates that all data has been read"""
188 self._idx = -1
189 """Index into the cached commits"""
190 self._topo_list = []
191 """List of commits objects in topological order"""
193 cached = property(lambda self: self._cached)
194 """Return True when no commits remain to be read"""
197 def __len__(self):
198 return len(self._topo_list)
200 def reset(self):
201 CommitFactory.reset()
202 if self._proc:
203 self._topo_list = []
204 self._proc.kill()
205 self._proc = None
206 self._cached = False
208 def __iter__(self):
209 if self._cached:
210 return self
211 self.reset()
212 return self
214 def next(self):
215 if self._cached:
216 try:
217 self._idx += 1
218 return self._topo_list[self._idx]
219 except IndexError:
220 self._idx = -1
221 raise StopIteration
223 if self._proc is None:
224 ref_args = utils.shell_split(self.dag.ref)
225 cmd = self._cmd + ['-%d' % self.dag.count] + ref_args
226 self._proc = utils.start_command(cmd)
227 self._topo_list = []
229 log_entry = core.readline(self._proc.stdout).rstrip()
230 if not log_entry:
231 self._cached = True
232 self._proc.wait()
233 self._proc = None
234 raise StopIteration
236 sha1 = log_entry[:40]
237 try:
238 return self._objects[sha1]
239 except KeyError:
240 c = CommitFactory.new(log_entry=log_entry)
241 self._objects[c.sha1] = c
242 self._topo_list.append(c)
243 return c
245 def __getitem__(self, sha1):
246 return self._objects[sha1]
248 def items(self):
249 return self._objects.items()