doc: add Ved to the credits
[git-cola.git] / cola / models / dag.py
blob1e69af9e8c775740a2da1636a63ba764e394b56f
1 from __future__ import division, absolute_import, unicode_literals
2 import json
4 from .. import core
5 from .. import utils
6 from ..git import git
7 from ..observable import Observable
9 # put summary at the end b/c it can contain
10 # any number of funky characters, including the separator
11 logfmt = 'format:%H%x01%P%x01%d%x01%an%x01%ad%x01%ae%x01%s'
12 logsep = chr(0x01)
15 class CommitFactory(object):
16 root_generation = 0
17 commits = {}
19 @classmethod
20 def reset(cls):
21 cls.commits.clear()
22 cls.root_generation = 0
24 @classmethod
25 def new(cls, sha1=None, log_entry=None):
26 if not sha1 and log_entry:
27 sha1 = log_entry[:40]
28 try:
29 commit = cls.commits[sha1]
30 if log_entry and not commit.parsed:
31 commit.parse(log_entry)
32 cls.root_generation = max(commit.generation,
33 cls.root_generation)
34 except KeyError:
35 commit = Commit(sha1=sha1,
36 log_entry=log_entry)
37 if not log_entry:
38 cls.root_generation += 1
39 commit.generation = max(commit.generation,
40 cls.root_generation)
41 cls.commits[sha1] = commit
42 return commit
45 class DAG(Observable):
46 ref_updated = 'ref_updated'
47 count_updated = 'count_updated'
49 def __init__(self, ref, count):
50 Observable.__init__(self)
51 self.ref = ref
52 self.count = count
53 self.overrides = {}
55 def set_ref(self, ref):
56 changed = ref != self.ref
57 if changed:
58 self.ref = ref
59 self.notify_observers(self.ref_updated)
60 return changed
62 def set_count(self, count):
63 changed = count != self.count
64 if changed:
65 self.count = count
66 self.notify_observers(self.count_updated)
67 return changed
69 def set_arguments(self, args):
70 if args is None:
71 return
72 if self.set_count(args.count):
73 self.overrides['count'] = args.count
75 if hasattr(args, 'args') and args.args:
76 ref = core.list2cmdline(args.args)
77 if self.set_ref(ref):
78 self.overrides['ref'] = ref
80 def overridden(self, opt):
81 return opt in self.overrides
83 def paths(self):
84 all_refs = utils.shell_split(self.ref)
85 if '--' in all_refs:
86 all_refs = all_refs[all_refs.index('--'):]
88 return [p for p in all_refs if p and core.exists(p)]
91 class Commit(object):
92 root_generation = 0
94 __slots__ = ('sha1',
95 'summary',
96 'parents',
97 'children',
98 'tags',
99 'author',
100 'authdate',
101 'email',
102 'generation',
103 'parsed')
105 def __init__(self, sha1=None, log_entry=None):
106 self.sha1 = sha1
107 self.summary = None
108 self.parents = []
109 self.children = []
110 self.tags = set()
111 self.email = None
112 self.author = None
113 self.authdate = None
114 self.parsed = False
115 self.generation = CommitFactory.root_generation
116 if log_entry:
117 self.parse(log_entry)
119 def parse(self, log_entry, sep=logsep):
120 self.sha1 = log_entry[:40]
121 after_sha1 = log_entry[41:]
122 details = after_sha1.split(sep, 5)
123 (parents, tags, author, authdate, email, summary) = details
125 self.summary = summary and summary or ''
126 self.author = author and author or ''
127 self.authdate = authdate or ''
128 self.email = email and email or ''
130 if parents:
131 generation = None
132 for parent_sha1 in parents.split(' '):
133 parent = CommitFactory.new(sha1=parent_sha1)
134 parent.children.append(self)
135 if generation is None:
136 generation = parent.generation+1
137 self.parents.append(parent)
138 generation = max(parent.generation+1, generation)
139 self.generation = generation
141 if tags:
142 for tag in tags[2:-1].split(', '):
143 self.add_label(tag)
145 self.parsed = True
146 return self
148 def add_label(self, tag):
149 """Add tag/branch labels from `git log --decorate ....`"""
151 if tag.startswith('tag: '):
152 tag = tag[5:] # tag: refs/
153 elif tag.startswith('refs/remotes/'):
154 tag = tag[13:] # refs/remotes/
155 elif tag.startswith('refs/heads/'):
156 tag = tag[11:] # refs/heads/
157 if tag.endswith('/HEAD'):
158 return
160 # Git 2.4 Release Notes (draft)
161 # =============================
163 # Backward compatibility warning(s)
164 # ---------------------------------
166 # This release has a few changes in the user-visible output from
167 # Porcelain commands. These are not meant to be parsed by scripts, but
168 # the users still may want to be aware of the changes:
170 # * Output from "git log --decorate" (and "%d" format specifier used in
171 # the userformat "--format=<string>" parameter "git log" family of
172 # command takes) used to list "HEAD" just like other tips of branch
173 # names, separated with a comma in between. E.g.
175 # $ git log --decorate -1 master
176 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD, master)
177 # ...
179 # This release updates the output slightly when HEAD refers to the tip
180 # of a branch whose name is also shown in the output. The above is
181 # shown as:
183 # $ git log --decorate -1 master
184 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD -> master)
185 # ...
187 # C.f. http://thread.gmane.org/gmane.linux.kernel/1931234
189 head_arrow = 'HEAD -> '
190 if tag.startswith(head_arrow):
191 self.tags.add('HEAD')
192 self.tags.add(tag[len(head_arrow):])
193 else:
194 self.tags.add(tag)
196 def __str__(self):
197 return self.sha1
199 def data(self):
200 return {
201 'sha1': self.sha1,
202 'summary': self.summary,
203 'author': self.author,
204 'authdate': self.authdate,
205 'parents': [p.sha1 for p in self.parents],
206 'tags': self.tags,
209 def __repr__(self):
210 return json.dumps(self.data(), sort_keys=True, indent=4)
212 def is_fork(self):
213 ''' Returns True if the node is a fork'''
214 return len(self.children) > 1
216 def is_merge(self):
217 ''' Returns True if the node is a fork'''
218 return len(self.parents) > 1
221 class RepoReader(object):
223 def __init__(self, ctx, git=git):
224 self.ctx = ctx
225 self.git = git
226 self._proc = None
227 self._objects = {}
228 self._cmd = ['git', 'log',
229 '--topo-order',
230 '--reverse',
231 '--pretty='+logfmt]
232 self._cached = False
233 """Indicates that all data has been read"""
234 self._idx = -1
235 """Index into the cached commits"""
236 self._topo_list = []
237 """List of commits objects in topological order"""
239 cached = property(lambda self: self._cached)
240 """Return True when no commits remain to be read"""
242 def __len__(self):
243 return len(self._topo_list)
245 def reset(self):
246 CommitFactory.reset()
247 if self._proc:
248 self._topo_list = []
249 self._proc.kill()
250 self._proc = None
251 self._cached = False
253 def __iter__(self):
254 if self._cached:
255 return self
256 self.reset()
257 return self
259 def next(self):
260 if self._cached:
261 try:
262 self._idx += 1
263 return self._topo_list[self._idx]
264 except IndexError:
265 self._idx = -1
266 raise StopIteration
268 if self._proc is None:
269 ref_args = utils.shell_split(self.ctx.ref)
270 cmd = self._cmd + ['-%d' % self.ctx.count] + ref_args
271 self._proc = core.start_command(cmd)
272 self._topo_list = []
274 log_entry = core.readline(self._proc.stdout).rstrip()
275 if not log_entry:
276 self._cached = True
277 self._proc.wait()
278 self.returncode = self._proc.returncode
279 self._proc = None
280 raise StopIteration
282 sha1 = log_entry[:40]
283 try:
284 return self._objects[sha1]
285 except KeyError:
286 c = CommitFactory.new(log_entry=log_entry)
287 self._objects[c.sha1] = c
288 self._topo_list.append(c)
289 return c
291 __next__ = next # for Python 3
293 def __getitem__(self, sha1):
294 return self._objects[sha1]
296 def items(self):
297 return self._objects.items()