% ct find . -version "!version(/main/0)" -printThis is a csh'ism. The "!" is interpreted as an event reference, and this is done prior to resolving the quotes. Use "\!" as in:
% ct find . -version "\!version(/main/0)" -print
Make sure no process has its working directory set to a path beginning with the vob's vob tag. One can use fuser(1) to determine which processes are using that file system.
More specifically, go to /view (view root of the ClearCase Unix Server) and type 'fuser -cu'. If there are entries, try 'fuser -ck' (as root).
It has been observed that this isn't enough in some cases. In extreme cases, only a reboot will clear out the locks. A classic one is when /data (where you find 'clearcase/vobstore') is a link to a remote file system. If that file system is removed (typically, during DRP - Disaster Recovery Plan -) whereas ClearCase server is still up... you can only reboot (the mvfs module will deny any attempt to unmount itself: device busy)
First, start by monitoring your license usage over the day. There are some nice packages that allow you to do this, For example Ed Finch's ClGraph.
The next thing is to reduce the license timeout to the minimum allowed: 30 minutes. Do this by adding a line saying: -timeout 30 into your /var/adm/atria/license.db file. You may also want to give a list of priority users by adding lines saying: -user userid.
If you still run out of licenses (and you can't afford to buy more), you can ask people to say clearlicense -release, but keep in mind you can only do this a limited number of times per day (approx. twice the number of licenses).
Touching the /var/adm/atria/license.db file releases all licenses at once, but again this can only be done 12 times per day. I use a crontab script similar to the one below, touching the file at a frequency that roughly matches my usage pattern:
5 9,10,11,1,2,3,4,5,6 * * * /bin/touch /var/adm/atria/license.db
Yet another way to reduce license usage is to encourage developers to use snapshot views and ensure that your change process knows how to deal with snapshot views, as there are a couple of pitfalls. Developers usually need little encouragement, as they will gladly sacrifice dynamic views for faster build performance, especially on NT systems.
Most sites disable the rmelem command, therefore you cannot undo a mkelem directly. Instead, you should use the rmname (rm for short) command.
Suppose, for example, you accidentally created the subdir element as a file element instead of a directory element. Use this procedure to correct the problem:
% ct mkelem subdir <== error! Created element "subdir" (type "compressed_file"). Checked out "subdir" from version "/main/0". % ct ci -identical -nc subdir Checked in "subdir". % ct rm subdir cleartool: Warning: Object "subdir" no longer referenced. cleartool: Warning: Moving object to vob lost+found directory as "subdir.9e881d0d390711d5b3ee000180a933fe". Removed "subdir". % ct mkdir subdir Created directory element "subdir". Checked out "subdir" from version "/main/0". %
The checkin is mainly for cosmetic reasons and to avoid confusion if this procedure is used in a snapshot view. In the end, the element isn't removed but just relocated into the trash bin. The ClearCase administrator will empty out the trash once in a while.
This procedure will not work as described if you don't notice the error immediatly and check in the containing directory. If you later check out the directory and remove the element, you will notice that it doesn't get relocated into lost+found, since the previous version of the directory still has a reference to that element. If you then attempt to create a directory element with the same name, the evil twin trigger will get in your way. You can fool the trigger, though, by first creating a directory element with a different name and then renaming it using the ct mv command, assuming the evil twin trigger fires only on mkelem, not lnname.
The important thing to remember is that renames, additions and removals of elements from a directory are all harmless and recoverable, so don't panic!
It helps to visualize additions, removals and renames as editing the containing directory. It's very much like adding, removing or changing lines in a file, and the recovery procedures are similar.
The easiest thing to do is to simply cancel the checkout of the directory. This will restore the previous version of the directory, and the removed elements will reappear, as if by magic:
% ct co -nc . Checked out "." from version "/main/234". % ct rm file <=== OOOPS!! Removed "file". % ct unco . Checkout cancelled for ".". % ls file file %
If you don't notice the error immediately and have already checked in your directory, you can take advantage of the evil twin trigger and allow it to help you. Simply attempt to re-mkelem the deleted element and follow the instructions:
% ct co -nc .
Checked out "." from version "/main/234".
% ct mkelem file
ERROR: An element named "file" already exists in
in some other version of ".":
Instead of creating a new element, you probably want to
create a hard link to the existing element, like so:
cleartool ln .@@/main/LATEST/file .
% ct ln .@@/main/LATEST/file .
Link created: "./file".
%
In general, directories don't contain elements but contain named links to elements. Removing an element doesn't destroy the element, it just removes the link. It is therefore almost trivial to resurrect a removed element.
One consequence is that removing directories and removing files are really the same operation, and the recovery procedure is identical. In particular, if you accidentally removed a whole tree, you only need to recreate the link to the topmost directory of that tree.
You are probably dealing with an eclipsed file. Run "cleartool ls" on the file, and if you see "[eclipsed]", simply rename the file out of the way using a plain ordinary "mv" and retry the operation.
The most common way this happens is when developers copy files from other views. Don't do that. Merge the containing directories instead.
A more precise formulation would be: "How do I locate elements that are linked into two or more different directory elements?". There is no really good method to do this except by exhaustive search, for example by using this technique:
Run this command:
% find . -print | record_inodes
where record_inodes is this little perl script:
#!/bin/perl
%links = (); # hash indexed by inodes containing an array of
# directory locations
%twice = (); # hash indexed by those inodes referenced by two
# or more directories
while (<>) {
chomp;
next if $_ eq '.'; # skip top level
next if -l $_; # skip over symlinks
($dir = $_) =~ s,/[^/]*$,,; # determine directory of element
# get inode of version 0, if it exists. If not, use
# just stat the file itself. This way, this script can
# be used outside of clearcase too.
$zero_version = $_.'@@/main/0';
($dev, $inode) = (-e $zero_version
? stat($zero_version)
: stat($_));
if (exists($links{$inode})) {
# register new location and register inode as "interesting"
push(@{$links{$inode}}, $_);
$twice{$inode}++;
} else {
# initialize array of locations
$links{$inode} = [ $_ ];
}
}
if (%twice) {
print "------------------------------------------------------------\n";
for $inode (keys(%twice)) {
print(map(" $_\n", @{$links{$inode}}));
print "------------------------------------------------------------\n";
}
} else {
print "No element linked to multiple directories found.\n";
}
This is probably the single most often asked question. The simple answer is that ClearCase doesn't support RCS keyword expansion, mainly for one reason: it would break merges.
Since two different versions would always have a different RCS keyword expansion at the same location within the file, any merge between those two versions would invariably cause a conflict that couldn't be resolved automatically.
In general, "inband info" (i.e. storing information about an object within an object) is a bad idea, since care must be taken not to confuse any random string with the actual metadata. A vivid demonstration of the danger can be found when attempting to check into RCS files that explain how RCS keywords work.
Instead of storing metadata within the data, one should take advantage of ClearCase's extensive metadata types (attributes, hyperlinks, comments etc...).
The obvious follow-up question then is: "How do I access the metadata when I can't connect to ClearCase?" There are really two cases here:
The correct solution to the first case is to insert the RCS keywords at build or packaging time. There are tools to insert keywords into binaries, for example the contributed package T0039 (ccwhat).
The second case is the best argument for implementing RCS keywords, and if it's truely important to your environment, read on...
There are ways to make RCS keywords work within ClearCase, but they are by no means trivial. The best method so far is to use the contributed package T0027, which contains both a trigger and a type manager. The trigger does the actual work of substituting RCS keywords, and the type manager avoids the merge conflicts by interposing itself between the file and the real type manager, removing all RCS keywords.
Note that even with this solution, your branching strategy should be set up to deal with the case described in the diagram on the right. Assume that the blue developer delivers a change (1) of some file into the red delivery branch. He then never touches that file again. The green developer then makes another change and delivers it (2). The blue developer, having done changes in some other files wants to sync up and executes a findmerge from the red branch, which will cause green's change to be merged over as a copy merge (3). Now, the blue developer wants to deliver his other changes, but since the merge in (3) caused the RCS keywords to change, findmerge will think that blue modified the file even though he didn't and cause (4) to happen, which is somewhat bewildering.
The reason this happens is because the findmerge algorithm has a case where the actual file content is compared. Unfortunately, findmerge does not use the type manager's compare function, but something hard coded and will think the files are different even though the only difference is in the keywords.
There are two possible workarounds:
On UNIX:
% cleartool find path -print | awk '{print "co -nc \""$0"\""}' | cleartool
Most often, this is done in the context of upgrading third party software or doing some other form of mass checkin. Recently, Rational came up with clearfsimport, a tool that will apparently do the right thing. Stay tuned...
I consider it a nuisance. One common development strategy is for every developer to create their own development branch by using a view with a config spec similar to this one:
element * CHECKEDOUT element * .../mybranch/LATEST element * BASELINE -mkbranch mybranch element * /main/0 -mkbranch mybranch
Note that whenever a developer starts working on some file he hasn't touched, a branch gets created at that point. As work progresses, more and more elements will have a "mybranch" branch.
Once in a while, the BASELINE label gets moved to the newest approved base line, and developers are encouraged to rebase via a merge. The merge will do something for every element that has a "mybranch" branch and for which the version that BASELINE selects changed since the branch was made.
If one leaves zero versions on the "mybranch" branch behind, then this will cause copy merges, which won't affect the logical concistency of your view of the source tree, but will affect performance. Besides, zero versions just look sloppy.
Most sites add a post-rmbranch and post-unco trigger to remove the dangeling branch with only a zero version. The following code implements this trigger. Paul D. Smith wrote this trigger, and I added some code to deal with snapshot view weirdness:
#!/bin/perl
#--------------------------------------------------------------------
# Nothing special needed Perl-wise - allow Rational Perl to be used
#--------------------------------------------------------------------
require 5.001;
#--------------------------------------------------------------------
# Debugging aid. Overloading the semantics of the standard
# CLEARCASE_TRACE_TRIGGERS EV: if it's set to -2 we dump
# the runtime environment into a file in the current dir.
#--------------------------------------------------------------------
if (int($ENV{CLEARCASE_TRACE_TRIGGERS}) < 0 &&
$ENV{CLEARCASE_TRACE_TRIGGERS} & 0x2) {
open (EV, ">rm_empty_branch.txt");
print EV "$x=$y\n" while (($x,$y) = each %ENV);
close EV;
}
#--------------------------------------------------------------------
# See if the user wants to suppress this trigger's actions:
#--------------------------------------------------------------------
exit 0 if $ENV{CCASE_NO_RM_EMPTY_BRANCH};
#--------------------------------------------------------------------
# Use safest quoting possible - java creates files with $ in
# them, so I can't just universally use "
#--------------------------------------------------------------------
$q = ($ENV{OS} eq "Windows_NT" ? '"' : "'");
# Remove empty branches: if a branch has no elements (except 0 of
# course) after an uncheckout or rmver, or the parent of a
# just-rmbranched branch is now empty, remove it.
#
# If the branch in question is /main, don't do anything (another option
# would be to rmelem the entire element, but that seems like a very bad
# idea to me).
#
# Install like this:
#
# ct mktrtype -element -global -postop uncheckout,rmver,rmbranch \
# -c "Remove empty branches after uncheckout, rmver, or rmbranch." \
# -exec /TRIGGERS/rm_empty_branch RM_EMPTY_BRANCH
#
# CAVEATS:
# - Ignores attributes!
# - Won't remove branches where the 0th element is labeled.
# - Will fail if any branch names contain spaces.
# - May fail if any branch or label is named ".*" or "*" exactly.
#
# CREATED BY:
# Paul D. Smith <psmith@baynetworks.com>
#
$xname = $ENV{CLEARCASE_XPN};
$xname =~ s,\\,/,g if $ENV{OS} eq 'Windows_NT';
# For uncheckout commands, if the version isn't 0 we can punt early
#
exit 0 if ($ENV{CLEARCASE_OP_KIND} eq 'uncheckout' &&
$xname !~ m,/0$,);
# Don't try to remove the /main branch
#
($branch = $xname) =~ s,/[^/]*$,,;
exit 0 if $branch =~ m,\@\@/main$,;
# Check if there are other versions, other branches, labels, or checked
# out versions on this branch: if so, don't do anything.
#
if (opendir(D, $branch)) {
# this opendir succeeds only in a dynamic view
@other_stuff = readdir(D);
closedir(D);
# in an empty branch, there are four thingies:
# ".", "..", "0" and "LATEST". If there are more, then
# it isn't an empty branch.
exit 0 if (scalar(@other_stuff) != 4);
} else {
# version extended name space not available implies
# we're in a snapshot view, and we will have to work
# a little harder here...
($pname, $brpath) = split($ENV{CLEARCASE_XN_SFX}, $branch);
# an rmbranch will not reload the element...
system("cleartool update -log /dev/null $q$pname$q")
if ($ENV{CLEARCASE_OP_KIND} eq 'rmbranch');
@vtree = `cleartool lsvtree -branch $brpath $q$pname$q`;
chomp($latest = pop(@vtree));
$latest =~ tr,\\,/, if $ENV{OS} eq 'Windows_NT';
exit 0 unless $latest =~ m,$brpath/0$,;
}
# Remove it!
system("cleartool rmbranch -force -nc $q$branch$q");
exit 0;
You use the cleartool ln command. See 1.2.14 for figuring out how to specify the invisible file as an argument to that command.
An eclipsed element is seen only in dynamic views. It's basically when a view-private file/directory has the same name as an element in that same directory. Views always display view-private items first. It often occurs when one or more developers are working in the same area adding new features (for example: new classes/packages/namespaces for java/.net development). Since files typically start out as view-private, someone in one view may add their files to source control, then merge them into an integration branch. If a different view also has some of those files/directories with the exact same name and in the same directory (as view-private), then those view-private files/directories will "eclipse" those elements that now become visible.
To fix an eclipsed file/directory, simply rename it in that view that shows as eclipsed. You can then diff the now-visible element against the renamed view-private and if they are the same, delete the renamed view-private. If they are different, you may need to check out the now-visible element then manually merge the contents of the renamed view-private with the element. Note that you can't use cleartool merge/findmerge, as those commands work on different versions of the same element, not element vs. view-private or element vs. a different element.
Consider simply cancelling the checkout, but be aware that if the checkout was the result of a merge, cancelling the checkout will result in the merge arrow disappearing, forcing you to redo the merge next time. In this case, using the -identical flag of the checkin command may be preferable.
Answered by cg@miaow.com
The reason why you're not seeing the file is that the version of the directory selected by your view doesn't have a link to that file. Therefore the solution is to figure out which version of that directory element has the link.
The easiest way to access such a file is to use a different view, namely, one that selects the appropriate version of the directory. Even if you are set to some view that doesn't, you can use the so-called view extended path to refer to the other view. On UNIX, this is done be prepending /view/viewtag to the full pathname.
In some situations, it may not be convenient or safe to use view extended paths. One problem is that while you are using a view extended path, someone else may be changing the config spec of that view. For logging purposes in particular, you are better off storing the object id of the element and letting ClearCase tell you a patnname that is valid in your view. In other words, first take a view that selects your invisible file and run this:
% cleartool setview otherview % cleartool describe -fmt '%On\n' path/to/invisible/file@@ 03fcf938.39c011d5.b891.00:01:80:ab:ed:ac
Note that the final @@ at the end of the path tells ClearCase that you are interested in the element's object id. Omitting the final @@ would give you the version's object id, whih may or may not be what you need. Now to retrieve a pathname valid in your view, do this:
% cleartool setview myview % cleartool describe -fmt '%p\n' oid:03fcf938.39c011d5.b891.00:01:80:ab:ed:ac /vob/somevobtag/path@@/main/2/to/main/1/invisible/main/3/file@@
Note how we enter version extended name space (or history mode on NT) very early in the path. This is natural, since our premise is that the file isn't visible in our current view. These paths can become quite long, easily exploding NT's stupid limit on path name length or command line length if you every try to use that pathname for some command.
Note that this technique is very useful for tracking down relocations. If in the example above the file was relocated to some unknown but visibile location, Clearcase would have shown that location instead. ClearCase is amazingly smart in figuring out the shortest possible pathname to a specific element.
Answered by cg@miaow.com
Assuming that you always label all visible elements, and assuming that LABEL1 is applied prior to LABEL2, the following will do:
% ct find someplace -element 'lbtype_sub(LABEL1) && !lbtype_sub(LABEL2)'\
-print
% ct find vobtag> -element 'lbtype_sub(LABEL2) && !lbtype_sub(LABEL1)'\
-print
% ct find vobtag -version 'lbtype(LABEL2) && !lbtype(LABEL1)'
Use the created_since query construct, like so:
% ct find vobtag -version 'created_since(early) && !created_since(later)'
Note that this will exclude versions created exactly at timestamp later, so it's not an excact equivalent of the label based queries described in 1.3.1.
% ct lsvob pathname
In order to set up a merge, findmerge needs to find the common ancestor of two versions. In complicated version trees, there can be many common ancestors, and finding the best one requires a non-trivial graph traversal algorithm.
The main problem is that while findmerge is looking for a common ancestor, it is holding a lock on the vob DB, preventing others from writing to the DB. In order to prevent an inordinately long amount of time in the lock, findmerge will interrupt its search, release and reacquire the lock and restart it at ever increasing intervals. There are two environment variables that can be used to tweak this behaviour, which may improve performance for specific elements:
Check out tech note 747 for more details.
There are two good techniques to speed up findmerge, often by orders of magnitude:
% ct findmerge someplace\
-element 'brtype(dev_branch)'\
-fver .../del_branch/LATEST -merge
Using -avobs instead of recursive descent can also improve performance, but is a more risky techniques if directory merges are involved. Since you don't control the order of merges when using -avobs, you could conceivably merge a directory element which isn't visible yet because the parent directory element hasn't been merged yet. Using -avobs -visible will avoid error messages, but may cause you to miss some merges entirely.
First figure out the UUID of the view by running:
% cleartool describe -long vob:vobtag
versioned object base "vobtag"
created 31-Dec-00.16:23:00 by ClearCase VOB admin account (vobadm.staff)
VOB family feature level: 2
VOB storage host:pathname "someplace"
VOB storage global pathname "someplace"
database schema version: 53
VOB ownership:
owner someone
group some group
Additional groups:
...
VOB holds objects from the following views:
? [uuid c00c3821.f94411d4.ba94.00:01:80:a9:33:fe]
...
You then can remove all the references to the non-existing view by running:
% cleartool rmview -force -avobs -uuid c00c3821.f94411d4.ba94.00:01:80:a9:33:fe Removing references ...
First end the view server processes by entering:
% cleartool endview -server oldtag
If the view is used on multiple clients, you may have to go from client to client to terminate the view, otherwise you may later get "stale NFS file handle" error messages and possible hangups.
You then remove the view tag using the rmtag command:
% cleartool rmtag -view oldtag
If you also want to rename or relocate the storage directory, unregister the current location, rename and register the new location:
% cleartool unregister -view old-location % mv old-location new-location % cleartool register -view new-location
Now you can create a new tag using mktag:
% cleartool mktag -view -tag newtag new-location
You may need to specify more arguments (triple path) depending on the type of the view. You essentially need to replicate most of the arguments you used when creating the view.
You can experiment with setting various access permissions in the view stoirage directory, but this isn't really recommended. Instead, you should figure out why developers are changing the config specs and why this would be a problem.
My prefered method for dealing with this is to encourage the use of view maintenance wrapper script that include config spec generators. These scripts should fit in flawlessly with your process and simplify it. Your measure of success will be whether the developers prefer the scripts to hacking their own config specs.
You can't and shouldn't, at least for those people who need access to the code. Obviously, you need to keep unauthorized people away, but this can be accomplished by properly setting access permissions to vobs. In other words, you can't prevent people from creating views but you can prevent them from seeing anything with them.
Views really are light weight and ephermal. Creating views shouldn't be a big deal. Your change process should encourage people to check in their work often and essentially treat views as temporary storage areas.
They are located on the machine that serves the view. You can determine this machine by running:
% cleartool lsview -long viewtag % ct lsview -l lt Tag: viewtag Global path: someplace Server host: view-server Region: this-region Active: YES View tag uuid:48b5b6f8.752b11d4.a259.00:01:80:a9:33:fe View export ID (registry): 18 ...
The quick way is to just remove the view and re-create it if you want to get rid of all your view-private files/directories/links. If, on the otherhand, you want to keep some of those view-privates, you'll have to find all the view-private files, which is different depending on if you use dynamic vs. snapshot views. There's no GUI to help you, you have to go commandline via DOS prompt or unix commandline.
For dynamic views, set the the view or go to the appropriate drive (and view directory), then run:
cleartool lsprivate -other
For snapshot views, cd to the view/VOB, then run:
cleartool ls -recurse -view_only
Then, rather than blindly remove all view-privates (you may as well just remove then create the view), I re-direct the output to a log file, delete the entries for the view-private files I want to keep, then run a shell-script (unix) or perl script (DOS) to delete each entry (directories are handled differently than files)
If you cannot log into the the system (person's machine died or was replaced) or left the company and cleartool rmview fails, and either the view doesn't have checkouts or you want to throw them away, you can do the following (works on DOS prompt and unix commandline):
- cleartool lsview -l <view-tag>
This should give you it's uuid (view uuid: a bunch of hexadecimal stuff with colons and periods in it)
Copy the 2nd uuid entry and paste it in whereever you see <uuid> in the steps below (if you copy the first entry, you'll have
uuid:<uuid> instead of <uuid>, which will fail):
- cleartool rmtag -view <view-tag>
- cleartool unregister -view -uuid <uuid>
- cleartool rmview -force -all -uuid <uuid> (or use -avobs instead of -all)
Then you can delete the view-store (if it can be found, the view-store is also found in the cleartool lsview -l <view-tag>'s global path and/or view server access path.)
This can happen if you a) check out a version in the version tree or b) if a mkbranch rule has been added to your config spec which creates a new branch that is not selected in your view.
Under Windows, open the version tree and look for the icon that resembles an "eye", it indicates which version is currently selected in your view. Now look for the checked out version (a circle with no version number in it), in most cases the branch is different! Examine your config spec to find out what has happened.
In Unix enter cleartool ls {element name} to find out which rule ClearCase uses to select the version, enter cleartool lsco {element name} to see which version has been checked out.
The "linked storage area" is the physical storage location of view private files. This storage location can be on external devices, for example filers, whereas the view database storage needs to be on a local disk.
Recent versions of ClearCase have relaxed this condition somehwat, and if you're using filer hardware recommended by Rational, you can put the whole view storage directory on the filer. In other words, linked storage areas are somewhat obsolescent and designed to work around a problem that is disappearing.
You must lock vobs to keep the database consistent with the storage containers where all the data (element versions) is stored. Write operations which occur during the backup break the consistency, the keys in the database are not fully correct and you may have trouble accessing certain elements. You can use cleartool checkvob -pool -source to check for errors.
Remember, inconsistencies in the database are hard to notice without the cleartool checkvob command. You might think that everything is okay but months later you will run into problems.
Use the checkvob command to repair broken hyperlinks:
cleartool checkvob -hlinks [-force]
Every Vob has a tag in each region. The easiest way to rename your vob is to remove the tag and create a new one:
cleartool lsvob -l <your old vob tag> cleartool rmtag -vob <your old vob tag> cleartool mktag -vob -tag <new vob tag> <vob storage from lsvob output above>
Make sure that you use the correct host and access path for your new tag and add -public if necessary. Also, you must have access to the vob storage path or you won't be able to create the tag.
As you know, metadata types like branches, labels, attributes etc. are stored per vob. Before you can create instances of them (e.g. mklabel) you have to create the object itself (e.g. mklbtype). If you have a project where different vobs belong together you usually want to make the types available in all vobs but of course you don't want to create them n times for n vobs. Admin vobs are made for this situation, you only create the types once in your admin vob and they are available in all other vobs which are linked to the admin vob.
In UCM, the project vob becomes automatically the admin vob and stores all necessary information.
In lost+found ClearCase puts all elements which are not referenced anymore. Imagine you remove a directory with rmname or rmelem. ClearCase won't recursively remove the elements in this directory but they cannot be accessed neither. Instead, they will be moved to lost+found.
The following script will take as input the output of any set of "lsvtree -short" commands, for example the output produced by:
% cleartool find . -print | xargs cleartool lsvtree -s
It will then produce an indented list of branch/sub-branch relationships and also detect cycles (for example if you have both /main/x/y and /main/y/x).
#!/bin/perl
while (<>) {
next unless m,\@\@/(main/[^\@]*)$,;
$branches = $1;
@branches = split('/', $branches);
pop(@branches); # remove version
$parent = shift; # parent is first entry (main)
for $branch (@branches) {
$ancestor_of{$branch}{$parent} = 1;
$offspring_of{$parent}{$branch} = 1;
$parent = $branch;
}
}
make_node('main', 0);
sub make_node {
my ($parent, $indent) = @_;
print(' 'x$indent.$parent."\n");
my $children = 0;
my @offspring = sort(keys(%{$offspring_of{$parent}}));
for my $child (@offspring) {
if (scalar(keys(%{$ancestor_of{$child}})) == 1) {
# a "real" child is one that has only one ancestor: $parent
$children = 1;
make_node($child, $indent+2);
}
}
if (@offspring && !$children) {
print(' 'x$indent.' Cycle detected: '.join(' ',@offspring)."\n");
}
}