t8400: export tests for all three modes with remotes
[topgit/pro.git] / awk / topgit_navigate.awk
blob9c7a0a9babb3acf2430564ff1b3b5bc4579b8e14
1 #!/usr/bin/awk -f
3 # topgit_navigate - TopGit awk utility script used by tg--awksome
4 # Copyright (C) 2017 Kyle J. McKay <mackyle@gmail.com>
5 # All rights reserved.
6 # License GPLv2
8 # topgit_navigate
10 # variable arguments (-v):
12 # brfile if non-empty, read TopGit branch names from here
13 # rmbr if true run system rm on brfile (after reading) if non-empty brfile
14 # anfile if non-empty, annihilated branch names are read from here
15 # rman if true run system rm on anfile (after reading) if non-empty anfile
16 # withan if true, mostly pretend anfile was empty (this is a convenience knob)
17 # exclbr whitespace separated list of names to exclude
18 # inclbr whitespace separated list of names to include
19 # startb starting branch name(s); may be empty (see below)
20 # pruneb zero or more (space separated) nodes to limit results to (see below)
21 # steps how many steps to move, negative means as many as possible
22 # chklps always check for loops even when not necessary
23 # rev if true move in reverse order (towards start/first rather than end/last)
24 # pin if true and steps is > 0 and runs off the end keep last visited node
25 # tgonly if true only nodes listed in brfile are output
26 # fldone output only the first field on each line
28 # if inclbr is non-empty a branch name must be listed to appear on stdout
30 # if a branch name appears in exclbr it is omitted from stdout trumping inclbr
32 # if a branch is excluded (either by not being in a non-empty inclbr list or
33 # by being listed in the exclbr list) then it will not be recursed into either
35 # input is a list of edges as output by run_awk_topgit_deps
37 # using the startb starting point the graph is walked forward (backward if rev)
38 # exactly steps times unless steps is negative in which case it's walked to the
39 # end (and pin is always implicitly true in that case)
41 # "forward" moves toward later patches whereas "backwards" moves towards earlier
42 # ones where earlier patches must be applied before later ones
44 # if startb is not empty and the starting node is excluded by !withan and/or
45 # tgonly, then if steps == 0 it's not output otherwise the first step will be
46 # to a non-excluded (by !withan || tgonly) node. Otherwise a single "step" is
47 # always from an included node to an included node. This allows starting from
48 # an annihilated or non-tgish node and navigating to a non-annihilated tgish
49 # node, for example. And then possibly continuing to step further.
51 # if pruneb is non-empty after splitting a loop check will be forced and then
52 # items are taken as positive refs (unless they are prefixed with "^") until
53 # a sole "^" which flips state so unprefixed refs are takes as negative (unless
54 # they are prefixed with "^") and so on -- very much like git rev-list except
55 # that an isolated "^" takes the place of "--not". However, if there are no
56 # positive refs found then all nodes in the input start out as positive.
57 # Then any positive refs are walked including all reachable nodes then any
58 # negative refs are walked excluding those so negative refs always trump
59 # positive refs just like git rev-list does; anything left over and not
60 # included acts like it was never there in the first place -- can't be visited
61 # or traversed
63 # if startb contains more than one branch name, the requested operation is
64 # performed for each one and the results combined in that order, but there will
65 # be no way to distinguish where the boundary between results for the different
66 # branches lies in the output, but since sometimes that doesn't matter it's a
67 # helpful mode to have
69 # if steps is 0 and startb is empty it's an error otherwise startb just gets
70 # dumped right back out unless it's been excluded (and no loop checking is
71 # performed in that case either unless chklps is true) on one line together
72 # with the containing head(s); this provides "contains" functionality and is an
73 # obvious special case of the general navigation described below
75 # if startb is empty then one step forward (!rev) moves to all the roots or
76 # "starting" nodes whereas one step backwards (rev) moves to all the heads or
77 # "ending" nodes; so an empty startb with a negative steps (all the way) and a
78 # forward (!rev) direction starts at the roots and moves to the heads resulting
79 # in the same thing as one step in the backwards direction from the empty node
80 # and vice-versa for an empty startb with negative steps and backwards (rev)
81 # direction; the two cases single step cases (empty startb and steps == 1) are
82 # recognized and converted to the equivalent -1 steps version automatically as
83 # both of# the -1 steps versions are optimized and do NOT walk the graph nor
84 # cause loop checking (unless check chklps is true) and they also only output
85 # one field on each output line (the head or root) rather than two
87 # with all but the two special cases just mentioned, loop detection is
88 # performed first to avoid problems and will cause an exit status of EX_DATAERR
89 # (65) if loops are detected in which case no output is produced; the
90 # recommended way to check for loops is with an empty startb, steps=-1 and
91 # chklps=1 and redirecting output to /dev/null (or capturing the heads output
92 # on success) and then testing the exit status for an EX_DATAERR (65) result
94 # the effect of "navigation" is as though the heads containing startb are first
95 # determined (by walking "forward" as far as possible) then those heads are
96 # enumerated in postfix order (each node is only visited once though -- the
97 # first time it's encountered in the enumeration) forming one or more linear
98 # lists (this step is not possible if loops are present hence the loop
99 # detection check). Then the requested number of steps are taken from the
100 # location startb appears in each list and the possibly none, possibly many
101 # results are output one per line like so:
103 # <result_branch_name> <containing_topgit_head_branch_name>...
105 # there will always be at least one containing branch name even if it's the
106 # same as the result branch name (unless startb is empty and steps is negative
107 # or 1), but there could be more (space separated) if the branch is part of
108 # more than one patch series; if fldone is true the only the first field shown
109 # aove (the result branch name) will be output on each line no matter what
112 BEGIN { exitcode = ""; stderr = "exec cat>&2"; }
113 function exitnow(e) { exitcode=e; exit e }
114 END { if (exitcode != "") exit exitcode }
116 BEGIN {
117 if (steps !~ /^-?[0-9]+$/) exitnow(2)
118 steps = 0 + steps
119 startcnt = 0
120 cnt = split(startb, ascratch, " ")
121 for (i = 1; i <= cnt; ++i) {
122 if (!(ascratch[i] in seen)) {
123 starts[++startcnt] = ascratch[i]
124 seen[ascratch[i]] = 1
127 if (!steps && !startcnt) exitnow(2)
128 if (!startcnt && steps == 1) {
129 rev = !rev
130 steps = -1
132 if (steps < 0) steps = -1
133 inconly = 0
134 cnt = split(inclbr, scratch, " ")
135 if (cnt) {
136 inconly = 1
137 for (i = 1; i <= cnt; ++i) incnames[scratch[i]] = 1
139 cnt = split(exclbr, scratch, " ")
140 for (i = 1; i <= cnt; ++i) excnames[scratch[i]] = 1
141 prunecnt = split(pruneb, prunes, " ")
142 nots = 0
143 for (node in prunes) if (node == "^") ++nots
144 if (nots >= prunecnt) prunecnt = 0
145 ordidx = 0
148 function quotevar(v) {
149 gsub(/\047/, "\047\\\047\047", v)
150 return "\047" v "\047"
153 function init(abranch, _e) {
154 rmlist = ""
155 if (brfile != "") {
156 if (tgonly) {
157 while ((_e = (getline abranch <brfile)) > 0) {
158 if (abranch != "") tgish[abranch] = 1
160 close(brfile)
161 if (_e < 0) exitnow(2)
163 if (rmbr) rmlist = rmlist " " quotevar(brfile)
165 if (anfile != "") {
166 if (!withan) {
167 while ((_e = (getline abranch <anfile)) > 0) {
168 if (abranch != "") ann[abranch] = 1
170 close(anfile)
171 if (_e < 0) exitnow(2)
173 if (rman) rmlist = rmlist " " quotevar(anfile)
175 if (rmlist != "") system("rm -f" rmlist)
178 function included(abranch) {
179 return (!inconly || incnames[abranch]) && !excnames[abranch]
182 function wanted(abranch) {
183 return !(abranch in ann) && (!tgonly || (abranch in tgish))
186 NR == 1 {init()}
188 function addlink(anarray, anode, alink, _linkstr) {
189 if (alink == "") return
190 _linkstr = anarray[anode]
191 if (length(_linkstr) < 3) {
192 _linkstr = " " alink " "
193 } else if (!index(_linkstr, " " alink " ")) {
194 _linkstr = _linkstr alink " "
196 anarray[anode] = _linkstr
199 NF == 2 && $1 != "" && $2 != "" {
200 isexcl = 2
201 if (included($2)) {
202 if (!($2 in nodes)) {
203 nodes[$2] = 1
204 ordered[++ordidx] = $2
206 --isexcl
208 if (included($1)) {
209 if (!($1 in nodes)) {
210 nodes[$1] = 1
211 ordered[++ordidx] = $1
213 --isexcl
215 if (!isexcl && $1 != $2) {
216 addlink(incoming, $2, $1)
217 addlink(outgoing, $1, $2)
218 edgenodes[$1] = 1
219 edgenodes[$2] = 1
223 function checkloops() {
224 for (edge in incoming) curinc[edge] = incoming[edge]
225 for (node in edgenodes) if (!(node in curinc)) curnodes[node] = 1
226 for (;;) {
227 node = ""
228 for (node in curnodes) break
229 if (node == "") break
230 delete curnodes[node]
231 if ((node in outgoing) && split(outgoing[node], links, " ")) {
232 for (linki in links) {
233 link = links[linki]
234 if (link in curinc) {
235 inclist = curinc[link]
236 if ((idx = index(inclist, " " node " "))) {
237 inclist = substr(inclist, 1, idx) \
238 substr(inclist,
239 idx + length(node) + 2)
240 if (length(inclist) < 3) {
241 delete curinc[link]
242 curnodes[link] = 1
243 } else {
244 curinc[link] = inclist
250 delete edgenodes[node]
252 for (node in edgenodes) exitnow(65)
255 function collectstarts(anarray, _i) {
256 for (_i = 1; _i <= ordidx; ++_i) {
257 if ((ordered[_i] in nodes) && !(ordered[_i] in anarray))
258 starts[++startcnt] = ordered[_i]
262 function marknodes(anode, val, _outlinks, _oneout) {
263 if (!(anode in nodes)) return
264 if (nodes[anode] == val) return
265 nodes[anode] = val
266 if (anode in outgoing) {
267 split(outgoing[anode], _outlinks, " ")
268 for (_oneout in _outlinks) marknodes(_outlinks[_oneout], val)
272 function getheads_(anode, theheads, seen, headcnt, _cnt, _i, _inlinks) {
273 if (!(anode in nodes)) return
274 if (!(anode in incoming)) {
275 if (!(anode in seen)) {
276 seen[anode] = 1
277 theheads[++headcnt[0]] = anode
279 return
281 _cnt = split(incoming[anode], _inlinks, " ")
282 for (_i = 1; _i <= _cnt; ++_i)
283 getheads_(_inlinks[_i], theheads, seen, headcnt)
286 function getheads(anode, theheads, _seen, _headcnt) {
287 split("", theheads, " ")
288 split("", _seen, " ")
289 _headcnt[0] = 0
290 getheads_(anode, theheads, _seen, _headcnt)
291 return _headcnt[0]
294 function getpath_(anode, pnodes, arlinks, _seen, _pcnt, _children, _ccnt, _i) {
295 if (anode in _seen) return
296 _seen[anode] = 1
297 if (anode in arlinks) {
298 _ccnt = split(arlinks[anode], _children, " ")
299 for (_i = 1; _i <= _ccnt; ++_i)
300 getpath_(_children[_i], pnodes, arlinks, _seen, _pcnt)
302 pnodes[++_pcnt[0]] = anode
305 function getpath(anode, pnodes, arlinks, _seen, _pcnt, _z) {
306 split("", pnodes, " ");
307 split("", _seen, " ")
308 _pcnt[0] = 0
309 getpath_(anode, pnodes, arlinks, _seen, _pcnt)
310 return _pcnt[0]
311 printf "%s", "PATH " anode " |"
312 for (_z = 1; _z <= _pcnt[0]; ++_z) printf " %s" pnodes[_z]
313 printf "\n"
316 END {
317 if (chklps || startcnt || prunecnt || steps >= 0) checkloops()
318 if (prunecnt) {
319 state = 1
320 for (i = 1; i <= prunecnt; ++ i) {
321 onep = prunes[i]
322 if (onep == "^") {
323 state = 1 - state
324 continue
326 if (substr(onep, 1, 1) == "^") {
327 if (state) negnodes[substr(onep, 2)] = 1
328 else poznodes[substr(onep, 2)] = 1
329 } else {
330 if (state) poznodes[onep] = 1
331 else negnodes[onep] = 1
334 for (onep in poznodes) {
335 for (anode in nodes) nodes[anode] = ""
336 break
338 for (onep in poznodes) marknodes(onep, 1)
339 for (onep in negnodes) {
340 marknodes(onep, 0)
341 if (onep in incoming && split(incoming[onep], links, " ")) {
342 for (linki in links) {
343 link = links[linki]
344 if (link in outgoing) {
345 outlist = outgoing[link]
346 if ((idx = index(outlist, " " onep " "))) {
347 outlist = substr(outlist, 1, idx) \
348 substr(outlist,
349 idx + length(onep) + 2)
350 if (length(outlist) < 3)
351 delete outgoing[link]
352 else
353 outgoing[link] = outlist
359 for (onep in nodes) if (!nodes[onep]) delete nodes[onep]
361 if (!startcnt) {
362 if ((steps < 0 && !rev) || (steps > 0 && rev))
363 collectstarts(incoming)
364 else if ((steps < 0 && rev) || (steps > 0 && !rev))
365 collectstarts(outgoing)
366 if (steps < 0 || !startcnt) {
367 for (i = 1; i <= startcnt; ++i)
368 if (wanted(starts[i])) print starts[i]
369 exit 0
372 resultcnt = 0
373 for (i = 1; i <= startcnt; ++i) {
374 headcnt = getheads(starts[i], heads)
375 for (j = 1; j <= headcnt; ++j) {
376 pathcnt = getpath(heads[j], path, outgoing)
377 for (pathidx = 1; pathidx <= pathcnt; ++pathidx)
378 if (path[pathidx] == starts[i]) break
379 if (pathidx > pathcnt) continue
380 adjsteps = steps
381 if (!wanted(path[pathidx])) {
382 if (!steps) continue
383 if (steps > 0) --adjsteps
384 incr = rev ? -1 : 1
385 do pathidx += incr
386 while (pathidx >= 1 && pathidx <= pathcnt &&
387 !wanted(path[pathidx]))
389 if (pathidx >= 1 && pathidx <= pathcnt) {
390 oldcnt = pathcnt
391 newstart = path[pathidx]
392 pathidx = 0
393 pathcnt = 0
394 for (k = 1; k <= oldcnt; ++k) {
395 if (wanted(path[k])) {
396 path[++pathcnt] = path[k]
397 if (!pathidx && path[pathcnt] == newstart)
398 pathidx = pathcnt
401 if (!pathidx) {
402 print "internal error: wanted disappeared" |stderr
403 exitnow(70) # EX_SOFTWARE
406 if (steps < 0) {
407 pathidx = rev ? 1 : pathcnt
408 } else {
409 if (rev) pathidx -= adjsteps
410 else pathidx += adjsteps
412 if (pin) {
413 if (pathidx < 1) pathidx = 1
414 else if (pathidx > pathcnt) pathidx = pathcnt
416 if (pathidx < 1 || pathidx > pathcnt) continue
417 aresult = path[pathidx]
418 if (aresult in seenresults) {
419 resultidx = seenresults[aresult]
420 resultlist = results[resultidx]
421 } else {
422 resultidx = ++resultcnt
423 resultnames[resultidx] = aresult
424 seenresults[aresult] = resultidx
425 resultlist = " "
427 if (!index(resultlist, " " heads[j] " ")) {
428 resultlist = resultlist heads[j] " "
429 results[resultidx] = resultlist
433 for (i = 1; i <= resultcnt; ++i) {
434 if (fldone) print resultnames[i]
435 else print resultnames[i] substr(results[i], 1, length(results[i]) - 1)