4344
|
1 # ASCII graph log extension for Mercurial
|
|
2 #
|
|
3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
|
|
4 #
|
|
5 # This software may be used and distributed according to the terms of
|
|
6 # the GNU General Public License, incorporated herein by reference.
|
|
7
|
|
8 import sys
|
|
9 from mercurial.cmdutil import revrange, show_changeset
|
|
10 from mercurial.i18n import _
|
|
11 from mercurial.node import nullid, nullrev
|
|
12 from mercurial.util import Abort
|
|
13
|
|
14 def revision_grapher(repo, start_rev, stop_rev):
|
|
15 """incremental revision grapher
|
|
16
|
|
17 This generator function walks through the revision history from
|
|
18 revision start_rev to revision stop_rev (which must be less than
|
|
19 or equal to start_rev) and for each revision emits tuples with the
|
|
20 following elements:
|
|
21
|
|
22 - Current revision.
|
|
23 - Current node.
|
|
24 - Column of the current node in the set of ongoing edges.
|
|
25 - Edges; a list of (col, next_col) indicating the edges between
|
|
26 the current node and its parents.
|
|
27 - Number of columns (ongoing edges) in the current revision.
|
|
28 - The difference between the number of columns (ongoing edges)
|
|
29 in the next revision and the number of columns (ongoing edges)
|
|
30 in the current revision. That is: -1 means one column removed;
|
|
31 0 means no columns added or removed; 1 means one column added.
|
|
32 """
|
|
33
|
|
34 assert start_rev >= stop_rev
|
|
35 curr_rev = start_rev
|
|
36 revs = []
|
|
37 while curr_rev >= stop_rev:
|
|
38 node = repo.changelog.node(curr_rev)
|
|
39
|
|
40 # Compute revs and next_revs.
|
|
41 if curr_rev not in revs:
|
|
42 # New head.
|
|
43 revs.append(curr_rev)
|
|
44 rev_index = revs.index(curr_rev)
|
|
45 next_revs = revs[:]
|
|
46
|
|
47 # Add parents to next_revs.
|
|
48 parents = get_rev_parents(repo, curr_rev)
|
|
49 parents_to_add = []
|
|
50 for parent in parents:
|
|
51 if parent not in next_revs:
|
|
52 parents_to_add.append(parent)
|
|
53 parents_to_add.sort()
|
|
54 next_revs[rev_index:rev_index + 1] = parents_to_add
|
|
55
|
|
56 edges = []
|
|
57 for parent in parents:
|
|
58 edges.append((rev_index, next_revs.index(parent)))
|
|
59
|
|
60 n_columns_diff = len(next_revs) - len(revs)
|
|
61 yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
|
|
62
|
|
63 revs = next_revs
|
|
64 curr_rev -= 1
|
|
65
|
|
66 def get_rev_parents(repo, rev):
|
|
67 return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
|
|
68
|
|
69 def fix_long_right_edges(edges):
|
|
70 for (i, (start, end)) in enumerate(edges):
|
|
71 if end > start:
|
|
72 edges[i] = (start, end + 1)
|
|
73
|
|
74 def draw_edges(edges, nodeline, interline):
|
|
75 for (start, end) in edges:
|
|
76 if start == end + 1:
|
|
77 interline[2 * end + 1] = "/"
|
|
78 elif start == end - 1:
|
|
79 interline[2 * start + 1] = "\\"
|
|
80 elif start == end:
|
|
81 interline[2 * start] = "|"
|
|
82 else:
|
|
83 nodeline[2 * end] = "+"
|
|
84 if start > end:
|
|
85 (start, end) = (end,start)
|
|
86 for i in range(2 * start + 1, 2 * end):
|
|
87 if nodeline[i] != "+":
|
|
88 nodeline[i] = "-"
|
|
89
|
|
90 def format_line(line, level, logstr):
|
|
91 text = "%-*s %s" % (2 * level, "".join(line), logstr)
|
|
92 return "%s\n" % text.rstrip()
|
|
93
|
|
94 def get_nodeline_edges_tail(
|
|
95 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
|
|
96 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
|
|
97 # Still going in the same non-vertical direction.
|
|
98 if n_columns_diff == -1:
|
|
99 start = max(node_index + 1, p_node_index)
|
|
100 tail = ["|", " "] * (start - node_index - 1)
|
|
101 tail.extend(["/", " "] * (n_columns - start))
|
|
102 return tail
|
|
103 else:
|
|
104 return ["\\", " "] * (n_columns - node_index - 1)
|
|
105 else:
|
|
106 return ["|", " "] * (n_columns - node_index - 1)
|
|
107
|
|
108 def get_padding_line(ni, n_columns, edges):
|
|
109 line = []
|
|
110 line.extend(["|", " "] * ni)
|
|
111 if (ni, ni - 1) in edges or (ni, ni) in edges:
|
|
112 # (ni, ni - 1) (ni, ni)
|
|
113 # | | | | | | | |
|
|
114 # +---o | | o---+
|
|
115 # | | c | | c | |
|
|
116 # | |/ / | |/ /
|
|
117 # | | | | | |
|
|
118 c = "|"
|
|
119 else:
|
|
120 c = " "
|
|
121 line.extend([c, " "])
|
|
122 line.extend(["|", " "] * (n_columns - ni - 1))
|
|
123 return line
|
|
124
|
|
125 def get_limit(limit_opt):
|
|
126 if limit_opt:
|
|
127 try:
|
|
128 limit = int(limit_opt)
|
|
129 except ValueError:
|
|
130 raise Abort(_("limit must be a positive integer"))
|
|
131 if limit <= 0:
|
|
132 raise Abort(_("limit must be positive"))
|
|
133 else:
|
|
134 limit = sys.maxint
|
|
135 return limit
|
|
136
|
|
137 def get_revs(repo, rev_opt):
|
|
138 if rev_opt:
|
|
139 revs = revrange(repo, rev_opt)
|
|
140 return (max(revs), min(revs))
|
|
141 else:
|
|
142 return (repo.changelog.count() - 1, 0)
|
|
143
|
|
144 def graphlog(ui, repo, *args, **opts):
|
|
145 """show revision history alongside an ASCII revision graph
|
|
146
|
|
147 Print a revision history alongside a revision graph drawn with
|
|
148 ASCII characters.
|
|
149
|
|
150 Nodes printed as an @ character are parents of the working
|
|
151 directory.
|
|
152 """
|
|
153
|
|
154 limit = get_limit(opts["limit"])
|
|
155 (start_rev, stop_rev) = get_revs(repo, opts["rev"])
|
|
156 stop_rev = max(stop_rev, start_rev - limit + 1)
|
|
157 if start_rev == nullrev:
|
|
158 return
|
|
159 cs_printer = show_changeset(ui, repo, opts)
|
|
160 grapher = revision_grapher(repo, start_rev, stop_rev)
|
|
161 repo_parents = repo.dirstate.parents()
|
|
162 prev_n_columns_diff = 0
|
|
163 prev_node_index = 0
|
|
164
|
|
165 for (rev, node, node_index, edges, n_columns, n_columns_diff) in grapher:
|
|
166 # log_strings is the list of all log strings to draw alongside
|
|
167 # the graph.
|
|
168 ui.pushbuffer()
|
|
169 cs_printer.show(rev, node)
|
|
170 log_strings = ui.popbuffer().split("\n")[:-1]
|
|
171
|
|
172 if n_columns_diff == -1:
|
|
173 # Transform
|
|
174 #
|
|
175 # | | | | | |
|
|
176 # o | | into o---+
|
|
177 # |X / |/ /
|
|
178 # | | | |
|
|
179 fix_long_right_edges(edges)
|
|
180
|
|
181 # add_padding_line says whether to rewrite
|
|
182 #
|
|
183 # | | | | | | | |
|
|
184 # | o---+ into | o---+
|
|
185 # | / / | | | # <--- padding line
|
|
186 # o | | | / /
|
|
187 # o | |
|
|
188 add_padding_line = \
|
|
189 len(log_strings) > 2 and \
|
|
190 n_columns_diff == -1 and \
|
|
191 [x for (x, y) in edges if x + 1 < y]
|
|
192
|
|
193 # fix_nodeline_tail says whether to rewrite
|
|
194 #
|
|
195 # | | o | | | | o | |
|
|
196 # | | |/ / | | |/ /
|
|
197 # | o | | into | o / / # <--- fixed nodeline tail
|
|
198 # | |/ / | |/ /
|
|
199 # o | | o | |
|
|
200 fix_nodeline_tail = len(log_strings) <= 2 and not add_padding_line
|
|
201
|
|
202 # nodeline is the line containing the node character (@ or o).
|
|
203 nodeline = ["|", " "] * node_index
|
|
204 if node in repo_parents:
|
|
205 node_ch = "@"
|
|
206 else:
|
|
207 node_ch = "o"
|
|
208 nodeline.extend([node_ch, " "])
|
|
209
|
|
210 nodeline.extend(
|
|
211 get_nodeline_edges_tail(
|
|
212 node_index, prev_node_index, n_columns, n_columns_diff,
|
|
213 prev_n_columns_diff, fix_nodeline_tail))
|
|
214
|
|
215 # shift_interline is the line containing the non-vertical
|
|
216 # edges between this entry and the next.
|
|
217 shift_interline = ["|", " "] * node_index
|
|
218 if n_columns_diff == -1:
|
|
219 n_spaces = 1
|
|
220 edge_ch = "/"
|
|
221 elif n_columns_diff == 0:
|
|
222 n_spaces = 2
|
|
223 edge_ch = "|"
|
|
224 else:
|
|
225 n_spaces = 3
|
|
226 edge_ch = "\\"
|
|
227 shift_interline.extend(n_spaces * [" "])
|
|
228 shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
|
|
229
|
|
230 # Draw edges from the current node to its parents.
|
|
231 draw_edges(edges, nodeline, shift_interline)
|
|
232
|
|
233 # lines is the list of all graph lines to print.
|
|
234 lines = [nodeline]
|
|
235 if add_padding_line:
|
|
236 lines.append(get_padding_line(node_index, n_columns, edges))
|
|
237 lines.append(shift_interline)
|
|
238
|
|
239 # Make sure that there are as many graph lines as there are
|
|
240 # log strings.
|
|
241 while len(log_strings) < len(lines):
|
|
242 log_strings.append("")
|
|
243 if len(lines) < len(log_strings):
|
|
244 extra_interline = ["|", " "] * (n_columns + n_columns_diff)
|
|
245 while len(lines) < len(log_strings):
|
|
246 lines.append(extra_interline)
|
|
247
|
|
248 # Print lines.
|
|
249 indentation_level = max(n_columns, n_columns + n_columns_diff)
|
|
250 for (line, logstr) in zip(lines, log_strings):
|
|
251 ui.write(format_line(line, indentation_level, logstr))
|
|
252
|
|
253 # ...and start over.
|
|
254 prev_node_index = node_index
|
|
255 prev_n_columns_diff = n_columns_diff
|
|
256
|
|
257 cmdtable = {
|
|
258 "glog":
|
|
259 (graphlog,
|
|
260 [("l", "limit", "", _("limit number of changes displayed")),
|
|
261 ("p", "patch", False, _("show patch")),
|
|
262 ("r", "rev", [], _("show the specified revision or range")),
|
|
263 ("", "style", "", _("display using template map file")),
|
|
264 ("", "template", "", _("display with template"))],
|
|
265 "hg glog [OPTIONS]"),
|
|
266 }
|