To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / extra / soundsoftware @ 1575:42618fc5ab46

1 7:3c16ed8faa07 Chris
package Apache::Authn::SoundSoftware;
2
3
=head1 Apache::Authn::SoundSoftware
4
5
SoundSoftware - a mod_perl module for Apache authentication against a
6
Redmine database and optional LDAP implementing the access control
7
rules required for the SoundSoftware.ac.uk repository site.
8
9
=head1 SYNOPSIS
10
11
This module is closely based on the Redmine.pm authentication module
12
provided with Redmine.  It is intended to be used for authentication
13
in front of a repository service such as hgwebdir.
14
15
Requirements:
16
17
1. Clone/pull from repo for public project: Any user, no
18
authentication required
19
20
2. Clone/pull from repo for private project: Project members only
21
22
3. Push to repo for public project: "Permitted" users only (this
23 8:0c83d98252d9 Chris
probably means project members who are also identified in the hgrc web
24
section for the repository and so will be approved by hgwebdir?)
25 7:3c16ed8faa07 Chris
26 8:0c83d98252d9 Chris
4. Push to repo for private project: "Permitted" users only (as above)
27 7:3c16ed8faa07 Chris
28 300:034e9b00b341 chris
5. Push to any repo that is tracking an external repo: Refused always
29
30 7:3c16ed8faa07 Chris
=head1 INSTALLATION
31
32
Debian/ubuntu:
33
34
  apt-get install libapache-dbi-perl libapache2-mod-perl2 \
35
    libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl
36
37
Note that LDAP support is hardcoded "on" in this script (it is
38
optional in the original Redmine.pm).
39
40
=head1 CONFIGURATION
41
42
   ## This module has to be in your perl path
43
   ## eg:  /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm
44
   PerlLoadModule Apache::Authn::SoundSoftware
45
46
   # Example when using hgwebdir
47
   ScriptAlias / "/var/hg/hgwebdir.cgi/"
48
49
   <Location />
50
       AuthName "Mercurial"
51
       AuthType Basic
52
       Require valid-user
53
       PerlAccessHandler Apache::Authn::SoundSoftware::access_handler
54
       PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler
55
       SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost"
56
       SoundSoftwareDbUser "redmine"
57
       SoundSoftwareDbPass "password"
58
       Options +ExecCGI
59
       AddHandler cgi-script .cgi
60
       ## Optional where clause (fulltext search would be slow and
61
       ## database dependant).
62
       # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)"
63 8:0c83d98252d9 Chris
       ## Optional prefix for local repository URLs
64
       # SoundSoftwareRepoPrefix "/var/hg/"
65 7:3c16ed8faa07 Chris
  </Location>
66
67
See the original Redmine.pm for further configuration notes.
68
69
=cut
70
71
use strict;
72
use warnings FATAL => 'all', NONFATAL => 'redefine';
73
74
use DBI;
75
use Digest::SHA1;
76
use Authen::Simple::LDAP;
77
use Apache2::Module;
78
use Apache2::Access;
79
use Apache2::ServerRec qw();
80
use Apache2::RequestRec qw();
81
use Apache2::RequestUtil qw();
82
use Apache2::Const qw(:common :override :cmd_how);
83
use APR::Pool ();
84
use APR::Table ();
85
86
my @directives = (
87
  {
88
    name => 'SoundSoftwareDSN',
89
    req_override => OR_AUTHCFG,
90
    args_how => TAKE1,
91
    errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
92
  },
93
  {
94
    name => 'SoundSoftwareDbUser',
95
    req_override => OR_AUTHCFG,
96
    args_how => TAKE1,
97
  },
98
  {
99
    name => 'SoundSoftwareDbPass',
100
    req_override => OR_AUTHCFG,
101
    args_how => TAKE1,
102
  },
103
  {
104
    name => 'SoundSoftwareDbWhereClause',
105
    req_override => OR_AUTHCFG,
106
    args_how => TAKE1,
107
  },
108
  {
109 8:0c83d98252d9 Chris
    name => 'SoundSoftwareRepoPrefix',
110 7:3c16ed8faa07 Chris
    req_override => OR_AUTHCFG,
111
    args_how => TAKE1,
112
  },
113
);
114
115
sub SoundSoftwareDSN {
116 8:0c83d98252d9 Chris
    my ($self, $parms, $arg) = @_;
117
    $self->{SoundSoftwareDSN} = $arg;
118
    my $query = "SELECT
119 7:3c16ed8faa07 Chris
                 hashed_password, auth_source_id, permissions
120
              FROM members, projects, users, roles, member_roles
121
              WHERE
122
                projects.id=members.project_id
123
                AND member_roles.member_id=members.id
124
                AND users.id=members.user_id
125
                AND roles.id=member_roles.role_id
126
                AND users.status=1
127
                AND login=?
128
                AND identifier=? ";
129 8:0c83d98252d9 Chris
    $self->{SoundSoftwareQuery} = trim($query);
130 7:3c16ed8faa07 Chris
}
131
132
sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); }
133
sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); }
134
sub SoundSoftwareDbWhereClause {
135 8:0c83d98252d9 Chris
    my ($self, $parms, $arg) = @_;
136
    $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." ");
137 7:3c16ed8faa07 Chris
}
138
139 8:0c83d98252d9 Chris
sub SoundSoftwareRepoPrefix {
140
    my ($self, $parms, $arg) = @_;
141
    if ($arg) {
142
	$self->{SoundSoftwareRepoPrefix} = $arg;
143
    }
144 7:3c16ed8faa07 Chris
}
145
146
sub trim {
147 8:0c83d98252d9 Chris
    my $string = shift;
148
    $string =~ s/\s{2,}/ /g;
149
    return $string;
150 7:3c16ed8faa07 Chris
}
151
152
sub set_val {
153 8:0c83d98252d9 Chris
    my ($key, $self, $parms, $arg) = @_;
154
    $self->{$key} = $arg;
155 7:3c16ed8faa07 Chris
}
156
157
Apache2::Module::add(__PACKAGE__, \@directives);
158
159
160
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
161
162
sub access_handler {
163 8:0c83d98252d9 Chris
    my $r = shift;
164 7:3c16ed8faa07 Chris
165 152:a389c77da9fd Chris
    print STDERR "SoundSoftware.pm: In access handler at " . scalar localtime() . "\n";
166 7:3c16ed8faa07 Chris
167 8:0c83d98252d9 Chris
    unless ($r->some_auth_required) {
168
	$r->log_reason("No authentication has been configured");
169
	return FORBIDDEN;
170
    }
171 7:3c16ed8faa07 Chris
172 8:0c83d98252d9 Chris
    my $method = $r->method;
173 7:3c16ed8faa07 Chris
174 8:0c83d98252d9 Chris
    print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n";
175
    print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n";
176 7:3c16ed8faa07 Chris
177 8:0c83d98252d9 Chris
    my $dbh = connect_database($r);
178 152:a389c77da9fd Chris
    unless ($dbh) {
179
	print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n";
180
	return FORBIDDEN;
181
    }
182
183 300:034e9b00b341 chris
    print STDERR "Connected to db, dbh is " . $dbh . "\n";
184 7:3c16ed8faa07 Chris
185 8:0c83d98252d9 Chris
    my $project_id = get_project_identifier($dbh, $r);
186 300:034e9b00b341 chris
187
    if (!defined $read_only_methods{$method}) {
188
        print STDERR "SoundSoftware.pm: Method is not read-only\n";
189
        if (project_repo_is_readonly($dbh, $project_id, $r)) {
190
            print STDERR "SoundSoftware.pm: Project repo is read-only, refusing access\n";
191
	    return FORBIDDEN;
192
        } else {
193
	    print STDERR "SoundSoftware.pm: Project repo is read-write, authentication handler required\n";
194
            return OK;
195
        }
196
    }
197
198 8:0c83d98252d9 Chris
    my $status = get_project_status($dbh, $project_id, $r);
199 7:3c16ed8faa07 Chris
200 8:0c83d98252d9 Chris
    $dbh->disconnect();
201
    undef $dbh;
202 7:3c16ed8faa07 Chris
203 8:0c83d98252d9 Chris
    if ($status == 0) { # nonexistent
204
	print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n";
205
	return FORBIDDEN;
206
    } elsif ($status == 1) { # public
207
	print STDERR "SoundSoftware.pm: Project is public, no restriction here\n";
208
	$r->set_handlers(PerlAuthenHandler => [\&OK])
209
    } else { # private
210
	print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n";
211
    }
212 7:3c16ed8faa07 Chris
213 8:0c83d98252d9 Chris
    return OK
214 7:3c16ed8faa07 Chris
}
215
216
sub authen_handler {
217 8:0c83d98252d9 Chris
    my $r = shift;
218
219 152:a389c77da9fd Chris
    print STDERR "SoundSoftware.pm: In authentication handler at " . scalar localtime() . "\n";
220 7:3c16ed8faa07 Chris
221 8:0c83d98252d9 Chris
    my $dbh = connect_database($r);
222 152:a389c77da9fd Chris
    unless ($dbh) {
223
        print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n";
224
        return AUTH_REQUIRED;
225
    }
226 8:0c83d98252d9 Chris
227
    my $project_id = get_project_identifier($dbh, $r);
228
    my $realm = get_realm($dbh, $project_id, $r);
229
    $r->auth_name($realm);
230
231
    my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
232
    unless ($res == OK) {
233
	$dbh->disconnect();
234
	undef $dbh;
235
	return $res;
236
    }
237
238
    print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n";
239
240
    my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r);
241
242
    $dbh->disconnect();
243
    undef $dbh;
244
245
    if ($permitted) {
246
	return OK;
247
    } else {
248
	print STDERR "SoundSoftware.pm: Not permitted\n";
249
	$r->note_auth_failure();
250
	return AUTH_REQUIRED;
251
    }
252 7:3c16ed8faa07 Chris
}
253
254
sub get_project_status {
255 8:0c83d98252d9 Chris
    my $dbh = shift;
256 7:3c16ed8faa07 Chris
    my $project_id = shift;
257
    my $r = shift;
258 8:0c83d98252d9 Chris
259
    if (!defined $project_id or $project_id eq '') {
260
	return 0; # nonexistent
261
    }
262 7:3c16ed8faa07 Chris
263
    my $sth = $dbh->prepare(
264
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
265
    );
266
267
    $sth->execute($project_id);
268 8:0c83d98252d9 Chris
    my $ret = 0; # nonexistent
269 7:3c16ed8faa07 Chris
    if (my @row = $sth->fetchrow_array) {
270
    	if ($row[0] eq "1" || $row[0] eq "t") {
271
	    $ret = 1; # public
272
    	} else {
273 8:0c83d98252d9 Chris
	    $ret = 2; # private
274 7:3c16ed8faa07 Chris
	}
275
    }
276
    $sth->finish();
277
    undef $sth;
278
279
    $ret;
280
}
281
282 300:034e9b00b341 chris
sub project_repo_is_readonly {
283
    my $dbh = shift;
284
    my $project_id = shift;
285
    my $r = shift;
286
287
    if (!defined $project_id or $project_id eq '') {
288
        return 0; # nonexistent
289
    }
290
291
    my $sth = $dbh->prepare(
292
        "SELECT repositories.is_external FROM repositories, projects WHERE projects.identifier = ? AND repositories.project_id = projects.id;"
293
    );
294
295
    $sth->execute($project_id);
296
    my $ret = 0; # nonexistent
297
    if (my @row = $sth->fetchrow_array) {
298 301:6d3f8aeb51b7 chris
        if (defined($row[0]) && ($row[0] eq "1" || $row[0] eq "t")) {
299 300:034e9b00b341 chris
            $ret = 1; # read-only (i.e. external)
300
        } else {
301
            $ret = 0; # read-write
302
        }
303
    }
304
    $sth->finish();
305
    undef $sth;
306
307
    $ret;
308
}
309
310 8:0c83d98252d9 Chris
sub is_permitted {
311
    my $dbh = shift;
312
    my $project_id = shift;
313
    my $redmine_user = shift;
314
    my $redmine_pass = shift;
315
    my $r = shift;
316 7:3c16ed8faa07 Chris
317 8:0c83d98252d9 Chris
    my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
318 7:3c16ed8faa07 Chris
319 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
320
	(__PACKAGE__, $r->server, $r->per_dir_config);
321 7:3c16ed8faa07 Chris
322 8:0c83d98252d9 Chris
    my $query = $cfg->{SoundSoftwareQuery};
323
    my $sth = $dbh->prepare($query);
324
    $sth->execute($redmine_user, $project_id);
325 7:3c16ed8faa07 Chris
326 8:0c83d98252d9 Chris
    my $ret;
327
    while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) {
328 7:3c16ed8faa07 Chris
329 8:0c83d98252d9 Chris
	# Test permissions for this user before we verify credentials
330
	# -- if the user is not permitted this action anyway, there's
331
	# not much point in e.g. contacting the LDAP
332 7:3c16ed8faa07 Chris
333 8:0c83d98252d9 Chris
	my $method = $r->method;
334 7:3c16ed8faa07 Chris
335 8:0c83d98252d9 Chris
	if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/)
336
	    || $permissions =~ /:commit_access/) {
337
338
	    # User would be permitted this action, if their
339
	    # credentials checked out -- test those now
340
341
	    print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n";
342
343
	    unless ($auth_source_id) {
344
		if ($hashed_password eq $pass_digest) {
345
		    print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n";
346
		    $ret = 1;
347
		    last;
348
		}
349
	    } else {
350
		my $sthldap = $dbh->prepare(
351
		    "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;"
352
		    );
353
		$sthldap->execute($auth_source_id);
354
		while (my @rowldap = $sthldap->fetchrow_array) {
355
		    my $ldap = Authen::Simple::LDAP->new(
356
			host    => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
357
			port    => $rowldap[1],
358
			basedn  => $rowldap[5],
359
			binddn  => $rowldap[3] ? $rowldap[3] : "",
360
			bindpw  => $rowldap[4] ? $rowldap[4] : "",
361
			filter  => "(".$rowldap[6]."=%s)"
362
			);
363
		    if ($ldap->authenticate($redmine_user, $redmine_pass)) {
364
			print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n";
365
			$ret = 1;
366
		    }
367
		}
368
		$sthldap->finish();
369
		undef $sthldap;
370
	    }
371
	} else {
372
	    print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n";
373
	}
374 7:3c16ed8faa07 Chris
    }
375
376 8:0c83d98252d9 Chris
    $sth->finish();
377
    undef $sth;
378
379
    $ret;
