core: add list2cmdline() wrapper
[git-cola.git] / cola / models / dag.py
blob955caf028d6959709a94f7c76b63a056d526e19b
1 from __future__ import division, absolute_import, unicode_literals
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 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__ = ('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, 5)
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 self.add_label(tag)
142 self.parsed = True
143 return self
145 def add_label(self, tag):
146 """Add tag/branch labels from `git log --decorate ....`"""
148 if tag.startswith('tag: '):
149 tag = tag[5:] # tag: refs/
150 elif tag.startswith('refs/remotes/'):
151 tag = tag[13:] # refs/remotes/
152 elif tag.startswith('refs/heads/'):
153 tag = tag[11:] # refs/heads/
154 if tag.endswith('/HEAD'):
155 return
157 # Git 2.4 Release Notes (draft)
158 # =============================
160 # Backward compatibility warning(s)
161 # ---------------------------------
163 # This release has a few changes in the user-visible output from
164 # Porcelain commands. These are not meant to be parsed by scripts, but
165 # the users still may want to be aware of the changes:
167 # * Output from "git log --decorate" (and "%d" format specifier used in
168 # the userformat "--format=<string>" parameter "git log" family of
169 # command takes) used to list "HEAD" just like other tips of branch
170 # names, separated with a comma in between. E.g.
172 # $ git log --decorate -1 master
173 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD, master)
174 # ...
176 # This release updates the output slightly when HEAD refers to the tip
177 # of a branch whose name is also shown in the output. The above is
178 # shown as:
180 # $ git log --decorate -1 master
181 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD -> master)
182 # ...
184 # C.f. http://thread.gmane.org/gmane.linux.kernel/1931234
186 head_arrow = 'HEAD -> '
187 if tag.startswith(head_arrow):
188 self.tags.add('HEAD')
189 self.tags.add(tag[len(head_arrow):])
190 else:
191 self.tags.add(tag)
193 def __str__(self):
194 return self.sha1
196 def __repr__(self):
197 return ("{\n"
198 " sha1: " + self.sha1 + "\n"
199 " summary: " + self.summary + "\n"
200 " author: " + self.author + "\n"
201 " authdate: " + self.authdate + "\n"
202 " parents: [" + ', '.join([p.sha1 for p in self.parents]) + "]\n"
203 " tags: [" + ', '.join(self.tags) + "]\n"
204 "}")
206 def is_fork(self):
207 ''' Returns True if the node is a fork'''
208 return len(self.children) > 1
210 def is_merge(self):
211 ''' Returns True if the node is a fork'''
212 return len(self.parents) > 1
215 class RepoReader(object):
217 def __init__(self, ctx, git=git):
218 self.ctx = ctx
219 self.git = git
220 self._proc = None
221 self._objects = {}
222 self._cmd = ['git', 'log',
223 '--topo-order',
224 '--reverse',
225 '--pretty='+logfmt]
226 self._cached = False
227 """Indicates that all data has been read"""
228 self._idx = -1
229 """Index into the cached commits"""
230 self._topo_list = []
231 """List of commits objects in topological order"""
233 cached = property(lambda self: self._cached)
234 """Return True when no commits remain to be read"""
237 def __len__(self):
238 return len(self._topo_list)
240 def reset(self):
241 CommitFactory.reset()
242 if self._proc:
243 self._topo_list = []
244 self._proc.kill()
245 self._proc = None
246 self._cached = False
248 def __iter__(self):
249 if self._cached:
250 return self
251 self.reset()
252 return self
254 def next(self):
255 if self._cached:
256 try:
257 self._idx += 1
258 return self._topo_list[self._idx]
259 except IndexError:
260 self._idx = -1
261 raise StopIteration
263 if self._proc is None:
264 ref_args = utils.shell_split(self.ctx.ref)
265 cmd = self._cmd + ['-%d' % self.ctx.count] + ref_args
266 self._proc = core.start_command(cmd)
267 self._topo_list = []
269 log_entry = core.readline(self._proc.stdout).rstrip()
270 if not log_entry:
271 self._cached = True
272 self._proc.wait()
273 self.returncode = self._proc.returncode
274 self._proc = None
275 raise StopIteration
277 sha1 = log_entry[:40]
278 try:
279 return self._objects[sha1]
280 except KeyError:
281 c = CommitFactory.new(log_entry=log_entry)
282 self._objects[c.sha1] = c
283 self._topo_list.append(c)
284 return c
286 __next__ = next # for Python 3
288 def __getitem__(self, sha1):
289 return self._objects[sha1]
291 def items(self):
292 return self._objects.items()