3 # topgit_navigate - TopGit awk utility script used by tg--awksome
4 # Copyright (C) 2017 Kyle J. McKay <mackyle@gmail.com>
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
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
}
117 if (steps !~
/^
-?
[0-9]+$
/) exitnow
(2)
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) {
132 if (steps
< 0) steps =
-1
134 cnt =
split(inclbr
, scratch
, " ")
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
, " ")
143 for (node in prunes
) if (node ==
"^") ++nots
144 if (nots
>= prunecnt
) prunecnt =
0
148 function quotevar
(v
) {
149 gsub(/\047/, "\047\\\047\047", v
)
150 return "\047" v
"\047"
153 function init
(abranch
, _e
) {
157 while ((_e =
(getline abranch
<brfile
)) > 0) {
158 if (abranch
!= "") tgish
[abranch
] =
1
161 if (_e
< 0) exitnow
(2)
163 if (rmbr
) rmlist = rmlist
" " quotevar
(brfile
)
167 while ((_e =
(getline abranch
<anfile
)) > 0) {
168 if (abranch
!= "") ann
[abranch
] =
1
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
))
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 != "" {
202 if (!
($
2 in nodes
)) {
204 ordered
[++ordidx
] = $
2
209 if (!
($
1 in nodes
)) {
211 ordered
[++ordidx
] = $
1
215 if (!isexcl
&& $
1 != $
2) {
216 addlink
(incoming
, $
2, $
1)
217 addlink
(outgoing
, $
1, $
2)
223 function checkloops
() {
224 for (edge in incoming
) curinc
[edge
] = incoming
[edge
]
225 for (node in edgenodes
) if (!
(node in curinc
)) curnodes
[node
] =
1
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
) {
234 if (link in curinc
) {
235 inclist = curinc
[link
]
236 if ((idx =
index(inclist
, " " node
" "))) {
237 inclist =
substr(inclist
, 1, idx
) \
239 idx
+ length(node
) + 2)
240 if (length(inclist
) < 3) {
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
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
)) {
277 theheads
[++headcnt
[0]] = anode
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
, " ")
290 getheads_
(anode
, theheads
, _seen
, _headcnt
)
294 function getpath_
(anode
, pnodes
, arlinks
, _seen
, _pcnt
, _children
, _ccnt
, _i
) {
295 if (anode in _seen
) return
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
, " ")
309 getpath_
(anode
, pnodes
, arlinks
, _seen
, _pcnt
)
311 printf "%s", "PATH " anode
" |"
312 for (_z =
1; _z
<= _pcnt
[0]; ++_z
) printf " %s" pnodes
[_z
]
317 if (chklps
|| startcnt
|| prunecnt
|| steps
>=
0) checkloops
()
320 for (i =
1; i
<= prunecnt
; ++ i
) {
326 if (substr(onep
, 1, 1) ==
"^") {
327 if (state
) negnodes
[substr(onep
, 2)] =
1
328 else poznodes
[substr(onep
, 2)] =
1
330 if (state
) poznodes
[onep
] =
1
331 else negnodes
[onep
] =
1
334 for (onep in poznodes
) {
335 for (anode in nodes
) nodes
[anode
] =
""
338 for (onep in poznodes
) marknodes
(onep
, 1)
339 for (onep in negnodes
) {
341 if (onep in incoming
&& split(incoming
[onep
], links
, " ")) {
342 for (linki in links
) {
344 if (link in outgoing
) {
345 outlist = outgoing
[link
]
346 if ((idx =
index(outlist
, " " onep
" "))) {
347 outlist =
substr(outlist
, 1, idx
) \
349 idx
+ length(onep
) + 2)
350 if (length(outlist
) < 3)
351 delete outgoing
[link
]
353 outgoing
[link
] = outlist
359 for (onep in nodes
) if (!nodes
[onep
]) delete nodes
[onep
]
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
]
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
381 if (!wanted
(path
[pathidx
])) {
383 if (steps
> 0) --adjsteps
386 while (pathidx
>=
1 && pathidx
<= pathcnt
&&
387 !wanted
(path
[pathidx
]))
389 if (pathidx
>=
1 && pathidx
<= pathcnt
) {
391 newstart = path
[pathidx
]
394 for (k =
1; k
<= oldcnt
; ++k
) {
395 if (wanted
(path
[k
])) {
396 path
[++pathcnt
] = path
[k
]
397 if (!pathidx
&& path
[pathcnt
] == newstart
)
402 print "internal error: wanted disappeared" |stderr
403 exitnow
(70) # EX_SOFTWARE
407 pathidx = rev ?
1 : pathcnt
409 if (rev
) pathidx
-= adjsteps
410 else pathidx
+= adjsteps
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
]
422 resultidx =
++resultcnt
423 resultnames
[resultidx
] = aresult
424 seenresults
[aresult
] = resultidx
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)