380 7:3c16ed8faa07 Chris
}
381
382
sub get_project_identifier {
383 8:0c83d98252d9 Chris
    my $dbh = shift;
384 7:3c16ed8faa07 Chris
    my $r = shift;
385
386
    my $location = $r->location;
387
    my ($repo) = $r->uri =~ m{$location/*([^/]+)};
388 10:2c10dc5f122d Chris
389
    return $repo if (!$repo);
390
391 7:3c16ed8faa07 Chris
    $repo =~ s/[^a-zA-Z0-9\._-]//g;
392
393 8:0c83d98252d9 Chris
    # The original Redmine.pm returns the string just calculated as
394
    # the project identifier.  That won't do for us -- we may have
395
    # (and in fact already do have, in our test instance) projects
396
    # whose repository names differ from the project identifiers.
397
398
    # This is a rather fundamental change because it means that almost
399
    # every request needs more than one database query -- which
400
    # prompts us to start passing around $dbh instead of connecting
401
    # locally within each function as is done in Redmine.pm.
402
403 7:3c16ed8faa07 Chris
    my $sth = $dbh->prepare(
404
        "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
405
    );
406
407 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
408
	(__PACKAGE__, $r->server, $r->per_dir_config);
409
410
    my $prefix = $cfg->{SoundSoftwareRepoPrefix};
411
    if (!defined $prefix) { $prefix = '%/'; }
412
413 7:3c16ed8faa07 Chris
    my $identifier = '';
414
415 8:0c83d98252d9 Chris
    $sth->execute($prefix . $repo);
416 7:3c16ed8faa07 Chris
    my $ret = 0;
417
    if (my @row = $sth->fetchrow_array) {
418
	$identifier = $row[0];
419
    }
420
    $sth->finish();
421
    undef $sth;
422
423 8:0c83d98252d9 Chris
    print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n";
424 7:3c16ed8faa07 Chris
425
    $identifier;
426
}
427
428 8:0c83d98252d9 Chris
sub get_realm {
429
    my $dbh = shift;
430
    my $project_id = shift;
431
    my $r = shift;
432
433
    my $sth = $dbh->prepare(
434
        "SELECT projects.name FROM projects WHERE projects.identifier = ?;"
435
    );
436
437
    my $name = $project_id;
438
439
    $sth->execute($project_id);
440
    my $ret = 0;
441
    if (my @row = $sth->fetchrow_array) {
442
	$name = $row[0];
443
    }
444
    $sth->finish();
445
    undef $sth;
446
447
    # be timid about characters not permitted in auth realm and revert
448
    # to project identifier if any are found
449
    if ($name =~ m/[^\w\d\s\._-]/) {
450
	$name = $project_id;
451
    }
452
453
    my $realm = '"Mercurial repository for ' . "'$name'" . '"';
454
455
    $realm;
456
}
457
458 7:3c16ed8faa07 Chris
sub connect_database {
459
    my $r = shift;
460
461 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
462
	(__PACKAGE__, $r->server, $r->per_dir_config);
463
464
    return DBI->connect($cfg->{SoundSoftwareDSN},
465 152:a389c77da9fd Chris
	                $cfg->{SoundSoftwareDbUser},
466
		        $cfg->{SoundSoftwareDbPass});
467 7:3c16ed8faa07 Chris
}
468
469
1;
470
package Apache::Authn::SoundSoftware;
471
472
=head1 Apache::Authn::SoundSoftware
473
474
SoundSoftware - a mod_perl module for Apache authentication against a
475
Redmine database and optional LDAP implementing the access control
476
rules required for the SoundSoftware.ac.uk repository site.
477
478
=head1 SYNOPSIS
479
480
This module is closely based on the Redmine.pm authentication module
481
provided with Redmine.  It is intended to be used for authentication
482
in front of a repository service such as hgwebdir.
483
484
Requirements:
485
486
1. Clone/pull from repo for public project: Any user, no
487
authentication required
488
489
2. Clone/pull from repo for private project: Project members only
490
491
3. Push to repo for public project: "Permitted" users only (this
492 8:0c83d98252d9 Chris
probably means project members who are also identified in the hgrc web
493
section for the repository and so will be approved by hgwebdir?)
494 7:3c16ed8faa07 Chris
495 8:0c83d98252d9 Chris
4. Push to repo for private project: "Permitted" users only (as above)
496 7:3c16ed8faa07 Chris
497 300:034e9b00b341 chris
5. Push to any repo that is tracking an external repo: Refused always
498
499 7:3c16ed8faa07 Chris
=head1 INSTALLATION
500
501
Debian/ubuntu:
502
503
  apt-get install libapache-dbi-perl libapache2-mod-perl2 \
504 1575:42618fc5ab46 Chris
    libdbd-mysql-perl libdbd-pg-perl libio-socket-ssl-perl \
505
    libauthen-simple-ldap-perl
506 7:3c16ed8faa07 Chris
507
Note that LDAP support is hardcoded "on" in this script (it is
508
optional in the original Redmine.pm).
509
510
=head1 CONFIGURATION
511
512
   ## This module has to be in your perl path
513
   ## eg:  /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm
514
   PerlLoadModule Apache::Authn::SoundSoftware
515
516
   # Example when using hgwebdir
517
   ScriptAlias / "/var/hg/hgwebdir.cgi/"
518
519
   <Location />
520
       AuthName "Mercurial"
521
       AuthType Basic
522
       Require valid-user
523
       PerlAccessHandler Apache::Authn::SoundSoftware::access_handler
524
       PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler
525
       SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost"
526
       SoundSoftwareDbUser "redmine"
527
       SoundSoftwareDbPass "password"
528
       Options +ExecCGI
529
       AddHandler cgi-script .cgi
530
       ## Optional where clause (fulltext search would be slow and
531
       ## database dependant).
532
       # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)"
533 8:0c83d98252d9 Chris
       ## Optional prefix for local repository URLs
534
       # SoundSoftwareRepoPrefix "/var/hg/"
535 7:3c16ed8faa07 Chris
  </Location>
536
537
See the original Redmine.pm for further configuration notes.
538
539
=cut
540
541
use strict;
542
use warnings FATAL => 'all', NONFATAL => 'redefine';
543
544
use DBI;
545 1331:1e9b1bdd062e Chris
use Digest::SHA;
546 7:3c16ed8faa07 Chris
use Authen::Simple::LDAP;
547
use Apache2::Module;
548
use Apache2::Access;
549
use Apache2::ServerRec qw();
550
use Apache2::RequestRec qw();
551
use Apache2::RequestUtil qw();
552
use Apache2::Const qw(:common :override :cmd_how);
553
use APR::Pool ();
554
use APR::Table ();
555
556
my @directives = (
557
  {
558
    name => 'SoundSoftwareDSN',
559
    req_override => OR_AUTHCFG,
560
    args_how => TAKE1,
561
    errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
562
  },
563
  {
564
    name => 'SoundSoftwareDbUser',
565
    req_override => OR_AUTHCFG,
566
    args_how => TAKE1,
567
  },
568
  {
569
    name => 'SoundSoftwareDbPass',
570
    req_override => OR_AUTHCFG,
571
    args_how => TAKE1,
572
  },
573
  {
574
    name => 'SoundSoftwareDbWhereClause',
575
    req_override => OR_AUTHCFG,
576
    args_how => TAKE1,
577
  },
578
  {
579 8:0c83d98252d9 Chris
    name => 'SoundSoftwareRepoPrefix',
580 7:3c16ed8faa07 Chris
    req_override => OR_AUTHCFG,
581
    args_how => TAKE1,
582
  },
583 732:897bc2b63bfe Chris
  {
584
    name => 'SoundSoftwareSslRequired',
585
    req_override => OR_AUTHCFG,
586
    args_how => TAKE1,
587
  },
588 7:3c16ed8faa07 Chris
);
589
590
sub SoundSoftwareDSN {
591 8:0c83d98252d9 Chris
    my ($self, $parms, $arg) = @_;
592
    $self->{SoundSoftwareDSN} = $arg;
593
    my $query = "SELECT
594 301:6d3f8aeb51b7 chris
                 hashed_password, salt, auth_source_id, permissions
595 7:3c16ed8faa07 Chris
              FROM members, projects, users, roles, member_roles
596
              WHERE
597
                projects.id=members.project_id
598
                AND member_roles.member_id=members.id
599
                AND users.id=members.user_id
600
                AND roles.id=member_roles.role_id
601
                AND users.status=1
602
                AND login=?
603
                AND identifier=? ";
604 8:0c83d98252d9 Chris
    $self->{SoundSoftwareQuery} = trim($query);
605 7:3c16ed8faa07 Chris
}
606
607
sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); }
608
sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); }
609
sub SoundSoftwareDbWhereClause {
610 8:0c83d98252d9 Chris
    my ($self, $parms, $arg) = @_;
611
    $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." ");
612 7:3c16ed8faa07 Chris
}
613
614 8:0c83d98252d9 Chris
sub SoundSoftwareRepoPrefix {
615
    my ($self, $parms, $arg) = @_;
616
    if ($arg) {
617
	$self->{SoundSoftwareRepoPrefix} = $arg;
618
    }
619 7:3c16ed8faa07 Chris
}
620
621 732:897bc2b63bfe Chris
sub SoundSoftwareSslRequired { set_val('SoundSoftwareSslRequired', @_); }
622
623 7:3c16ed8faa07 Chris
sub trim {
624 8:0c83d98252d9 Chris
    my $string = shift;
625
    $string =~ s/\s{2,}/ /g;
626
    return $string;
627 7:3c16ed8faa07 Chris
}
628
629
sub set_val {
630 8:0c83d98252d9 Chris
    my ($key, $self, $parms, $arg) = @_;
631
    $self->{$key} = $arg;
632 7:3c16ed8faa07 Chris
}
633
634
Apache2::Module::add(__PACKAGE__, \@directives);
635
636
637
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
638
639
sub access_handler {
640 8:0c83d98252d9 Chris
    my $r = shift;
641 7:3c16ed8faa07 Chris
642 517:bd1d512f9e1b Chris
    print STDERR "SoundSoftware.pm:$$: In access handler at " . scalar localtime() . "\n";
643 7:3c16ed8faa07 Chris
644 8:0c83d98252d9 Chris
    unless ($r->some_auth_required) {
645
	$r->log_reason("No authentication has been configured");
646
	return FORBIDDEN;
647
    }
648 7:3c16ed8faa07 Chris
649 1575:42618fc5ab46 Chris
    if (!defined $r->user or $r->user eq '') {
650
        $r->user('*anon*'); # Apache 2.4+ requires auth module to set
651
                            # user even if no auth was needed
652
    }
653
654 8:0c83d98252d9 Chris
    my $method = $r->method;
655 7:3c16ed8faa07 Chris
656 517:bd1d512f9e1b Chris
    print STDERR "SoundSoftware.pm:$$: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n";
657
    print STDERR "SoundSoftware.pm:$$: Accept: " . $r->headers_in->{Accept} . "\n";
658 7:3c16ed8faa07 Chris
659 8:0c83d98252d9 Chris
    my $dbh = connect_database($r);
660 152:a389c77da9fd Chris
    unless ($dbh) {
661 517:bd1d512f9e1b Chris
	print STDERR "SoundSoftware.pm:$$: Database connection failed!: " . $DBI::errstr . "\n";
662 152:a389c77da9fd Chris
	return FORBIDDEN;
663
    }
664
665 300:034e9b00b341 chris
    print STDERR "Connected to db, dbh is " . $dbh . "\n";
666 7:3c16ed8faa07 Chris
667 8:0c83d98252d9 Chris
    my $project_id = get_project_identifier($dbh, $r);
668 300:034e9b00b341 chris
669 732:897bc2b63bfe Chris
    # We want to delegate most of the work to the authentication
670
    # handler (to ensure that user is asked to login even for
671
    # nonexistent projects -- so they can't tell whether a private
672
    # project exists or not without authenticating). So
673
    #
674
    # * if the project is public
675
    #   - if the method is read-only
676
    #     + set handler to OK, no auth needed
677
    #   - if the method is not read-only
678
    #     + if the repo is read-only, return forbidden
679
    #     + else require auth
680
    # * if the project is not public or does not exist
681
    #     + require auth
682
    #
683
    # If we are requiring auth and are not currently https, and
684
    # https is required, then we must return a redirect to https
685
    # instead of an OK.
686 300:034e9b00b341 chris
687 8:0c83d98252d9 Chris
    my $status = get_project_status($dbh, $project_id, $r);
688 732:897bc2b63bfe Chris
    my $readonly = project_repo_is_readonly($dbh, $project_id, $r);
689 7:3c16ed8faa07 Chris
690 8:0c83d98252d9 Chris
    $dbh->disconnect();
691
    undef $dbh;
692 7:3c16ed8faa07 Chris
693 734:1d1b8170c2f7 Chris
    my $auth_ssl_reqd = will_require_ssl_auth($r);
694
695 732:897bc2b63bfe Chris
    if ($status == 1) { # public
696
697
	print STDERR "SoundSoftware.pm:$$: Project is public\n";
698
699
	if (!defined $read_only_methods{$method}) {
700
701
	    print STDERR "SoundSoftware.pm:$$: Method is not read-only\n";
702
703
	    if ($readonly) {
704
		print STDERR "SoundSoftware.pm:$$: Project repo is read-only, refusing access\n";
705
		return FORBIDDEN;
706
	    } else {
707
		print STDERR "SoundSoftware.pm:$$: Project repo is read-write, auth required\n";
708
		# fall through, this is the normal case
709
	    }
710
711 734:1d1b8170c2f7 Chris
        } elsif ($auth_ssl_reqd and $r->unparsed_uri =~ m/cmd=branchmap/) {
712
713
            # A hac^H^H^Hspecial case. We want to ensure we switch to
714
            # https (if it will be necessarily for authentication)
715
            # before the first POST request, and this is what I think
716
            # will give us suitable warning for Mercurial.
717
718
            print STDERR "SoundSoftware.pm:$$: Switching to HTTPS in preparation\n";
719
            # fall through, this is the normal case
720
721 732:897bc2b63bfe Chris
	} else {
722
	    # Public project, read-only method -- this is the only
723
	    # case we can decide for certain to accept in this function
724
	    print STDERR "SoundSoftware.pm:$$: Method is read-only, no restriction here\n";
725
	    $r->set_handlers(PerlAuthenHandler => [\&OK]);
726
	    return OK;
727
	}
728
729
    } else { # status != 1, i.e. nonexistent or private -- equivalent here
730
731
	print STDERR "SoundSoftware.pm:$$: Project is private or nonexistent, auth required\n";
732
	# fall through
733 8:0c83d98252d9 Chris
    }
734 7:3c16ed8faa07 Chris
735 734:1d1b8170c2f7 Chris
    if ($auth_ssl_reqd) {
736
        my $redir_to = "https://" . $r->hostname() . $r->unparsed_uri();
737
        print STDERR "SoundSoftware.pm:$$: Need to switch to HTTPS, redirecting to $redir_to\n";
738
        $r->headers_out->add('Location' => $redir_to);
739
        return REDIRECT;
740 732:897bc2b63bfe Chris
    } else {
741 734:1d1b8170c2f7 Chris
        return OK;
742 732:897bc2b63bfe Chris
    }
743 7:3c16ed8faa07 Chris
}
744
745
sub authen_handler {
746 8:0c83d98252d9 Chris
    my $r = shift;
747
748 517:bd1d512f9e1b Chris
    print STDERR "SoundSoftware.pm:$$: In authentication handler at " . scalar localtime() . "\n";
749 7:3c16ed8faa07 Chris
750 8:0c83d98252d9 Chris
    my $dbh = connect_database($r);
751 152:a389c77da9fd Chris
    unless ($dbh) {
752 517:bd1d512f9e1b Chris
        print STDERR "SoundSoftware.pm:$$: Database connection failed!: " . $DBI::errstr . "\n";
753 152:a389c77da9fd Chris
        return AUTH_REQUIRED;
754
    }
755 8:0c83d98252d9 Chris
756
    my $project_id = get_project_identifier($dbh, $r);
757
    my $realm = get_realm($dbh, $project_id, $r);
758
    $r->auth_name($realm);
759
760
    my ($res, $redmine_pass) =  $r->get_basic_auth_pw();
761
    unless ($res == OK) {
762
	$dbh->disconnect();
763
	undef $dbh;
764
	return $res;
765
    }
766
767 517:bd1d512f9e1b Chris
    print STDERR "SoundSoftware.pm:$$: User is " . $r->user . ", got password\n";
768 8:0c83d98252d9 Chris
769 732:897bc2b63bfe Chris
    my $status = get_project_status($dbh, $project_id, $r);
770
    if ($status == 0) {
771
	# nonexistent, behave like private project you aren't a member of
772
	print STDERR "SoundSoftware.pm:$$: Project doesn't exist, not permitted\n";
773
	$dbh->disconnect();
774
	undef $dbh;
775
	$r->note_auth_failure();
776
	return AUTH_REQUIRED;
777
    }
778
779 8:0c83d98252d9 Chris
    my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r);
780
781
    $dbh->disconnect();
782
    undef $dbh;
783
784
    if ($permitted) {
785
	return OK;
786
    } else {
787 517:bd1d512f9e1b Chris
	print STDERR "SoundSoftware.pm:$$: Not permitted\n";
788 8:0c83d98252d9 Chris
	$r->note_auth_failure();
789
	return AUTH_REQUIRED;
790
    }
791 7:3c16ed8faa07 Chris
}
792
793
sub get_project_status {
794 8:0c83d98252d9 Chris
    my $dbh = shift;
795 7:3c16ed8faa07 Chris
    my $project_id = shift;
796
    my $r = shift;
797 8:0c83d98252d9 Chris
798
    if (!defined $project_id or $project_id eq '') {
799
	return 0; # nonexistent
800
    }
801 7:3c16ed8faa07 Chris
802
    my $sth = $dbh->prepare(
803
        "SELECT is_public FROM projects WHERE projects.identifier = ?;"
804
    );
805
806
    $sth->execute($project_id);
807 8:0c83d98252d9 Chris
    my $ret = 0; # nonexistent
808 7:3c16ed8faa07 Chris
    if (my @row = $sth->fetchrow_array) {
809
    	if ($row[0] eq "1" || $row[0] eq "t") {
810
	    $ret = 1; # public
811
    	} else {
812 8:0c83d98252d9 Chris
	    $ret = 2; # private
813 7:3c16ed8faa07 Chris
	}
814
    }
815
    $sth->finish();
816
    undef $sth;
817
818
    $ret;
819
}
820
821 734:1d1b8170c2f7 Chris
sub will_require_ssl_auth {
822
    my $r = shift;
823
824
    my $cfg = Apache2::Module::get_config
825
        (__PACKAGE__, $r->server, $r->per_dir_config);
826
827
    if ($cfg->{SoundSoftwareSslRequired} eq "on") {
828
        if ($r->dir_config('HTTPS') eq "on") {
829
            # already have ssl
830
            return 0;
831
        } else {
832
            # require ssl for auth, don't have it yet
833
            return 1;
834
        }
835
    } elsif ($cfg->{SoundSoftwareSslRequired} eq "off") {
836
        # don't require ssl for auth
837
        return 0;
838
    } else {
839
        print STDERR "WARNING: SoundSoftware.pm:$$: SoundSoftwareSslRequired should be either 'on' or 'off'\n";
840
        # this is safer
841
        return 1;
842
    }
843
}
844
845 300:034e9b00b341 chris
sub project_repo_is_readonly {
846
    my $dbh = shift;
847
    my $project_id = shift;
848
    my $r = shift;
849
850
    if (!defined $project_id or $project_id eq '') {
851
        return 0; # nonexistent
852
    }
853
854
    my $sth = $dbh->prepare(
855
        "SELECT repositories.is_external FROM repositories, projects WHERE projects.identifier = ? AND repositories.project_id = projects.id;"
856
    );
857
858
    $sth->execute($project_id);
859
    my $ret = 0; # nonexistent
860
    if (my @row = $sth->fetchrow_array) {
861 301:6d3f8aeb51b7 chris
        if (defined($row[0]) && ($row[0] eq "1" || $row[0] eq "t")) {
862 300:034e9b00b341 chris
            $ret = 1; # read-only (i.e. external)
863
        } else {
864
            $ret = 0; # read-write
865
        }
866
    }
867
    $sth->finish();
868
    undef $sth;
869
870
    $ret;
871
}
872
873 8:0c83d98252d9 Chris
sub is_permitted {
874
    my $dbh = shift;
875
    my $project_id = shift;
876
    my $redmine_user = shift;
877
    my $redmine_pass = shift;
878
    my $r = shift;
879 7:3c16ed8faa07 Chris
880 1331:1e9b1bdd062e Chris
    my $pass_digest = Digest::SHA::sha1_hex($redmine_pass);
881 7:3c16ed8faa07 Chris
882 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
883
	(__PACKAGE__, $r->server, $r->per_dir_config);
884 7:3c16ed8faa07 Chris
885 8:0c83d98252d9 Chris
    my $query = $cfg->{SoundSoftwareQuery};
886
    my $sth = $dbh->prepare($query);
887
    $sth->execute($redmine_user, $project_id);
888 7:3c16ed8faa07 Chris
889 8:0c83d98252d9 Chris
    my $ret;
890 301:6d3f8aeb51b7 chris
    while (my ($hashed_password, $salt, $auth_source_id, $permissions) = $sth->fetchrow_array) {
891 7:3c16ed8faa07 Chris
892 8:0c83d98252d9 Chris
	# Test permissions for this user before we verify credentials
893
	# -- if the user is not permitted this action anyway, there's
894
	# not much point in e.g. contacting the LDAP
895 7:3c16ed8faa07 Chris
896 8:0c83d98252d9 Chris
	my $method = $r->method;
897 7:3c16ed8faa07 Chris
898 8:0c83d98252d9 Chris
	if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/)
899
	    || $permissions =~ /:commit_access/) {
900
901
	    # User would be permitted this action, if their
902
	    # credentials checked out -- test those now
903
904
	    print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n";
905
906
	    unless ($auth_source_id) {
907 1331:1e9b1bdd062e Chris
                my $salted_password = Digest::SHA::sha1_hex($salt.$pass_digest);
908 301:6d3f8aeb51b7 chris
		if ($hashed_password eq $salted_password) {
909 8:0c83d98252d9 Chris
		    print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n";
910
		    $ret = 1;
911
		    last;
912
		}
913
	    } else {
914
		my $sthldap = $dbh->prepare(
915
		    "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;"
916
		    );
917
		$sthldap->execute($auth_source_id);
918
		while (my @rowldap = $sthldap->fetchrow_array) {
919
		    my $ldap = Authen::Simple::LDAP->new(
920
			host    => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
921
			port    => $rowldap[1],
922
			basedn  => $rowldap[5],
923
			binddn  => $rowldap[3] ? $rowldap[3] : "",
924
			bindpw  => $rowldap[4] ? $rowldap[4] : "",
925
			filter  => "(".$rowldap[6]."=%s)"
926
			);
927
		    if ($ldap->authenticate($redmine_user, $redmine_pass)) {
928 517:bd1d512f9e1b Chris
			print STDERR "SoundSoftware.pm:$$: User $redmine_user authenticated via LDAP\n";
929 8:0c83d98252d9 Chris
			$ret = 1;
930
		    }
931
		}
932
		$sthldap->finish();
933
		undef $sthldap;
934 735:8653bddf26a6 Chris
                last if ($ret);
935 8:0c83d98252d9 Chris
	    }
936
	} else {
937 517:bd1d512f9e1b Chris
	    print STDERR "SoundSoftware.pm:$$: User $redmine_user lacks required role for this project\n";
938 8:0c83d98252d9 Chris
	}
939 7:3c16ed8faa07 Chris
    }
940
941 8:0c83d98252d9 Chris
    $sth->finish();
942
    undef $sth;
943
944
    $ret;
945 7:3c16ed8faa07 Chris
}
946
947
sub get_project_identifier {
948 8:0c83d98252d9 Chris
    my $dbh = shift;
949 7:3c16ed8faa07 Chris
    my $r = shift;
950
    my $location = $r->location;
951 737:1ce6efe3db0e Chris
    my ($repo) = $r->uri =~ m{$location/*([^/]*)};
952 10:2c10dc5f122d Chris
953
    return $repo if (!$repo);
954
955 7:3c16ed8faa07 Chris
    $repo =~ s/[^a-zA-Z0-9\._-]//g;
956 736:51c97efbe241 Chris
957 8:0c83d98252d9 Chris
    # The original Redmine.pm returns the string just calculated as
958
    # the project identifier.  That won't do for us -- we may have
959
    # (and in fact already do have, in our test instance) projects
960
    # whose repository names differ from the project identifiers.
961
962
    # This is a rather fundamental change because it means that almost
963
    # every request needs more than one database query -- which
964
    # prompts us to start passing around $dbh instead of connecting
965
    # locally within each function as is done in Redmine.pm.
966
967 7:3c16ed8faa07 Chris
    my $sth = $dbh->prepare(
968
        "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;"
969
    );
970
971 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
972
	(__PACKAGE__, $r->server, $r->per_dir_config);
973
974
    my $prefix = $cfg->{SoundSoftwareRepoPrefix};
975
    if (!defined $prefix) { $prefix = '%/'; }
976 7:3c16ed8faa07 Chris
    my $identifier = '';
977
978 8:0c83d98252d9 Chris
    $sth->execute($prefix . $repo);
979 7:3c16ed8faa07 Chris
    my $ret = 0;
980
    if (my @row = $sth->fetchrow_array) {
981
	$identifier = $row[0];
982
    }
983
    $sth->finish();
984
    undef $sth;
985
986 517:bd1d512f9e1b Chris
    print STDERR "SoundSoftware.pm:$$: Repository '$repo' belongs to project '$identifier'\n";
987 7:3c16ed8faa07 Chris
988
    $identifier;
989
}
990
991 8:0c83d98252d9 Chris
sub get_realm {
992
    my $dbh = shift;
993
    my $project_id = shift;
994
    my $r = shift;
995
996
    my $sth = $dbh->prepare(
997
        "SELECT projects.name FROM projects WHERE projects.identifier = ?;"
998
    );
999
1000
    my $name = $project_id;
1001
1002
    $sth->execute($project_id);
1003
    my $ret = 0;
1004
    if (my @row = $sth->fetchrow_array) {
1005
	$name = $row[0];
1006
    }
1007
    $sth->finish();
1008
    undef $sth;
1009
1010
    # be timid about characters not permitted in auth realm and revert
1011
    # to project identifier if any are found
1012
    if ($name =~ m/[^\w\d\s\._-]/) {
1013
	$name = $project_id;
1014 733:c7a731db96e5 Chris
    } elsif ($name =~ m/^\s*$/) {
1015
	# empty or whitespace
1016
	$name = $project_id;
1017
    }
1018
1019
    if ($name =~ m/^\s*$/) {
1020
        # nothing even in $project_id -- probably a nonexistent project.
1021
        # use repo name instead (don't want to admit to user that project
1022
        # doesn't exist)
1023
        my $location = $r->location;
1024 737:1ce6efe3db0e Chris
        my ($repo) = $r->uri =~ m{$location/*([^/]*)};
1025 733:c7a731db96e5 Chris
        $name = $repo;
1026 8:0c83d98252d9 Chris
    }
1027
1028 1271:cf4cc816278a Chris
#    my $realm = '"Mercurial repository for ' . "'$name'" . '"';
1029
# see #577:
1030
    my $realm = '"Mercurial repository for ' . "$name" . '"';
1031 8:0c83d98252d9 Chris
1032
    $realm;
1033
}
1034
1035 7:3c16ed8faa07 Chris
sub connect_database {
1036
    my $r = shift;
1037
1038 8:0c83d98252d9 Chris
    my $cfg = Apache2::Module::get_config
1039
	(__PACKAGE__, $r->server, $r->per_dir_config);
1040
1041
    return DBI->connect($cfg->{SoundSoftwareDSN},
1042 152:a389c77da9fd Chris
	                $cfg->{SoundSoftwareDbUser},
1043
		        $cfg->{SoundSoftwareDbPass});
1044 7:3c16ed8faa07 Chris
}
1045
1046
1;
1047 217:ed8222a04634 chris
#!/usr/bin/env ruby
1048
1049
# == Synopsis
1050
#
1051 241:7658d21a1493 chris
# convert-external-repos: Update local Mercurial mirrors of external repos,
1052
# by running an external command for each project requiring an update.
1053 217:ed8222a04634 chris
#
1054
# == Usage
1055
#
1056
#    convert-external-repos [OPTIONS...] -s [DIR] -r [HOST]
1057
#
1058
# == Arguments (mandatory)
1059
#
1060 241:7658d21a1493 chris
#   -s, --scm-dir=DIR         use DIR as base directory for repositories
1061 217:ed8222a04634 chris
#   -r, --redmine-host=HOST   assume Redmine is hosted on HOST. Examples:
1062
#                             -r redmine.example.net
1063
#                             -r http://redmine.example.net
1064
#                             -r https://example.net/redmine
1065
#   -k, --key=KEY             use KEY as the Redmine API key
1066 241:7658d21a1493 chris
#   -c, --command=COMMAND     use this command to update each external
1067
#                             repository: command is called with the name
1068
#                             of the project, the path to its repo, and
1069
#                             its external repo url as its three args
1070 217:ed8222a04634 chris
#
1071
# == Options
1072
#
1073
#   --http-user=USER          User for HTTP Basic authentication with Redmine WS
1074
#   --http-pass=PASSWORD      Password for Basic authentication with Redmine WS
1075
#   -t, --test                only show what should be done
1076
#   -h, --help                show help and exit
1077
#   -v, --verbose             verbose
1078
#   -V, --version             print version and exit
1079
#   -q, --quiet               no log
1080
1081
1082
require 'getoptlong'
1083
require 'find'
1084
require 'etc'
1085
1086
Version = "1.0"
1087
1088
opts = GetoptLong.new(
1089 241:7658d21a1493 chris
                      ['--scm-dir',      '-s', GetoptLong::REQUIRED_ARGUMENT],
1090 217:ed8222a04634 chris
                      ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
1091
                      ['--key',          '-k', GetoptLong::REQUIRED_ARGUMENT],
1092
                      ['--http-user',          GetoptLong::REQUIRED_ARGUMENT],
1093
                      ['--http-pass',          GetoptLong::REQUIRED_ARGUMENT],
1094 241:7658d21a1493 chris
                      ['--command' ,     '-c', GetoptLong::REQUIRED_ARGUMENT],
1095 217:ed8222a04634 chris
                      ['--test',         '-t', GetoptLong::NO_ARGUMENT],
1096
                      ['--verbose',      '-v', GetoptLong::NO_ARGUMENT],
1097
                      ['--version',      '-V', GetoptLong::NO_ARGUMENT],
1098
                      ['--help'   ,      '-h', GetoptLong::NO_ARGUMENT],
1099
                      ['--quiet'  ,      '-q', GetoptLong::NO_ARGUMENT]
1100
                      )
1101
1102
$verbose      = 0
1103
$quiet        = false
1104
$redmine_host = ''
1105
$repos_base   = ''
1106
$http_user    = ''
1107
$http_pass    = ''
1108
$test         = false
1109
1110 437:102056ec2de9 chris
$mirrordir    = '/var/mirror'
1111
1112 217:ed8222a04634 chris
def log(text, options={})
1113
  level = options[:level] || 0
1114
  puts text unless $quiet or level > $verbose
1115
  exit 1 if options[:exit]
1116
end
1117
1118
def system_or_raise(command)
1119
  raise "\"#{command}\" failed" unless system command
1120
end
1121
1122
begin
1123
  opts.each do |opt, arg|
1124
    case opt
1125 241:7658d21a1493 chris
    when '--scm-dir';        $repos_base   = arg.dup
1126 217:ed8222a04634 chris
    when '--redmine-host';   $redmine_host = arg.dup
1127
    when '--key';            $api_key      = arg.dup
1128
    when '--http-user';      $http_user    = arg.dup
1129
    when '--http-pass';      $http_pass    = arg.dup
1130 241:7658d21a1493 chris
    when '--command';        $command      = arg.dup
1131 217:ed8222a04634 chris
    when '--verbose';        $verbose += 1
1132
    when '--test';           $test = true
1133
    when '--version';        puts Version; exit
1134 1336:b61a51fb42b9 Chris
    when '--help';           puts "Read source for documentation"; exit
1135 217:ed8222a04634 chris
    when '--quiet';          $quiet = true
1136
    end
1137
  end
1138
rescue
1139
  exit 1
1140
end
1141
1142
if $test
1143
  log("running in test mode")
1144
end
1145
1146 241:7658d21a1493 chris
if ($redmine_host.empty? or $repos_base.empty? or $command.empty?)
1147 1336:b61a51fb42b9 Chris
  puts "Read source for documentation"; exit
1148 217:ed8222a04634 chris
end
1149
1150
unless File.directory?($repos_base)
1151
  log("directory '#{$repos_base}' doesn't exist", :exit => true)
1152
end
1153
1154
begin
1155
  require 'active_resource'
1156
rescue LoadError
1157
  log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
1158
end
1159
1160
class Project < ActiveResource::Base
1161
  self.headers["User-agent"] = "SoundSoftware external repository converter/#{Version}"
1162 1336:b61a51fb42b9 Chris
  self.format = :xml
1163 217:ed8222a04634 chris
end
1164
1165
log("querying Redmine for projects...", :level => 1);
1166
1167
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
1168
$redmine_host.gsub!(/\/$/, '')
1169
1170
Project.site = "#{$redmine_host}/sys";
1171
Project.user = $http_user;
1172
Project.password = $http_pass;
1173
1174
begin
1175
  # Get all active projects that have the Repository module enabled
1176
  projects = Project.find(:all, :params => {:key => $api_key})
1177 1336:b61a51fb42b9 Chris
rescue ActiveResource::ForbiddenAccess
1178
  log("Request was denied by your Redmine server. Make sure that 'WS for repository management' is enabled in application settings and that you provided the correct API key.")
1179 217:ed8222a04634 chris
rescue => e
1180
  log("Unable to connect to #{Project.site}: #{e}", :exit => true)
1181
end
1182
1183
if projects.nil?
1184
  log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
1185
end
1186
1187
log("retrieved #{projects.size} projects", :level => 1)
1188
1189
projects.each do |project|
1190
  log("treating project #{project.name}", :level => 1)
1191
1192
  if project.identifier.empty?
1193
    log("\tno identifier for project #{project.name}")
1194
    next
1195 1445:0c7b3bb73517 Chris
  elsif not project.identifier.match(/^[a-z0-9_\-]+$/)
1196 217:ed8222a04634 chris
    log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
1197
    next
1198
  end
1199
1200
  if !project.respond_to?(:repository) or !project.repository.is_external?
1201
    log("\tproject #{project.identifier} does not use an external repository");
1202
    next
1203
  end
1204
1205
  external_url = project.repository.external_url;
1206
  log("\tproject #{project.identifier} has external repository url #{external_url}");
1207
1208
  if !external_url.match(/^[a-z][a-z+]{0,8}[a-z]:\/\//)
1209
    log("\tthis doesn't look like a plausible url to me, skipping")
1210
    next
1211
  end
1212
1213
  repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
1214
1215 241:7658d21a1493 chris
  unless File.directory?(repos_path)
1216
    log("\tproject repo directory '#{repos_path}' doesn't exist")
1217 217:ed8222a04634 chris
    next
1218
  end
1219
1220 241:7658d21a1493 chris
  system($command, project.identifier, repos_path, external_url)
1221 437:102056ec2de9 chris
1222
  $cache_clearance_file = File.join($mirrordir, project.identifier, 'url_changed')
1223
  if File.file?($cache_clearance_file)
1224
    log("\tproject repo url has changed, requesting cache clearance")
1225
    if project.post(:repository_cache, :key => $api_key)
1226
      File.delete($cache_clearance_file)
1227
    end
1228
  end
1229 217:ed8222a04634 chris
1230
end
1231
1232 1538:87bea4981d6d Chris
#!/usr/bin/env ruby
1233
1234
# Create authormap files for hg repos based on the changeset & project
1235
# member info available to Redmine.
1236
#
1237
# We have a set of hg repos in a given directory:
1238
#
1239
# /var/hg/repo_1
1240
# /var/hg/repo_2
1241
# /var/hg/repo_3
1242
#
1243
# and we want to produce authormap files in another directory:
1244
#
1245
# /var/repo-export/authormap/authormap_repo_1
1246
# /var/repo-export/authormap/authormap_repo_2
1247
# /var/repo-export/authormap/authormap_repo_3
1248
#
1249
# This script does that, if given the two directory names as arguments
1250
# to the -s and -o options. In the above example:
1251
#
1252 1543:05d639e5d59b Chris
# ./script/rails runner -e production extra/soundsoftware/create-repo-authormaps.rb -s /var/hg -o /var/repo-export/authormap
1253 1538:87bea4981d6d Chris
#
1254
# Note that this script will overwrite any existing authormap
1255
# files. (That's why the output files are given an authormap_ prefix,
1256
# so we're less likely to clobber something else if the user gets the
1257
# arguments wrong.)
1258
1259
require 'getoptlong'
1260
1261
opts = GetoptLong.new(
1262 1543:05d639e5d59b Chris
                      ['--scm-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
1263
                      ['--out-dir', '-o', GetoptLong::REQUIRED_ARGUMENT],
1264
                      ['--environment', '-e', GetoptLong::OPTIONAL_ARGUMENT]
1265 1538:87bea4981d6d Chris
)
1266
1267
$repos_base   = ''
1268
$out_base     = ''
1269
1270
def usage
1271
  puts "See source code for supported options"
1272
  exit
1273
end
1274
1275
begin
1276
  opts.each do |opt, arg|
1277
    case opt
1278
    when '--scm-dir';   $repos_base   = arg.dup
1279
    when '--out-dir';   $out_base     = arg.dup
1280
    end
1281
  end
1282
rescue
1283
  exit 1
1284
end
1285
1286
if ($repos_base.empty? or $out_base.empty?)
1287
  usage
1288
end
1289
1290
unless File.directory?($repos_base)
1291 1540:322d7b57e5f0 chris
  puts "input directory '#{$repos_base}' doesn't exist"
1292
  exit 1
1293 1538:87bea4981d6d Chris
end
1294
1295
unless File.directory?($out_base)
1296 1540:322d7b57e5f0 chris
  puts "output directory '#{$out_base}' doesn't exist"
1297
  exit 1
1298 1538:87bea4981d6d Chris
end
1299
1300
projects = Project.find(:all)
1301
1302
if projects.nil?
1303 1540:322d7b57e5f0 chris
  puts 'No projects found'
1304
  exit 1
1305 1538:87bea4981d6d Chris
end
1306
1307
projects.each do |proj|
1308 1548:fd4cc11ae096 Chris
1309
  next unless proj.is_public
1310
1311 1538:87bea4981d6d Chris
  next unless proj.respond_to?(:repository)
1312
1313
  repo = proj.repository
1314 1539:22d57b0e0a77 chris
  next if repo.nil? or repo.url.empty?
1315 1538:87bea4981d6d Chris
1316
  repo_url = repo.url
1317
  repo_url = repo_url.gsub(/^file:\/*/, "/");
1318
  if repo_url != File.join($repos_base, proj.identifier)
1319 1542:60acfbd8f6d6 Chris
    puts "Project #{proj.identifier} has repo in unsupported location #{repo_url}, skipping"
1320 1538:87bea4981d6d Chris
    next
1321
  end
1322
1323 1542:60acfbd8f6d6 Chris
  committers = repo.committers
1324 1538:87bea4981d6d Chris
1325
  authormap = ""
1326 1542:60acfbd8f6d6 Chris
  committers.each do |c, uid|
1327 1552:3a2254124fa8 Chris
1328
    # Some of our repos have broken email addresses in them: e.g. one
1329
    # changeset has a committer name of the form
1330
    #
1331
    # NAME <name <NAME <name@example.com">
1332
    #
1333
    # I don't know how it got like that... If the committer has more
1334
    # than one '<' in it, truncate it just before the first one, and
1335 1553:baac54711ee9 Chris
    # then look up the author name again.
1336
    #
1337 1552:3a2254124fa8 Chris
    if c =~ /<.*</ then
1338 1553:baac54711ee9 Chris
      # So this is a completely pathological case
1339
      user = User.find_by_id uid
1340
      if user.nil? then
1341
        # because the given committer is bogus, we must write something in the map
1342 1554:e5c9809534a2 Chris
        name = c.sub(/\s*<.*$/, "")
1343
        authormap << "#{c}=#{name} <unknown@example.com>\n"
1344 1553:baac54711ee9 Chris
      else
1345
        authormap << "#{c}=#{user.name} <#{user.mail}>\n"
1346
      end
1347
    elsif not c =~ /[^<]+<.*@.*>/ then
1348
      # This is the "normal" case that needs work, where a user has
1349
      # their name in the commit but no email address
1350 1542:60acfbd8f6d6 Chris
      user = User.find_by_id uid
1351 1539:22d57b0e0a77 chris
      authormap << "#{c}=#{user.name} <#{user.mail}>\n" unless user.nil?
1352 1538:87bea4981d6d Chris
    end
1353
  end
1354
1355 1539:22d57b0e0a77 chris
  File.open(File.join($out_base, "authormap_#{proj.identifier}"), "w") do |f|
1356 1538:87bea4981d6d Chris
    f.puts(authormap)
1357
  end
1358
1359
end
1360
1361 1570:ae2f71010562 Chris
1362
# For documentation and experimental purposes only. As a
1363
# reconstruction of the machine image that runs this application,
1364 1573:8edb54e29f00 Chris
# there are lots of things missing here; but as a good Docker
1365
# configuration, it fails by mixing together rather a lot of concerns.
1366 1570:ae2f71010562 Chris
1367 1569:26a4f99ec679 Chris
FROM ubuntu:16.04
1368
MAINTAINER Chris Cannam <cannam@all-day-breakfast.com>
1369 1570:ae2f71010562 Chris
1370 1569:26a4f99ec679 Chris
RUN apt-get update && \
1371
    apt-get install -y \
1372 1570:ae2f71010562 Chris
    apache2 \
1373
    apache2-dev \
1374
    apt-utils \
1375 1569:26a4f99ec679 Chris
    build-essential \
1376 1570:ae2f71010562 Chris
    cron \
1377
    curl \
1378
    doxygen \
1379
    exim4 \
1380
    git \
1381
    graphviz \
1382
    imagemagick \
1383
    libapache-dbi-perl \
1384
    libapache2-mod-perl2 \
1385
    libapr1-dev \
1386
    libaprutil1-dev \
1387
    libauthen-simple-ldap-perl \
1388
    libcurl4-openssl-dev \
1389
    libdbd-pg-perl \
1390
    libpq-dev \
1391
    libmagickwand-dev \
1392
    libio-socket-ssl-perl \
1393
    logrotate \
1394
    mercurial \
1395
    postgresql \
1396
    rsync \
1397
    ruby \
1398
    ruby-dev \
1399 1572:2b1b8ebb7d98 Chris
    sudo
1400
1401 1573:8edb54e29f00 Chris
# Also used on the live site, for javadoc extraction, but this is
1402
# would be by far the biggest package here: let's omit it while we're
1403
# not making use of it
1404
#   openjdk-9-jdk-headless
1405
1406 1572:2b1b8ebb7d98 Chris
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
1407 1570:ae2f71010562 Chris
1408
1409
# Passenger gets installed through gem, not apt
1410
1411
RUN gem install passenger -v 4.0.60 --no-rdoc --no-ri
1412
RUN passenger-install-apache2-module --languages=ruby
1413
1414
1415
# Copy across webapp, set up ownership
1416
1417
COPY . /var/www/code
1418
1419 1569:26a4f99ec679 Chris
RUN groupadd code
1420
RUN useradd -g code -G www-data code
1421
RUN chown -R code.www-data /var/www/code
1422 1570:ae2f71010562 Chris
RUN find /var/www/code -type d -exec chmod g+s \{\} \;
1423
1424
1425
# We're based in the code webapp directory from here on
1426
1427 1569:26a4f99ec679 Chris
WORKDIR /var/www/code
1428 1570:ae2f71010562 Chris
1429
1430 1574:7b23adecd963 Chris
# Set up database config etc
1431 1570:ae2f71010562 Chris
1432 1574:7b23adecd963 Chris
RUN cp extra/soundsoftware/dockertest/database.yml.interpolated config/database.yml
1433 1570:ae2f71010562 Chris
1434
1435 1573:8edb54e29f00 Chris
# Install Rails and dependencies (database.yml must be populated before this)
1436 1570:ae2f71010562 Chris
1437 1569:26a4f99ec679 Chris
RUN gem install bundler
1438
RUN bundle install
1439 1570:ae2f71010562 Chris
1440
1441 1573:8edb54e29f00 Chris
# Initialise Redmine token (bundler must be installed before this)
1442
1443
RUN bundle exec rake generate_secret_token
1444
1445
1446 1570:ae2f71010562 Chris
# Import Postgres database from postgres-dumpall file
1447
1448 1569:26a4f99ec679 Chris
RUN chown postgres postgres-dumpall
1449 1571:4c2b25b7e85f Chris
RUN /etc/init.d/postgresql start && sudo -u postgres psql -f postgres-dumpall postgres
1450 1570:ae2f71010562 Chris
1451
1452
# Install Perl auth module for Hg access
1453
1454
RUN mkdir -p /usr/local/lib/site_perl/Apache/Authn/
1455
RUN cp extra/soundsoftware/SoundSoftware.pm /usr/local/lib/site_perl/Apache/Authn/
1456
1457
1458 1571:4c2b25b7e85f Chris
# Set up Apache config (todo: insert variables)
1459 1570:ae2f71010562 Chris
1460 1571:4c2b25b7e85f Chris
RUN rm -f /etc/apache2/sites-enabled/000-default.conf
1461
1462
RUN cp extra/soundsoftware/dockertest/passenger.conf /etc/apache2/mods-available/
1463
RUN cp extra/soundsoftware/dockertest/passenger.load /etc/apache2/mods-available/
1464
RUN cp extra/soundsoftware/dockertest/perl.conf      /etc/apache2/mods-available/
1465
1466
RUN ln -s ../mods-available/passenger.conf  /etc/apache2/mods-enabled/
1467
RUN ln -s ../mods-available/passenger.load  /etc/apache2/mods-enabled/
1468
RUN ln -s ../mods-available/perl.conf       /etc/apache2/mods-enabled/
1469
RUN ln -s ../mods-available/expires.load    /etc/apache2/mods-enabled/
1470
RUN ln -s ../mods-available/rewrite.load    /etc/apache2/mods-enabled/
1471
1472 1574:7b23adecd963 Chris
RUN cp extra/soundsoftware/dockertest/code.conf.interpolated /etc/apache2/sites-available/code.conf
1473 1570:ae2f71010562 Chris
RUN ln -s ../sites-available/code.conf /etc/apache2/sites-enabled/10-code.conf
1474 1571:4c2b25b7e85f Chris
1475 1572:2b1b8ebb7d98 Chris
RUN apache2ctl configtest
1476 1571:4c2b25b7e85f Chris
1477
1478 1572:2b1b8ebb7d98 Chris
# Start Postgres and foregrounded Apache
1479
1480
RUN echo "#!/bin/bash"                      > container-run.sh
1481
RUN echo "/etc/init.d/postgresql start"    >> container-run.sh
1482
RUN echo "apache2ctl -D FOREGROUND"        >> container-run.sh
1483
RUN chmod +x container-run.sh
1484
1485 1571:4c2b25b7e85f Chris
EXPOSE 80
1486 1572:2b1b8ebb7d98 Chris
CMD ./container-run.sh
1487 1571:4c2b25b7e85f Chris
1488 1570:ae2f71010562 Chris
1489
# A test Apache config. Lacks SSL, lacks a desirable extra layer of
1490
# authentication for admin interface paths. Do not deploy this.
1491
1492
PerlLoadModule Apache::Authn::SoundSoftware
1493
1494
<VirtualHost *:80>
1495
        ServerName code.soundsoftware.ac.uk
1496
        ServerAdmin chris.cannam@soundsoftware.ac.uk
1497
1498
        DocumentRoot /var/www/code/public
1499
        PassengerRestartDir restart_files
1500
        PassengerHighPerformance on
1501
        PassengerMaxRequests 50000
1502
        PassengerStatThrottleRate 5
1503
	PassengerStartTimeout 60
1504 1571:4c2b25b7e85f Chris
	PassengerFriendlyErrorPages on
1505 1570:ae2f71010562 Chris
        RailsSpawnMethod smart
1506
        ExpiresDefault "access plus 1 minute"
1507
1508
        <DirectoryMatch "^/.*/\.svn/">
1509
                Order allow,deny
1510
                Deny from all
1511
                Satisfy All
1512
        </DirectoryMatch>
1513
1514
        <DirectoryMatch "^/.*/\.hg/">
1515
                Order allow,deny
1516
                Deny from all
1517
                Satisfy All
1518
        </DirectoryMatch>
1519
1520
        <DirectoryMatch "^/.*/\.git/">
1521
                Order allow,deny
1522
                Deny from all
1523
                Satisfy All
1524
        </DirectoryMatch>
1525
1526
        <Directory /var/www/code/public>
1527
                Options -MultiViews
1528
	</Directory>
1529
1530
        <Directory /var/www/code/public/themes/soundsoftware/stylesheets/fonts>
1531
		# Avoid other sites embedding our fonts
1532
		RewriteEngine on
1533
		RewriteCond %{HTTP_REFERER} !^$
1534
		RewriteCond %{HTTP_REFERER} !^http(s)?://code.soundsoftware.ac.uk/.*$ [NC]
1535
		RewriteRule \.(ttf|woff|eot|otf|svg|zip|gz|html|txt)$ - [F]
1536
	</Directory>
1537
1538
	ScriptAlias /hg "/var/hg/index.cgi"
1539
1540
	<Location /hg>
1541
               	AuthName "Mercurial"
1542
                AuthType Basic
1543
                Require valid-user
1544
		PerlAccessHandler Apache::Authn::SoundSoftware::access_handler
1545
      		PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler
1546
		PerlSetVar HTTPS "on"
1547
		SoundSoftwareDSN "dbi:Pg:database=code;host=localhost"
1548
    		SoundSoftwareDbUser "code"
1549
     		SoundSoftwareDbPass "INSERT_POSTGRES_PASSWORD_HERE"
1550
		SoundSoftwareRepoPrefix "/var/hg/"
1551
		SoundSoftwareSslRequired "on"
1552
		Options +ExecCGI
1553
		AddHandler cgi-script .cgi
1554
		ExpiresDefault now
1555
        </Location>
1556
1557
	Alias /git "/var/files/git-mirror"
1558
1559
	<Directory "/var/files/git-mirror">
1560
		Options -Indexes +FollowSymLinks
1561
                Order allow,deny
1562
                Allow from all
1563
	</Directory>
1564
	<Directory ~ "/var/files/git-mirror/.*\.workdir">
1565
		Order allow,deny
1566
		Deny from all
1567
	</Directory>
1568
	<Directory ~ "/var/files/git-mirror/__.*">
1569
                Order allow,deny
1570
                Deny from all
1571
	</Directory>
1572
1573
	ErrorLog /var/log/apache2/code-error.log
1574
	CustomLog /var/log/apache2/code-access.log vhost_combined
1575
1576
        LogLevel warn
1577
        ServerSignature Off
1578
1579
</VirtualHost>
1580
1581
production:
1582
  adapter: postgresql
1583
  database: code
1584
  host: localhost
1585
  username: code
1586
  password: "INSERT_POSTGRES_PASSWORD_HERE"
1587
1588 1571:4c2b25b7e85f Chris
PassengerMaxPoolSize 60
1589
1590
LoadModule passenger_module /var/lib/gems/2.3.0/gems/passenger-4.0.60/buildout/apache2/mod_passenger.so
1591
PassengerRoot /var/lib/gems/2.3.0/gems/passenger-4.0.60
1592
PassengerDefaultRuby /usr/bin/ruby2.3
1593
# Apache::DBI is supposed to be a transparent replacement for Perl DBI with
1594
# better performance when multiple connections are made with common DSN, user
1595
# and password
1596
PerlModule Apache::DBI
1597 1569:26a4f99ec679 Chris
#!/bin/bash
1598
1599 1574:7b23adecd963 Chris
dbpwd="$1"
1600
if [ -z "$dbpwd" ]; then
1601
    echo "Usage: $0 <database-password>" 1>&2
1602
    exit 2
1603
fi
1604
1605 1569:26a4f99ec679 Chris
set -eu
1606
1607
dockerdir=./extra/soundsoftware/dockertest
1608
if [ ! -d "$dockerdir" ]; then
1609
    echo "Run this script from the root of a working copy of soundsoftware-site"
1610
    exit 2
1611
fi
1612
1613 1574:7b23adecd963 Chris
for f in database.yml code.conf ; do
1614
    cat "$dockerdir/$f" |
1615
        sed 's/INSERT_POSTGRES_PASSWORD_HERE/'"$dbpwd"'/g' > \
1616
            "$dockerdir/$f.interpolated"
1617
done
1618
1619 1569:26a4f99ec679 Chris
dockertag="cannam/soundsoftware-site"
1620
1621
sudo docker build -t "$dockertag" -f "$dockerdir/Dockerfile" .
1622 1571:4c2b25b7e85f Chris
sudo docker run -p 8080:80 -d "$dockertag"
1623 1569:26a4f99ec679 Chris
1624 226:5b028aef59a7 chris
#!/usr/bin/perl -w
1625
1626
# Read a Doxyfile and print it out again to stdout, with only
1627
# whitelisted keys in it and with some keys set to pre-fixed values.
1628
#
1629
# Note that OUTPUT_DIRECTORY is not included; it should be added by
1630
# the caller
1631
1632
use strict;
1633
1634
my $txt = join "", <>;
1635
$txt =~ s/^\s*#.*$//gm;
1636
$txt =~ s/\\\n//gs;
1637
$txt =~ s/\r//g;
1638
$txt =~ s/\n\s*\n/\n/gs;
1639
1640
my %fixed = (
1641
    FULL_PATH_NAMES => "NO",
1642
    SYMBOL_CACHE_SIZE => 2,
1643
    EXCLUDE_SYMLINKS => "YES",
1644
    GENERATE_HTML => "YES",
1645
    PERL_PATH => "/usr/bin/perl",
1646
    HAVE_DOT => "YES",
1647
    HTML_OUTPUT => ".",
1648 228:3c084a25d8ab chris
    HTML_DYNAMIC_SECTIONS => "NO",
1649 226:5b028aef59a7 chris
    SEARCHENGINE => "NO",
1650
    DOT_FONTNAME => "FreeMono",
1651
    DOT_FONTSIZE => 10,
1652
    DOT_FONTPATH => "/usr/share/fonts/truetype/freefont",
1653
    DOT_IMAGE_FORMAT => "png",
1654
    DOT_PATH => "/usr/bin/dot",
1655
    DOT_TRANSPARENT => "YES",
1656
);
1657
1658 233:df89e7aa3ce8 Chris
# These are the keys that are safe to take from the output and include
1659
# in the output; they may still need to be checked for safe values (if
1660
# file paths).
1661 226:5b028aef59a7 chris
my @safe = qw(
1662 233:df89e7aa3ce8 Chris
INPUT
1663
FILE_PATTERNS
1664
EXAMPLE_PATH
1665
EXAMPLE_PATTERNS
1666
IMAGE_PATH
1667
INCLUDE_PATH
1668
INCLUDE_FILE_PATTERNS
1669 226:5b028aef59a7 chris
DOXYFILE_ENCODING
1670
PROJECT_NAME
1671
PROJECT_NUMBER
1672
CREATE_SUBDIRS
1673
OUTPUT_LANGUAGE
1674
BRIEF_MEMBER_DESC
1675
REPEAT_BRIEF
1676
ABBREVIATE_BRIEF
1677
ALWAYS_DETAILED_SEC
1678
INLINE_INHERITED_MEMB
1679
STRIP_FROM_PATH
1680
STRIP_FROM_INC_PATH
1681
JAVADOC_AUTOBRIEF
1682
QT_AUTOBRIEF
1683
MULTILINE_CPP_IS_BRIEF
1684
INHERIT_DOCS
1685
SEPARATE_MEMBER_PAGES
1686
TAB_SIZE
1687
ALIASES
1688
OPTIMIZE_OUTPUT_FOR_C
1689
OPTIMIZE_OUTPUT_JAVA
1690
OPTIMIZE_FOR_FORTRAN
1691
OPTIMIZE_OUTPUT_VHDL
1692
EXTENSION_MAPPING
1693
BUILTIN_STL_SUPPORT
1694
CPP_CLI_SUPPORT
1695
SIP_SUPPORT
1696
IDL_PROPERTY_SUPPORT
1697
DISTRIBUTE_GROUP_DOC
1698
SUBGROUPING
1699
TYPEDEF_HIDES_STRUCT
1700
EXTRACT_ALL
1701
EXTRACT_PRIVATE
1702
EXTRACT_STATIC
1703
EXTRACT_LOCAL_CLASSES
1704
EXTRACT_LOCAL_METHODS
1705
EXTRACT_ANON_NSPACES
1706
HIDE_UNDOC_MEMBERS
1707
HIDE_UNDOC_CLASSES
1708
HIDE_FRIEND_COMPOUNDS
1709
HIDE_IN_BODY_DOCS
1710
INTERNAL_DOCS
1711
HIDE_SCOPE_NAMES
1712
SHOW_INCLUDE_FILES
1713
FORCE_LOCAL_INCLUDES
1714
INLINE_INFO
1715
SORT_MEMBER_DOCS
1716
SORT_BRIEF_DOCS
1717
SORT_MEMBERS_CTORS_1ST
1718
SORT_GROUP_NAMES
1719
SORT_BY_SCOPE_NAME
1720
GENERATE_TODOLIST
1721
GENERATE_TESTLIST
1722
GENERATE_BUGLIST
1723
GENERATE_DEPRECATEDLIST
1724
ENABLED_SECTIONS
1725
MAX_INITIALIZER_LINES
1726
SHOW_USED_FILES
1727
SHOW_DIRECTORIES
1728
SHOW_FILES
1729
SHOW_NAMESPACES
1730
QUIET
1731
WARNINGS
1732
WARN_IF_UNDOCUMENTED
1733
WARN_IF_DOC_ERROR
1734
WARN_NO_PARAMDOC
1735
INPUT_ENCODING
1736
RECURSIVE
1737
EXCLUDE
1738
EXCLUDE_SYMLINKS
1739
EXCLUDE_PATTERNS
1740
EXCLUDE_SYMBOLS
1741
EXAMPLE_RECURSIVE
1742
SOURCE_BROWSER
1743
INLINE_SOURCES
1744
STRIP_CODE_COMMENTS
1745
REFERENCED_BY_RELATION
1746
REFERENCES_RELATION
1747
REFERENCES_LINK_SOURCE
1748
VERBATIM_HEADERS
1749
ALPHABETICAL_INDEX
1750
COLS_IN_ALPHA_INDEX
1751
IGNORE_PREFIX
1752
HTML_TIMESTAMP
1753
HTML_ALIGN_MEMBERS
1754
ENABLE_PREPROCESSING
1755
MACRO_EXPANSION
1756
EXPAND_ONLY_PREDEF
1757
SEARCH_INCLUDES
1758
PREDEFINED
1759
EXPAND_AS_DEFINED
1760
SKIP_FUNCTION_MACROS
1761
ALLEXTERNALS
1762
EXTERNAL_GROUPS
1763
CLASS_DIAGRAMS
1764
HIDE_UNDOC_RELATIONS
1765
CLASS_GRAPH
1766
COLLABORATION_GRAPH
1767
GROUP_GRAPHS
1768
UML_LOOK
1769
TEMPLATE_RELATIONS
1770
INCLUDE_GRAPH
1771
INCLUDED_BY_GRAPH
1772
CALL_GRAPH
1773
CALLER_GRAPH
1774
GRAPHICAL_HIERARCHY
1775
DIRECTORY_GRAPH
1776
DOT_GRAPH_MAX_NODES
1777
MAX_DOT_GRAPH_DEPTH
1778
DOT_MULTI_TARGETS
1779
DOT_CLEANUP
1780
);
1781
1782
my %safehash;
1783
for my $sk (@safe) { $safehash{$sk} = 1; }
1784
1785
my @lines = split "\n", $txt;
1786
1787
my %settings;
1788
1789
sub is_safe {
1790
    my $key = shift;
1791
    defined $safehash{$key} and $safehash{$key} == 1;
1792
}
1793
1794
sub has_file_path {
1795
    # Returns true if the given key expects a file path as a value.
1796
    # We only need to test keys that are safe; unsafe keys have been
1797
    # rejected already.
1798
    my $key = shift;
1799
    $key eq "INPUT" or
1800
	$key =~ /^OUTPUT_/ or
1801
	$key =~ /_PATH$/ or
1802
	$key =~ /_PATTERNS$/;
1803
}
1804
1805
sub is_safe_file_path {
1806
    my $value = shift;
1807
    not $value =~ /^\// and not $value =~ /\.\./;
1808
}
1809
1810
foreach my $line (@lines) {
1811
1812
    chomp $line;
1813
    my ($key, $value) = split /\s*=\s*/, $line;
1814
1815
    next if !defined $key;
1816
1817
    if ($key =~ /^GENERATE_/ and not $key =~ /LIST$/) {
1818
	print STDERR "NOTE: Setting $key explicitly to NO\n";
1819
	$settings{$key} = "NO";
1820
	next;
1821
    }
1822
1823
    if (!is_safe($key)) {
1824
	print STDERR "NOTE: Skipping non-whitelisted key $key\n";
1825
	next;
1826
    }
1827
1828
    if (has_file_path($key) and !is_safe_file_path($value)) {
1829
	print STDERR "ERROR: Unsafe file path \"$value\" for key $key\n";
1830
	exit 1;
1831
    }
1832
1833
    $settings{$key} = $value;
1834
}
1835
1836 228:3c084a25d8ab chris
foreach my $key (keys %fixed) {
1837
    my $value = $fixed{$key};
1838
    print STDERR "NOTE: Setting $key to fixed value $value\n";
1839
    $settings{$key} = $value;
1840
}
1841
1842 226:5b028aef59a7 chris
print join "\n", map { "$_ = $settings{$_}" } keys %settings;
1843
print "\n";
1844 1543:05d639e5d59b Chris
#!/bin/bash
1845
1846
set -e
1847
1848
progdir=$(dirname $0)
1849
case "$progdir" in
1850
    /*) ;;
1851
    *) progdir="$(pwd)/$progdir" ;;
1852
esac
1853
1854
rails_scriptdir="$progdir/../../script"
1855
rails="$rails_scriptdir/rails"
1856
1857
if [ ! -x "$rails" ]; then
1858
    echo "Expected to find rails executable at $rails"
1859
    exit 2
1860
fi
1861
1862
fastexport="$progdir/../fast-export/hg-fast-export.sh"
1863
if [ ! -x "$fastexport" ]; then
1864
    echo "Expected to find hg-fast-export.sh executable at $fastexport"
1865
    exit 2
1866
fi
1867
1868 1546:248c402992ba Chris
environment="$1"
1869
hgdir="$2"
1870
gitdir="$3"
1871 1543:05d639e5d59b Chris
1872
if [ -z "$hgdir" ] || [ -z "$gitdir" ]; then
1873 1546:248c402992ba Chris
    echo "Usage: $0 <environment> <hgdir> <gitdir>"
1874
    echo "  where"
1875
    echo "  - environment is the Rails environment (development or production)"
1876
    echo "  - hgdir is the directory containing project Mercurial repositories"
1877
    echo "  - gitdir is the directory in which output git repositories are to be"
1878
    echo "    created or updated"
1879 1543:05d639e5d59b Chris
    exit 2
1880
fi
1881
1882
if [ ! -d "$hgdir" ]; then
1883
    echo "Mercurial repository directory $hgdir not found"
1884
    exit 1
1885
fi
1886
1887
if [ ! -d "$gitdir" ]; then
1888
    echo "Target git repository dir $gitdir not found (please create at least the empty directory)"
1889
    exit 1
1890
fi
1891
1892
set -u
1893
1894 1550:7d825cbd76c8 Chris
authordir="$gitdir/__AUTHORMAPS"
1895 1543:05d639e5d59b Chris
mkdir -p "$authordir"
1896
1897 1550:7d825cbd76c8 Chris
wastedir="$gitdir/__WASTE"
1898
mkdir -p "$wastedir"
1899
1900 1557:9d6d2f696782 Chris
echo
1901
echo "$0 starting at $(date)"
1902
1903 1547:bca3b5e5bbf2 Chris
echo "Extracting author maps..."
1904 1546:248c402992ba Chris
1905 1550:7d825cbd76c8 Chris
# Delete any existing authormap files, because we want to ensure we
1906
# don't have an authormap for any project that was exportable but has
1907
# become non-exportable (e.g. has gone private)
1908 1551:36dddb6755f6 Chris
rm -f "$authordir/*"
1909 1550:7d825cbd76c8 Chris
1910 1546:248c402992ba Chris
"$rails" runner -e "$environment" "$progdir/create-repo-authormaps.rb" \
1911 1543:05d639e5d59b Chris
	 -s "$hgdir" -o "$authordir"
1912
1913
for hgrepo in "$hgdir"/*; do
1914
1915
    if [ ! -d "$hgrepo/.hg" ]; then
1916
	echo "Directory $hgrepo does not appear to be a Mercurial repo, skipping"
1917
	continue
1918
    fi
1919
1920
    reponame=$(basename "$hgrepo")
1921
    authormap="$authordir/authormap_$reponame"
1922 1559:21098b932cb8 Chris
1923
    git_repodir="$gitdir/$reponame"
1924 1543:05d639e5d59b Chris
1925
    if [ ! -f "$authormap" ]; then
1926 1547:bca3b5e5bbf2 Chris
	echo "No authormap file was created for repo $reponame, skipping"
1927 1550:7d825cbd76c8 Chris
1928
	# If there is no authormap file, then we should not have a git
1929
	# mirror -- this is a form of access control, not just an
1930
	# optimisation (authormap files are expected to exist for all
1931
	# exportable projects, even if empty). So if a git mirror
1932
	# exists, we move it away
1933 1560:2c67e414ab46 Chris
	if [ -d "$git_repodir" ]; then
1934
	    mv "$git_repodir" "$wastedir/$(date +%s).$reponame"
1935 1550:7d825cbd76c8 Chris
	fi
1936
1937 1543:05d639e5d59b Chris
	continue
1938
    fi
1939
1940 1559:21098b932cb8 Chris
    if [ ! -d "$git_repodir" ]; then
1941 1561:6074fffd8a1d Chris
	git init --bare "$git_repodir"
1942 1543:05d639e5d59b Chris
    fi
1943
1944 1547:bca3b5e5bbf2 Chris
    echo
1945 1546:248c402992ba Chris
    echo "About to run fast export for repo $reponame..."
1946 1543:05d639e5d59b Chris
1947
    (
1948 1561:6074fffd8a1d Chris
	cd "$git_repodir"
1949 1549:28cde511f312 Chris
1950
        # Force is necessary because git-fast-import (or git) can't handle
1951
        # branches having more than one head ("Error: repository has at
1952
        # least one unnamed head"), which happens from time to time in
1953
        # valid Hg repos. With --force apparently it will just pick one
1954
        # of the two heads arbitrarily, which is also alarming but is
1955
        # more likely to be useful
1956 1558:a0c46d6fe7bc Chris
	"$fastexport" --quiet -r "$hgrepo" --hgtags -A "$authormap" --force
1957 1555:1786830cc35d Chris
1958
        git update-server-info
1959 1543:05d639e5d59b Chris
    )
1960
1961
    echo "Fast export done"
1962
1963
done
1964
1965 1557:9d6d2f696782 Chris
echo "$0 finishing at $(date)"
1966 1543:05d639e5d59b Chris
1967 203:1e55195bca45 chris
#!/bin/bash
1968
1969
# Run this script from anywhere
1970
1971
# Enumerate Hg repos; make sure they're up to date; extract docs for
1972
# each
1973
1974
hgdir="/var/hg"
1975
docdir="/var/doc"
1976 223:c3544e9fd588 chris
logfile="/var/www/test-cannam/log/extract-docs.log"
1977
1978
redgrp="redmine"
1979 203:1e55195bca45 chris
1980 218:292cde42265a chris
apikey=""
1981 339:5410d82c12df chris
apischeme="https"
1982 218:292cde42265a chris
apihost=""
1983
apiuser=""
1984
apipass=""
1985
1986 203:1e55195bca45 chris
progdir=$(dirname $0)
1987
case "$progdir" in
1988
    /*) ;;
1989
    *) progdir="$(pwd)/$progdir" ;;
1990
esac
1991
1992 411:e7ba81c8dc5a chris
types="doxygen javadoc matlabdocs" # Do Doxygen first (it can be used for Java too)
1993 203:1e55195bca45 chris
1994
for x in $types; do
1995
    if [ ! -x "$progdir/extract-$x.sh" ]; then
1996
	echo "Helper script not available: $progdir/extract-$x.sh"
1997
	exit 1
1998
    fi
1999
done
2000
2001 218:292cde42265a chris
enable_embedded()
2002
{
2003
    p="$1"
2004 228:3c084a25d8ab chris
    if [ -n "$apikey" ]; then
2005
	if [ -n "$apiuser" ]; then
2006 339:5410d82c12df chris
	    sudo -u docgen curl -u "$apiuser":"$apipass" "$apischeme://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d ""
2007 228:3c084a25d8ab chris
	else
2008 339:5410d82c12df chris
	    sudo -u docgen curl "$apischeme://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d ""
2009 228:3c084a25d8ab chris
	fi
2010
    else
2011
	echo "Can't enable Embedded, API not configured" 1>&2
2012 218:292cde42265a chris
    fi
2013
}
2014
2015 223:c3544e9fd588 chris
# We want to ensure the doc extraction is done by the unprivileged
2016
# user docgen, which is not a member of any interesting group
2017
#
2018
# To this end, we create the tmpdir with user docgen and group
2019
# www-data, and use the www-data user to pull out an archive of the Hg
2020
# repo tip into a location beneath that, before using the docgen user
2021
# to extract docs from that location and write them into the tmpdir
2022
2023
# Same tmpdir for each project: we delete and recreate to avoid
2024
# cleanup duty from lots of directories being created
2025
#
2026
tmpdir=$(mktemp -d "$docdir/tmp_XXXXXX")
2027
2028
fail()
2029
{
2030
    message="$1"
2031
    echo "$message" 1>&2
2032
    case "$tmpdir" in
2033
	*/tmp*) rm -rf "$tmpdir";;
