Revision control

Copy as Markdown

#!/usr/bin/env perl
# Copyright (C) 2017 The Qt Company Ltd and/or its subsidiary(-ies).
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
use 5.010;
use strict;
use warnings;
=head1 NAME
10-git-qtqa-combine.t - basic git-qtqa-combine test
=cut
use English qw(-no_match_vars);
use File::Spec::Functions;
use File::Temp qw(tempdir);
use File::chdir;
use FindBin;
use Readonly;
use Test::More;
use autodie;
use lib "$FindBin::Bin/../../lib/perl5";
Readonly my $GIT_COMBINE => catfile( $FindBin::Bin, '..', 'git-qtqa-combine' );
# Like system(), but die on failure.
sub system_or_die
{
my (@cmd) = @_;
if (system( @cmd ) != 0) {
die "@cmd exited with status $?";
}
}
# Create an empty file with the given $filename, if it does not yet exist.
sub touch
{
my ($filename) = @_;
open( my $fh, '>>', $filename );
close( $fh );
return;
}
# Given a $ref and (optional) $dir, returns the SHA1 of that $ref.
# $dir defaults to '.' if omitted.
sub get_ref
{
my ($ref, $dir) = @_;
if (!$dir) {
$dir = '.';
}
local $CWD = $dir;
my $commit = qx(git rev-parse "$ref");
($? == 0) || die;
chomp $commit;
return $commit;
}
# Given a $rev (ref or SHA1), returns a list of parent commits of that revision.
sub get_parent_commits
{
my ($rev) = @_;
my $out = qx(git log -n1 "$rev" --format=format:%P);
($? == 0) || die;
chomp $out;
my @parents = split( /\s/, $out );
return @parents;
}
# Creates a git repo and populates some refs.
# Takes the following named arguments:
#
# dir => the directory where the repo shall be created.
# refs => a hashref with git ref names as keys, and
# arrayrefs containing filenames to create as values.
#
# All created files are empty.
sub make_git_repo
{
my (%args) = @_;
my $dir = $args{ dir };
my %refs = %{ $args{ refs } || {} };
system_or_die( 'git', 'init', '--quiet', $dir );
local $CWD = $dir;
# Allow pushing to checked out branch
system_or_die( 'git', 'config', 'receive.denyCurrentBranch', 'ignore' );
if (!%refs) {
return;
}
# For simplicity, we always have one initial commit from which all others are descended.
touch( 'dummy' );
system_or_die( 'git', 'add', 'dummy' );
system_or_die( 'git', 'commit', '--quiet', '-m', 'Initial commit' );
my $initial_commit = get_ref( 'refs/heads/master' );
my $last_ref;
system_or_die( 'git', 'checkout', '--quiet', '-b', '_workbranch' );
while (my ($ref, $files_arrayref) = each %refs) {
my @files = @{ $files_arrayref };
system_or_die( 'git', 'reset', '--quiet', '--hard', $initial_commit );
system_or_die( 'git', 'rm', '--quiet', 'dummy' );
foreach my $file (@files) {
touch( $file );
}
system_or_die( 'git', 'add', @files );
system_or_die( 'git', 'commit', '--quiet', '-m', 'Committed files' );
my $commit = get_ref( 'refs/heads/_workbranch' );
system_or_die( 'git', 'update-ref', $ref, $commit );
$last_ref = $ref;
}
system_or_die( 'git', 'checkout', '--quiet', $last_ref );
system_or_die( 'git', 'branch', '-D', '_workbranch' );
if (!$refs{ 'refs/heads/master' }) {
system_or_die( 'git', 'branch', '-D', 'master' );
}
return;
}
# Verify the state of a particular ref in a particular git repo.
# Takes an argument hash with the following keys:
#
# dir => directory of the git repo (defaults to '.')
# ref => git ref to check
# testname => name of the test, for Test::More messages
# expected_files => arrayref of expected files to be present
# when 'ref' is checked out
# expected_parents => arrayref of expected parent SHA1s of 'ref'
#
sub verify_git_state
{
my (%args) = @_;
my $dir = $args{ dir } || '.';
my $ref = $args{ 'ref' };
my $testname = $args{ testname };
my @expected_files = @{ $args{ expected_files } || [] };
my @expected_parents = @{ $args{ expected_parents } || [] };
local $CWD = $dir;
system_or_die( qw(git reset --hard --quiet), $ref );
my @files = sort glob '*';
is_deeply(
\@files,
[ sort @expected_files ],
"$testname: correct files exist"
);
my @parents = get_parent_commits( $ref );
is_deeply(
[ sort @parents ],
[ sort @expected_parents ],
"$testname: merge commit parents look correct"
);
return;
}
# Basic test combining three input repos into one output, then updating when
# one of the input repos is modified.
sub test_basic_combine
{
local $CWD = tempdir( 'git-qtqa-combine.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
mkdir 'work';
my @all_in = qw(in1 in2 in3);
for my $in (@all_in) {
make_git_repo(
dir => $in,
refs => {
'refs/heads/master' => [ "file_from_$in" ],
},
);
}
make_git_repo( dir => 'out1' );
my (@all_in_revs) = map { get_ref( 'refs/heads/master', $_ ) } @all_in;
my @cmd = (
$GIT_COMBINE,
qw(--workdir work),
map( { ('--in', "url=$CWD/$_", 'ref=refs/heads/master') } @all_in ),
'--out', "url=$CWD/out1", 'ref=refs/heads/master',
);
is( system( @cmd ), 0, 'basic: exit code 0' );
my %state = (
dir => 'out1',
'ref' => 'refs/heads/master',
expected_files => [ map { "file_from_$_" } @all_in ],
expected_parents => \@all_in_revs,
);
verify_git_state( testname => 'basic', %state );
# Check that running again is a no-op
is( system( @cmd ), 0, 'basic no-op: exit code 0' );
verify_git_state( testname => 'basic no-op', %state );
# Check that, if only one of the inputs now changes, the change is merged OK
# and the merge commit only merges the new history.
my $old_out_rev = get_ref( 'refs/heads/master', 'out1' );
my $new_in2_rev;
# Create a new commit in 'in2'; the file from in2 is renamed so we can tell if
# the content change is correctly merged.
{
local $CWD = 'in2';
system_or_die( 'git', 'checkout', '--quiet', 'master' );
system_or_die( 'git', 'mv', 'file_from_in2', 'renamed_file_from_in2' );
system_or_die( 'git', 'commit', '--quiet', '-m', 'Moved a file' );
$new_in2_rev = get_ref( 'refs/heads/master' );
}
is( system( @cmd ), 0, 'basic update: exit code 0' );
# file rename should be reflected in the update ...
$state{ expected_files } = [ 'file_from_in1', 'renamed_file_from_in2', 'file_from_in3' ];
# and the merge commit should only be merging the changed input repo into the output repo
$state{ expected_parents } = [ $old_out_rev, $new_in2_rev ];
verify_git_state( testname => 'basic update', %state );
return;
}
# Test combining three input repositories into two output repositories in a few different
# ways, with a single command line and with usage of 'files' patterns.
sub test_multi_combine_with_files
{
local $CWD = tempdir( 'git-qtqa-combine.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
mkdir 'work';
my @all_in = qw(in1 in2 in3);
my @all_out = qw(out1 out2);
for my $in (@all_in) {
make_git_repo(
dir => $in,
refs => {
"refs/heads/$in" => [ "file_from_$in.txt", "file_from_$in.cpp" ],
# Let in2 have a "master" branch, while the others don't.
# This checks that having an unused master branch doesn't hurt, and
# having no master branch also doesn't hurt.
(($in eq 'in2') ? ("refs/heads/master" => [ "some_other_file.txt" ]) : ()),
},
);
}
for my $out (@all_out) {
make_git_repo( dir => $out );
}
my ($in1_rev, $in2_rev, $in3_rev) = map { get_ref( "refs/heads/$_", $_ ) } @all_in;
is(
system(
$GIT_COMBINE,
qw(--workdir work),
'--in', "url=$CWD/in1", 'ref=refs/heads/in1', 'files=*.txt',
'--in', "url=$CWD/in2", 'ref=refs/heads/in2', 'files=*.cpp',
'--out', "url=$CWD/out1", 'ref=refs/heads/one_txt_two_cpp',
'--in', "url=$CWD/in1", 'ref=refs/heads/in1', 'files=*.cpp',
'--in', "url=$CWD/in2", 'ref=refs/heads/in2', 'files=*.txt',
'--out', "url=$CWD/out1", 'ref=refs/heads/one_cpp_two_txt',
'--in', "url=$CWD/in2", 'ref=refs/heads/in2', 'files=*.cpp',
'--in', "url=$CWD/in3", 'ref=refs/heads/in3', 'files=*.txt',
'--out', "url=$CWD/out2", 'ref=refs/heads/two_cpp_three_txt',
),
0,
'multi: exit code 0'
);
verify_git_state(
testname => 'multi one_txt_two_cpp',
dir => 'out1',
'ref' => 'refs/heads/one_txt_two_cpp',
expected_files => [ 'file_from_in1.txt', 'file_from_in2.cpp' ],
expected_parents => [ $in1_rev, $in2_rev ],
);
verify_git_state(
testname => 'multi one_cpp_two_txt',
dir => 'out1',
'ref' => 'refs/heads/one_cpp_two_txt',
expected_files => [ 'file_from_in1.cpp', 'file_from_in2.txt' ],
expected_parents => [ $in1_rev, $in2_rev ],
);
verify_git_state(
testname => 'multi two_cpp_three_txt',
dir => 'out2',
'ref' => 'refs/heads/two_cpp_three_txt',
expected_files => [ 'file_from_in2.cpp', 'file_from_in3.txt' ],
expected_parents => [ $in2_rev, $in3_rev ],
);
return;
}
# Test usage of the 'submodule' option
sub test_submodule_combine
{
local $CWD = tempdir( 'git-qtqa-combine.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
mkdir 'work';
foreach my $in (qw(in1 in2)) {
make_git_repo(
dir => $in,
refs => {
'refs/heads/master' => [ "file_from_$in" ],
},
);
}
make_git_repo(
dir => 'sub',
refs => {
'refs/heads/master' => [ 'file_from_sub' ],
},
);
# Let in2 have sub as a submodule.
{
local $CWD = 'in2';
system_or_die( 'git', 'checkout', '--quiet', 'master' );
system_or_die( 'git', 'submodule', 'add', "$CWD/../sub", 'sub' );
system_or_die( 'git', 'commit', '-m', 'Added submodule' );
}
my ($in1_rev, $sub_rev) = map { get_ref( 'refs/heads/master', $_ ) } qw(in1 sub);
make_git_repo( dir => 'out1' );
my @cmd = (
$GIT_COMBINE,
qw(--workdir work),
'--in', "url=$CWD/in1", 'ref=refs/heads/master',
'--in', "url=$CWD/in2", 'ref=refs/heads/master', 'submodule=sub',
'--out', "url=$CWD/out1", 'ref=refs/heads/master',
);
is( system( @cmd ), 0, 'submodule: exit code 0' );
my %state = (
dir => 'out1',
'ref' => 'refs/heads/master',
expected_files => [ 'file_from_in1', 'file_from_sub' ],
expected_parents => [ $in1_rev, $sub_rev ],
);
verify_git_state( testname => 'submodule', %state );
# Create a new commit in 'sub'
my $new_sub_rev;
{
local $CWD = 'sub';
system_or_die( 'git', 'checkout', '--quiet', 'master' );
system_or_die( 'git', 'mv', 'file_from_sub', 'renamed_file_from_sub' );
system_or_die( 'git', 'commit', '--quiet', '-m', 'Moved a file' );
$new_sub_rev = get_ref( 'refs/heads/master' );
}
# Check that running again is a no-op; i.e. the new commit in 'sub' is _not_ taken,
# because the parent repo wasn't updated.
is( system( @cmd ), 0, 'submodule no-op: exit code 0' );
verify_git_state( testname => 'submodule no-op', %state );
my $old_out_rev = get_ref( 'refs/heads/master', 'out1' );
# Now update submodule pointer
{
local $CWD = 'in2';
{
local $CWD = 'sub';
system_or_die( 'git', 'fetch', '--quiet', "../../sub", 'refs/heads/master' );
system_or_die( 'git', 'reset', '--hard', 'FETCH_HEAD' );
}
system_or_die( 'git', 'add', 'sub' );
system_or_die( 'git', 'commit', '-m', 'Updated submodule.' );
}
is( system( @cmd ), 0, 'submodule update: exit code 0' );
# file rename should be reflected in the update ...
$state{ expected_files } = [ 'file_from_in1', 'renamed_file_from_sub' ];
# and the merge commit should only be merging the changed input repo (submodule) into the output repo
$state{ expected_parents } = [ $old_out_rev, $new_sub_rev ];
verify_git_state( testname => 'basic update', %state );
return;
}
sub run
{
if ($OSNAME =~ m{win32}i) {
plan skip_all => "git-qtqa-combine is not supported on $OSNAME";
}
# set a fake git user.name and user.email for the test;
# the account under which this test is running might not
# have any set
local $ENV{ GIT_COMMITTER_NAME } = '10-git-qtqa-combine.t';
local $ENV{ GIT_COMMITTER_EMAIL } = 'autotest@example.com';
local $ENV{ GIT_AUTHOR_NAME } = $ENV{ GIT_COMMITTER_NAME };
local $ENV{ GIT_AUTHOR_EMAIL } = $ENV{ GIT_COMMITTER_EMAIL };
test_basic_combine;
test_multi_combine_with_files;
test_submodule_combine;
done_testing;
return;
}
run if (!caller);
1;