3 # Written by Martin Bartosch for the OpenXPKI project 2006
4 # Copyright (c) 2006 by The OpenXPKI Project
21 # use Smart::Comments;
29 my $git_svn = 'git-svn';
36 ###########################################################################
41 return unless (defined $filename && -r
$filename);
44 open my $HANDLE, '<', $filename or return;
46 die "Could not open file '$filename' for reading. Stopped";
49 local $INPUT_RECORD_SEPARATOR; # long version of $/
56 sub get_revision_info
{
59 # first get SVN revision information
61 if (! open $fh, "$svn info -R 2>/dev/null |") {
68 if (m{ \A Revision: \s* (\d+) }xms) {
69 $result->{revision
} = $1;
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;
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`) ) {
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
| };
103 while (my $line = <$fh>) {
105 # first git-svn-id line encountered wins
106 if ($line =~ m{ \A git-svn-id: .*? @(\d+) \s+ }xms) {
107 $result->{revision
} = $1;
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
| };
126 while (my $line = <$fh>) {
128 if ($line =~ m{ \A HEAD \s+ (?: .*?/)?(.*) }xms) {
129 if ($1 ne 'undefined') {
130 $result->{'git-tag'} ||= $1;
141 sub upwards_find_file
{
142 my $filename = shift;
144 return undef unless defined $filename;
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;
163 my @dirs = File
::Spec
->splitdir($dir);
165 $dir = File
::Spec
->catdir(@dirs);
174 while ($last_arg ne $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));
191 ###########################################################################
193 # make sure that system output is not localized
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
};
233 foreach my $component (keys %config) {
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
} ];
242 die "No keyword defined for component $component. Stopped";
245 # built-in components
246 if ($component =~ m
{ \A
(?
: revision
|
247 last-changed
-revision
|
249 git
-abbreviated
-commit
-hash
|
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},
266 my $optional = (defined $config{$component}{optional
}
267 && ($config{$component}{optional
} =~ m{ \A (?:yes|true|1) \z }ixms));
270 if (! defined $config{$component}{filename
}) {
271 die "No 'filename' defined for component '$component'. Stopped";
274 my $file = upwards_find_file
($config{$component}{filename
});
278 $content = slurp
($file);
279 if (! defined $content) {
285 die "Could not find version file '$config{$component}{filename}' for component '$component' upwards from here. Stopped";
290 $mapping_of->{component
}->{$component} = {
291 keyword
=> $config{$component}->{keyword
},
298 foreach my $component (keys %{$mapping_of->{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,
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
});
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};
346 eval "printf(qq($dumpformat), \@values);";
356 vergen - Version management tool
360 vergen [options] COMMAND
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
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
389 It is then possible to print the resulting version string using
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.
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 ... )
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
427 --dumpformat 'KEYWORD="VALUE"\n'
428 --dumpformat 'export KEYWORD="VALUE"\n'
432 Show which files are processed for determination of the version number
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
443 =head2 Format definition
445 The format definition section is started by the [FORMAT_DEFINITIONS] tag.
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
460 In order to create a custom version number component you should define
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.
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.
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
496 =head3 Subversion built-ins
498 The following named sections are hardcoded
499 and can be used to access Subversion information.
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).
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.
526 'git-tag' expands to the first Git tag name found for the current HEAD,
527 undefined if the current HEAD is untagged.
531 =head2 .VERSION_DEFINITION
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
541 keyword: SVN_REVISION
543 [last-changed-revision]
544 keyword: SVN_LAST_CHANGED_REVISION
547 keyword: GIT_COMMIT_HASH
549 [git-abbreviated-commit-hash]
550 keyword: GIT_ABBREVIATED_COMMIT_HASH
558 filename: .VERSION_MAJOR
562 filename: .VERSION_MINOR
566 filename: .VERSION_RELEASE
570 filename: .VERSION_SUFFIX
577 Using the above configuration file:
579 vergen --format daily_snapshot
580 vergen --format MAJOR.MINOR