2034
	*);;
2035
    esac
2036
    exit 1
2037
}
2038
2039
case "$tmpdir" in
2040
    /*) ;;
2041
    *) fail "Temporary directory creation failed";;
2042
esac
2043
2044
chown docgen.www-data "$tmpdir" || fail "Temporary directory ownership change failed"
2045
chmod g+rwx "$tmpdir" || fail "Temporary directory permissions change failed"
2046
2047 203:1e55195bca45 chris
for projectdir in "$hgdir"/* ; do
2048
2049
    if [ -d "$projectdir" ] && [ -d "$projectdir/.hg" ]; then
2050
2051 223:c3544e9fd588 chris
	if ! sudo -u www-data hg -R "$projectdir" -q update; then
2052
	    echo "Failed to update Hg in $projectdir, skipping" 1>&2
2053
	    continue
2054
	fi
2055
2056 203:1e55195bca45 chris
	project=$(basename "$projectdir")
2057
2058 223:c3544e9fd588 chris
	tmptargetdir="$tmpdir/doc"
2059
	snapshotdir="$tmpdir/hgsnapshot"
2060 203:1e55195bca45 chris
2061 223:c3544e9fd588 chris
	rm -rf "$tmptargetdir" "$snapshotdir"
2062
2063 226:5b028aef59a7 chris
	mkdir -m 770 "$tmptargetdir" || fail "Temporary target directory creation failed"
2064
	chown docgen.www-data "$tmptargetdir" || fail "Temporary target directory ownership change failed"
2065 223:c3544e9fd588 chris
2066
	mkdir -m 770 "$snapshotdir" || fail "Snapshot directory creation failed"
2067
	chown docgen.www-data "$snapshotdir" || fail "Snapshot directory ownership change failed"
2068
2069
	hgparents=$(sudo -u www-data hg -R "$projectdir" parents)
2070
	if [ -z "$hgparents" ]; then
2071
	    echo "Hg repo at $projectdir has no working copy (empty repo?), skipping"
2072
	    continue
2073
	else
2074
	    echo "Found non-empty Hg repo: $projectdir for project $project"
2075
	fi
2076
2077
	if ! sudo -u www-data hg -R "$projectdir" archive -r tip -t files "$snapshotdir"; then
2078
	    echo "Failed to pick archive from $projectdir, skipping" 1>&2
2079
	    continue
2080
	fi
2081 203:1e55195bca45 chris
2082
	targetdir="$docdir/$project"
2083
2084 223:c3544e9fd588 chris
	echo "Temporary dir is $tmpdir, temporary doc dir is $tmptargetdir, snapshot dir is $snapshotdir, eventual target is $targetdir"
2085 203:1e55195bca45 chris
2086
	for x in $types; do
2087 226:5b028aef59a7 chris
	    if sudo -u docgen "$progdir/extract-$x.sh" "$project" "$snapshotdir" "$tmptargetdir" >> "$logfile" 2>&1; then
2088
		break
2089
	    else
2090 203:1e55195bca45 chris
		echo "Failed to extract via type $x"
2091
	    fi
2092
	done
2093
2094 223:c3544e9fd588 chris
        if [ -f "$tmptargetdir/index.html" ]; then
2095 203:1e55195bca45 chris
	    echo "Processing resulted in an index.html being created, looks good!"
2096
	    if [ ! -d "$targetdir" ] || [ ! -f "$targetdir/index.html" ]; then
2097 223:c3544e9fd588 chris
		echo "This project hasn't had doc extracted before: enabling Embedded"
2098 218:292cde42265a chris
		enable_embedded "$project"
2099 203:1e55195bca45 chris
	    fi
2100
2101
	    if [ -d "$targetdir" ]; then
2102
		mv "$targetdir" "$targetdir"_"$$" && \
2103 223:c3544e9fd588 chris
		    mv "$tmptargetdir" "$targetdir" && \
2104 203:1e55195bca45 chris
		    rm -rf "$targetdir"_"$$"
2105 223:c3544e9fd588 chris
		chgrp -R "$redgrp" "$targetdir"
2106 203:1e55195bca45 chris
	    else
2107 223:c3544e9fd588 chris
		mv "$tmptargetdir" "$targetdir"
2108
		chgrp -R "$redgrp" "$targetdir"
2109 203:1e55195bca45 chris
	    fi
2110 228:3c084a25d8ab chris
	else
2111
	    echo "Processing did not result in an index.html being created"
2112 203:1e55195bca45 chris
	fi
2113
    fi
2114
done
2115
2116 223:c3544e9fd588 chris
rm -rf "$tmpdir"
2117 203:1e55195bca45 chris
#!/bin/bash
2118
2119
docdir="/var/doc"
2120
2121 228:3c084a25d8ab chris
progdir=$(dirname $0)
2122
case "$progdir" in
2123
    /*) ;;
2124
    *) progdir="$(pwd)/$progdir" ;;
2125
esac
2126
2127 203:1e55195bca45 chris
project="$1"
2128 223:c3544e9fd588 chris
projectdir="$2"
2129
targetdir="$3"
2130 203:1e55195bca45 chris
2131 223:c3544e9fd588 chris
if [ -z "$project" ] || [ -z "$targetdir" ] || [ -z "$projectdir" ]; then
2132
    echo "Usage: $0 <project> <projectdir> <targetdir>"
2133 203:1e55195bca45 chris
    exit 2
2134
fi
2135
2136 223:c3544e9fd588 chris
if [ ! -d "$projectdir" ]; then
2137
    echo "Project directory $projectdir not found"
2138 203:1e55195bca45 chris
    exit 1
2139
fi
2140
2141
if [ ! -d "$targetdir" ]; then
2142
    echo "Target dir $targetdir not found"
2143
    exit 1
2144
fi
2145
2146
if [ -f "$targetdir/index.html" ]; then
2147
    echo "Target dir $targetdir already contains index.html"
2148
    exit 1
2149
fi
2150
2151
doxyfile=$(find "$projectdir" -type f -name Doxyfile -print | head -1)
2152
2153
if [ -z "$doxyfile" ]; then
2154
    echo "No Doxyfile found for project $project"
2155
    exit 1
2156
fi
2157
2158
echo "Project $project contains a Doxyfile at $doxyfile"
2159
2160
cd "$projectdir" || exit 1
2161
2162 228:3c084a25d8ab chris
"$progdir/doxysafe.pl" "$doxyfile" | \
2163
    sed -e '$a OUTPUT_DIRECTORY='"$targetdir" | \
2164 203:1e55195bca45 chris
    doxygen -
2165
2166
#!/bin/bash
2167 168:c1e9f2dab1d5 chris
2168 203:1e55195bca45 chris
docdir="/var/doc"
2169 168:c1e9f2dab1d5 chris
2170 203:1e55195bca45 chris
project="$1"
2171 223:c3544e9fd588 chris
projectdir="$2"
2172
targetdir="$3"
2173 168:c1e9f2dab1d5 chris
2174 223:c3544e9fd588 chris
if [ -z "$project" ] || [ -z "$targetdir" ] || [ -z "$projectdir" ]; then
2175
    echo "Usage: $0 <project> <projectdir> <targetdir>"
2176 203:1e55195bca45 chris
    exit 2
2177 168:c1e9f2dab1d5 chris
fi
2178
2179 223:c3544e9fd588 chris
if [ ! -d "$projectdir" ]; then
2180
    echo "Project directory $projectdir not found"
2181 203:1e55195bca45 chris
    exit 1
2182
fi
2183 168:c1e9f2dab1d5 chris
2184 203:1e55195bca45 chris
if [ ! -d "$targetdir" ]; then
2185
    echo "Target dir $targetdir not found"
2186
    exit 1
2187
fi
2188 168:c1e9f2dab1d5 chris
2189 203:1e55195bca45 chris
if [ -f "$targetdir/index.html" ]; then
2190
    echo "Target dir $targetdir already contains index.html"
2191
    exit 1
2192 178:2cec5c53cd68 chris
fi
2193 168:c1e9f2dab1d5 chris
2194 191:0d1c6fa50d3a chris
# Identify Java files whose packages match the trailing parts of their
2195
# paths, and list the resulting packages and the path prefixes with
2196
# the packages removed (so as to find code in subdirs,
2197
# e.g. src/com/example/...)
2198
2199
# Regexp match is very rough; check what is actually permitted for
2200
# package declarations
2201
2202 203:1e55195bca45 chris
find "$projectdir" -type f -name \*.java \
2203 989:3549525ba22a Chris
    -exec egrep '^ *package +[a-zA-Z][a-zA-Z0-9\._-]*;.*$' \{\} /dev/null \; |
2204
    sed -e 's/\/[^\/]*: *package */:/' -e 's/;.*$//' |
