dag: display HEAD and local branches before remote branches
[git-cola.git] / cola / models / dag.py
blobf2339642a959db36b8ac49fdb13e6e087453c81c
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 import json
4 from .. import core
5 from .. import utils
7 # put summary at the end b/c it can contain
8 # any number of funky characters, including the separator
9 logfmt = r'format:%H%x01%P%x01%d%x01%an%x01%ad%x01%ae%x01%s'
10 logsep = chr(0x01)
13 class CommitFactory(object):
14 root_generation = 0
15 commits = {}
17 @classmethod
18 def reset(cls):
19 cls.commits.clear()
20 cls.root_generation = 0
22 @classmethod
23 def new(cls, oid=None, log_entry=None):
24 if not oid and log_entry:
25 oid = log_entry[:40]
26 try:
27 commit = cls.commits[oid]
28 if log_entry and not commit.parsed:
29 commit.parse(log_entry)
30 cls.root_generation = max(commit.generation, cls.root_generation)
31 except KeyError:
32 commit = Commit(oid=oid, log_entry=log_entry)
33 if not log_entry:
34 cls.root_generation += 1
35 commit.generation = max(commit.generation, cls.root_generation)
36 cls.commits[oid] = commit
37 return commit
40 class DAG(object):
41 def __init__(self, ref, count):
42 self.ref = ref
43 self.count = count
44 self.overrides = {}
46 def set_ref(self, ref):
47 changed = ref != self.ref
48 if changed:
49 self.ref = ref
50 return changed
52 def set_count(self, count):
53 changed = count != self.count
54 if changed:
55 self.count = count
56 return changed
58 def set_arguments(self, args):
59 if args is None:
60 return
61 if self.set_count(args.count):
62 self.overrides['count'] = args.count
64 if hasattr(args, 'args') and args.args:
65 ref = core.list2cmdline(args.args)
66 if self.set_ref(ref):
67 self.overrides['ref'] = ref
69 def overridden(self, opt):
70 return opt in self.overrides
72 def paths(self):
73 all_refs = utils.shell_split(self.ref)
74 if '--' in all_refs:
75 all_refs = all_refs[all_refs.index('--') :]
77 return [p for p in all_refs if p and core.exists(p)]
80 class Commit(object):
81 root_generation = 0
83 __slots__ = (
84 'oid',
85 'summary',
86 'parents',
87 'children',
88 'branches',
89 'tags',
90 'author',
91 'authdate',
92 'email',
93 'generation',
94 'column',
95 'row',
96 'parsed',
99 def __init__(self, oid=None, log_entry=None):
100 self.oid = oid
101 self.summary = None
102 self.parents = []
103 self.children = []
104 self.tags = []
105 self.branches = []
106 self.email = None
107 self.author = None
108 self.authdate = None
109 self.parsed = False
110 self.generation = CommitFactory.root_generation
111 self.column = None
112 self.row = None
113 if log_entry:
114 self.parse(log_entry)
116 def parse(self, log_entry, sep=logsep):
117 self.oid = log_entry[:40]
118 after_oid = log_entry[41:]
119 details = after_oid.split(sep, 5)
120 (parents, tags, author, authdate, email, summary) = details
122 self.summary = summary if summary else ''
123 self.author = author if author else ''
124 self.authdate = authdate if authdate else ''
125 self.email = email if email else ''
127 if parents:
128 generation = None
129 for parent_oid in parents.split(' '):
130 parent = CommitFactory.new(oid=parent_oid)
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:] # strip off "tag: " leaving refs/tags/
151 if tag.startswith('refs/heads/'):
152 branch = tag[11:]
153 self.branches.append(branch)
155 if tag.startswith('refs/'):
156 # strip off refs/ leaving just tags/XXX remotes/XXX heads/XXX
157 tag = tag[5:]
159 if tag.endswith('/HEAD'):
160 return
162 # Git 2.4 Release Notes (draft)
163 # =============================
165 # Backward compatibility warning(s)
166 # ---------------------------------
168 # This release has a few changes in the user-visible output from
169 # Porcelain commands. These are not meant to be parsed by scripts, but
170 # the users still may want to be aware of the changes:
172 # * Output from "git log --decorate" (and "%d" format specifier used in
173 # the userformat "--format=<string>" parameter "git log" family of
174 # command takes) used to list "HEAD" just like other tips of branch
175 # names, separated with a comma in between. E.g.
177 # $ git log --decorate -1 main
178 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD, main)
179 # ...
181 # This release updates the output slightly when HEAD refers to the tip
182 # of a branch whose name is also shown in the output. The above is
183 # shown as:
185 # $ git log --decorate -1 main
186 # commit bdb0f6788fa5e3cacc4315e9ff318a27b2676ff4 (HEAD -> main)
187 # ...
189 # C.f. http://thread.gmane.org/gmane.linux.kernel/1931234
191 head_arrow = 'HEAD -> '
192 if tag.startswith(head_arrow):
193 self.tags.append('HEAD')
194 self.add_label(tag[len(head_arrow) :])
195 else:
196 self.tags.append(tag)
198 def __str__(self):
199 return self.oid
201 def data(self):
202 return {
203 'oid': self.oid,
204 'summary': self.summary,
205 'author': self.author,
206 'authdate': self.authdate,
207 'parents': [p.oid for p in self.parents],
208 'tags': self.tags,
211 def __repr__(self):
212 return json.dumps(self.data(), sort_keys=True, indent=4, default=list)
214 def is_fork(self):
215 '''Returns True if the node is a fork'''
216 return len(self.children) > 1
218 def is_merge(self):
219 '''Returns True if the node is a fork'''
220 return len(self.parents) > 1
223 class RepoReader(object):
224 def __init__(self, context, params):
225 self.context = context
226 self.params = params
227 self.git = context.git
228 self.returncode = 0
229 self._proc = None
230 self._objects = {}
231 self._cmd = [
232 'git',
233 '-c',
234 'log.abbrevCommit=false',
235 '-c',
236 'log.showSignature=false',
237 'log',
238 '--topo-order',
239 '--reverse',
240 '--decorate=full',
241 '--pretty=' + logfmt,
243 self._cached = False
244 """Indicates that all data has been read"""
245 self._topo_list = []
246 """List of commits objects in topological order"""
248 cached = property(lambda self: self._cached)
249 """Return True when no commits remain to be read"""
251 def __len__(self):
252 return len(self._topo_list)
254 def reset(self):
255 CommitFactory.reset()
256 if self._proc:
257 self._proc.kill()
258 self._proc = None
259 self._cached = False
260 self._topo_list = []
262 def get(self):
263 """Generator function returns Commit objects found by the params"""
264 if self._cached:
265 idx = 0
266 while True:
267 try:
268 yield self._topo_list[idx]
269 except IndexError:
270 break
271 idx += 1
272 return
274 self.reset()
275 ref_args = utils.shell_split(self.params.ref)
276 cmd = self._cmd + ['-%d' % self.params.count] + ref_args
277 self._proc = core.start_command(cmd)
279 while True:
280 log_entry = core.readline(self._proc.stdout).rstrip()
281 if not log_entry:
282 self._cached = True
283 self._proc.wait()
284 self.returncode = self._proc.returncode
285 self._proc = None
286 break
287 oid = log_entry[:40]
288 try:
289 yield self._objects[oid]
290 except KeyError:
291 commit = CommitFactory.new(log_entry=log_entry)
292 self._objects[commit.oid] = commit
293 self._topo_list.append(commit)
294 yield commit
295 return
297 def __getitem__(self, oid):
298 return self._objects[oid]
300 def items(self):
301 return list(self._objects.items())