Removed /usr/local from CDPATH
[clearscm.git] / cc / etf.pl
1 #!/usr/bin/perl
2
3 =pod
4
5 =head1 NAME $RCSfile: etf.pl,v $
6
7 Evil Twin Finder
8
9 =head1 VERSION
10
11 =over
12
13 =item Author
14
15 Andrew DeFaria <Andrew@ClearSCM.com>
16
17 =item Revision:
18
19 $Revision: 1.5 $
20
21 =item Created:
22
23 Fri Apr 23 09:40:31 PDT 2010
24
25 =item Modified:
26
27 $Date: 2011/01/09 01:00:28 $
28
29 =back
30
31 =head1 SYNOPSIS
32
33  Usage: eft.pl [-u|sage] [-ve|rbose] [-d|ebug] [-di|rectory <dir>]
34
35  Where:
36
37   -u|sage:       Displays usage
38   -ve|rbose:     Be verbose
39   -d|ebug:       Output debug messages
40
41   -dir           Directory to process
42
43 =head1 DESCRIPTION
44
45 This script will search for existing evil twins in a Clearcase vob. It is
46 intended to be used in the context of a base Clearcase view with a default
47 config spec.
48
49 An evil twin is defined as two or more Clearcase elements that share the same
50 path, and same name, but have different OIDs thus having different version
51 histories. This can occur when a user creates an element in a directory that
52 used to exist in this same directory on another branch or on a previous version
53 of the same branch. By default Clearcase will create an element with a new OID.
54 This new, evil twin will then develop it's own version history. This then
55 becomes a problem when you attempt to merge branches - which twin (OID) should
56 Clearcase keep track of?
57
58 Most Clearcase users implement an evil twin trigger to prevent the creation of
59 evil twins but sometimes evil twins have already been created. This script helps
60 identify these already existing evil twins.
61
62 Note: Evil twins can also happen if you only apply your evil twin trigger to the
63 mkelem Clearcase action. It should be applied to the lnname action as elements
64 come into creation by things like the cleartool ln, mv and mkdir commands. These
65 all eventually do an lnname so that's where you should put your evil twin
66 trigger.
67
68 =head1 ALGORITHM
69
70  TODO: Is cleartool find really needed? I mean since we are going through
71        the extended version namespace don't we by default find all
72        subdirectories?
73
74 This script will use cleartool find to process all directory elements from
75 $startingDir (Default '.'). For each version of the directory a hash will be
76 built up containing all of the element names in that directory version.
77 Elements are always added and never deleted in this hash as we are looking for
78 all elements that have ever existed in the directory at any point in time.
79
80 This script then dives into the view extended namespace for directory elements
81 examining the internal Clearcase structures. If we find a branch we recurse or 
82 numbered directory version we recurse looking for file elements (TODO: What 
83 about directory evil twins?). Note that we skip version 0 as version 0 is never
84 interesting - it is always a duplicate of what it branched from and empty.
85
86 Directory versions that are not numbered are labels or baselines that point to
87 numbered directory versions so we don't need to look at them again. 
88
89 For each file element we find we use the cleartool dump command to get the OID
90 of this particiular versioned element and build up an array of hashes of all the
91 elements in the directory. For each element version we maintain a hash keyed by
92 the OID. The structure also contains a count of the number of times the OID was
93 found. An evil twin therefore will have multiple OIDs for the same element
94 version name.
95
96 After the directory is processed we look though the array of hashes for elements
97 that have multiple OIDs and report them. Then we proceed to the next directory.
98
99 =cut
100
101 use strict;
102 use warnings;
103
104 use Getopt::Long;
105 use File::Basename;
106 use Cwd;
107
108 use FindBin;
109 use lib "$FindBin::Bin/../lib";
110
111 use Clearcase;
112 use Clearcase::Element;
113 use Display;
114 use Logger;
115 use TimeUtils;
116 use Utils;
117
118 my $VERSION = '1.0';
119
120 my (%total, %dirInfo, $log, $startTime);
121
122 =pod
123
124 =head2 reportDir (%directoryInfo)
125
126 Report any evil twins found in %directoryInfo
127
128 Parameters:
129
130 =for html <blockquote>
131
132 =over
133
134 =item %directoryInfo
135
136 Structure representing the OIDs of element in a direcotry
137
138 =back
139
140 =for html </blockquote>
141
142 Returns:
143
144 =for html <blockquote>
145
146 =over
147
148 =item nothing
149
150 =back
151
152 =for html </blockquote>
153
154 =cut
155
156 sub reportDir (%) {
157   my (%directoryInfo) = @_;
158
159   my $ets = 0;
160
161   foreach my $filename (sort keys %directoryInfo) {
162     my @oids = @{$directoryInfo{$filename}};
163
164     if (scalar @oids > 1) {
165       $ets++;
166
167       $log->msg ("File: $filename");
168
169       foreach (@oids) {
170         $log->msg ("\tOID: $$_{OID} ($$_{count})");
171         $log->msg ("\tFirst detected \@: $$_{version}");
172       } # foreach
173     } # if
174   } # foreach
175
176   return $ets;
177 } # reportDir
178
179 =pod
180
181 =head2 proceedDir $dirName
182
183 Build up a data structure for $dirName looking for evil twins
184
185 Parameters:
186
187 =for html <blockquote>
188
189 =over
190
191 =item $dirName
192
193 Directory to examine
194
195 =back
196
197 =for html </blockquote>
198
199 Returns:
200
201 =for html <blockquote>
202
203 =over
204
205 =item %dirInfo
206
207 Directory info hash keyed by element name whose value is an array of oidInfo
208 hashes containing a unique OID and a count of how many occurences of that OID
209 exist for that element.
210
211 =back
212
213 =for html </blockquote>
214
215 =cut
216
217 sub processDir ($);
218 sub processDir ($) {
219   my ($dirName) = @_;
220
221   opendir my $dir, $dirName
222     or $log->err ("Unable to open directory $dirName - $!", 1);
223
224   my @dirVersions = grep {!/^\./} readdir $dir;
225
226   closedir $dir;
227
228   my ($directory, $version) = split /$Clearcase::SFX/, $dirName;
229
230   $directory = basename (cwd)
231     if $directory eq '.';
232
233   my $displayName = "$directory$Clearcase::SFX$version";
234
235   # We only want to deal with branches and numbered versions. Non-numbered
236   # versions which are not branches represent labels and baselines which are
237   # just aliases for directory and file elements. Branches represent recursion
238   # points and numbered versions represent unique directory versions.
239   my @elements;
240
241   foreach (@dirVersions) {
242     my ($status, @output) = $Clearcase::CC->execute (
243       "describe -fmt %m $dirName/$_"
244     );
245     my $objkind = $output[0];
246
247     if ($objkind =~ / element/) {
248       push @elements, $_;
249     } elsif (/^\d/ or $objkind eq 'branch') {
250       # Skip 0 element - it's never interesting.
251       next if $_ eq '0';
252
253       # Recurse for branches and numbered directory versions
254       if ($objkind eq 'branch') {
255         $total{branches}++;
256       } else {
257         $total{'directory versions'}++;
258       } # if
259
260       verbose_nolf '.';
261
262       #$log->log ("Recurse:\t$displayName/$_");
263
264       %dirInfo = processDir "$dirName/$_";
265
266       next;
267     } # if
268   } # foreach
269
270   foreach (@elements) {
271     $total{'element versions'}++;
272
273     #$log->log ("Element:\t$displayName/$_");
274
275     # Get oid using the helper function
276     my $oid = Clearcase::Element::oid "$dirName/$_";
277
278     if ($dirInfo{$_}) {
279       my $found = 0;
280
281       # Search our %dirInfo for a version matching $version     
282       foreach (@{$dirInfo{$_}}) {
283         # Increment count if we find a matching oid
284         if ($$_{OID} eq $oid) {
285           $$_{count}++;
286           $found = 1;
287           last;
288         } # if
289       } # foreach
290
291       unless ($found) {
292         # If we didn't find a match then make a new %objInfo starting with a
293         # count of 1. Also save this current $version, which is the first
294         # instance of this new oid. 
295         push @{$dirInfo{$_}}, {
296           OID     => $oid,
297           count   => 1,
298           version => "$dirName/$_",
299         };
300       } # unless
301     } else {
302       $dirInfo{$_} = [{
303         OID     => $oid,
304         count   => 1,
305         version => "$dirName/$_",
306       }];
307    } # if
308   } # foreach
309
310   return %dirInfo;
311 } # processDir
312
313 =pod
314
315 =head2 proceedDirs $startingDir
316
317 Process all directories under $startingDir
318
319
320 Parameters:
321
322 =for html <blockquote>
323
324 =over
325
326 =item $startingDir
327
328 Directory to start processing
329
330 =back
331
332 =for html </blockquote>
333
334 Returns:
335
336 =for html <blockquote>
337
338 =over
339
340 =item $total{etf}
341
342 Total number of evil twins found
343
344 =back
345
346 =for html </blockquote>
347
348 =cut
349
350 sub processDirs ($) {
351   my ($startingDir) = @_;
352
353   my $cmd = "cleartool find \"$startingDir\" -type d -print";
354
355   open my $dirs, '-|', $cmd
356     or $log->err ("Unable to execute $cmd - $!", 1);
357
358   while (<$dirs>) {
359     chomp; chop if /\r$/;
360
361     my $displayName = $_;
362
363     $displayName =~ s/\@\@$//;
364
365     if ($displayName eq '.') {
366       $displayName = basename (cwd);
367     } # if
368
369     $log->msg ("Processing $displayName");
370
371     my $startingTime  = time;
372     my %directoryInfo = processDir $_;
373
374     verbose '';
375
376     display_duration $startingTime, $log;
377
378     $total{'evil twins'} += reportDir %dirInfo;
379   } # while
380
381   close $dirs
382     or $log->err ("Unable to close $cmd - $!");
383
384   return $total{'evil twins'};
385 } # processDirs
386
387 # Main
388 local $| = 1;
389
390 my $startingDir = '.';
391
392 GetOptions (
393   usage         => sub { Usage },
394   verbose       => sub { set_verbose },
395   debug         => sub { set_debug },
396   'directory=s' => \$startingDir,
397 ) or Usage 'Invalid parameter';
398
399 $startTime = time;
400
401 $log = Logger->new;
402
403 $log->msg ("Evil Twin Finder $FindBin::Script v$VERSION");
404
405 processDirs $startingDir;
406
407 Stats \%total, $log;
408
409 $log->msg ("$FindBin::Script finished @ " . localtime);
410
411 display_duration $startTime, $log;
412
413 =pod
414
415 =head1 CONFIGURATION AND ENVIRONMENT
416
417 DEBUG: If set then $debug is set to this level.
418
419 VERBOSE: If set then $verbose is set to this level.
420
421 TRACE: If set then $trace is set to this level.
422
423 =head1 DEPENDENCIES
424
425 =head2 Perl Modules
426
427 L<Cwd>
428
429 L<File::Basename|File::Basename>
430
431 L<FindBin>
432
433 L<Getopt::Long|Getopt::Long>
434
435 =head2 ClearSCM Perl Modules
436
437 =begin man 
438
439  Clearcase
440  Clearcase::Element
441  Display
442  Logger
443  TimeUtils
444  Utils
445
446 =end man
447
448 =begin html
449
450 <blockquote>
451 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase.pm">Clearcase</a><br>
452 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase/Element.pm">Element</a><br>
453 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
454 <a href="http://clearscm.com/php/scm_man.php?file=lib/Logger.pm">Logger</a><br>
455 <a href="http://clearscm.com/php/scm_man.php?file=lib/TimeUtils.pm">TimeUtils</a><br>
456 <a href="http://clearscm.com/php/scm_man.php?file=lib/Utils.pm">Utils</a><br>
457 </blockquote>
458
459 =end html
460
461 =head1 BUGS AND LIMITATIONS
462
463 There are no known bugs in this script
464
465 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
466
467 =head1 LICENSE AND COPYRIGHT
468
469 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
470
471 =cut