2205 191:0d1c6fa50d3a chris
    sort | uniq | (
2206
	current_prefix=
2207
	current_packages=
2208
	while IFS=: read filepath package; do
2209
	    echo "Looking at $package in $filepath"
2210
	    packagepath=${package//./\/}
2211
	    prefix=${filepath%$packagepath}
2212 203:1e55195bca45 chris
	    prefix=${prefix:=$projectdir}
2213 191:0d1c6fa50d3a chris
	    if [ "$prefix" = "$filepath" ]; then
2214
		echo "Package $package does not match suffix of path $filepath, skipping"
2215
		continue
2216
	    fi
2217
	    if [ "$prefix" != "$current_prefix" ]; then
2218 450:73401a15037b Chris
		echo "Package $package matches file path and has new prefix $prefix"
2219 191:0d1c6fa50d3a chris
		if [ -n "$current_packages" ]; then
2220
		    echo "Running Javadoc for packages $current_packages from prefix $current_prefix"
2221 450:73401a15037b Chris
		    echo "Command is: javadoc -sourcepath "$current_prefix" -d "$targetdir" -subpackages $current_packages"
2222 203:1e55195bca45 chris
		    javadoc -sourcepath "$current_prefix" -d "$targetdir" -subpackages $current_packages
2223 191:0d1c6fa50d3a chris
		fi
2224
		current_prefix="$prefix"
2225 450:73401a15037b Chris
		current_packages="$package"
2226 191:0d1c6fa50d3a chris
	    else
2227 450:73401a15037b Chris
		echo "Package $package matches file path with same prefix as previous file"
2228 191:0d1c6fa50d3a chris
		current_packages="$current_packages $package"
2229
	    fi
2230
	done
2231 203:1e55195bca45 chris
	prefix=${prefix:=$projectdir}
2232 191:0d1c6fa50d3a chris
	if [ -n "$current_packages" ]; then
2233
	    echo "Running Javadoc for packages $current_packages in prefix $current_prefix"
2234 450:73401a15037b Chris
  	    echo "Command is: javadoc -sourcepath "$current_prefix" -d "$targetdir" -subpackages $current_packages"
2235 203:1e55195bca45 chris
	    javadoc -sourcepath "$current_prefix" -d "$targetdir" -subpackages $current_packages
2236 191:0d1c6fa50d3a chris
	fi
2237
    )
2238
2239 450:73401a15037b Chris
if [ -f "$targetdir"/overview-tree.html ]; then
2240
    cp "$targetdir"/overview-tree.html "$targetdir"/index.html
2241
fi
2242
2243 203:1e55195bca45 chris
# for exit code:
2244
[ -f "$targetdir/index.html" ]
2245 168:c1e9f2dab1d5 chris
2246 411:e7ba81c8dc5a chris
#!/bin/bash
2247
2248
docdir="/var/doc"
2249
2250
progdir=$(dirname $0)
2251
case "$progdir" in
2252
    /*) ;;
2253
    *) progdir="$(pwd)/$progdir" ;;
2254
esac
2255
2256
project="$1"
2257
projectdir="$2"
2258
targetdir="$3"
2259
2260
if [ -z "$project" ] || [ -z "$targetdir" ] || [ -z "$projectdir" ]; then
2261
    echo "Usage: $0 <project> <projectdir> <targetdir>"
2262
    exit 2
2263
fi
2264
2265
if [ ! -d "$projectdir" ]; then
2266
    echo "Project directory $projectdir not found"
2267
    exit 1
2268
fi
2269
2270
if [ ! -d "$targetdir" ]; then
2271
    echo "Target dir $targetdir not found"
2272
    exit 1
2273
fi
2274
2275
if [ -f "$targetdir/index.html" ]; then
2276
    echo "Target dir $targetdir already contains index.html"
2277
    exit 1
2278
fi
2279
2280
mfile=$(find "$projectdir" -type f -name \*.m -print0 | xargs -0 grep -l '^% ' | head -1)
2281
2282
if [ -z "$mfile" ]; then
2283
    echo "No MATLAB files with comments found for project $project"
2284
    exit 1
2285
fi
2286
2287
echo "Project $project contains at least one MATLAB file with comments"
2288
2289
cd "$projectdir" || exit 1
2290
2291
perl "$progdir/matlab-docs.pl" -c "$progdir/matlab-docs.conf" -d "$targetdir"
2292
2293 1563:171c31a5cca4 Chris
#!/bin/bash
2294
#
2295
# Convert an Hg repo with subrepos into a new repo in which the
2296
# subrepo contents are included in the main repo. The history of the
2297
# original and its subrepos is retained.
2298
#
2299
# Note that this script invokes itself, in order to handle nested
2300
# subrepos.
2301
#
2302
# While this does work, I'm not convinced it's entirely a good
2303
# idea. The history ends up a bit of a mess, and if it's a preliminary
2304
# to converting to git (which is one obvious reason to do this), the
2305
# history ends up even messier after that conversion.
2306
2307
set -ex
2308
2309
repo="$1"
2310
target="$2"
2311
target_subdir="$3"
2312
revision="$4"
2313
2314
if [ -z "$repo" ] || [ -z "$target" ]; then
2315
    echo "usage: $0 <repo-url> <target-dir> [<target-subdir> <revision>]"
2316
    exit 2
2317
fi
2318
2319
set -u
2320
2321
myname="$0"
2322
mydir=$(dirname "$myname")
2323
2324
reponame=$(basename "$repo")
2325
tmpdir="/tmp/flatten_$$"
2326
mkdir -p "$tmpdir"
2327
trap "rm -rf ${tmpdir}" 0
2328
2329
filemap="$tmpdir/filemap"
2330
tmprepo="$tmpdir/tmprepo"
2331
subtmp="$tmpdir/subtmp"
2332
2333
if [ -n "$revision" ]; then
2334
    hg clone -r "$revision" "$repo" "$tmprepo"
2335
else
2336
    hg clone "$repo" "$tmprepo"
2337
fi
2338
2339
read_sub() {
2340
    if [ -f "$tmprepo/.hgsub" ]; then
2341
	cat "$tmprepo/.hgsub" | sed 's/ *= */,/'
2342
    fi
2343
}
2344
2345
(   echo "exclude .hgsub"
2346
    echo "exclude .hgsubstate"
2347
    read_sub | while IFS=, read dir uri; do
2348
	echo "exclude $dir"
2349
    done
2350
    if [ -n "$target_subdir" ]; then
2351
	echo "rename . $target_subdir"
2352
    fi
2353
) > "$filemap"
2354
2355
hg convert --filemap "$filemap" "$tmprepo" "$target"
2356
(   cd "$target"
2357
    hg update
2358
)
2359
2360
read_sub | while IFS=, read dir uri; do
2361
    rm -rf "$subtmp"
2362
    revision=$(grep ' '"$dir"'$' "$tmprepo/.hgsubstate" | awk '{ print $1; }')
2363
    if [ -n "$target_subdir" ]; then
2364
	"$myname" "$tmprepo/$dir" "$subtmp" "$target_subdir/$dir" "$revision"
2365
    else
2366
	"$myname" "$tmprepo/$dir" "$subtmp" "$dir" "$revision"
2367
    fi
2368
    (   cd "$target"
2369
	hg pull -f "$subtmp" &&
2370
	    hg merge --tool internal:local &&
2371
	    hg commit -m "Merge former subrepo $dir"
2372
    )
2373
done
2374
2375 978:bbb88c44f805 Chris
2376 980:9b4919de5317 Chris
# Read an Apache log file in SoundSoftware site format from stdin and
2377
# produce some per-project stats.
2378 978:bbb88c44f805 Chris
#
2379
# Invoke with e.g.
2380
#
2381
# cat /var/log/apache2/code-access.log | \
2382
#   script/runner -e production extra/soundsoftware/get-apache-log-stats.rb
2383
2384 975:198f764e734c Chris
2385
# Use the ApacheLogRegex parser, a neat thing
2386
# See http://www.simonecarletti.com/blog/2009/02/apache-log-regex-a-lightweight-ruby-apache-log-parser/
2387
require 'apachelogregex'
2388
2389
# This is the format defined in our httpd.conf
2390
vhost_combined_format = '%v:%p %h %{X-Forwarded-For}i %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"'
2391
2392
parser = ApacheLogRegex.new(vhost_combined_format)
2393
2394
# project name -> count of hg clones
2395
clones = Hash.new(0)
2396
2397
# project name -> count of hg pulls
2398
pulls = Hash.new(0)
2399
2400 978:bbb88c44f805 Chris
# project name -> count of hg pushes
2401
pushes = Hash.new(0)
2402 975:198f764e734c Chris
2403
# project name -> count of hg archive requests (i.e. Download as Zip)
2404
zips = Hash.new(0)
2405
2406
# project name -> count of hits to pages under /projects/projectname
2407
hits = Hash.new(0)
2408
2409 978:bbb88c44f805 Chris
# project name -> Project object
2410
@projects = Hash.new
2411
2412 975:198f764e734c Chris
parseable = 0
2413
unparseable = 0
2414
2415 979:56a38a9f6204 Chris
def is_public_project?(project)
2416 978:bbb88c44f805 Chris
  if !project
2417
    false
2418 983:a97e573d7f87 Chris
  elsif project =~ /^\d+$/
2419
    # ignore numerical project ids, they are only used when editing projects
2420
    false
2421 978:bbb88c44f805 Chris
  elsif @projects.key?(project)
2422 979:56a38a9f6204 Chris
    @projects[project].is_public?
2423 978:bbb88c44f805 Chris
  else
2424
    pobj = Project.find_by_identifier(project)
2425
    if pobj
2426
      @projects[project] = pobj
2427 979:56a38a9f6204 Chris
      pobj.is_public?
2428 978:bbb88c44f805 Chris
    else
2429 979:56a38a9f6204 Chris
      print "Project not found: ", project, "\n"
2430 978:bbb88c44f805 Chris
      false
2431
    end
2432
  end
2433
end
2434
2435 980:9b4919de5317 Chris
def print_stats(h)
2436
  h.keys.sort { |a,b| h[b] <=> h[a] }.each do |p|
2437 982:6edb748be064 Chris
    if h[p] > 0
2438 983:a97e573d7f87 Chris
      print h[p], " ", @projects[p].name, " [", p, "]\n"
2439 982:6edb748be064 Chris
    end
2440 980:9b4919de5317 Chris
  end
2441
end
2442
2443 979:56a38a9f6204 Chris
STDIN.each do |line|
2444 975:198f764e734c Chris
2445
  record = parser.parse(line)
2446
2447
  # most annoyingly, the parser can't handle the comma-separated list
2448
  # in X-Forwarded-For where it has more than one element. If it has
2449 983:a97e573d7f87 Chris
  # failed, remove any IP addresses or the word "unknown" with
2450
  # trailing commas and try again
2451 975:198f764e734c Chris
  if not record
2452 983:a97e573d7f87 Chris
    filtered = line.gsub(/(unknown|([0-9]+\.){3}[0-9]+),\s*/, "")
2453 975:198f764e734c Chris
    record = parser.parse(filtered)
2454
  end
2455
2456
  # discard, but count, unparseable lines
2457
  if not record
2458 979:56a38a9f6204 Chris
    print "Line not parseable: ", line, "\n"
2459 975:198f764e734c Chris
    unparseable += 1
2460
    next
2461
  end
2462
2463
  # discard everything that isn't a 200 OK response
2464
  next if record["%>s"] != "200"
2465
2466
  # discard anything apparently requested by a crawler
2467
  next if record["%{User-Agent}i"] =~ /(bot|slurp|crawler|spider|Redmine)\b/i
2468
2469
  # pull out request e.g. GET / HTTP/1.0
2470
  request = record["%r"]
2471
2472
  # split into method, path, protocol
2473
  if not request =~ /^[^\s]+ ([^\s]+) [^\s]+$/
2474 979:56a38a9f6204 Chris
    print "Line not parseable (bad method, path, protocol): ", line, "\n"
2475 975:198f764e734c Chris
    unparseable += 1
2476
    next
2477
  end
2478
2479
  # get the path e.g. /projects/weevilmatic and split on /
2480
  path = $~[1]
2481
  components = path.split("/")
2482
2483
  # should have at least two elements unless path is "/"; first should
2484
  # be empty (begins with /)
2485
  if path != "/" and (components.size < 2 or components[0] != "")
2486 979:56a38a9f6204 Chris
    print "Line not parseable (degenerate path): ", line, "\n"
2487 975:198f764e734c Chris
    unparseable += 1
2488
    next
2489
  end
2490
2491
  if components[1] == "hg"
2492
2493
    # path is /hg/project?something or /hg/project/something
2494
2495
    project = components[2].split("?")[0]
2496 979:56a38a9f6204 Chris
    if not is_public_project?(project)
2497 978:bbb88c44f805 Chris
      next
2498
    end
2499 975:198f764e734c Chris
2500
    if components[2] =~ /&roots=00*$/
2501
      clones[project] += 1
2502
    elsif components[2] =~ /cmd=capabilities/
2503
      pulls[project] += 1
2504 978:bbb88c44f805 Chris
    elsif components[2] =~ /cmd=unbundle/
2505
      pushes[project] += 1
2506 975:198f764e734c Chris
    elsif components[3] == "archive"
2507
      zips[project] += 1
2508
    end
2509
2510
  elsif components[1] == "projects"
2511
2512
    # path is /projects/project or /projects/project/something
2513
2514
    project = components[2]
2515 979:56a38a9f6204 Chris
    project = project.split("?")[0] if project
2516
    if not is_public_project?(project)
2517 978:bbb88c44f805 Chris
      next
2518 975:198f764e734c Chris
    end
2519
2520 978:bbb88c44f805 Chris
    hits[project] += 1
2521
2522 975:198f764e734c Chris
  end
2523
2524
  parseable += 1
2525
end
2526
2527
# Each clone is also a pull; deduct it from the pulls hash, because we
2528
# want that to contain only non-clone pulls
2529
2530
clones.keys.each do |project|
2531
  pulls[project] -= 1
2532
end
2533
2534 982:6edb748be064 Chris
print parseable, " parseable\n"
2535
print unparseable, " unparseable\n"
2536
2537
2538 980:9b4919de5317 Chris
print "\nMercurial clones:\n"
2539
print_stats clones
2540
2541
print "\nMercurial pulls (excluding clones):\n"
2542
print_stats pulls
2543
2544
print "\nMercurial pushes:\n"
2545
print_stats pushes
2546
2547
print "\nMercurial archive (zip file) downloads:\n"
2548
print_stats zips
2549
2550 982:6edb748be064 Chris
print "\nProject page hits (excluding crawlers):\n"
2551 980:9b4919de5317 Chris
print_stats hits
2552 975:198f764e734c Chris
2553
2554 1536:e2a3230f61fa Chris
2555
# Print out an authormap file for hg-to-git conversion using
2556
# hg-fast-export
2557
#
2558
# Invoke with the project identifier as argument, e.g.
2559
#
2560
# ./script/rails runner -e production extra/soundsoftware/get-repo-authormap.rb soundsoftware-site
2561
2562 1537:e55cbb9ba8bf Chris
proj_ident = ARGV.last
2563 1536:e2a3230f61fa Chris
proj = Project.find_by_identifier(proj_ident)
2564
repo = Repository.where(:project_id => proj.id).first
2565
csets = Changeset.where(:repository_id => repo.id)
2566
committers = csets.map do |c| c.committer end.sort.uniq
2567
committers.each do |c|
2568 1537:e55cbb9ba8bf Chris
  if not c =~ /[^<]+<.*@.*>/ then
2569
    u = repo.find_committer_user c
2570
    print "#{c}=#{u.name} <#{u.mail}>\n" unless u.nil?
