maint: prefer functions over methods
[git-cola.git] / cola / models / dag.py
blob670d158ab2c79f3fbfcf8c40d6ea648271ecd70c
1 from __future__ import division, absolute_import, unicode_literals
2 import json
4 from .. import core
5 from .. import utils
6 from ..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 = r'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, oid=None, log_entry=None):
25 if not oid and log_entry:
26 oid = log_entry[:40]
27 try:
28 commit = cls.commits[oid]
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(oid=oid,
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[oid] = 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 hasattr(args, 'args') and args.args:
75 ref = core.list2cmdline(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__ = ('oid',
94 'summary',
95 'parents',
96 'children',
97 'tags',
98 'author',
99 'authdate',
100 'email',
101 'generation',
102 'column',
103 'row',
104 'parsed')
106 def __init__(self, oid=None, log_entry=None):
107 self.oid = oid
108 self.summary = None
109 self.parents = []
110 self.children = []
111 self.tags = set()
112 self.email = None
113 self.author = None
114 self.authdate = None
115 self.parsed = False
116 self.generation = CommitFactory.root_generation
117 self.column = None
118 self.row = None
119 if log_entry:
120 self.parse(log_entry)
122 def parse(self, log_entry, sep=logsep):
123 self.oid = log_entry[:40]
124 after_oid = log_entry[41:]
125 details = after_oid.split(sep, 5)
126 (parents, tags, author, authdate, email, summary) = details
128 self.summary = summary if summary else ''
129 self.author = author if author else ''
130 self.authdate = authdate if authdate else ''
131 self.email = email if email else ''
133 if parents:
134 generation = None
135 for parent_oid in parents.split(' '):
136 parent = CommitFactory.new(oid=parent_oid)
137 parent.children.append(self)
138 if generation is None:
139 generation = parent.generation+1
140 self.parents.append(parent)
141 generation = max(parent.generation+1, generation)
142 self.generation = generation
144 if tags:
145 for tag in tags[2:-1].split(', '):
146 self.add_label(tag)
148 self.parsed = True
149 return self
151 def add_label(self, tag):
152 """Add tag/branch labels from `git log --decorate ....`"""
154 if tag.startswith('tag: '):
155 tag = tag[5:] # strip off "tag: " leaving refs/tags/
157 if tag.startswith('refs/'):
158 # strip off refs/ leaving just tags/XXX remotes/XXX heads/XXX
159 tag = tag[5:]
161 if tag.endswith('/HEAD'):
162 return
164 # Git 2.4 Release Notes (draft)
165 # =============================
167 # Backward compatibility warning(s)
168 # ---------------------------------
170 # This release has a few changes in the user-visible output from
171 # Porcelain commands. These are not meant to be parsed by scripts, but
172 # the users still may want to be aware of the changes:
174 # * Output from "git log --decorate" (and "%d" format specifier used in
175 # the userformat "--format=<string>" parameter "git log" family of
176 # command takes) used to list "HEAD" just like other tips of branch
177 # names, separated with a comma in between. E.g.
179 # $ git log --decorate -1 master
180 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD, master)
181 # ...
183 # This release updates the output slightly when HEAD refers to the tip
184 # of a branch whose name is also shown in the output. The above is
185 # shown as:
187 # $ git log --decorate -1 master
188 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD -> master)
189 # ...
191 # C.f. http://thread.gmane.org/gmane.linux.kernel/1931234
193 head_arrow = 'HEAD -> '
194 if tag.startswith(head_arrow):
195 self.tags.add('HEAD')
196 self.add_label(tag[len(head_arrow):])
197 else:
198 self.tags.add(tag)
200 def __str__(self):
201 return self.oid
203 def data(self):
204 return {
205 'oid': self.oid,
206 'summary': self.summary,
207 'author': self.author,
208 'authdate': self.authdate,
209 'parents': [p.oid for p in self.parents],
210 'tags': self.tags,
213 def __repr__(self):
214 return json.dumps(self.data(), sort_keys=True, indent=4, default=list)
216 def is_fork(self):
217 ''' Returns True if the node is a fork'''
218 return len(self.children) > 1
220 def is_merge(self):
221 ''' Returns True if the node is a fork'''
222 return len(self.parents) > 1
225 class RepoReader(object):
227 def __init__(self, context, params):
228 self.context = context
229 self.params = params
230 self.git = context.git
231 self.returncode = 0
232 self._proc = None
233 self._objects = {}
234 self._cmd = ['git',
235 '-c', 'log.abbrevCommit=false',
236 '-c', 'log.showSignature=false',
237 'log',
238 '--topo-order',
239 '--reverse',
240 '--decorate=full',
241 '--pretty='+logfmt]
242 self._cached = False
243 """Indicates that all data has been read"""
244 self._topo_list = []
245 """List of commits objects in topological order"""
247 cached = property(lambda self: self._cached)
248 """Return True when no commits remain to be read"""
250 def __len__(self):
251 return len(self._topo_list)
253 def reset(self):
254 CommitFactory.reset()
255 if self._proc:
256 self._proc.kill()
257 self._proc = None
258 self._cached = False
259 self._topo_list = []
261 def get(self):
262 """Generator function returns Commit objects found by the params"""
263 if self._cached:
264 idx = 0
265 while True:
266 try:
267 yield self._topo_list[idx]
268 except IndexError:
269 break
270 idx += 1
271 return
273 self.reset()
274 ref_args = utils.shell_split(self.params.ref)
275 cmd = self._cmd + ['-%d' % self.params.count] + ref_args
276 self._proc = core.start_command(cmd)
278 while True:
279 log_entry = core.readline(self._proc.stdout).rstrip()
280 if not log_entry:
281 self._cached = True
282 self._proc.wait()
283 self.returncode = self._proc.returncode
284 self._proc = None
285 break
286 oid = log_entry[:40]
287 try:
288 yield self._objects[oid]
289 except KeyError:
290 commit = CommitFactory.new(log_entry=log_entry)
291 self._objects[commit.oid] = commit
292 self._topo_list.append(commit)
293 yield commit
294 return
296 def __getitem__(self, oid):
297 return self._objects[oid]
299 def items(self):
300 return list(self._objects.items())