tg-info.sh: support --deps and --dependents options
[topgit/pro.git] / awk / topgit_recurse.awk
blobb1fc3aa93f25e70a474a922d1f2607aa6f2fb5f7
1 #!/usr/bin/awk -f
3 # topgit_recurse - 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_recurse
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 # hdfile if non-empty, existing head names are read from here
17 # rmhd if true run system rm on hdfile (after reading) if non-empty hdfile
18 # cuthd if true extract and cut cuthd field of each "refs/heads/" line
19 # rtfile if non-empty, read non-annihilated remote tg branch names from here
20 # rmrt if true run system rm on rtfile (after reading) if non-empty rtfile
21 # usermt if non-empty output remote base lines using this prefix if in rtfile
22 # withan if true, include L == 2 output lines
23 # withbr if true, output a line for the top-level branch
24 # preord if true, output the branch line before its .topdeps instead of after
25 # exclbr whitespace separated list of names to exclude
26 # inclbr whitespace separated list of names to include
27 # startb whitespace separated list of start branch plus extra path items
28 # multib if "1" startb is list of multiple start nodes (with no extra path)
29 # filter if 1 or 2 output dependency instead of recurse lines (see below)
30 # once if > 0 nodes when 1st visited only if < 0 deps on 1st visit only
31 # leaves if true omit output lines where L != 1 (withbr recommended if set)
32 # tgonly if true only T != 0 (or M == 1) lines are output
33 # showlp if true output a :loop: line for any loops
35 # in multi start mode (multib is true) duplicate start names are ignored
36 # (using a true value for "multib" other than "1" may have undefined behavior)
38 # if inclbr is non-empty a branch name must be listed to appear on stdout
40 # if a branch name appears in exclbr it is omitted from stdout trumping inclbr
42 # except for branches removed entirely from consideration by exclbr/inclbr
43 # or !withan and being in anfile, any other branch found to be missing
44 # (i.e. no hdfile entry) will generate a missing (M=1) line regardless of
45 # any !withbr or leaves or tgonly settings
47 # annihilated branches listed in anfile may also appear in brfile without harm
48 # but they do not need to for correct results (i.e. same results either way)
50 # Note that if non-empty, usermt must be the FULL prefix for remote base ref
51 # names for example "refs/remotes/origin/top-bases" works (if that's the
52 # correct top-bases location of course)
54 # if a branch is excluded (either by not being in a non-empty inclbr list or
55 # by being listed in the exclbr list) then it will not be recursed into either
57 # to get accurate output, brfile, anfile and hdfile must all be provided and,
58 # obviously, if remote information is needed rtfile as well (usermt will be
59 # effectively ignored unless rtfile is provided)
61 # input is a list of edges as output by run_awk_topgit_deps
63 # using the startb starting point the graph is walked outputting one line for
64 # each visited node with the following format:
66 # M T L <node> [<parent> <branch> <chain> <names>]
68 # where M T L are single numeric digits with the following meanings:
70 # M=0 branch actually exists (i.e. it's NOT missing)
71 # M=1 branch does not exist but was named in a .topdeps or startb (if withbr)
73 # T=0 branch is NOT tgish or NOT local (missing and remotes are always 0)
74 # T=1 branch IS local tgish (annihilated branches are always 1)
75 # T=2 branch IS local tgish and has a non-annihilated remote tgish branch
77 # L=0 not a leaf node (missing and remotes are always 0)
78 # L=1 IS a leaf node (might or might NOT be tgish)
79 # L=2 an annihilated tgish branch (they can never be leaves anyway)
81 # contrary to the non-awk code this replaces, L == 1 IS possible with preord
83 # note that <node> will always be present and non-empty
84 # unless withbr is true then <parent> will also always be present and non-empty
85 # even if withbr IS true <parent> will be non-empty if extra path items were
86 # provided (the first path item becomes the parent and the rest the chain)
87 # the rest of the path items show the link chain from <node> up to <startb>
88 # with any extra path items output on the end
90 # An output line might look like this:
92 # 0 1 1 t/foo/leaf t/foo/int t/stage
94 # L=2 "a leaf" means any node that is either not a TopGit branch or is a
95 # non-annihilated TopGit branch with NO non-annihilated dependencies (that
96 # means NO non-tgish dependencies either)
98 # loops are detected and avoided (the link causing the loop is dropped) and
99 # if showlp is true a line like the following will be output whenever one is
100 # encountered:
102 # :loop: t/foo/int t/foo/leaf t/foo/int t/stage
104 # the two branch names immediately after LOOP show the link that was dropped
105 # to avoid the loop and the rest of the path is the normal branch chain
107 # in filter mode (filter is 1 or 2) the output is a list of deps (1) or
108 # edges (2) instead of recursion lines so the above sample output line would
109 # end up being this when filter mode is 1:
111 # t/foo/leaf
113 # to get the "patch series" list used for navigation use withbr=1 once=1 and
114 # filter=1
116 # and just this output when filter mode is 2:
118 # t/foo/int t/foo/leaf
120 # the first two node items from the recursion line are reversed (if withbr
121 # is active the single node name will be doubled to make an edge to itself)
122 # because the edge format is "<node-with-topdeps-line> <for-this-node>" whereas
123 # the normal recursion lines have the opposite order.
125 # extra items in startb are ignored in filter mode unless multib is "1" in
126 # which case they're then treated as additional starting nodes (just like
127 # normal multib=1 mode does).
129 # when filter mode is active the rtfile file and usermt settings are ignored
130 # (but rmrt will still work) while preord does work it's mostly pointless
131 # in filter mode. the leaves option still works exactly the same way as it's
132 # just the final format of the output line that's affected by filter mode not
133 # which lines (other than omitting remote lines) are output. Lines for
134 # missing (M == 1) items are, however, totally suppressed in filter mode
135 # since they're just not meaningful in that case. loop lines will still be
136 # output in exactly the same format if showlp is true.
138 # filter mode can be helpful when the intent is to ultimately run
139 # awk_topgit_navigate on a subset of the TopGit dependency tree
142 BEGIN { exitcode = "" }
143 function exitnow(e) { exitcode=e; exit e }
144 END { if (exitcode != "") exit exitcode }
146 BEGIN {
147 inconly = 0
148 cnt = split(inclbr, scratch, " ")
149 if (cnt) {
150 inconly = 1
151 for (i = 1; i <= cnt; ++i) incnames[scratch[i]] = 1
153 cnt = split(exclbr, scratch, " ")
154 for (i = 1; i <= cnt; ++i) excnames[scratch[i]] = 1
155 cnt = split(startb, scratch, " ")
156 if (!cnt) exitnow(2)
157 if (multib) {
158 startcnt = 0
159 for (i = 1; i <= cnt; ++i) {
160 if (!seenstartbr[scratch[i]]) {
161 startbr[++startcnt] = scratch[i]
162 extrabr[startcnt] = ""
163 seenstartr[scratch[i]] = 1
166 } else {
167 startbr[1] = scratch[1]
168 xtrapth = ""
169 for (i = 2; i <= cnt; ++i) xtrapth = xtrapth " " scratch[i]
170 extrabr[1] = xtrapth;
171 startcnt = 1
173 sub(/\/+$/, "", usermt)
174 if (usermt != "") usermt = usermt "/"
175 if (filter != "" && filter != 0 && filter != 1 && filter != 2) exitnow(2)
178 function quotevar(v) {
179 gsub(/\047/, "\047\\\047\047", v)
180 return "\047" v "\047"
183 function init(abranch, _e) {
184 rmlist = ""
185 if (brfile != "") {
186 while ((_e = (getline abranch <brfile)) > 0) {
187 if (abranch != "") tgish[abranch] = 1
189 close(brfile)
190 if (_e < 0) exitnow(2)
191 if (rmbr) rmlist = rmlist " " quotevar(brfile)
193 if (anfile != "") {
194 while ((_e = (getline abranch <anfile)) > 0) {
195 if (abranch != "") ann[abranch] = 1
197 close(anfile)
198 if (_e < 0) exitnow(2)
199 if (rman) rmlist = rmlist " " quotevar(anfile)
201 if (hdfile != "") {
202 if (cuthd) {
203 fno = 1
204 if (cuthd ~ /^[1-9][0-9]*$/) fno = 0 + cuthd
206 while ((_e = (getline abranch <hdfile)) > 0) {
207 if (fno) {
208 if (split(abranch, scratch, " ") < fno || length(scratch[fno]) < 12 ||
209 substr(scratch[fno], 1, 11) != "refs/heads/") continue
210 abranch = substr(scratch[fno], 12)
211 sub(/[~:^].*$/, "", abranch)
213 if (abranch != "") heads[abranch] = 1
215 close(hdfile)
216 if (_e < 0) exitnow(2)
217 if (rmhd) rmlist = rmlist " " quotevar(hdfile)
219 if (rtfile != "") {
220 if (!filter) {
221 while ((_e = (getline abranch <rtfile)) > 0) {
222 if (abranch != "") tgishr[abranch] = 1
224 close(rtfile)
225 if (_e < 0) exitnow(2)
227 if (rmrt) rmlist = rmlist " " quotevar(rtfile)
229 if (rmlist != "") system("rm -f" rmlist)
232 function included(abranch) {
233 return (!inconly || incnames[abranch]) && !excnames[abranch]
236 NR == 1 {init()}
238 NF == 2 && $1 != "" && $2 != "" && $1 != $2 &&
239 included($1) && included($2) && !ann[$1] && (withan || !ann[$2]) {
240 linkval = links[$1]
241 if (linkval != "") {
242 if (index(" " linkval " ", " " $2 " ")) next
243 links[$1] = linkval " " $2
244 } else {
245 links[$1] = $2
247 if (withan && !ann[$2]) {
248 # when using withan, linksx is the tree !withan would generate
249 # (it eXcludes all annihilated links)
250 # no need for it if !withan in effect as it would match links
251 linkval = linksx[$1]
252 if (linkval != "") linksx[$1] = linkval " " $2
253 else linksx[$1] = $2
257 function walktree(node, trail, level,
258 oncenodes, istgish, isleaf, children, childcnt, parent, child, visited, i)
260 if (once > 0 && (node in oncenodes)) return
261 if (!heads[node]) {
262 if (!filter) print "1 0 0 " node trail
263 if (once) oncenodes[node] = 1
264 return
266 if (filter == 2) {
267 parent = substr(trail, 2)
268 sub(/ .*$/, "", parent)
269 if (parent == "") parent = node
270 parent = parent " "
272 istgish = 0
273 isleaf = 0
274 if (ann[node]) {
275 istgish = 1
276 isleaf = 2
277 } else if (tgish[node]) {
278 istgish = tgishr[node] ? 2 : 1
280 if (isleaf != 2) isleaf = !istgish || (withan?linksx[node]:links[node]) == ""
281 if (preord && (level > 0 || withbr) && (!leaves || isleaf == 1) && (!tgonly || istgish)) {
282 if (filter) print parent node
283 else print "0 " istgish " " isleaf " " node trail
285 if (!once || !(node in oncenodes)) {
286 if (istgish == 2 && usermt && !leaves && !tgonly) print "0 0 0 " usermt node " " node trail
287 if ((childcnt = split(links[node], children, " "))) {
288 visited = " " node trail " "
289 for (i = 1; i <= childcnt; ++i) {
290 child = children[i]
291 if (index(visited, " " child " ")) {
292 if (showlp) print ":loop: " child " " node trail
293 continue
295 walktree(child, " " node trail, level + 1, oncenodes)
299 if (!preord && (level > 0 || withbr) && (!leaves || isleaf == 1) && (!tgonly || istgish)) {
300 if (filter) print parent node
301 else print "0 " istgish " " isleaf " " node trail
303 if (once) oncenodes[node] = 1
306 END {
307 for (startidx = 1; startidx <= startcnt; ++startidx) {
308 astart = startbr[startidx]
309 if (included(astart) && (!heads[astart] || withan || !ann[astart]))
310 walktree(astart, extrabr[startidx], 0)