2571
  end
2572 1536:e2a3230f61fa Chris
end
2573 970:6bd8364eafae luis
2574 1453:b554eb79ec7b luis
# Log user and project information
2575
#
2576
# Invoke with e.g.
2577
#
2578
# ./script/rails runner -e production extra/soundsoftware/get-statistics.rb
2579 970:6bd8364eafae luis
#
2580
2581 1454:02a05da0bedc luis
projectStats =  {
2582
        :all => Project.active.all.count,
2583
        :private => Project.active.find(:all, :conditions => {:is_public => false}).count,
2584
        :top_level => Project.active.find(:all, :conditions => {:parent_id => nil}).count,
2585
        :top_level_and_private => Project.active.find(:all, :conditions => {:is_public => false, :parent_id => nil}).count
2586
      }
2587 970:6bd8364eafae luis
2588 1453:b554eb79ec7b luis
userStats = {:all => User.active.all.count}
2589 1002:f6ede18f3e6e Chris
2590 1453:b554eb79ec7b luis
stats = {:date => Date.today, :projects => projectStats, :users => userStats}.to_json
2591 1002:f6ede18f3e6e Chris
2592 1453:b554eb79ec7b luis
print "#{stats}\n"
2593 1002:f6ede18f3e6e Chris
2594 383:47ae83ce8db8 Chris
<div style="clear: both; float: right"><small><i>Produced by mtree2html by Hartmut Pohlheim</i></small></div>
2595
# configuration file for generation of html-docu from m-files
2596
#
2597
# Author:   Hartmut Pohlheim
2598
# History:  05.11.2000  file created (parameters for mtree2html2001)
2599
#
2600
# The following options/variables must be changed/adapted:
2601
#   dirmfiles
2602
#   dirhtml
2603
#   csslink
2604
#   texttitleframelayout
2605
#   texttitlefiles
2606
#
2607
# The following options/variables should be adapted:
2608
#   authorfile
2609
#   filenametopframe
2610
#   codeheadmeta
2611
2612
#========================================================================
2613
# Variables (possible keywords: set)
2614
# to use the built-in settings, comment the line using # in first column
2615
#========================================================================
2616
2617
#------------------------------------------------------------------------
2618
# dirmfiles: name of directory containing Matlab m-files
2619
# dirhtml: name of directory to place the html-files into
2620
# exthtml: extension used for the html files (.html or .htm)
2621
#          don't forget the point in front of the extension
2622
#------------------------------------------------------------------------
2623
set dirmfiles = .
2624
set dirhtml = doc-output
2625
set exthtml = .html
2626
2627
#------------------------------------------------------------------------
2628
# authorfile:   name of file containing info about author (in html)
2629
#               if defined, this text is included at the bottom of the
2630
#               html files
2631
#------------------------------------------------------------------------
2632 410:675de8e6becf chris
set authorfile = matlab-docs-credit.html
2633 383:47ae83ce8db8 Chris
2634
#------------------------------------------------------------------------
2635
# csslink:   text for linking to css file (style sheets)
2636
#            the text defined here is directly included into the head
2637
#            of the html file
2638
#------------------------------------------------------------------------
2639 410:675de8e6becf chris
#set csslink = <link rel=stylesheet type="text/css" href="CSSFILENAME.css" />
2640 383:47ae83ce8db8 Chris
2641
#------------------------------------------------------------------------
2642
# links2filescase: this is a bit difficult
2643
#                  Matlab is case sensitive on UNIX, but case insensitive
2644
#                  on Windows. Under UNIX Matlab function calls work
2645
#                  only, when the case of file name and function call are
2646
#                  identical, under Windows you can do what you want.
2647
#                  This scripts help you, to keep an exact case in your
2648
#                  project.
2649
#          exact - internal links are only generated, when case of file
2650
#                  name and in source code are identical
2651
#            all - case doesn't matter
2652
#     exactupper - same as exact, additionally links are also vreated to
2653
#                  all upper case function names in source code (often
2654
#                  used by Mathworks)
2655
#      exactvery - same as exact, additionally info about not matching
2656
#                  case is written to screen (stdout), this can be very
2657
#                  helpful in cleaning up the case in a project
2658
#------------------------------------------------------------------------
2659
set links2filescase = all
2660
2661
#------------------------------------------------------------------------
2662
# texttitleframelayout:    text of title for frame layout file (whole docu)
2663
#------------------------------------------------------------------------
2664 389:0bc92382a86b chris
set texttitleframelayout = MATLAB Function Documentation
2665 383:47ae83ce8db8 Chris
2666
#------------------------------------------------------------------------
2667
# texttitle/headerindexalldirs: text of title and header for directory index
2668
#------------------------------------------------------------------------
2669
set texttitleindexalldirs = Index of Directories
2670
set textheaderindexalldirs = Index of Directories
2671
2672
#------------------------------------------------------------------------
2673
# texttitle/headerindex:    text of title and header for index file
2674
#------------------------------------------------------------------------
2675
set texttitleindex = A-Z Index of Functions
2676
set textheaderindex = A-Z Index of Functions
2677
2678
#------------------------------------------------------------------------
2679
# texttitle/headerfiles:    text of title and header for files
2680
#                           name of file will be added at the end
2681
#------------------------------------------------------------------------
2682 389:0bc92382a86b chris
set texttitlefiles = Function
2683 383:47ae83ce8db8 Chris
set textheaderfiles = Documentation of
2684
2685
#------------------------------------------------------------------------
2686
# frames: whether to use frames in layout (yes or no)
2687
#------------------------------------------------------------------------
2688
set frames = no
2689
2690
#------------------------------------------------------------------------
2691
# filenametopframe: name of file including frame layout (highest level file)
2692
# [default: index]
2693
#------------------------------------------------------------------------
2694
set filenametopframe = index
2695
2696
#------------------------------------------------------------------------
2697
# textjumpindexglobal: text displayed for jump to index of all files
2698
#                      (global)
2699
# textjumpindexlocal:  text displayed for jump to index of files in actual
2700
#                      directory (local)
2701
#------------------------------------------------------------------------
2702 389:0bc92382a86b chris
set textjumpindexglobal = <b>Index of</b> all files:
2703
set textjumpindexlocal = this subdirectory only:
2704 383:47ae83ce8db8 Chris
2705
#------------------------------------------------------------------------
2706
# includesource: include source of m-files in documentation [YES|no]
2707
#------------------------------------------------------------------------
2708
set includesource = yes
2709
2710
#------------------------------------------------------------------------
2711
# usecontentsm: use contents.m files as well for structured
2712
#               (hopefully) index [YES|no]
2713
#------------------------------------------------------------------------
2714
set usecontentsm = no
2715
2716
#------------------------------------------------------------------------
2717
# includesource: write/update contents.m files [yes|NO]
2718
#------------------------------------------------------------------------
2719
set writecontentsm = no
2720
2721
#------------------------------------------------------------------------
2722
# processtree:  parse whole directory tree recursively [YES|no]
2723
#------------------------------------------------------------------------
2724
set processtree = yes
2725
2726
#------------------------------------------------------------------------
2727
# producetree:  produce tree for html-files in same structure than
2728
#		          tree of m-files [yes|NO]
2729
#               if no, all files are saved in the same directory, often
2730
#               easier for outside linking to files
2731
#------------------------------------------------------------------------
2732 401:a0f6e994657f chris
set producetree = yes
2733 383:47ae83ce8db8 Chris
2734
#------------------------------------------------------------------------
2735
# codebodyindex/files: HTML-code for adding to BODY tag
2736
#                      can be used for defining colors and
2737
#                      backgroundimages of the files
2738
#                      No longer recommended, use the css file
2739
#------------------------------------------------------------------------
2740
set codebodyindex =
2741
set codebodyfiles =
2742
2743
#------------------------------------------------------------------------
2744
# codeheadmeta: HTML-code added in HEAD area, use for supplying META info
2745
#------------------------------------------------------------------------
2746
set codeheadmeta =
2747
2748
#------------------------------------------------------------------------
2749
# codehr: HTML-code used to define a <HR>, do what you want
2750
#------------------------------------------------------------------------
2751 411:e7ba81c8dc5a chris
set codehr = <hr>
2752 383:47ae83ce8db8 Chris
2753
#------------------------------------------------------------------------
2754
# codeheader: HTML-code added to <H*> tags, use for centering header text
2755
#             or changing the colour/size/font of the header text
2756
#------------------------------------------------------------------------
2757 389:0bc92382a86b chris
set codeheader =
2758 383:47ae83ce8db8 Chris
2759
2760
# End of parameter file
2761 381:2dc8163e9150 Chris
@rem = '--*-Perl-*--';
2762
@rem = '
2763
@echo off
2764
perl -w -S %0.bat %1 %2 %3 %4 %5 %6 %7 %8 %9
2765
goto endofperl
2766
@rem ';
2767
# perl -w -S %0.bat "$@"
2768 382:baff1c482d98 Chris
#!/usr/bin/perl
2769 381:2dc8163e9150 Chris
#
2770
# mtree2html_2000 - produce html files from Matlab m-files.
2771
#                   use configuration file for flexibility
2772
#                   can process tree of directories
2773
#
2774
# Copyright (C) 1996-2000 Hartmut Pohlheim.  All rights reserved.
2775
# includes small parts of m2html from Jeffrey C. Kantor 1995
2776
#
2777
# Author:  Hartmut Pohlheim
2778
# History: 06.03.1996  file created
2779
#          07.03.1996  first working version
2780
#          08.03.1996  modularized, help text only once included
2781
#          11.03.1996  clean up, some functions rwritten
2782
#          18.04.1996  silent output with writing in one line only
2783
#                      version 0.20 fixed
2784
#          14.05.1996  start of adding tree structure, could create tree
2785
#          15.05.1996  creating of index files for every directory
2786
#          17.05.1996  first working version except compact A-Z index
2787
#          20.05.1996  cleanup of actual version, more variables and
2788
#                      configurable settings
2789
#          21.05.1996  reading, update and creation of contents.m added
2790
#          22.05.1996  creation of short index started
2791
#          28.05.1996  jump letters for short index,
2792
#                      3 different directory indexes (short/long/contents)
2793
#          29.05.1996  major cleanup, short and long index created from one function
2794
#                      links for HTML and Indexes from 1 function,
2795
#                      version 0.9
2796
#          30.05.1996  contents.m changed to Contents.m (because unix likes it)
2797
#                      function definition can be in first line of m file before comments
2798
#                      version 0.91 fixed
2799
#          03.06.1996  contents file can be written as wanted, the links will be correct
2800
#                      cross references in help block of m-file will be found and
2801
#                      converted, even if the name of the function is written upper case
2802
#                      version 0.92 fixed
2803
#          05.06.1996  construction of dependency matrix changed, is able now to process
2804
#                      even the whole matlab tree (previous version needed to much memory)
2805
#                      removed warning for contents files in different directories
2806
#                      version 0.94 fixed
2807
#          06.06.1996  new link name matrices for ConstructHTMLFile created,
2808
#                      everything is done in ConstructDependencyMatrix,
2809
#                      both dependencies (calls and called) and matrix
2810
#                      with all mentioned names in this m-file, thus, much
2811
#                      less scanning in html construction
2812
#                      script is now (nearly) linear scalable, thus, matlab-toolbox
2813
#                      tree takes less than 1 hour on a Pentium120, with source
2814
#                      version 0.96 fixed
2815
#          10.06.1996  order of creation changed, first all indexes (includes
2816
#                      update/creation of contents.m) and then ConstructDepency
2817
#                      thus, AutoAdd section will be linked as well
2818
#                      excludenames extended, some more common word function names added
2819
#                      version 0.97 fixed
2820
#          17.02.1998  writecontentsm as command line parameter added
2821
#                      error of file not found will even appear when silent
2822
#                      version 1.02
2823
#          21.05.2000  mark comments in source code specially (no fully correct,
2824
#                      can't handle % in strings)
2825
#                      version 1.11
2826
#          05.11.2000  link also to upper and mixed case m-files
2827
#                      searching for .m files now really works (doesn't find grep.com any longer)
2828
#                      file renamed to mtree2html2001
2829
#                      generated html code now all lower case
2830
#                      inclusion of meta-description and meta-keywords in html files
2831
#                      HTML4 compliance done (should be strict HTML4.0, quite near XHTML)
2832
#                      version 1.23
2833
#
2834 383:47ae83ce8db8 Chris
#	   29.03.2011  (Chris Cannam) add frames option.
2835 381:2dc8163e9150 Chris
2836
$VERSION  = '1.23';
2837
($PROGRAM = $0) =~ s@.*/@@; $PROGRAM = "\U$PROGRAM\E";
2838 411:e7ba81c8dc5a chris
$debug = 1;
2839 381:2dc8163e9150 Chris
2840
#------------------------------------------------------------------------
2841
# Define platform specific things
2842
#------------------------------------------------------------------------
2843
# suffix for files to search is defined twice
2844
# the first ($suffix) is for string creation and contains the . as well
2845
# the second ($suffixforsearch) is for regular expression, handling of . is quite special
2846
$suffix = ".m";
2847
$suffixforsearch = "m";
2848
# the directory separator
2849
$dirsep = "/";
2850
# what is the current directory
2851
$diract = ".";
2852
2853
#------------------------------------------------------------------------
2854 382:baff1c482d98 Chris
#  Define all variables and their standard settings
2855 381:2dc8163e9150 Chris
#  documentation of variables is contained in accompanying rc file
2856
#------------------------------------------------------------------------
2857
%var =
2858
(
2859
   'authorfile',                '',
2860
   'codebodyfiles',             '',
2861
   'codebodyindex',             '',
2862
   'codeheadmeta',              '<meta name="author of conversion perl script" content="Hartmut Pohlheim" />',
2863
   'codehr',                    '<hr size="3" noshade="noshade" />',
2864
   'codeheader',                '',
2865 383:47ae83ce8db8 Chris
   'configfile',                'matlab-docs.conf',
2866 381:2dc8163e9150 Chris
   'csslink',                   '',
2867
   'dirmfiles',                 $diract,
2868
   'dirhtml',                   $diract,
2869
   'exthtml',                   '.html',
2870 382:baff1c482d98 Chris
   'frames',                    'yes',
2871 381:2dc8163e9150 Chris
   'filenametopframe',          'index',
2872
   'filenameindexlongglobal',   'indexlg',
2873
   'filenameindexlonglocal',    'indexll',
2874
   'filenameindexshortglobal',  'indexsg',
2875
   'filenameindexshortlocal',   'indexsl',
2876
   'filenameextensionframe',    'f',
2877
   'filenameextensionindex',    'i',
2878
   'filenameextensionjump',     'j',
2879
   'filenamedirshort',          'dirtops',
2880
   'filenamedirlong',           'dirtopl',
2881
   'filenamedircontents',       'dirtopc',
2882
   'includesource',             'yes',
2883
   'links2filescase',           'all',
2884
   'processtree',               'yes',
2885
   'producetree',               'yes',
2886
   'textjumpindexlocal',        'Local Index',
2887
   'textjumpindexglobal',       'Global Index',
2888
   'texttitleframelayout',      'Documentation of Matlab Files',
2889
   'texttitleindexalldirs',     'Index of Directories',
2890
   'textheaderindexalldirs',    'Index of Directories',
2891
   'texttitleindex',            '',
2892
   'textheaderindex',           '',
2893
   'texttitlefiles',            'Documentation of ',
2894
   'textheaderfiles',           'Documentation of ',
2895
   'usecontentsm',              'yes',
2896
   'writecontentsm',            'no'
2897
);
2898
2899
2900
# define all m-file names, that should be excluded from linking
2901
# however, files will still be converted
2902
@excludenames = ( 'all','ans','any','are',
2903
                  'cs',
2904
                  'demo','dos',
2905
                  'echo','edit','else','elseif','end','exist',
2906
                  'flag','for','function',
2907
                  'global',
2908
                  'help',
2909
                  'i','if','inf','info',
2910
                  'j',
2911
                  'more',
2912
                  'null',
2913
                  'return',
2914
                  'script','strings',
2915
                  'what','which','while','who','whos','why',
2916
                );
2917
2918
# Text for inclusion in created HTML/Frame files: Doctype and Charset
2919
$TextDocTypeHTML  = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">';
2920
$TextDocTypeFrame = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" "http://www.w3.org/TR/REC-html40/frameset.dtd">';
2921
$TextMetaCharset = '<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />';
2922
2923
#------------------------------------------------------------------------
2924
# Read the command line arguments
2925
#------------------------------------------------------------------------
2926
if (@ARGV == 0) {
2927
   &DisplayHelp()  if &CheckFileName($var{'configfile'}, 'configuration file');
2928
}
2929
2930
# Print provided command line arguments on screen
2931
foreach (@ARGV) { print "   $_\n      "; }
2932
2933
# Get the options
2934
use Getopt::Long;
2935
@options = ('help|h', 'todo|t', 'version|v',
2936
            'authorfile|a=s', 'configfile|c=s', 'dirhtml|html|d=s',
2937
            'dirmfiles|mfiles|m=s', 'includesource|i=s',
2938
            'processtree|r=s', 'producetree|p=s',
2939
            'silent|quiet|q', 'writecontentsm|w=s');
