Web interface: protect against "Surf Jacking" by marking the authentication cookie...
[openxpki.git] / tools / vergen
blobc0ca65245f15810344e1a80b8e463abf4939d36c
1 #!/usr/bin/env perl
3 # Written by Martin Bartosch for the OpenXPKI project 2006
4 # Copyright (c) 2006 by The OpenXPKI Project
5 # $Revision: 589 $
8 use warnings;
9 use strict;
10 use English;
12 use Data::Dumper;
14 use Pod::Usage;
15 use Getopt::Long;
16 use POSIX;
17 use File::Spec;
18 use Config::Std;
19 use Cwd;
21 # use Smart::Comments;
23 my %params;
24 my %config;
27 my $svn = 'svn';
28 my $git = 'git';
29 my $git_svn = 'git-svn';
31 my $revision_info;
32 my $mapping_of;
33 my $format_of;
34 my $saved_directory;
36 ###########################################################################
38 sub slurp {
39 my $filename = shift;
41 return unless (defined $filename && -r $filename);
43 my $result = do {
44 open my $HANDLE, '<', $filename or return;
45 if (! $HANDLE) {
46 die "Could not open file '$filename' for reading. Stopped";
48 # slurp mode
49 local $INPUT_RECORD_SEPARATOR; # long version of $/
50 <$HANDLE>;
52 return $result;
56 sub get_revision_info {
57 my $result = {};
59 # first get SVN revision information
60 my $fh;
61 if (! open $fh, "$svn info -R 2>/dev/null |") {
62 return $result;
65 SVN_INFO:
66 while (<$fh>) {
67 chomp;
68 if (m{ \A Revision: \s* (\d+) }xms) {
69 $result->{revision} = $1;
70 next SVN_INFO;
72 if (m{ \A Last\ Changed\ Rev: \s* (\d+) }xms) {
73 if (! exists $result->{'last-changed-revision'} ||
74 ($1 > $result->{'last-changed-revision'})) {
75 $result->{'last-changed-revision'} = $1;
79 close $fh;
81 # check if we are running in a git checkout
82 # getting revision info from SVN did not work, try git
83 if ( (`which $git 2>/dev/null`) && (`$git branch 2>/dev/null`) ) {
84 # try via git-svn...
85 if (! exists $result->{revision}) {
86 # try to get revision from git-svn
87 my $last_log = `$git_svn log --oneline --limit 1 2>/dev/null`;
88 my ($revision) = ($last_log =~ m{\A r(\d+)\ \|\ .*}xms);
89 if (defined $revision) {
90 $result->{revision} = $revision;
94 # ... and via plain git
95 if (! exists $result->{revision}) {
96 # revision could not be obtained by git-svn
97 # reason may be that this repository does not rebase via git-svn,
98 # but rather pulls from another repository. assume that the
99 # svn revision information is available in git full text logs.
101 open $fh, qq{ $git log --pretty=email | };
102 GITLOGLINE:
103 while (my $line = <$fh>) {
104 chomp $line;
105 # first git-svn-id line encountered wins
106 if ($line =~ m{ \A git-svn-id: .*? @(\d+) \s+ }xms) {
107 $result->{revision} = $1;
108 last GITLOGLINE;
111 close $fh;
113 # fake last-changed-revision
114 $result->{'last-changed-revision'} ||= $result->{revision};
116 # get git commit hashes
117 $result->{'git-abbreviated-commit-hash'} = `$git show-ref -h -s --abbrev HEAD`;
118 chomp $result->{'git-abbreviated-commit-hash'};
120 $result->{'git-commit-hash'} = `$git show-ref -h -s HEAD`;
121 chomp $result->{'git-commit-hash'};
123 # get git tag information
124 open $fh, qq{ $git name-rev --tags HEAD | };
125 GITLOGLINE:
126 while (my $line = <$fh>) {
127 chomp $line;
128 if ($line =~ m{ \A HEAD \s+ (?: .*?/)?(.*) }xms) {
129 if ($1 ne 'undefined') {
130 $result->{'git-tag'} ||= $1;
134 close $fh;
137 return $result;
141 sub upwards_find_file {
142 my $filename = shift;
144 return undef unless defined $filename;
146 my $dir = getcwd;
148 DIRECTORY:
149 while ($dir ne '/') {
150 if (exists $params{verbose}) {
151 print STDERR "* scanning $dir for $filename\n";
154 my $absolute_filename = File::Spec->catfile($dir, $filename);
155 if (-e $absolute_filename) {
156 if (exists $params{verbose}) {
157 print STDERR "* found $absolute_filename\n";
159 return $absolute_filename;
162 # one level up
163 my @dirs = File::Spec->splitdir($dir);
164 pop @dirs;
165 $dir = File::Spec->catdir(@dirs);
167 return;
170 sub expand_keyword {
171 my $arg = shift;
173 my $last_arg = "";
174 while ($last_arg ne $arg) {
175 $last_arg = $arg;
176 foreach my $keyword (keys %{$mapping_of->{keyword}}) {
177 if ($arg =~ m{ $keyword }xms) {
178 my $value = $mapping_of->{keyword}->{$keyword}->{entry}->{value};
179 if (! defined $value) {
180 die "Could not get value for keyword '$keyword'. Stopped";
182 $arg =~ s{ $keyword }{$value}xmsg;
185 $arg = strftime($arg, gmtime(time));
188 return $arg;
191 ###########################################################################
193 # make sure that system output is not localized
194 $ENV{LANG} = 'C';
196 GetOptions(\%params,
198 help|?
200 verbose
201 format=s
202 directory=s
203 dump
204 dumpformat=s
205 )) or pod2usage(-verbose => 0);
207 pod2usage(-exitstatus => 0, -verbose => 2) if $params{man};
208 pod2usage(-verbose => 1) if ($params{help});
210 if (defined $params{directory}) {
211 $saved_directory = getcwd;
212 if (! chdir $params{directory}) {
213 die "Could not change to directory $params{directory}. Stopped";
217 my $def_file = upwards_find_file('.VERSION_DEFINITION');
219 if (! defined $def_file) {
220 die "Could not find version definition file anywhere. Stopped";
222 read_config($def_file, %config);
224 if (exists $config{FORMAT_DEFINITIONS}) {
225 $format_of = $config{FORMAT_DEFINITIONS};
226 delete $config{FORMAT_DEFINITIONS};
229 ### %config
230 ### $format_of
232 COMPONENT:
233 foreach my $component (keys %config) {
234 ### $component...
236 # coerce keyword into arrayref
237 if (defined $config{$component}->{keyword}) {
238 if (ref $config{$component}->{keyword} eq '') {
239 $config{$component}->{keyword} = [ $config{$component}->{keyword} ];
241 } else {
242 die "No keyword defined for component $component. Stopped";
245 # built-in components
246 if ($component =~ m{ \A (?: revision |
247 last-changed-revision |
248 git-commit-hash |
249 git-abbreviated-commit-hash |
250 git-tag
251 ) \z }xms) {
252 $revision_info ||= get_revision_info();
254 if (! scalar keys %{$revision_info}) {
255 die "Could not determine revision info. (Hint: is svn installed and in path?)";
258 $mapping_of->{component}->{$component} = {
259 keyword => $config{$component}->{keyword},
260 value => $revision_info->{$component},
261 type => 'built-in',
263 next COMPONENT;
266 my $optional = (defined $config{$component}{optional}
267 && ($config{$component}{optional} =~ m{ \A (?:yes|true|1) \z }ixms));
268 ### $optional
270 if (! defined $config{$component}{filename}) {
271 die "No 'filename' defined for component '$component'. Stopped";
274 my $file = upwards_find_file($config{$component}{filename});
275 my $content = '';
277 if (defined $file) {
278 $content = slurp($file);
279 if (! defined $content) {
280 $content = '';
282 chomp $content;
283 } else {
284 if (! $optional) {
285 die "Could not find version file '$config{$component}{filename}' for component '$component' upwards from here. Stopped";
287 $content = '';
290 $mapping_of->{component}->{$component} = {
291 keyword => $config{$component}->{keyword},
292 value => $content,
293 type => 'component',
297 # remap entries
298 foreach my $component (keys %{$mapping_of->{component}}) {
299 ### $component
300 my $entry = $mapping_of->{component}->{$component};
301 foreach my $keyword (@{ $mapping_of->{component}->{$component}->{keyword} }) {
302 $mapping_of->{keyword}->{$keyword}->{component} = $component,
303 $mapping_of->{keyword}->{$keyword}->{entry} = $entry,
307 ### $mapping_of
309 if (defined $params{format}) {
310 my $format = $params{format};
312 if (exists $format_of->{$format}) {
313 $format = $format_of->{$format};
315 my $value = expand_keyword($format);
317 eval "printf('%s', qq($value));";
320 if (defined $params{'dump'}) {
321 foreach my $component (keys %{$mapping_of->{component}}) {
322 my $dumpformat = 'KEYWORD=VALUE\n';
323 if (defined $params{dumpformat}) {
324 $dumpformat = $params{dumpformat};
327 my $keyword = $mapping_of->{component}->{$component}->{keyword}->[0];
328 my $value = expand_keyword($mapping_of->{keyword}->{$keyword}->{entry}->{value});
329 my %vars = (
330 KEYWORD => $keyword,
331 VALUE => $value,
334 my @values = ();
336 MAKEFORMAT:
337 while (1) {
338 my ($item) = ($dumpformat =~ m{ (KEYWORD|VALUE) }xms);
339 last MAKEFORMAT if (! defined $item);
340 $dumpformat =~ s{ $item }{%s}xms;
341 push @values, $vars{$item};
343 ### $dumpformat
344 ### @values
346 eval "printf(qq($dumpformat), \@values);";
350 exit 0;
352 __END__
354 =head1 NAME
356 vergen - Version management tool
358 =head1 USAGE
360 vergen [options] COMMAND
362 Options:
363 --help brief help message
364 --man full documentation
365 --format <arg> print version using format 'arg'
366 --directory <dir> pretend to be in directory 'dir'
367 --dump dump keyword assignments
368 --dumpformat <string> use <string> as dump format
369 (default: KEYWORD=VALUE\n)
370 --verbose show details about information obtained
372 =head1 DESCRIPTION
374 B<vergen> is a tool that can help to maintain consistent version numbers
375 in a project. If desired it uses Subversion and/or Git to determine
376 revision control information.
378 Starting from the current directory, vergen searches upwards for its
379 configuration file '.VERSION_DEFINITION' and reads the first file found
380 (see B<The .VERSION_DEFINITION File> for a detailed description.)
381 After the configuration file has been read, vergen again traverses the
382 directory tree upwards from the current directory and searches for all
383 file names defined in the version definition file. Once a file is found,
384 its content is read and stored as the value for the corresponding component.
386 Components can arbitrarily be combined to 'formats'. A format consists
387 of a string that may contain any number of custom and built-in component
388 names.
389 It is then possible to print the resulting version string using
390 the --format option.
392 When used within a directory hierarchy under revision control via
393 Subversion and/or Git, B<vergen> is able to obtain and use information about
394 the current checkout.
397 =head1 OPTIONS
399 =over 8
401 =item B<--format> arg
403 Print format string, using the version numbers obtained by vergen. The
404 output format is determined by 'arg' and may either be a symbolic name
405 referencing a format string in the FORMAT_DEFINITIONS section of
406 the corresponding .VERSION_DEFINITION file or a custom string specifying
407 the version number. You may use all keywords defined in .VERSION_DEFINITION
408 and all conversion specifications as defined by strftime(3).
410 =item B<--directory> dir
412 Makes it possible to simulate calling vergen in another directory.
413 This is identical to calling
415 ( cd <dir> && vergen ... )
417 =item B<--dump>
419 Dump all keyword definitions in a format that can be sources by sh.
421 =item B<--dumpformat> string
423 Use specified string as template for keyword dump. Defaults to
424 'KEYWORD=VALUE\n'.
425 Useful dump formats:
427 --dumpformat 'KEYWORD="VALUE"\n'
428 --dumpformat 'export KEYWORD="VALUE"\n'
430 =item B<--verbose>
432 Show which files are processed for determination of the version number
433 components.
435 =back
437 =head1 The .VERSION_DEFINITION File
439 The version definition file contains format and version number component
440 definitions. It is split in a number of sections that define the composition
441 of version numbers.
443 =head2 Format definition
445 The format definition section is started by the [FORMAT_DEFINITIONS] tag.
447 Example:
448 [FORMAT_DEFINITIONS]
449 simple: MAJOR.MINOR
450 advanced: MAJOR.MINOR.RELEASE
452 This configuration defines two named formats called 'simple' and 'advanced'
453 that can be used with the --format option. The value following the colon
454 is used as the output format in this case.
456 =head2 Custom component definitions
458 Section names are interpreted as a custom version component
459 definition.
460 In order to create a custom version number component you should define
461 the following keys:
463 =head3 filename
465 File name to use. vergen searches upwards from the current directory
466 until it finds the specified file.
467 The file contents will be assigned as the component value.
469 =head3 keyword
471 Symbolic name(s) describing the version component that can be used
472 to reference its value. More than one keyword can be used for
473 aliasing the component. Each of these keyword can be used to reference
474 the component value. It is possible to use the keyword in a format (--format)
475 or even in the contents of the version component files. The program resolves
476 recursive references to keywords properly.
478 Only the first keyword defined is used for --dump.
480 =head3 optional
482 If set to 'yes' or 'true', this component is not mandatory.
484 =head2 Revision control built-ins
486 When called within a directory tree that is under version control by
487 Subversion and/or Git it is possible to obtain certain information from the
488 revision control system.
490 B<vergen> support Subversion-only operation, Subversion with Git (using
491 git-svn to rebase the Subversion repository) and Git-only. Git-only mode
492 also tries to guess Subversion version numbers from the Git log (e. g.
493 when pulling from a Git repository that uses git-svn to rebase from
494 Subversion).
496 =head3 Subversion built-ins
498 The following named sections are hardcoded
499 and can be used to access Subversion information.
501 =head4 revision
503 'revision' expands to the global SVN revision number
505 =head4 last-changed-revision
507 'last-changed-revision' determines the highest "Last Changed Rev"
508 below the current directory (recursively).
511 =head3 Git built-ins
513 The following named sections are hardcoded and can be used to access Git
514 version information (if current directory is inside a Git repository).
516 =head4 git-commit-hash
518 'git-commit-hash' expands to the Git commit hash for the current HEAD.
520 =head4 git-abbreviated-commit-hash
522 'git-abbreviated-commit-hash' expands to the abbreviated commit hash.
524 =head4 git-tag
526 'git-tag' expands to the first Git tag name found for the current HEAD,
527 undefined if the current HEAD is untagged.
529 =head1 Examples
531 =head2 .VERSION_DEFINITION
533 # Format definitions
534 [FORMAT_DEFINITIONS]
535 version: MAJOR.MINOR.RELEASESUFFIX\n
536 daily_snapshot: %F-MAJOR.MINOR.RELEASESUFFIX\n
537 git-commit: MAJOR.MINOR.GIT_TAG
539 # Built-in components
540 [revision]
541 keyword: SVN_REVISION
543 [last-changed-revision]
544 keyword: SVN_LAST_CHANGED_REVISION
546 [git-commit-hash]
547 keyword: GIT_COMMIT_HASH
549 [git-abbreviated-commit-hash]
550 keyword: GIT_ABBREVIATED_COMMIT_HASH
552 [git-tag]
553 keyword: GIT_TAG
554 optional: yes
556 # Custom components
557 [major]
558 filename: .VERSION_MAJOR
559 keyword: MAJOR
561 [minor]
562 filename: .VERSION_MINOR
563 keyword: MINOR
565 [release]
566 filename: .VERSION_RELEASE
567 keyword: RELEASE
569 [suffix]
570 filename: .VERSION_SUFFIX
571 keyword: SUFFIX
572 optional: yes
575 =head2 Invocation
577 Using the above configuration file:
579 vergen --format daily_snapshot
580 vergen --format MAJOR.MINOR
581 vergen --dump