#!/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;