2940
&GetOptions(@options) || die "use -h switch to display help statement\n";
2941
2942
2943
# Display help or todo list, when requested
2944
&DisplayHelp()                         if $opt_help;
2945
&DisplayTodo()                         if $opt_todo;
2946
die "$PROGRAM v$VERSION\n"             if $opt_version;
2947
2948
$exit_status = 0;
2949
2950
#------------------------------------------------------------------------
2951
# Read the config file
2952
#------------------------------------------------------------------------
2953
$var{'configfile'} = $opt_configfile         if $opt_configfile;
2954
&GetConfigFile($var{'configfile'});
2955
2956
2957
#------------------------------------------------------------------------
2958
# Process/Check the command line otions
2959
#------------------------------------------------------------------------
2960
$var{'dirhtml'}   = $opt_dirhtml              if $opt_dirhtml;
2961
if (!(substr($var{'dirhtml'}, -1, 1) eq $dirsep)) { $var{'dirhtml'} = $var{'dirhtml'}.$dirsep; }
2962
$var{'dirmfiles'} = $opt_dirmfiles            if $opt_dirmfiles;
2963
if (!(substr($var{'dirmfiles'}, -1, 1) eq $dirsep)) { $var{'dirmfiles'} = $var{'dirmfiles'}.$dirsep; }
2964
2965
$var{'authorfile'} = $opt_author              if $opt_author;
2966
$var{'includesource'} = $opt_includesource    if $opt_includesource;
2967
if ($var{'includesource'} ne 'no') { $var{'includesource'} = 'yes'; }
2968
$var{'processtree'} = $opt_processtree        if $opt_processtree;
2969
if ($var{'processtree'} ne 'no') { $var{'processtree'} = 'yes'; }
2970
$var{'producetree'} = $opt_producetree        if $opt_producetree;
2971
if ($var{'producetree'} ne 'no') { $var{'producetree'} = 'yes'; }
2972
if ($var{'processtree'} eq 'no') { $var{'producetree'} = 'no'; }
2973 382:baff1c482d98 Chris
if ($var{'frames'} ne 'no') { $var{'frames'} = 'yes'; }
2974 381:2dc8163e9150 Chris
# if (($var{'processtree'} eq 'yes') && ($var{'producetree'} eq 'no')) { $var{'usecontentsm'} = 'no'; }
2975
2976
$var{'writecontentsm'} = $opt_writecontentsm  if $opt_writecontentsm;
2977
2978
#------------------------------------------------------------------------
2979
# Do the real stuff
2980
#------------------------------------------------------------------------
2981
2982
# Print variables on screen, when not silent
2983
&ListVariables                          if !$opt_silent;
2984
2985
# Check the author file
2986
if ($var{'authorfile'} ne '') {
2987 410:675de8e6becf chris
    if (!($var{'authorfile'} =~ m,^/,)) {
2988
	# relative path: treat as relative to config file
2989
	my $cfd = $var{'configfile'};
2990
	$cfd =~ s,/[^/]*$,/,;
2991
	$cfd =~ s,^[^/]*$,.,;
2992
	$var{'authorfile'} = "$cfd/" . $var{'authorfile'};
2993
    }
2994
    if (&CheckFileName($var{'authorfile'}, 'author file')) {
2995
	$var{'authorfile'} = '';
2996
	if (!$opt_silent) { print "   Proceeding without author information!\n"; }
2997
    }
2998 381:2dc8163e9150 Chris
}
2999
3000
# Call the function doing all the real work
3001
&ConstructNameMatrix;
3002
3003
&ConstructDependencyMatrix;
3004
3005
&ConstructAllIndexFiles;
3006
3007
&ConstructHTMLFiles;
3008
3009
exit $exit_status;
3010
3011
#------------------------------------------------------------------------
3012
# Construct list of all mfile names and initialize various data arrays.
3013
#------------------------------------------------------------------------
3014
sub ConstructNameMatrix
3015
{
3016
   local(*MFILE);
3017
   local($file, $dirname);
3018
   local(@newdirectories);
3019
   local(%localnames);
3020
3021
   $RecDeep = 0;
3022
   &ParseTreeReadFiles($var{'dirmfiles'}, $RecDeep);
3023
3024
   foreach $dirname (@directories) {
3025
      if ($dirnumbermfiles{$dirname} > 0) {
3026
         push(@newdirectories, $dirname);
3027
         if (! defined($contentsname{$dirname})) {
3028
            $contentsname{$dirname} = 'Contents';
3029
            if (($var{'writecontentsm'} eq 'no') && ($var{'usecontentsm'} eq 'yes')) {
3030
               print "\r ParseTree - for directory  $dirname  no contents file found!\n";
3031
               print   "             create one or enable writing of contents file (writecontentsm = yes)!\n";
3032
            }
3033
         }
3034
      }
3035
   }
3036
   @alldirectories = @directories;
3037
   @directories = @newdirectories;
3038
3039
   foreach $dirname (@directories) {
3040
      if ($debug > 0) { print "Dir: $dirname \t\t $dirnumbermfiles{$dirname} \t$contentsname{$dirname}\n"; }
3041
   }
3042
3043
   @names = sort(keys %mfile);
3044
3045
   # check, if name of directory is identical to name of file
3046
   @dirsinglenames = values(%dirnamesingle);
3047
   grep($localnames{$_}++, @dirsinglenames);
3048
   @dirandfilename = grep($localnames{$_}, @names);
3049
   if (@dirandfilename) {
3050
      print "\r   Name clash between directory and file name: @dirandfilename\n";
3051
      print   "      These files will be excluded from linking!\n";
3052
      push(@excludenames, @dirandfilename);
3053
   }
3054
3055
   # construct names matrix for help text linking
3056
   #    exclude some common words (and at the same time m-functions) from linking in help text
3057
   grep($localnames{$_}++, @excludenames);
3058
   @linknames = grep(!$localnames{$_}, @names);
3059
3060
   if ($debug > 2) { print "linknames (names of found m-files):\n    @linknames\n"; }
3061
3062
}
3063
3064
#------------------------------------------------------------------------
3065
# Parse tree and collect all Files
3066
#------------------------------------------------------------------------
3067
sub ParseTreeReadFiles
3068
{
3069
   local($dirname, $localRecDeep) = @_;
3070
   local($file, $name, $filewosuffix);
3071
   local($dirhtmlname, $dirmode);
3072
   local($relpath, $relpathtoindex, $replacevardir);
3073
   local(*CHECKDIR, *AKTDIR);
3074
   local(@ALLEFILES);
3075
3076
   opendir(AKTDIR, $dirname) || die "ParseTree - Can't open directory $dirname: $!";
3077
   if ($debug > 1) { print "\nDirectory: $dirname\n"; }
3078
3079
   # create relative path
3080
   $_ = $dirname; $replacevardir = $var{'dirmfiles'};
3081
   s/$replacevardir//; $relpath = $_;
3082
   s/[^\/]+/../g; $relpathtoindex = $_;
3083
3084
   # producetree no
3085
   if ($var{'producetree'} eq 'no') { $relpath = ''; $relpathtoindex = ''; }
3086
3087
   # names of directories (top-level and below top-level m-file-directory)
3088
   push(@directories, $dirname);
3089
   $dirnumbermfiles{$dirname} = 0;    # set number of m-files for this dir to zero
3090
   # relative path from top-level directory, depends on directory name
3091
   $dirnamerelpath{$dirname} = $relpath;
3092
   # relative path from actual directory to top-level directory, depends on directory name
3093
   $dirnamerelpathtoindex{$dirname} = $relpathtoindex;
3094
   # recursion level for directory, depends on directory name
3095
   $dirnamerecdeep{$dirname} = $localRecDeep;
3096
3097
   # only the name of the directory, without path
3098
   $rindexprint = rindex($dirname, $dirsep, length($dirname)-2);
3099
   $rindsub = substr($dirname, $rindexprint+1, length($dirname)-$rindexprint-2);
3100
   $dirnamesingle{$dirname} = $rindsub;
3101
3102
   # create name of html-directories
3103
   $_ = $dirname;
3104
   s/$var{'dirmfiles'}/$var{'dirhtml'}/;
3105
   $dirhtmlname = $_;
3106
   if ($var{'producetree'} eq 'no') { $dirhtmlname = $var{'dirhtml'}; }
3107
   # try to open html directory, if error, then create directory,
3108
   # use same mode as for corresponding m-file directory
3109
   opendir(CHECKDIR,"$dirhtmlname") || do {
3110
      $dirmode = (stat($dirname))[2]; # print "$dirmode\n";
3111
      mkdir("$dirhtmlname", $dirmode) || die ("Cannot create directory $dirhtmlname: $! !");
3112
   };
3113
   closedir(CHECKDIR);
3114
3115
3116
   # read everything from this directory and process them
3117
   @ALLEFILES = readdir(AKTDIR);
3118
3119
   foreach $file (@ALLEFILES) {
3120
      # exclude . and .. directories
3121
      next if $file eq '.';  next if $file eq '..';
3122
3123
      # test for existense of entry (redundant, used for debugging)
3124
      if (-e $dirname.$file) {
3125
         # if it's a directory, call this function recursively
3126
         if (-d $dirname.$file) {
3127
            if ($var{'processtree'} eq 'yes') {
3128
               &ParseTreeReadFiles($dirname.$file.$dirsep, $localRecDeep+1);
3129
            }
3130
         }
3131
         # if it's a file - test for m-file, save name and create some arrays
3132
         elsif (-f $dirname.$file) {
3133
            if ($file =~ /\.$suffixforsearch$/i) {
3134
               # Remove the file suffix to establish the matlab identifiers
3135
               $filewosuffix = $file;
3136
               $filewosuffix =~ s/\.$suffixforsearch$//i;
3137
               # $filename = $name;
3138
3139
               # Contents file in unix must start with a capital letter (Contents.m)
3140
               # ensure, that m-file name is lower case, except the contents file
3141
               if (! ($filewosuffix =~ /^contents$/i)) {
3142 388:dad587ecb8d0 chris
		   # if ($var{'links2filescase'}  eq 'low') { $filewosuffix = "\L$filewosuffix\E"; }
3143 381:2dc8163e9150 Chris
                  $filewosuffixlow = "\L$filewosuffix\E";
3144
               }
3145
               else { $contentsname{$dirname} = $filewosuffix; }
3146
3147
               # internal handle name is always lower case
3148
               $name     = $filewosuffixlow;
3149
               # file name is not lower case
3150
               $filename = $filewosuffix;
3151
3152
               # if don't use C|contents.m, then forget all C|contents.m
3153
               if ($var{'usecontentsm'} eq 'no') { if ($name =~ /contents/i) { next; } }
3154
3155
               # if m-file with this name already exists, use directory and name for name
3156
               # only the first occurence of name will be used for links
3157
               if (defined $mfile{$name}) {
3158
                  if (! ($name =~ /^contents$/i) ) {
3159
                     print "\r ParseTree - Name conflict:  $name in $dirname already exists: $mfile{$name} !\n";
3160
                     print   "             $mfile{$name}  will be used for links!\n";
3161
                  }
3162
                  $name = $dirname.$name;
3163
               }
3164
               # mfile name with path
3165
               $mfile{$name} = $dirname.$file;
3166
               # mfile name (without path)
3167
               $mfilename{$name} = $filename;
3168
               # mfile directory
3169
               $mfiledir{$name} = $dirname;
3170
3171
               # html file name and full path, special extension of Contents files
3172
               if ($name =~ /contents/i) { $extrahtmlfilename = $dirnamesingle{$dirname}; }
3173
               else { $extrahtmlfilename = ''; }
3174
               $hfile{$name} = $dirhtmlname.$mfilename{$name}.$extrahtmlfilename.$var{'exthtml'};
3175
3176
               # save relative html path
3177
               # if ($var{'producetree'} eq 'yes') {
3178
               $hfilerelpath{$name} = $relpath;
3179
               # } else { # if no tree to produce, relative path is empty
3180
               #    $hfilerelpath{$name} = '';
3181
               # }
3182
3183
               # create relative path from html file to directory with global index file
3184
               $hfileindexpath{$name} = $relpathtoindex;
3185
3186
               # Function declaration, if one exists, set default to script
3187
               $synopsis{$name} = "";
3188
               $mtype{$name} = "script";
3189
3190
               # First comment line
3191
               $apropos{$name} = "";
3192
3193
               # count number of m-files in directories
3194
               $dirnumbermfiles{$dirname}++;
3195
3196
               if ($debug > 1) {
3197
                  if ($opt_silent) { print "\r"; }
3198
                  print "   ParseTree: $name \t\t $mfile{$name} \t\t $hfile{$name}\t\t";
3199
                  if (!$opt_silent) { print "\n"; }
3200
               }
3201
            }
3202
         }
3203
         else {
3204
            print "Unknown type of file in $dirname: $file\n";
3205
         }
3206
      }
3207
      else { print "Error: Not existing file in $dirname: $file\n"; }
3208
   }
3209
3210
   closedir(AKTDIR)
3211
3212
}
3213
3214
#------------------------------------------------------------------------
3215
# Construct Dependency matrix
3216
#    $dep{$x,$y} > 0 if $x includes a reference to $y.
3217
#------------------------------------------------------------------------
3218
sub ConstructDependencyMatrix
3219
{
3220
   &ConstructDependencyMatrixReadFiles('all');
3221
   &ConstructDependencyMatrixReally;
3222
}
3223
3224
3225
#------------------------------------------------------------------------
3226
# Construct Dependency matrix
3227
#    $dep{$x,$y} > 0 if $x includes a reference to $y.
3228
#------------------------------------------------------------------------
3229
sub ConstructDependencyMatrixReadFiles
3230
{
3231
   local($whatstring) = @_;
3232
   local(*MFILE);
3233
   local($name, $inames);
3234
   local(%symbolsdep, %symbolsall);
3235
3236
   # Initialize as all zeros.
3237
   # foreach $name (@names) { grep($dep{$name,$_}=0,@names); if ($debug > 0) { print "\r   DepMatrix anlegen: $name\t$#names\t"; } }
3238
3239
   # Compute the dependency matrix
3240
   $inames = -1;
3241
   foreach $name (@names) {
3242
      # Read each file and tabulate the distinct alphanumeric identifiers in
3243
      # an array of symbols. Also scan for:
3244
      #   synopsis: The function declaration line
3245
      #   apropos:  The first line of the help text
3246
3247
      # look for whatstring, if all: process every file, if contents: process only contents files
3248
      if ($whatstring eq 'contents') { if (! ($name =~ /contents$/i) ) { next; } }
3249
      elsif ($whatstring eq 'all') { }    # do nothing
3250
      else { print "\r   ConstructDependency: Unknown parameter whatstring: $whatstring \n"; }
3251
3252
      undef %symbolsall; undef %symbolsdep;
3253
      open(MFILE,"<$mfile{$name}") || die("Can't open $mfile{$name}: $!\n");
3254
      while (<MFILE>) {
3255
         chop;
3256
3257
         # Split on nonalphanumerics, then look for all words, used for links later
3258
         # this one for all references
3259
         @wordsall = grep(/[a-zA-Z]\w*/, split('\W',$_));
3260
         # set all words to lower case for link checking
3261
         undef @wordsall2;
3262
         # do case conversion not, case checking is done later
3263
         foreach (@wordsall) { push(@wordsall2, "\L$_\E"); }
3264
         # @wordsall2 = @wordsall;
3265
         grep($symbolsall{$_}++, @wordsall2);
3266
3267
         # Store first comment line, skip all others.
3268
         if (/^\s*%/) {
3269
            if (!$apropos{$name}) {
3270
               s/^\s*%\s*//;   # remove % and leading white spaces on line
3271
               $_ = &SubstituteHTMLEntities($_);
3272
               $apropos{$name} = $_;
3273
            }
3274
            next;
3275
         }
3276
3277
         # If it's the function declaration line, then store it and skip
3278
         # but only, when first function definition (multiple function lines when private subfunctions in file
3279
         if ($synopsis{$name} eq '') {
3280
            if (/^\s*function/) {
3281
               s/^\s*function\s*//;
3282
               $synopsis{$name} = $_;
3283
               $mtype{$name} = "function";
3284
               next;
3285
            }
3286
         }
3287
3288
         # Split off any trailing comments
3289
         if ($_ ne '') {
3290
            # this one for references in program code only
3291
            # when syntax parsing, here is a working place
3292
            ($statement) = split('%',$_,1);
3293
            @wordsdep = grep(/[a-zA-Z]\w*/,split('\W',$statement));
3294
            # do case conversion not, case checking is done later
3295
            undef @wordsdep2;
3296
            foreach (@wordsdep) { push(@wordsdep2, "\L$_\E"); }
3297
            grep($symbolsdep{$_}++, @wordsdep2);
3298
         }
3299
      }
3300
      close MFILE;
3301
3302
      # compute intersection between %symbolsall and @linknames
3303
      delete($symbolsall{$name});
3304
      # foreach $localsumall ($symbolsall) {
3305
      #    $localsumall = "\L$localsumall\E";
3306
      # }
3307
      @{'all'.$name} = grep($symbolsall{$_}, @linknames);
3308
3309
      # compute intersection between %symbolsdep and @linknames
3310
      delete($symbolsdep{$name});
3311
      @{'depcalls'.$name} = grep($symbolsdep{$_}, @linknames);
3312
3313
      $inames++; print "\r   DepCallsMatrix: $inames/$#names\t $name\t";
3314
      if ($debug > 2) { print "\n      depnames: @{'depcalls'.$name}\n      all: @{'all'.$name}\n"; }
3315
   }
3316
}
3317
3318
3319
#------------------------------------------------------------------------
3320
# Construct Dependency matrix
3321
#    $dep{$x,$y} > 0 if $x includes a reference to $y.
3322
#------------------------------------------------------------------------
3323
sub ConstructDependencyMatrixReally
3324
{
3325
   local($inames, $name);
3326
3327
   $inames = -1;
3328
   foreach $name (@names) { undef %{'depint'.$name}; }
3329
   foreach $name (@names) {
3330
      grep(${'depint'.$_}{$name}++, @{'depcalls'.$name});
3331
      $inames++; print "\r   DepCalledMatrix1: $inames/$#names\t $name\t";
3332
   }
3333
   $inames = -1;
3334
   foreach $name (@names) {
3335
      # compute intersection between %depint.name{$_} and @linknames
3336
      if (defined (%{'depint'.$name})) { @{'depcalled'.$name} = grep(${'depint'.$name}{$_}, @linknames); }
3337
      $inames++; print "\r   DepCalledMatrix2: $inames/$#names\t $name\t";
3338
      if ($debug > 2) { print "\n      depcalled: @{'depcalled'.$name}\n"; }
3339
   }
3340
3341
}
3342
3343
3344
#========================================================================
3345
# Construct all index files
3346
#========================================================================
3347
sub ConstructAllIndexFiles
3348
{
3349
   local(@localnames);
3350
   local($ActDir);
3351
   local($name);
3352
3353
   # define variables and names for frame target
3354
   $GlobalNameFrameMainLeft = 'Cont_Main';
3355
   $GlobalNameFrameMainRight = 'Cont_Lower';
3356
   $GlobalNameFrameAZIndexsmall = 'IndexAZindex';
3357
   $GlobalNameFrameAZIndexjump = 'IndexAZjump';
3358
3359
   $indexcreated = 0;
3360
3361
   &ConstructHighestIndexFile;
3362
   $indexcreated++;
3363
3364
   # if ($var{'producetree'} eq 'yes') {
3365
      # moved next 2 lines out of if for producetree no
3366
      # &ConstructHighestIndexFile;
3367
      # $indexcreated++;
3368
3369
      foreach $ActDir (@directories) {
3370
         undef @localnames;
3371
         foreach $name (@names) {
3372
            local($pathsubstr) = substr($mfile{$name}, 0, rindex($mfile{$name}, "/")+1);
3373
            if ($ActDir eq $pathsubstr) {
3374
               if ($debug > 1) { print "IndexFile: $pathsubstr    ActDir: $ActDir   Hfilerelpath: $hfilerelpath{$name}\n"; }
3375
               push(@localnames, $name);
3376
            }
3377
         }
3378
         if ($debug > 2) { print "localnames: @localnames\n"; }
3379
         # create contents file and short|long index of files in local directory
3380
         &ConstructContentsmFile($ActDir, @localnames);
3381
         &ConstructAZIndexFile($ActDir, 'short', 'local', @localnames);
3382
         &ConstructAZIndexFile($ActDir, 'long', 'local', @localnames);
3383
         $indexcreated+=2;
3384
      }
3385
   # } else {
3386
   #    &ConstructContentsmFile($var{'dirmfiles'}, @names);
3387
   # }
3388
3389
   # create short|long index of files in all directory
3390
   &ConstructAZIndexFile($var{'dirmfiles'}, 'short', 'global', @names);
3391
   &ConstructAZIndexFile($var{'dirmfiles'}, 'long', 'global', @names);
3392
   $indexcreated+=2;
3393
3394
   # if contents.m were created or updated, the dependency matrices should
3395
   # be updated as well
3396
   if ($var{'writecontentsm'} eq 'yes') { &ConstructDependencyMatrixReadFiles('contents');; }
3397
}
3398
3399
3400
#========================================================================
3401
# Construct the highest level index file
3402
#========================================================================
3403
sub ConstructHighestIndexFile
3404
{
3405
   local(*IFILE);
3406
   local($indexfile, $filename);
3407
3408
   # Build the frame layout file, this files includes the layout of the frames
3409
   # Build the frame layout file name (highest one)
3410
   $indexfile = $var{'dirhtml'}.$var{'filenametopframe'}.$var{'exthtml'};
3411
3412 382:baff1c482d98 Chris
   if ($var{'frames'} eq 'yes') {
3413 381:2dc8163e9150 Chris
3414 382:baff1c482d98 Chris
       open(IFILE,">$indexfile") || die("Cannot open frame layout file $indexfile\n");
3415 381:2dc8163e9150 Chris
3416 382:baff1c482d98 Chris
       # Write the header of frame file
3417
       print IFILE "$TextDocTypeFrame\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n";
3418
       print IFILE "   <title>$var{'texttitleframelayout'}</title>\n";
3419
       print IFILE "</head>\n";
3420 381:2dc8163e9150 Chris
3421 382:baff1c482d98 Chris
       # definition of 2 frames, left the tree of directories,
3422
       # right the index of that directory or the docu of a file
3423
       print IFILE "<frameset  cols=\"25%,75%\">\n";
3424
       print IFILE "   <frame src=\"$var{'filenamedirshort'}$var{'exthtml'}\" name=\"$GlobalNameFrameMainLeft\" />\n";
3425
       print IFILE "   <frame src=\"$var{'filenameindexshortglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\" name=\"$GlobalNameFrameMainRight\" />\n";   print IFILE "</frameset>\n";
3426 381:2dc8163e9150 Chris
3427 382:baff1c482d98 Chris
       print IFILE "</html>\n";
3428
3429
       close(IFILE);
3430
3431
       if ($opt_silent) { print "\r"; }
3432
       print "   Frame layout file created: $indexfile\t";
3433
       if (!$opt_silent) { print "\n"; }
3434
   }
3435 381:2dc8163e9150 Chris
3436
   for($irun=0; $irun <= 2; $irun++) {
3437
      # Build the top directory index file, these files include the directory tree
3438
      # Build the directory tree index file name
3439
3440
      # Create no directory file for contents, when no contents to use
3441
      if (($irun == 2) && ($var{'usecontentsm'} eq 'no')) { next; }
3442
3443
      # Assign the correct index file name
3444
      if ($irun == 0) { $filename = $var{'filenamedirshort'}; }
3445
      elsif ($irun == 1) { $filename = $var{'filenamedirlong'}; }
3446
      elsif ($irun == 2) { $filename = $var{'filenamedircontents'}; }
3447
3448
      $indexfile = $var{'dirhtml'}.$filename.$var{'exthtml'};
3449
3450
      open(IFILE,">$indexfile") || die("Cannot open directory tree index file $indexfile\n");
3451
      # Write header of HTML file
3452
      print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n";
3453
3454
      if ($var{'texttitleindexalldirs'} eq '') {
3455
         print IFILE "<title>Index of Directories of $var{'dirmfiles'}</title>\n";
3456
      } else {
3457
         print IFILE "<title>$var{'texttitleindexalldirs'}</title>\n";
3458
      }
3459 382:baff1c482d98 Chris
3460
      if ($var{'frames'} eq 'yes') {
3461
	  print IFILE "<base target=\"$GlobalNameFrameMainRight\" />\n";
3462
      }
3463
3464 381:2dc8163e9150 Chris
      print IFILE "</head>\n";
3465
      print IFILE "<body $var{'codebodyindex'}>\n";
3466 410:675de8e6becf chris
      print IFILE "<div id=\"matlabdoc\">\n";
3467 381:2dc8163e9150 Chris
      if ($var{'textheaderindexalldirs'} eq '') {
3468
         print IFILE "<h1 $var{'codeheader'}>Index of Directories of <em>$var{'dirmfiles'}</em></h1>\n";
3469
      } else {
3470
         print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindexalldirs'}</h1>\n";
3471
      }
3472 389:0bc92382a86b chris
      print IFILE "<p>\n";
3473 382:baff1c482d98 Chris
3474
      if ($var{'frames'} eq 'yes') {
3475
	  if ($irun == 0) { print IFILE "<strong>short</strong>\n"; }
3476
	  else { print IFILE "<a href=\"$var{'filenamedirshort'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">short</a>\n"; }
3477
	  if ($irun == 1) { print IFILE " | <strong>long</strong>\n"; }
3478
	  else { print IFILE " | <a href=\"$var{'filenamedirlong'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">long</a>\n"; }
3479
	  if ($var{'usecontentsm'} eq 'yes') {
3480
	      if ($irun == 2) { print IFILE " | <strong>contents</strong>\n"; }
3481
	      else { print IFILE " | <a href=\"$var{'filenamedircontents'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">contents</a>\n"; }
3482
	  }
3483
      } else {
3484
	  if ($irun == 0) { print IFILE "<strong>short</strong>\n"; }
3485
	  else { print IFILE "<a href=\"$var{'filenamedirshort'}$var{'exthtml'}\">short</a>\n"; }
3486
	  if ($irun == 1) { print IFILE " | <strong>long</strong>\n"; }
3487
	  else { print IFILE " | <a href=\"$var{'filenamedirlong'}$var{'exthtml'}\">long</a>\n"; }
3488
	  if ($var{'usecontentsm'} eq 'yes') {
3489
	      if ($irun == 2) { print IFILE " | <strong>contents</strong>\n"; }
3490
	      else { print IFILE " | <a href=\"$var{'filenamedircontents'}$var{'exthtml'}\">contents</a>\n"; }
3491
	  }
3492 381:2dc8163e9150 Chris
      }
3493
3494
      print IFILE "</p><br />\n\n";
3495
      print IFILE "<ul>\n";
3496
3497
      # go through all directories and create a list entry for each one,
3498
      # depending on recursion level create sublists
3499
      $prevrecdeeplevel = 0;
3500
      foreach $name (@alldirectories) {
3501
         $actrecdeeplevel = $dirnamerecdeep{$name};
3502
         for( ; $prevrecdeeplevel < $actrecdeeplevel; $prevrecdeeplevel++ ) { print IFILE "<ul>\n"; }
3503
         for( ; $prevrecdeeplevel > $actrecdeeplevel; $prevrecdeeplevel-- ) { print IFILE "</ul>\n"; }
3504
         if ($irun == 0) { $indexfilenameused = $var{'filenameindexshortlocal'}.$var{'filenameextensionframe'}; }
3505
         elsif ($irun == 1) { $indexfilenameused = $var{'filenameindexlonglocal'}.$var{'filenameextensionframe'}; }
3506
         elsif ($irun == 2) { $indexfilenameused = $contentsname{$name}; }
3507
         else { die "ConstructHighestIndexFile: Unknown value of irun"; }
3508
         if ($dirnumbermfiles{$name} > 0) {
3509
            # producetree no
3510
            # if ($var{'producetree'} eq 'no') { $dirnamehere = ''; }
3511
            # else { $dirnamehere = '$dirnamerelpath{$name}'; }
3512
            # print IFILE "<LI><A HREF=\"$dirnamehere$indexfilenameused_$dirnamesingle{$name}$var{'exthtml'}\">$dirnamesingle{$name}</A>\n";
3513
            print IFILE "<li><a href=\"$dirnamerelpath{$name}$indexfilenameused$dirnamesingle{$name}$var{'exthtml'}\">$dirnamesingle{$name}</a></li>\n";
3514
         } else {
3515
            # print directories with no m-files inside not
3516
            # print IFILE "<li>$dirnamesingle{$name}</li>\n";
3517
         }
3518
      }
3519
      $actrecdeeplevel = 0;
3520
      for( ; $prevrecdeeplevel > $actrecdeeplevel; $prevrecdeeplevel-- ) { print IFILE "</ul>\n"; }
3521
      print IFILE "</ul>\n<br />$var{'codehr'}\n";
3522
3523
      # Include info about author from authorfile
3524
      &WriteFile2Handle($var{'authorfile'}, IFILE);
3525
3526
      print IFILE "<!--navigate-->\n";
3527
      print IFILE "<!--copyright-->\n";
3528 410:675de8e6becf chris
      print IFILE "</div>\n</body>\n</html>\n";
3529 381:2dc8163e9150 Chris
3530
      close(IFILE);
3531
3532
      if ($opt_silent) { print "\r"; }
3533
      print "   Directory - Indexfile created: $indexfile\t";
3534
      if (!$opt_silent) { print "\n"; }
3535
   }
3536
}
3537
3538
3539
#========================================================================
3540
# Construct the A-Z index file (global/local and/or short/long)
3541
#========================================================================
3542
sub ConstructAZIndexFile
3543
{
3544
   local($LocalActDir, $LocalShortLong, $LocalGlobalLocal, @localnames) = @_;
3545
   local(*IFILE);
3546
   local($name, $indexfilename, $dirpath);
3547
   local($firstletter, $firstone);
3548
3549
   if ($debug > 2) { print "localnames in AZ small: @localnames\n"; print "     ActDir in A-Z: $LocalActDir\n"; }
3550
3551
   # extract filename of index file from parameters of function
3552
   if ($LocalShortLong eq 'short') {
3553
      if ($LocalGlobalLocal eq 'global') { $indexfilename = $var{'filenameindexshortglobal'}; }
3554
      elsif ($LocalGlobalLocal eq 'local') { $indexfilename = $var{'filenameindexshortlocal'}; }
3555
      else { die "wrong parameter for LocalGlobalLocal in ConstructAZIndexFile: $LocalGlobalLocal."; }
3556
   } elsif ($LocalShortLong eq 'long') {
3557
      if ($LocalGlobalLocal eq 'global') { $indexfilename = $var{'filenameindexlongglobal'}; }
3558
      elsif ($LocalGlobalLocal eq 'local') { $indexfilename = $var{'filenameindexlonglocal'}; }
3559
      else { die "wrong parameter for LocalGlobalLocal in ConstructAZIndexFile: $LocalGlobalLocal."; }
3560
   } else { die "wrong parameter for LocalShortLong in ConstructAZIndexFile: $LocalShortLong."; }
3561
3562
   # producetree no
3563
   # if ($var{'producetree'} eq 'no') { $dirnamehere = ''; }
3564
   # else { $dirnamehere = '$dirnamerelpath{$LocalActDir}'; }
3565
   # Build the index file name
3566
   # handle the global index file case separately (no extra directory name in file)
3567
   #    the local index file name must be extended by the name of the directory
3568
   if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; }
3569
   else { $extradirfilename = $dirnamesingle{$LocalActDir}; }
3570
   $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionindex'}.$extradirfilename.$var{'exthtml'};
3571 382:baff1c482d98 Chris
3572 401:a0f6e994657f chris
   if ($LocalShortLong eq 'short' and $extradirfilename eq '' and $var{'frames'} ne 'yes') {
3573
       # With no frames and no subdir path, this must go in the
3574
       # top-level index file instead
3575 382:baff1c482d98 Chris
       $indexfile = $var{'dirhtml'}.$var{'filenametopframe'}.$var{'exthtml'};
3576
   }
3577
3578 381:2dc8163e9150 Chris
   if ($debug > 2) { print "   indexfilename (a-z small): $indexfile\n"; }
3579
3580
   open(IFILE,">$indexfile") || die("Cannot open index file $indexfile: $!\n");
3581
3582
   # Write the header of HTML file
3583
   print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n";
3584
3585 410:675de8e6becf chris
   my $dirToPrint = $LocalActDir;
3586
   $dirToPrint =~ s,^./,,;
3587
3588 381:2dc8163e9150 Chris
   if ($var{'texttitleindex'} eq '') {
3589 410:675de8e6becf chris
      print IFILE "<title>Index of Matlab Files in Directory $dirToPrint</title>\n";
3590 381:2dc8163e9150 Chris
   } else {
3591
      if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; }
3592 410:675de8e6becf chris
      else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; }
3593 381:2dc8163e9150 Chris
   }
3594 382:baff1c482d98 Chris
3595
   if ($var{'frames'} eq 'yes') {
3596
       print IFILE "<base target=\"$GlobalNameFrameMainRight\" />\n";
3597
   }
3598 381:2dc8163e9150 Chris
   print IFILE "</head>\n";
3599 382:baff1c482d98 Chris
3600 381:2dc8163e9150 Chris
   print IFILE "<body $var{'codebodyindex'}>\n";
3601 410:675de8e6becf chris
   print IFILE "<div id=\"matlabdoc\">\n";
3602 381:2dc8163e9150 Chris
   if ($var{'textheaderindex'} eq '') {
3603 410:675de8e6becf chris
      print IFILE "<h1 $var{'codeheader'}>Index of Matlab Files in Directory $dirToPrint</h1>\n";
3604 381:2dc8163e9150 Chris
   } else {
3605
      if ($LocalGlobalLocal eq 'global') { print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindex'}</h1>\n"; }
3606 410:675de8e6becf chris
      else { print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindex'} in Directory $dirToPrint</h1>\n"; }
3607 381:2dc8163e9150 Chris
   }
3608
3609
   # include links to indexes
3610
   &ConstructLinks2Index(IFILE, $dirnamerelpathtoindex{$LocalActDir}, $LocalActDir, $LocalGlobalLocal);
3611
3612
   # Collect the starting letters of m files in this directory or all m-files
3613
   for('a'..'z') { undef @{$_}; }
3614
   foreach $name (@localnames) {
3615
      if (! ($mfilename{$name} =~ /contents/i)) {
3616
         $firstletter = substr($mfilename{$name}, 0, 1);
3617
         # convert first letter always to lower case
3618
         # needed for reference to lower and upper case m-files
3619
         $firstletter = "\L$firstletter\E";
3620
         push(@{$firstletter}, $name);
3621
      }
3622
   }
3623
3624
   if ($LocalShortLong eq 'short') {
3625
      # begin create short index
3626
      print IFILE "<table width=\"100%\">\n";
3627
3628
      for('a'..'z') {
3629
         # print "   $_: @{$_}\n";
3630
         $numberofletter = $#{$_}+1;
3631 410:675de8e6becf chris
	 $cols = 3;
3632 381:2dc8163e9150 Chris
         if ($numberofletter > 0) {
3633 410:675de8e6becf chris
            print IFILE "\n<tr><td><br/><strong><a name=\"\U$_\E$_\"></a><span class=\"an\">\U$_\E</span></strong></td></tr>\n";
3634
            for ($count = 0; $count < $numberofletter; $count++) {
3635
		if (($count % $cols) == 0) {
3636
		    if ($count > 0) {
3637
			print IFILE "</tr><tr>\n";
3638
		    }
3639
		    print IFILE "<tr><td></td>";
3640
		}
3641
		$name = @{$_}[$count];
3642
		if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; }
3643
		print IFILE "<td><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td>";
3644
	    }
3645
3646
	    print IFILE "</tr>\n";
3647
3648
            # $numberhalf = ($numberofletter + 1 - (($numberofletter+1) % 2))/2;
3649
            # if ($debug > 2) { print "   $_: @{$_} \t $numberhalf \t $numberofletter\n"; }
3650
            # for($count = 0; $count < $numberhalf; $count++) {
3651
            #    $name = @{$_}[$count];
3652
            #    if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; }
3653
            #    print IFILE "<tr><td width=\"50%\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td>";
3654
            #    if (($count + $numberhalf) < $numberofletter) {
3655
            #       $name = @{$_}[$count + $numberhalf];
3656
            #       if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; }
3657
            #       print IFILE "<td width=\"50%\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td></tr>\n";
3658
            #    } else {
3659
            #       print IFILE "<td width=\"50%\"></td></tr>\n";
3660
            #    }
3661
            # }
3662 381:2dc8163e9150 Chris
         }
3663
      }
3664
      print IFILE "</table>\n<br />$var{'codehr'}\n";
3665
3666
   } elsif ($LocalShortLong eq 'long') {
3667
      # begin create long index
3668 410:675de8e6becf chris
      print IFILE "<table width=\"100%\">\n";
3669 401:a0f6e994657f chris
      print IFILE "<tr><th>Name</th><th>Synopsis</th></tr>\n";
3670 381:2dc8163e9150 Chris
3671
      for('a'..'z') {
3672
         # print "   $_: @{$_}\n";
3673
         $numberofletter = $#{$_}+1;
3674
         if ($numberofletter > 0) {
3675
            $firstone = 1;
3676
            foreach $name (@{$_}) {
3677
               if ($debug > 1) { print "   AZinforeach1: $name \t\t $hfilerelpath{$name} \t\t $dirnamerelpath{$LocalActDir}\n"; }
3678
               if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; }
3679
               if (! ($mfilename{$name} =~ /contents/i)) {
3680 400:4ad6499d7998 chris
                  if ($firstone == 1) { print IFILE "\n<tr><td colspan=\"2\"><br /><strong><a name=\"\U$_\E$_\"></a><span class=\"an\">\U$_\E</span></strong></td></tr>\n"; $firstone = 0; }
3681 381:2dc8163e9150 Chris
                  print IFILE "<tr><td valign=\"top\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td><td>$apropos{$name}</td></tr>\n";
3682
               }
3683
            }
3684
         }
3685
      }
3686
      print IFILE "</table>\n<br />$var{'codehr'}\n";
3687
   } else { die "wrong parameter for LocalShortLong in ConstructAZIndexFile: $LocalShortLong."; }
3688
3689
   # Include info about author from authorfile
3690
   &WriteFile2Handle($var{'authorfile'}, IFILE);
3691
3692
   print IFILE "<!--navigate-->\n";
3693
   print IFILE "<!--copyright-->\n";
3694 410:675de8e6becf chris
   print IFILE "</div>\n</body>\n</html>\n";
3695 381:2dc8163e9150 Chris
3696
   close(IFILE);
3697
3698
   if ($opt_silent) { print "\r"; }
3699
   print "   Indexfile small (A-Z) created: $indexfile\t";
3700
   if (!$opt_silent) { print "\n"; }
3701
3702
3703
   # Build the A-Z jump index file name
3704
   # handle the global index file case separately (no extra directory name in file)
3705
   if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; }
