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 -s --abbrev HEAD`;
118 chomp $result->{'git-abbreviated-commit-hash'};
120 $result->{'git-commit-hash'} = `$git show-ref -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 (defined $arg && ($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 if (! defined $values[1]) {
350 eval "printf(qq($dumpformat), \@values);";
360 vergen - Version management tool
364 vergen [options] COMMAND
367 --help brief help message
368 --man full documentation
369 --format <arg> print version using format 'arg'
370 --directory <dir> pretend to be in directory 'dir'
371 --dump dump keyword assignments
372 --dumpformat <string> use <string> as dump format
373 (default: KEYWORD=VALUE\n)
374 --verbose show details about information obtained
378 B<vergen> is a tool that can help to maintain consistent version numbers
379 in a project. If desired it uses Subversion and/or Git to determine
380 revision control information.
382 Starting from the current directory, vergen searches upwards for its
383 configuration file '.VERSION_DEFINITION' and reads the first file found
384 (see B<The .VERSION_DEFINITION File> for a detailed description.)
385 After the configuration file has been read, vergen again traverses the
386 directory tree upwards from the current directory and searches for all
387 file names defined in the version definition file. Once a file is found,
388 its content is read and stored as the value for the corresponding component.
390 Components can arbitrarily be combined to 'formats'. A format consists
391 of a string that may contain any number of custom and built-in component
393 It is then possible to print the resulting version string using
396 When used within a directory hierarchy under revision control via
397 Subversion and/or Git, B<vergen> is able to obtain and use information about
398 the current checkout.
405 =item B<--format> arg
407 Print format string, using the version numbers obtained by vergen. The
408 output format is determined by 'arg' and may either be a symbolic name
409 referencing a format string in the FORMAT_DEFINITIONS section of
410 the corresponding .VERSION_DEFINITION file or a custom string specifying
411 the version number. You may use all keywords defined in .VERSION_DEFINITION
412 and all conversion specifications as defined by strftime(3).
414 =item B<--directory> dir
416 Makes it possible to simulate calling vergen in another directory.
417 This is identical to calling
419 ( cd <dir> && vergen ... )
423 Dump all keyword definitions in a format that can be sources by sh.
425 =item B<--dumpformat> string
427 Use specified string as template for keyword dump. Defaults to
431 --dumpformat 'KEYWORD="VALUE"\n'
432 --dumpformat 'export KEYWORD="VALUE"\n'
436 Show which files are processed for determination of the version number
441 =head1 The .VERSION_DEFINITION File
443 The version definition file contains format and version number component
444 definitions. It is split in a number of sections that define the composition
447 =head2 Format definition
449 The format definition section is started by the [FORMAT_DEFINITIONS] tag.
454 advanced: MAJOR.MINOR.RELEASE
456 This configuration defines two named formats called 'simple' and 'advanced'
457 that can be used with the --format option. The value following the colon
458 is used as the output format in this case.
460 =head2 Custom component definitions
462 Section names are interpreted as a custom version component
464 In order to create a custom version number component you should define
469 File name to use. vergen searches upwards from the current directory
470 until it finds the specified file.
471 The file contents will be assigned as the component value.
475 Symbolic name(s) describing the version component that can be used
476 to reference its value. More than one keyword can be used for
477 aliasing the component. Each of these keyword can be used to reference
478 the component value. It is possible to use the keyword in a format (--format)
479 or even in the contents of the version component files. The program resolves
480 recursive references to keywords properly.
482 Only the first keyword defined is used for --dump.
486 If set to 'yes' or 'true', this component is not mandatory.
488 =head2 Revision control built-ins
490 When called within a directory tree that is under version control by
491 Subversion and/or Git it is possible to obtain certain information from the
492 revision control system.
494 B<vergen> support Subversion-only operation, Subversion with Git (using
495 git-svn to rebase the Subversion repository) and Git-only. Git-only mode
496 also tries to guess Subversion version numbers from the Git log (e. g.
497 when pulling from a Git repository that uses git-svn to rebase from
500 =head3 Subversion built-ins
502 The following named sections are hardcoded
503 and can be used to access Subversion information.
507 'revision' expands to the global SVN revision number
509 =head4 last-changed-revision
511 'last-changed-revision' determines the highest "Last Changed Rev"
512 below the current directory (recursively).
517 The following named sections are hardcoded and can be used to access Git
518 version information (if current directory is inside a Git repository).
520 =head4 git-commit-hash
522 'git-commit-hash' expands to the Git commit hash for the current HEAD.
524 =head4 git-abbreviated-commit-hash
526 'git-abbreviated-commit-hash' expands to the abbreviated commit hash.
530 'git-tag' expands to the first Git tag name found for the current HEAD,
531 undefined if the current HEAD is untagged.
535 =head2 .VERSION_DEFINITION
539 version: MAJOR.MINOR.RELEASESUFFIX\n
540 daily_snapshot: %F-MAJOR.MINOR.RELEASESUFFIX\n
541 git-commit: MAJOR.MINOR.GIT_TAG
543 # Built-in components
545 keyword: SVN_REVISION
547 [last-changed-revision]
548 keyword: SVN_LAST_CHANGED_REVISION
551 keyword: GIT_COMMIT_HASH
553 [git-abbreviated-commit-hash]
554 keyword: GIT_ABBREVIATED_COMMIT_HASH
562 filename: .VERSION_MAJOR
566 filename: .VERSION_MINOR
570 filename: .VERSION_RELEASE
574 filename: .VERSION_SUFFIX
581 Using the above configuration file:
583 vergen --format daily_snapshot
584 vergen --format MAJOR.MINOR