3706
   else { $extradirfilename = $dirnamesingle{$LocalActDir}; }
3707
3708 382:baff1c482d98 Chris
   if ($var{'frames'} eq 'yes') {
3709
3710
       $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionjump'}.$extradirfilename.$var{'exthtml'};
3711
       if ($debug > 2) { print "   indexfilename (a-z jump): $indexfile\n"; }
3712
       open(IFILE,">$indexfile") || die("Cannot open jump index file $indexfile: $!\n");
3713
3714
       # Write the header of HTML file
3715
       print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n";
3716
3717
       if ($var{'texttitleindex'} eq '') {
3718 410:675de8e6becf chris
	   print IFILE "<title>A-Z jump index in directory $dirToPrint</title>\n";
3719 382:baff1c482d98 Chris
       } else {
3720
	   if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; }
3721 410:675de8e6becf chris
	   else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; }
3722 382:baff1c482d98 Chris
       }
3723
3724
       if ($var{'frames'} eq 'yes') {
3725
	   print IFILE "<base target=\"$GlobalNameFrameAZIndexsmall\" />\n";
3726
       }
3727
       print IFILE "</head>\n";
3728
       print IFILE "<body $var{'codebodyindex'}>\n";
3729 410:675de8e6becf chris
       print IFILE "<div id=\"matlabdoc\">\n";
3730 382:baff1c482d98 Chris
3731
       # Write the A-Z jump line, generate link for letters with files starting with this letter
3732
       # and only letters for no files starting with this letter
3733
       # use previously generated arrays with names of files sorted by starting letter
3734
       for('a'..'z') {
3735
	   $numberofletter = $#{$_}+1;
3736
	   if ($numberofletter > 0) {
3737
	       print IFILE "<strong><a href=\"$indexfilename$var{'filenameextensionindex'}$extradirfilename$var{'exthtml'}#\U$_\E$_\">\U$_\E</a> </strong>\n";
3738
	   } else {
3739
	       print IFILE "\U$_\E \n";
3740
	   }
3741
       }
3742
3743 410:675de8e6becf chris
       print IFILE "</div></body>\n</html>\n";
3744 382:baff1c482d98 Chris
3745
       close(IFILE);
3746
3747
       if ($opt_silent) { print "\r"; }
3748
       print "   Indexfile small (A-Z jump) created: $indexfile\t";
3749
       if (!$opt_silent) { print "\n"; }
3750 381:2dc8163e9150 Chris
   }
3751
3752
3753
   # Build the frame layout file, this file includes the layout of the frames
3754
   # Build the frame layout file name (for small/compact A-Z index)
3755
   # handle the global index file case separately (no extra directory name in file)
3756
   if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; }
3757
   else { $extradirfilename = $dirnamesingle{$LocalActDir}; }
3758
3759 382:baff1c482d98 Chris
   if ($var{'frames'} eq 'yes') {
3760 381:2dc8163e9150 Chris
3761 382:baff1c482d98 Chris
       $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionframe'}.$extradirfilename.$var{'exthtml'};
3762
       if ($debug > 2) { print "   indexfilename (a-z frame): $indexfile\n"; }
3763 381:2dc8163e9150 Chris
3764 382:baff1c482d98 Chris
       open(IFILE,">$indexfile") || die("Cannot open jump index frame file $indexfile: $!\n");
3765
3766
       # Write the header of Frame file
3767
       print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n";
3768
3769
       if ($var{'texttitleindex'} eq '') {
3770 410:675de8e6becf chris
	   print IFILE "<title>Index of Matlab Files in Directory $dirToPrint</title>\n";
3771 382:baff1c482d98 Chris
       } else {
3772
	   if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; }
3773 410:675de8e6becf chris
	   else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; }
3774 382:baff1c482d98 Chris
       }
3775
       print IFILE "</head>\n";
3776
3777
       # definition of 2 frames, top the A-Z index, below the jump letter line
3778
       print IFILE "<frameset  rows=\"90%,10%\">\n";
3779
       print IFILE "   <frame src=\"$indexfilename$var{'filenameextensionindex'}$extradirfilename$var{'exthtml'}\" name=\"$GlobalNameFrameAZIndexsmall\" />\n";
3780
       print IFILE "   <frame src=\"$indexfilename$var{'filenameextensionjump'}$extradirfilename$var{'exthtml'}\" name=\"$GlobalNameFrameAZIndexjump\" />\n";
3781
       print IFILE "</frameset>\n";
3782
3783
       print IFILE "</html>\n";
3784
3785
       close(IFILE);
3786
3787
       if ($opt_silent) { print "\r"; }
3788
       print "   Frame layout file created: $indexfile\t";
3789
       if (!$opt_silent) { print "\n"; }
3790 381:2dc8163e9150 Chris
   }
3791
}
3792 382:baff1c482d98 Chris
3793 381:2dc8163e9150 Chris
3794
#========================================================================
3795
# Construct the links to all indexes
3796
#========================================================================
3797
sub ConstructLinks2Index
3798
{
3799
   local(*WRITEFILE, $LocalPath2Index, $PathContents, $LocalGlobalLocal) = @_;
3800
3801
   # include links to short/long - local/global index and C|contents.m
3802 389:0bc92382a86b chris
   print WRITEFILE "\n<p>";
3803
   print WRITEFILE "$var{'textjumpindexglobal'} ";
3804 382:baff1c482d98 Chris
3805
   if ($var{'frames'} eq 'yes') {
3806
       print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexshortglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\">short</a> | ";
3807 389:0bc92382a86b chris
       print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexlongglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\">long</a>\n";
3808 382:baff1c482d98 Chris
   } else {
3809 387:f89765996ef9 Chris
       print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenametopframe'}$var{'exthtml'}\">short</a> | ";
3810 389:0bc92382a86b chris
       print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexlongglobal'}$var{'filenameextensionindex'}$var{'exthtml'}\">long</a>\n";
3811 382:baff1c482d98 Chris
   }
3812
3813 381:2dc8163e9150 Chris
   if ($LocalGlobalLocal eq 'local') {
3814
      if ($var{'usecontentsm'} eq 'yes') {
3815
         print WRITEFILE " | <a href=\"$contentsname{$PathContents}$dirnamesingle{$PathContents}$var{'exthtml'}\">Local contents</a>\n";
3816
      }
3817 389:0bc92382a86b chris
      if ($var{'frames'} eq 'yes') {
3818
         print WRITEFILE " | $var{'textjumpindexlocal'} ";
3819 382:baff1c482d98 Chris
         print WRITEFILE "<a href=\"$var{'filenameindexshortlocal'}$var{'filenameextensionframe'}$dirnamesingle{$PathContents}$var{'exthtml'}\">short</a> | ";
3820 389:0bc92382a86b chris
         print WRITEFILE "<a href=\"$var{'filenameindexlonglocal'}$var{'filenameextensionframe'}$dirnamesingle{$PathContents}$var{'exthtml'}\">long</a>\n";
3821
      } else {
3822
         print WRITEFILE " | $var{'textjumpindexlocal'} ";
3823
         print WRITEFILE "<a href=\"$var{'filenameindexshortlocal'}$var{'filenameextensionindex'}$dirnamesingle{$PathContents}$var{'exthtml'}\">short</a> | ";
3824
         print WRITEFILE "<a href=\"$var{'filenameindexlonglocal'}$var{'filenameextensionindex'}$dirnamesingle{$PathContents}$var{'exthtml'}\">long</a>\n";
3825
      }
3826 381:2dc8163e9150 Chris
   }
3827
   print WRITEFILE "</p>\n\n";
3828
   print WRITEFILE "$var{'codehr'}\n";
3829
}
3830
3831
3832
#========================================================================
3833
# Construct the contents.m files or update
3834
#========================================================================
3835
sub ConstructContentsmFile
3836
{
3837
   local($LocalActDir, @localnames) = @_;
3838
   local(*CFILE, $name,$newline);
3839
   local($contentsfile, $isincontentsonly);
3840
   local(@lines, @autoaddlines, @emptylines);
3841
   local($autoadd) = 'AutoAdd';
3842
   local($autoaddsection) = 0;
3843
   local($emptylineflag) = 0;
3844
   local(%nameincontents);
3845
3846
   # Build the contents file name
3847
   $contentsfile = $LocalActDir.$contentsname{$LocalActDir}.$suffix;
3848
3849
   if (-e $contentsfile) {
3850
      open(CFILE,"<$contentsfile") || die("Cannot open contents file $contentsfile: $!\n");
3851
      while (<CFILE>) {
3852
         # Search for the specified string pattern
3853
         @words = split;
3854
         if ((@words >= 3) && ($words[2] eq '-')) {
3855
            $isincontentsonly = 0;
3856
            foreach $name (@localnames) {
3857
               if ($name eq $words[1]) {    # old
3858
               # if ($mfilename{$name} eq $words[1]) {
3859
                  $isincontentsonly = 1;
3860
                  $nameincontents{$name} = 1;
3861
                  $newline = sprintf("%% %-13s - %s\n", $mfilename{$name}, $apropos{$name});
3862
                  push(@lines, $newline);
3863
               }
3864
            }
3865
            # issue a warning, if file is in contents, but not as file in the directory
3866
            if ($isincontentsonly == 0) {
3867
               print "\rConstructContents: Obsolete entry  $words[1]  in  $contentsfile ! Entry not used.\n";
3868
            }
3869
         } else {
3870
            # look for the AutoAdd section, should be the second word
3871
            if ((@words >= 2) && ($words[1] eq $autoadd)) { $autoaddsection = 1; }
3872
            # push the red line in an array
3873
            push(@lines, $_);
3874
         }
3875
      }
3876
      close(CFILE);
3877
   } else {
3878
      $newline = "% MATLAB Files in directory  $LocalActDir\n%\n";
3879
      push(@lines, $newline);
3880
3881
   }
3882
3883
   # collect the file names, that were not included in original C|contents.m
3884
   foreach $name (@localnames) {
3885
      if (! defined $nameincontents{$name}) {
3886
         if (! ($mfilename{$name} =~ /contents/i)) {
3887
            $newline = sprintf("%% %-13s - %s\n", $mfilename{$name}, $apropos{$name});
3888
            push(@autoaddlines, $newline);
3889
         }
3890
      }
3891
   }
3892
3893
   # write/update C|contents.m only if variable is set
3894
   if ($var{'writecontentsm'} eq 'yes') {
3895
      unlink($contentsfile);
3896
      open(CFILE,">$contentsfile") || die("Cannot open contents file $contentsfile: $!\n");
3897
      # write old C|contents.m or header of new file, as long as comment lines
3898
      foreach $line (@lines) {
3899
         if ($emptylineflag == 0) {
3900
            if ($line =~ /^\s*%/) { print CFILE $line; }
3901
            else { $emptylineflag = 1; push(@emptylines, $line); }
3902
         } else { push(@emptylines, $line); }
3903
      }
3904
      # add header of AutoAdd section
3905
      if (($autoaddsection == 0) && (@autoaddlines > 0)) { print CFILE "%\n% $autoadd\n"; }
3906
      # add autoadd section lines (previously undocumented files
3907
      foreach $line (@autoaddlines) { print CFILE $line; }
3908
      # add tail of original C|contents.m (everything behind first non-comment line)
3909
      foreach $line (@emptylines)   { print CFILE $line; }
3910
      print CFILE "\n";
3911
      close CFILE;
3912
      if ($opt_silent) { print "\r"; }
3913
      print "   Contents file created/updated: $contentsfile\t";
3914
      if (!$opt_silent) { print "\n"; }
3915
   }
3916
}
3917
3918
3919
#========================================================================
3920
# Replace found special characters with their HTMl Entities
3921
#========================================================================
3922
sub SubstituteHTMLEntities {
3923
   local($_) = @_;
3924
3925
   # Replace & <-> &amp;  < <-> &lt;  > <-> &gt;  " <-> &quot;
3926
   s/&/&amp;/g; s/\</&lt;/g; s/\>/&gt;/g; s/\"/&quot;/g;
3927
   return $_;
3928
}
3929
3930
#========================================================================
3931
# Replace found m-filenamestring with full link.
3932
#========================================================================
3933
sub SubstituteName2Link {
3934
   local($_, $funname) = @_;
3935
   local($refstr1, $refstr2, $reffound);
3936
3937
   # Look for something matching in the line
3938
   if ( /(\W+)($funname)(\W+)/i ) {
3939
      $reffound = $2;
3940 388:dad587ecb8d0 chris
      $refstr1 = "<a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$funname}$mfilename{$funname}$var{'exthtml'}\">";
3941 381:2dc8163e9150 Chris
      $refstr2 = "<\/a>";
3942
      # Do links only for exact case match
3943
      if ( ($var{'links2filescase'}  eq 'exact') || ($var{'links2filescase'}  eq 'exactvery') ) {
3944
         if ( /(\W+)($funname)(\W+)/g ) {
3945
            s/(\W+)($funname)(\W+)/$1$refstr1$funname$refstr2$3/g;
3946
         }
3947
         else {
3948
            # Print info for not matching case in references, good for check up of files
3949
            if ( ($var{'links2filescase'}  eq 'exactvery') ) {
3950
               print "Diff in case found: $funname  (case of file name)   <->  $reffound  (case in source code)\n";
3951
               print "     (source line)  $_ \n";
3952
            }
3953
         }
3954
      }
3955
      # Do links for exact match and additionally for all upper case (often used in original matlab help text)
3956
      elsif ( ($var{'links2filescase'}  eq 'exactupper') ) {
3957
         s/(\W+)($funname)(\W+)/$1$refstr1$2$refstr2$3/g;
3958
         $funname2 = "\U$funname\E";
3959
         s/(\W+)($funname2)(\W+)/$1$refstr1$2$refstr2$3/g;
3960
      }
3961
      # Do links for all case mixes, this calls for trouble under LINUX/UNIX
3962
      else {  #elsif ( ($var{'links2filescase'}  eq 'all') )
3963
         s/(\W+)($funname)(\W+)/$1$refstr1$2$refstr2$3/ig;
3964
      }
3965
   }
3966
3967
   return $_;
3968
}
3969
3970
#========================================================================
3971
# Construct the html files for each matlab file.
3972
#    Need to reread each matlab file to find the help text.
3973
#    Note that we can't do this in a single loop because sometimes
3974
#    the help text maybe before the function declaration.
3975
#========================================================================
3976
sub ConstructHTMLFiles
3977
{
3978
   local(*MFILE);
3979
   local(*HFILE);
3980
3981
   local($filescreated) = 0;
3982
   local($functionline);
3983
3984
   foreach $name (@names) {
3985
      # Create cross reference information already here, used for keywords as well
3986
      # Construct list of referenced functions
3987
      @xref = @{'depcalls'.$name};    # the functions, that this m-file calls
3988
      @yref = @{'depcalled'.$name};   # the functions, that this m-file is called from
3989
      # print "   depcalls: @{'depcalls'.$name}\n   depcalled: @{'depcalled'.$name}\n";
3990
      # foreach $cname (@names) { next if $cname eq $name; push(@yref,$cname) if grep(/$name/,@{'depcalls'.$cname}); }
3991
3992
3993
      # Open m-file and html-file
3994
      open(MFILE,"<$mfile{$name}");
3995
      open(HFILE,">$hfile{$name}");
3996
3997
      # Write the header of HTML file
3998
      print HFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n";
3999
4000
      # Write meta tags: use apropos (one line function description) for description
4001
      # and cross reference function names for keywords (any better ideas?)
4002
      print HFILE "<meta name=\"description\" content=\" $apropos{$name} \" />\n";
4003
      print HFILE "<meta name=\"keywords\" content=\" @xref @yref \" />\n";
4004
4005
      # Write Title and start body of html-file
4006
      print HFILE "<title>$var{'texttitlefiles'} $mfilename{$name}</title>\n</head>\n";
4007
      print HFILE "<body $var{'codebodyfiles'}>\n";
4008 410:675de8e6becf chris
      print HFILE "<div id=\"matlabdoc\">\n";
4009 381:2dc8163e9150 Chris
      print HFILE "<h1 $var{'codeheader'}>$var{'textheaderfiles'} $mfilename{$name}</h1>\n";
4010 401:a0f6e994657f chris
4011
# http://test.soundsoftware.ac.uk/cannam/projects/smallbox/repository/annotate/DL/RLS-DLA/SolveFISTA.m
4012 410:675de8e6becf chris
#      print HFILE "<a href=\"" . $hfileindexpath{$name} . "../../projects/smallbox/repository/annotate/" . $mfiledir{$name}  . $mfilename{$name} . ".m\">View in repository</a>\n";
4013 401:a0f6e994657f chris
4014 381:2dc8163e9150 Chris
      print HFILE "$var{'codehr'}\n";
4015
4016
      # include links to short/long - local/global index and C|contents.m
4017
      &ConstructLinks2Index(HFILE, $hfileindexpath{$name}, $mfiledir{$name}, 'local');
4018
4019
      # If this is a function, then write out the first line as a synopsis
4020
      if ($mtype{$name} eq "function") {
4021
         print HFILE "<h2 $var{'codeheader'}>Function Synopsis</h2>\n";
4022
         print HFILE "<pre>$synopsis{$name}</pre>\n$var{'codehr'}\n";
4023
      }
4024
4025
      # Look for the matlab help text block
4026
      $functionline = "\n";
4027
      do {
4028
         $_ = <MFILE>;
4029
         # remember functionline, if before help text block
4030
         if (/^\s*function/) { $functionline = $_; }
4031
      } until (/^\s*%/ || eof);
4032
      if (! (eof(MFILE))) {
4033
         print HFILE "<h2 $var{'codeheader'}>Help text</h2>\n";
4034
         print HFILE "<pre>\n";
4035
         while (/^\s*%/) {
4036
            # First remove leading % and white space, then Substitute special characlers
4037
            s/^\s*%//;
4038
            $_ = &SubstituteHTMLEntities($_);
4039
4040
            # check/create cross references
4041
            foreach $funname (@{'all'.$name}) {
4042
               if ($funname =~ /simulink/) { print "\n Simulink - Filename: $name;  scanname: $funname\n"; }
4043
               next if $funname eq $name;
4044
               $_ = &SubstituteName2Link($_, $funname);
4045
            }
4046
            print HFILE $_;
4047
            if (! eof) { $_ = <MFILE>; }
4048
         }
4049
         print HFILE "</pre>\n$var{'codehr'}\n";
4050
      }
4051
4052
      # Write the cross reference information
4053
      if (@xref || @yref) {
4054
         print HFILE "<h2 $var{'codeheader'}>Cross-Reference Information</H2>\n";
4055
         print HFILE "<table border=\"0\" width=\"100%\">\n<tr align=\"left\">\n<th width=\"50%\">";
4056
         if (@xref) {
4057
            print HFILE "This $mtype{$name} calls";
4058
         }
4059
         print HFILE "</th>\n<th width=\"50%\">";
4060
         if (@yref) {
4061
            print HFILE "This $mtype{$name} is called by";
4062
         }
4063
         print HFILE "</th>\n</tr>\n<tr valign=\"top\"><td>";
4064
         if (@xref) {
4065
            print HFILE "\n<ul>\n";
4066
            foreach $cname (sort @xref) {
4067
               print HFILE "<li><a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$cname}$mfilename{$cname}$var{'exthtml'}\">$mfilename{$cname}</a></li>\n";
4068
            }
4069
            print HFILE "</ul>\n";
4070
         }
4071
         print HFILE "</td><td>";
4072
         if (@yref) {
4073
            print HFILE "\n<ul>\n";
4074
            foreach $cname (sort @yref) {
4075
               print HFILE "<li><a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$cname}$mfilename{$cname}$var{'exthtml'}\">$mfilename{$cname}</a></li>\n";
4076
            }
4077
            print HFILE "</ul>\n";
4078
         }
4079
         print HFILE "</td>\n</tr>\n</table>\n";
4080
         print HFILE "$var{'codehr'}\n";
4081
      }
4082
4083
      # Include source text if requested
4084
      if (($var{'includesource'} eq 'yes') && (! ($mfilename{$name} =~ /^contents$/i))) {
4085
         print HFILE "<h2 $var{'codeheader'}>Listing of $mtype{$name} $mfilename{$name}</h2>\n";
4086
         seek(MFILE,0,0);
4087
         print HFILE "<pre>\n";
4088
         $IsStillHelp = 2;
4089
         print HFILE $functionline;    # functionline from scanning of help
4090
         while (<MFILE>) {
4091
            if ($IsStillHelp == 2) {
4092
               next     if (/^\s*$/);
4093
               next     if (/^\s*function/);
4094
               if (/^\s*%/) { $IsStillHelp = 1; next; }
4095
            } elsif ($IsStillHelp == 1) {
4096
               next     if (/^\s*%/);
4097
               $IsStillHelp = 0;
4098
            }
4099
4100
            # Substritute special characters
4101
            $_ = &SubstituteHTMLEntities($_);
4102
4103
            # check for comment in line and format with css em
4104 400:4ad6499d7998 chris
            s/^(.*)%(.*?)([\s\r\n]+)$/$1<em class=\"mcom\">%$2<\/em>$3/;
4105 381:2dc8163e9150 Chris
4106
            # check/create cross references
4107
            foreach $funname (@{'all'.$name}) {
4108
               next if $funname eq $name;
4109
               $_ = &SubstituteName2Link($_, $funname);
4110
            }
4111
            print HFILE $_;
4112
         }
4113
         print HFILE "</pre>\n$var{'codehr'}\n";
4114
      }
4115
4116
      # Include info about author from authorfile
4117
      &WriteFile2Handle($var{'authorfile'}, HFILE)   ;
4118
4119
      print HFILE "<!--navigate-->\n";
4120
      print HFILE "<!--copyright-->\n";
4121 410:675de8e6becf chris
      print HFILE "</div>\n</body>\n</html>\n";
4122 381:2dc8163e9150 Chris
      close(MFILE);
4123
      close(HFILE);
4124
4125
      # Print name of finished file
4126
      if ($opt_silent) { print "\r"; }
4127
      print "   HTML-File created: $hfile{$name}\t";
4128
      if (!$opt_silent) { print "\n"; }
4129
      $filescreated++;
4130
   }
4131
4132
   print "\n$PROGRAM: $indexcreated index and $filescreated files created.\n";
4133
}
4134
4135
#========================================================================
4136
# Function:	CheckFileName
4137
# Purpose:	.
4138
#========================================================================
4139
sub CheckFileName {
4140
   local($filename, $description) = @_;
4141
   local(*CHECKFILE);
4142
4143
   open(CHECKFILE,"<$filename") || do {
4144
      if ($description eq '') {$description = 'file';}
4145
      # if (!$opt_silent) { print "Cannot open $description $filename: $!\n"; }
4146
      print "Cannot open $description $filename: $!\n";
4147
      return 1;
4148
   };
4149
   close(CHECKFILE);
4150
   return 0;
4151
4152
}
4153
4154
#========================================================================
4155
# Function:	CheckDirName
4156
# Purpose:	.
4157
#========================================================================
4158
sub CheckDirName {
4159
   local($dirname, $description) = @_;
4160
   local(*CHECKDIR);
4161
4162
   opendir(CHECKDIR,"$dirname") || die ("Cannot open $description directory $dirname: $!\n");
4163
   closedir(CHECKDIR);
4164
}
4165
4166
#========================================================================
4167
# Function:	WriteFile2Handle
4168
# Purpose:	.
4169
#========================================================================
4170
sub WriteFile2Handle {
4171
   local($filename, *WRITEFILE) = @_;
4172
   local(*READFILE);
4173
4174
   if ($filename ne '') {
4175
      open(READFILE,"<$filename");
4176
      @filecontents = <READFILE>;
4177
      close(READFILE);
4178
      print WRITEFILE "@filecontents\n";
4179
      # if (!$opt_silent) {print "      Contents of $filename added\n"};
4180
   }
4181
}
4182
4183
4184
#========================================================================
4185
# Function:	GetConfigFile
4186
# Purpose:	Read user's configuration file, if such exists.
4187
#========================================================================
4188
sub GetConfigFile
4189
{
4190
   local($filename) = @_;
4191
   local(*CONFIG);
4192
   local($value);
4193
4194
   if (&CheckFileName($filename, 'configuration file')) {
4195
      # if (!$opt_silent) { print "   Proceeding using built-in defaults for configuration.\n"; }
4196
      print "   Proceeding using built-in defaults for configuration.\n";
4197
      return 0;
4198
   };
4199
4200
   open(CONFIG,"< $filename");
4201
   while (<CONFIG>) {
4202
      s/#.*$//;
4203
      next if /^\s*$/o;
4204
4205
      # match keyword: process one or more arguments
4206
      # keyword set
4207
      if (/^\s*set\s+(\S+)\s*=\s*(.*)/) {
4208
         # setting a configuration variable
4209
         if (defined $var{$1}) {
4210
            $var{$1} = $2;
4211
            if ($debug > 3) { print "$1:   $var{$1}\n"; }
4212
         }
4213
         else {
4214
            print "$PROGRAM: unknown variable `$1' in configuration file\n"
4215
         }
4216
      } else {
4217
         chop($_);
4218
         print "$PROGRAM: unknown keyword in configuration file in line: `$_'\n"
4219
      }
4220
   }
4221
   close CONFIG;
4222
   1;
4223
}
4224
4225
4226
#------------------------------------------------------------------------
4227
# DisplayHelp - display help text using -h or -help command-line switch
4228
#------------------------------------------------------------------------
4229
sub DisplayHelp
4230
{
4231
   $help=<<EofHelp;
4232
   $PROGRAM v$VERSION - generate html documentation from Matlab m-files
4233
4234
   Usage: $PROGRAM [-h] [-c config_file] [-m|dirmfiles matlab_dir] [-d|dirhtml html_dir]
4235
                   [-i yes|no] [-r yes|no] [-p yes|no] [-quiet|q] [-a authorfile]
4236
4237
   $PROGRAM is a perl script that reads each matlab .m file in a directory
4238
   to produce a corresponding .html file of help documentation and cross
4239
   reference information. An index file is written with links to all of
4240
   the html files produced. The options are:
4241
4242
      -quiet         or -q : be silent, no status information during generation
4243
      -help          or -h : display this help message
4244
      -todo          or -t : print the todo list for $PROGRAM
4245
      -version       or -v : display version
4246
4247
      -configfile    or -c : name of configuration file (default to $var{'configfile'}).
4248
      -dirmfiles     or -m : top level directory containing matlab files to generate html for;
4249
                             default to actual directory.
4250
      -dirhtml       or -d : top level directory for generated html files;
4251
                             default to actual directory.
4252
4253
      -includesource or -i : Include matlab source in the html documentation [yes|no]
4254
                             default to yes.
4255
      -processtree   or -r : create docu for m-file directory and all subdirectories [yes|no];
4256
                             default to yes.
4257
      -producetree   or -p : create multi-level docu identical to directory structure
4258
                             of m-files [yes|no]; default to yes.
4259
      -writecontentsm or -w: update or write contents.m files into the matlab source
4260
                             directories [yes|no]; default to no.
4261
4262
      -authorfile    or -a : name of file including author information, last element in html;
4263
                             default to empty.
4264
4265
   The command line setting overwrite all other settings (built-in and configuration file).
4266
   The configuration file settings overwrite the built-in settings (and not the command
4267
   line settings).
4268
4269
   Typical usages are:
4270
     $PROGRAM
4271
        (use default parameters from perl script, if configuration
4272
         file is found -> generation of docu, else display of help)
4273
4274
     $PROGRAM -dirmfiles matlab -dirhtml html
4275
        (generate html documentation for all m-files in directory matlab,
4276
         place html files in directory html, use built-in defaults for
4277
         all other parameters, this way all m-files in the directory
4278
         matlab and below are converted and the generated html-files are
4279
         placed in the directory html and below producing the same
4280
         directory structure than below matlab)
4281
4282
     $PROGRAM -quiet
4283
        (use built-in parameters from perl script, if configuration
4284
         file is found use these settings as well, do generation,
4285
         no display except critical errors, status of conversion and result)
4286
4287
     $PROGRAM -m toolbox -dirhtml doc/html -r yes -p no
4288
        (convert all m-files in directory toolbox and below and place
4289
         the generated html files in directory doc/html, read all m-files
4290
         recursively, however, the generated html files are placed in one
4291
         directory)
4292
4293
     $PROGRAM -m toolbox -dirhtml doc/html -i no -r no
4294
        (convert all m-files in directory toolbox and place
4295
         the generated html files in directory doc/html, do not read m-files
4296
         recursively, do not include source code in documentation)
4297
4298
EofHelp
4299
4300
   die "$help";
4301
}
4302
4303
#------------------------------------------------------------------------
4304
# DisplayTodo - display ToDo list using -t or -todo command-line switch
4305
#------------------------------------------------------------------------
4306
sub DisplayTodo
4307
{
4308
   $todo=<<EofToDo;
4309
      $PROGRAM v$VERSION - ToDo list
4310
4311
       o	use more than one high level directory
4312
4313
       o	what should/could be done here???
4314
4315
EofToDo
4316
4317
   die "$todo";
4318
}
4319
4320
4321
#------------------------------------------------------------------------
4322
# ListVariables - list all defined variables and their values
4323
#------------------------------------------------------------------------
4324
sub ListVariables
4325
{
4326
   local($value);
4327
4328
   if ($debug > 0) {
4329
      print "List of all variables and their values\n";
4330
      foreach (sort keys %var)
4331
      {
4332
         if ($var{$_} eq '') {
4333
            $value = "empty";
4334
         } else {
4335
            $value = $var{$_};
4336
         }
4337
         print "   $_\n      $value\n";
4338
      }
4339
      print "\n\n";
4340
   }
4341
}
4342
4343
4344
__END__
4345
:endofperl
4346 0:513646585e45 Chris
#!/usr/bin/env ruby
4347
4348
# == Synopsis
4349
#
4350
# reposman: manages your repositories with Redmine
4351
#
4352
# == Usage
4353
#
4354
#    reposman [OPTIONS...] -s [DIR] -r [HOST]
4355 441:cbce1fd3b1b7 Chris
#
4356 0:513646585e45 Chris
#  Examples:
4357 241:7658d21a1493 chris
#    reposman --scm-dir=/var/svn --redmine-host=redmine.example.net --scm subversion
4358 0:513646585e45 Chris
#    reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git
4359
#
4360
# == Arguments (mandatory)
4361
#
4362 241:7658d21a1493 chris
#   -s, --scm-dir=DIR         use DIR as base directory for repositories
4363 0:513646585e45 Chris
#   -r, --redmine-host=HOST   assume Redmine is hosted on HOST. Examples:
4364
#                             -r redmine.example.net
4365
#                             -r http://redmine.example.net
4366
#                             -r https://example.net/redmine
4367 909:cbb26bc654de Chris
#   -k, --key=KEY             use KEY as the Redmine API key (you can use the
4368
#                             --key-file option as an alternative)
4369 0:513646585e45 Chris
#
4370
# == Options
4371
#
4372
#   -o, --owner=OWNER         owner of the repository. using the rails login
4373
#                             allow user to browse the repository within
4374
#                             Redmine even for private project. If you want to
4375
#                             share repositories through Redmine.pm, you need
4376
#                             to use the apache owner.
4377
#   -g, --group=GROUP         group of the repository. (default: root)
4378
#   --scm=SCM                 the kind of SCM repository you want to create (and
4379
#                             register) in Redmine (default: Subversion).
4380
#                             reposman is able to create Git and Subversion
4381
#                             repositories. For all other kind, you must specify
4382
#                             a --command option
4383
#   -u, --url=URL             the base url Redmine will use to access your
4384
#                             repositories. This option is used to automatically
4385
#                             register the repositories in Redmine. The project
4386
#                             identifier will be appended to this url. Examples:
4387
#                             -u https://example.net/svn
4388
#                             -u file:///var/svn/
4389 28:12420e46bed9 chris
#                             if this option isn't set, reposman will register
4390
#                             the repositories with local file paths in Redmine
4391 0:513646585e45 Chris
#   -c, --command=COMMAND     use this command instead of "svnadmin create" to
4392
#                             create a repository. This option can be used to
4393
#                             create repositories other than subversion and git
4394
#                             kind.
4395
#                             This command override the default creation for git
4396
#                             and subversion.
4397 13:80433603a2cd Chris
#   --http-user=USER          User for HTTP Basic authentication with Redmine WS
4398
#   --http-pass=PASSWORD      Password for Basic authentication with Redmine WS
4399 909:cbb26bc654de Chris
#       --key-file=PATH       path to a file that contains the Redmine API key
4400
#                             (use this option instead of --key if you don't
4401
#                             the key to appear in the command line)
4402 0:513646585e45 Chris
#   -t, --test                only show what should be done
4403
#   -h, --help                show help and exit
4404
#   -v, --verbose             verbose
4405
#   -V, --version             print version and exit
4406
#   -q, --quiet               no log
4407
#
4408
# == References
4409 441:cbce1fd3b1b7 Chris
#
4410 0:513646585e45 Chris
# You can find more information on the redmine's wiki : http://www.redmine.org/wiki/redmine/HowTos
4411
4412
4413
require 'getoptlong'
4414 1332:1d1cb01c0417 Chris
#require 'rdoc/usage'
4415 0:513646585e45 Chris
require 'find'
4416
require 'etc'
4417
4418
Version = "1.3"
4419
SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem )
4420
4421
opts = GetoptLong.new(
4422 241:7658d21a1493 chris
                      ['--scm-dir',      '-s', GetoptLong::REQUIRED_ARGUMENT],
4423 0:513646585e45 Chris
                      ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
4424
                      ['--key',          '-k', GetoptLong::REQUIRED_ARGUMENT],
4425 909:cbb26bc654de Chris
                      ['--key-file',           GetoptLong::REQUIRED_ARGUMENT],
4426 0:513646585e45 Chris
                      ['--owner',        '-o', GetoptLong::REQUIRED_ARGUMENT],
4427
                      ['--group',        '-g', GetoptLong::REQUIRED_ARGUMENT],
4428
                      ['--url',          '-u', GetoptLong::REQUIRED_ARGUMENT],
4429
                      ['--command' ,     '-c', GetoptLong::REQUIRED_ARGUMENT],
4430
                      ['--scm',                GetoptLong::REQUIRED_ARGUMENT],
4431 13:80433603a2cd Chris
                      ['--http-user',          GetoptLong::REQUIRED_ARGUMENT],
4432
                      ['--http-pass',          GetoptLong::REQUIRED_ARGUMENT],
4433 0:513646585e45 Chris
                      ['--test',         '-t', GetoptLong::NO_ARGUMENT],
4434
                      ['--verbose',      '-v', GetoptLong::NO_ARGUMENT],
4435
                      ['--version',      '-V', GetoptLong::NO_ARGUMENT],
4436
                      ['--help'   ,      '-h', GetoptLong::NO_ARGUMENT],
4437
                      ['--quiet'  ,      '-q', GetoptLong::NO_ARGUMENT]
4438
                      )
4439
4440
$verbose      = 0
4441
$quiet        = false
4442
$redmine_host = ''
4443
$repos_base   = ''
4444 13:80433603a2cd Chris
$http_user    = ''
4445
$http_pass    = ''
4446 0:513646585e45 Chris
$svn_owner    = 'root'
4447
$svn_group    = 'root'
4448
$use_groupid  = true
4449
$svn_url      = false
4450
$test         = false
4451
$scm          = 'Subversion'
4452
4453
def log(text, options={})
4454
  level = options[:level] || 0
4455
  puts text unless $quiet or level > $verbose
4456
  exit 1 if options[:exit]
4457
end
4458
4459
def system_or_raise(command)
4460
  raise "\"#{command}\" failed" unless system command
4461
end
4462
4463 1332:1d1cb01c0417 Chris
def usage
4464
  puts "See source code for supported options"
4465
  exit
4466
end
4467
4468 0:513646585e45 Chris
module SCM
4469
4470
  module Subversion
4471
    def self.create(path)
4472
      system_or_raise "svnadmin create #{path}"
4473
    end
4474
  end
4475
4476
  module Git
4477
    def self.create(path)
4478
      Dir.mkdir path
4479
      Dir.chdir(path) do
4480
        system_or_raise "git --bare init --shared"
4481
        system_or_raise "git update-server-info"
4482
      end
4483
    end
4484
  end
4485
4486
end
4487
4488
begin
4489
  opts.each do |opt, arg|
4490
    case opt
4491 241:7658d21a1493 chris
    when '--scm-dir';        $repos_base   = arg.dup
4492 0:513646585e45 Chris
    when '--redmine-host';   $redmine_host = arg.dup
4493
    when '--key';            $api_key      = arg.dup
4494 909:cbb26bc654de Chris
    when '--key-file'
4495
      begin
4496
        $api_key = File.read(arg).strip
4497
      rescue Exception => e
4498
        $stderr.puts "Unable to read the key from #{arg}: #{e.message}"
4499
        exit 1
4500
      end
4501 0:513646585e45 Chris
    when '--owner';          $svn_owner    = arg.dup; $use_groupid = false;
4502
    when '--group';          $svn_group    = arg.dup; $use_groupid = false;
4503
    when '--url';            $svn_url      = arg.dup
4504
    when '--scm';            $scm          = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm)
4505 13:80433603a2cd Chris
    when '--http-user';      $http_user    = arg.dup
4506
    when '--http-pass';      $http_pass    = arg.dup
4507 0:513646585e45 Chris
    when '--command';        $command =      arg.dup
4508
    when '--verbose';        $verbose += 1
4509
    when '--test';           $test = true
4510
    when '--version';        puts Version; exit
4511 1332:1d1cb01c0417 Chris
    when '--help';           usage
4512 0:513646585e45 Chris
    when '--quiet';          $quiet = true
4513
    end
4514
  end
4515
rescue
4516
  exit 1
4517
end
4518
4519
if $test
4520
  log("running in test mode")
4521
end
4522
4523
# Make sure command is overridden if SCM vendor is not handled internally (for the moment Subversion and Git)
4524
if $command.nil?
4525
  begin
4526
    scm_module = SCM.const_get($scm)
4527
  rescue
4528
    log("Please use --command option to specify how to create a #{$scm} repository.", :exit => true)
4529
  end
4530
end
4531
4532
$svn_url += "/" if $svn_url and not $svn_url.match(/\/$/)
4533
4534
if ($redmine_host.empty? or $repos_base.empty?)
4535 1332:1d1cb01c0417 Chris
  usage
4536 0:513646585e45 Chris
end
4537
4538
unless File.directory?($repos_base)
4539 217:ed8222a04634 chris
  log("directory '#{$repos_base}' doesn't exist", :exit => true)
4540 0:513646585e45 Chris
end
4541
4542
begin
4543
  require 'active_resource'
4544
rescue LoadError
4545
  log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
4546
end
4547
4548 37:94944d00e43c chris
class Project < ActiveResource::Base
4549 217:ed8222a04634 chris
  self.headers["User-agent"] = "SoundSoftware repository manager/#{Version}"
4550 909:cbb26bc654de Chris
  self.format = :xml
4551 37:94944d00e43c chris
end
4552 0:513646585e45 Chris
4553
log("querying Redmine for projects...", :level => 1);
4554
4555
$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
4556
$redmine_host.gsub!(/\/$/, '')
4557
4558
Project.site = "#{$redmine_host}/sys";
4559 13:80433603a2cd Chris
Project.user = $http_user;
4560
Project.password = $http_pass;
4561 0:513646585e45 Chris
4562
begin
4563
  # Get all active projects that have the Repository module enabled
4564
  projects = Project.find(:all, :params => {:key => $api_key})
4565 909:cbb26bc654de Chris
rescue ActiveResource::ForbiddenAccess
4566
  log("Request was denied by your Redmine server. Make sure that 'WS for repository management' is enabled in application settings and that you provided the correct API key.")
4567 0:513646585e45 Chris
rescue => e
4568
  log("Unable to connect to #{Project.site}: #{e}", :exit => true)
4569
end
4570
4571
if projects.nil?
4572 909:cbb26bc654de Chris
  log('No project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
4573 0:513646585e45 Chris
end
4574
4575 1107:4f45ab104990 Chris
log("found #{projects.size} projects at " + Time.now.inspect);
4576 0:513646585e45 Chris
4577
def set_owner_and_rights(project, repos_path, &block)
4578 441:cbce1fd3b1b7 Chris
  if mswin?
4579 0:513646585e45 Chris
    yield if block_given?
4580
  else
4581
    uid, gid = Etc.getpwnam($svn_owner).uid, ($use_groupid ? Etc.getgrnam(project.identifier).gid : Etc.getgrnam($svn_group).gid)
4582 34:09b1d4349da3 Chris
    right = project.is_public ? 02775 : 02770
4583 0:513646585e45 Chris
    yield if block_given?
4584
    Find.find(repos_path) do |f|
4585
      File.chmod right, f
4586
      File.chown uid, gid, f
4587
    end
4588
  end
4589
end
4590
4591
def other_read_right?(file)
4592
  (File.stat(file).mode & 0007).zero? ? false : true
4593
end
4594
4595
def owner_name(file)
4596
  mswin? ?
4597
    $svn_owner :
4598 441:cbce1fd3b1b7 Chris
    Etc.getpwuid( File.stat(file).uid ).name
4599 0:513646585e45 Chris
end
4600 441:cbce1fd3b1b7 Chris
4601 0:513646585e45 Chris
def mswin?
4602
  (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
4603
end
4604
4605
projects.each do |project|
4606 1107:4f45ab104990 Chris
  log("inspecting project #{project.name}", :level => 1)
4607 0:513646585e45 Chris
4608
  if project.identifier.empty?
4609 1107:4f45ab104990 Chris
    log("\tno identifier for project #{project.name}!")
4610 0:513646585e45 Chris
    next
4611 1445:0c7b3bb73517 Chris
  elsif not project.identifier.match(/^[a-z0-9_\-]+$/)
4612 1107:4f45ab104990 Chris
    log("\tinvalid identifier for project #{project.name} : #{project.identifier}!");
4613 0:513646585e45 Chris
    next;
4614
  end
4615
4616
  repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
4617
4618 28:12420e46bed9 chris
  create_repos = false
4619
  # Logic required for SoundSoftware.ac.uk repositories:
4620
  #
4621
  # * If the project has a repository path declared already,
4622
  #   - if it's a local path,
4623
  #     - if it does not exist
4624
  #       - if it has the right root
4625
  #         - create it
4626
  #   - else
4627
  #     - leave alone (remote repository)
4628
  # * else
4629
  #   - create repository with same name as project
4630
  #   - set to project
4631
4632
  if project.respond_to?(:repository)
4633
4634
    repos_url = project.repository.url;
4635 1107:4f45ab104990 Chris
    log("\texisting url for project #{project.identifier} is #{repos_url}", :level => 2);
4636 28:12420e46bed9 chris
4637
    if repos_url.match(/^file:\//) || repos_url.match(/^\//)
4638
4639
      repos_url = repos_url.gsub(/^file:\/*/, "/");
4640 1107:4f45ab104990 Chris
      log("\tthis is a local file path, at #{repos_url}", :level => 2);
4641 28:12420e46bed9 chris
4642
      if repos_url.slice(0, $repos_base.length) != $repos_base
4643
        # leave repos_path set to our original suggestion
4644 1107:4f45ab104990 Chris
        log("\tpreparing to replace incorrect repo location #{repos_url} for #{project.name} with #{repos_path}");
4645 28:12420e46bed9 chris
        create_repos = true
4646
      else
4647
        if !File.directory?(repos_url)
4648 1107:4f45ab104990 Chris
          log("\tpreparing to create repo for #{project.name} at #{repos_url}");
4649 28:12420e46bed9 chris
          repos_path = repos_url
4650
          create_repos = true
4651
        else
4652 1107:4f45ab104990 Chris
          log("\tit exists and is in the right place", :level => 2);
4653 28:12420e46bed9 chris
        end
4654
      end
4655
    else
4656 1107:4f45ab104990 Chris
      log("\tthis is a remote path, leaving alone", :level => 2);
4657 28:12420e46bed9 chris
    end
4658
  else
4659 1107:4f45ab104990 Chris
    log("\tpreparing to set repo location and create for #{project.name} at #{repos_url}")
4660 28:12420e46bed9 chris
#    if File.directory?(repos_path)
4661
#      log("\trepository path #{repos_path} already exists, not creating")
4662
#    else
4663
      create_repos = true
4664
#    end
4665
  end
4666
4667
  if create_repos
4668
4669
    registration_url = repos_path
4670
    if $svn_url
4671
      registration_url = "#{$svn_url}#{project.identifier}"
4672
    end
4673 0:513646585e45 Chris
4674
    if $test
4675 28:12420e46bed9 chris
      log("\tproposal: create repository #{repos_path}")
4676
      log("\tproposal: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}")
4677 0:513646585e45 Chris
      next
4678
    end
4679
4680 52:8c3409528d3a Chris
# No -- we need "other" users to be able to read it.  Access control
4681
# is not handled through Unix user id anyway
4682
#    project.is_public ? File.umask(0002) : File.umask(0007)
4683
    File.umask(0002)
4684
4685 28:12420e46bed9 chris
    log("\taction: create repository #{repos_path}")
4686 0:513646585e45 Chris
4687
    begin
4688 28:12420e46bed9 chris
      if !File.directory?(repos_path)
4689
        set_owner_and_rights(project, repos_path) do
4690
          if scm_module.nil?
4691
            log("\trunning command: #{$command} #{repos_path}")
4692
            system_or_raise "#{$command} #{repos_path}"
4693
          else
4694
            scm_module.create(repos_path)
4695
          end
4696 0:513646585e45 Chris
        end
4697
      end
4698
    rescue => e
4699
      log("\tunable to create #{repos_path} : #{e}\n")
4700
      next
4701
    end
4702
4703 28:12420e46bed9 chris
    begin
4704
      log("\taction: register repository #{repos_path} in Redmine with vendor #{$scm}, url #{registration_url}");
4705
      project.post(:repository, :vendor => $scm, :repository => {:url => "#{registration_url}"}, :key => $api_key)
4706
    rescue => e
4707
      log("\trepository #{repos_path} not registered in Redmine: #{e.message}");
4708 0:513646585e45 Chris
    end
4709
    log("\trepository #{repos_path} created");
4710
  end
4711 37:94944d00e43c chris
end
4712 0:513646585e45 Chris
4713 1107:4f45ab104990 Chris
log("project review completed at " + Time.now.inspect);
4714
4715 241:7658d21a1493 chris
#!/bin/sh
4716
4717 242:bde4f47b6427 chris
mirrordir="/var/mirror"
4718 1336:b61a51fb42b9 Chris
hg="/usr/bin/hg"
4719 242:bde4f47b6427 chris
4720 241:7658d21a1493 chris
project="$1"
4721
local_repo="$2"
4722
remote_repo="$3"
4723
4724
if [ -z "$project" ] || [ -z "$local_repo" ] || [ -z "$remote_repo" ]; then
4725
    echo "Usage: $0 <project> <local-repo-path> <remote-repo-url>"
4726
    exit 2
4727
fi
4728
4729
  # We need to handle different source repository types separately.
4730
  #
4731
  # The convert extension cannot convert directly from a remote git
4732
  # repo; we'd have to mirror to a local repo first.  Incremental
4733
  # conversions do work though.  The hg-git plugin will convert
4734
  # directly from remote repositories, but not via all schemes
4735
  # (e.g. https is not currently supported).  It's probably easier to
4736
  # use git itself to clone locally and then convert or hg-git from
4737
  # there.
4738
  #
4739
  # We can of course convert directly from remote Subversion repos,
4740
  # but we need to keep track of that -- you can ask to convert into a
4741
  # repo that has already been used (for Mercurial) and it'll do so
4742
  # happily; we don't want that.
4743
  #
4744
  # Converting from a remote Hg repo should be fine!
4745
  #
4746
  # One other thing -- we can't actually tell the difference between
4747
  # the various SCM types based on URL alone.  We have to try them
4748
  # (ideally in an order determined by a guess based on the URL) and
4749
  # see what happens.
4750
4751 242:bde4f47b6427 chris
project_mirror="$mirrordir/$project"
4752
mkdir -p "$project_mirror"
4753
project_repo_mirror="$project_mirror/repo"
4754 241:7658d21a1493 chris
4755 242:bde4f47b6427 chris
  # Some test URLs:
4756
  #
4757
  # http://aimc.googlecode.com/svn/trunk/
4758
  # http://aimc.googlecode.com/svn/
4759
  # http://vagar.org/git/flam
4760
  # https://github.com/wslihgt/IMMF0salience.git
4761
  # http://hg.breakfastquay.com/dssi-vst/
4762
  # git://github.com/schacon/hg-git.git
4763
  # http://svn.drobilla.net/lad (externals!)
4764
4765
# If we are importing from another distributed system, then our aim is
4766
# to create either a Hg repo or a git repo at $project_mirror, which
4767
# we can then pull from directly to the Hg repo at $local_repo (using
4768
# hg-git, in the case of a git repo).
4769
4770
# Importing from SVN, we should use hg convert directly to the target
4771
# hg repo (or should we?) but keep a record of the last changeset ID
4772
# we brought in, and test each time whether it matches the last
4773
# changeset ID actually in the repo
4774
4775
success=""
4776
4777 436:4eb486dbf730 Chris
# If we have a record of the last successfully updated remote repo
4778
# URL, check it against our current remote URL: if it has changed, we
4779
# will need to start again with a new clone rather than pulling
4780
# updates into the existing local mirror
4781
4782
successfile="$project_mirror/last_successful_url"
4783
if [ -f "$successfile" ]; then
4784
    last=$(cat "$successfile")
4785 437:102056ec2de9 chris
    if [ x"$last" = x"$remote_repo" ]; then
4786 436:4eb486dbf730 Chris
	echo "$$: Remote URL is unchanged from last successful update"
4787
    else
4788
	echo "$$: Remote URL has changed since last successful update:"
4789
	echo "$$: Last URL was $last, current is $remote_repo"
4790
	suffix="$$.$(date +%s)"
4791
	echo "$$: Moving existing repos to $suffix suffix and starting afresh"
4792
	mv "$project_repo_mirror" "$project_repo_mirror"."$suffix"
4793
	mv "$local_repo" "$local_repo"."$suffix"
4794
	mv "$successfile" "$successfile"."$suffix"
4795 437:102056ec2de9 chris
	touch "$project_mirror/url_changed"
4796 436:4eb486dbf730 Chris
    fi
4797
fi
4798
4799 242:bde4f47b6427 chris
if [ -d "$project_repo_mirror" ]; then
4800
4801
    # Repo mirror exists: update it
4802
    echo "$$: Mirror for project $project exists at $project_repo_mirror, updating" 1>&2
4803
4804
    if [ -d "$project_repo_mirror/.hg" ]; then
4805 433:7fd72f22a42b Chris
	"$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror" && success=true
4806 439:d3faf348b287 chris
	if [ -z "$success" ]; then
4807
	    ( cd "$project_repo_mirror" && "$hg" pull "$remote_repo" ) && success=true
4808
	fi
4809 242:bde4f47b6427 chris
    elif [ -d "$project_repo_mirror/.git" ]; then
4810 431:d3af621ba9d4 Chris
	( cd "$project_repo_mirror" && git pull "$remote_repo" master ) && success=true
4811 242:bde4f47b6427 chris
    else
4812
	echo "$$: ERROR: Repo mirror dir $project_repo_mirror exists but is not an Hg or git repo" 1>&2
4813
    fi
4814
4815
else
4816
4817
    # Repo mirror does not exist yet
4818
    echo "$$: Mirror for project $project does not yet exist at $project_repo_mirror, trying to convert or clone" 1>&2
4819
4820
    case "$remote_repo" in
4821
	*git*)
4822
	    git clone "$remote_repo" "$project_repo_mirror" ||
4823 433:7fd72f22a42b Chris
	    "$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror"
4824 242:bde4f47b6427 chris
	    ;;
4825
	*)
4826 433:7fd72f22a42b Chris
	    "$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror" ||
4827 299:defe55be97b9 chris
	    git clone "$remote_repo" "$project_repo_mirror" ||
4828 433:7fd72f22a42b Chris
	    "$hg" clone "$remote_repo" "$project_repo_mirror"
4829 242:bde4f47b6427 chris
	    ;;
4830
    esac && success=true
4831
4832
fi
4833
4834
echo "Success=$success"
4835
4836
if [ -n "$success" ]; then
4837
    echo "$$: Update successful, pulling into local repo at $local_repo"
4838 436:4eb486dbf730 Chris
    if [ ! -d "$local_repo" ]; then
4839
	"$hg" init "$local_repo"
4840
    fi
4841 242:bde4f47b6427 chris
    if [ -d "$project_repo_mirror/.git" ]; then
4842 436:4eb486dbf730 Chris
	( cd "$local_repo" && "$hg" --config extensions.hggit= pull "$project_repo_mirror" ) && echo "$remote_repo" > "$successfile"
4843 242:bde4f47b6427 chris
    else
4844 436:4eb486dbf730 Chris
	( cd "$local_repo" && "$hg" pull "$project_repo_mirror" ) && echo "$remote_repo" > "$successfile"
4845 242:bde4f47b6427 chris
    fi
4846
fi