# SPDX-FileCopyrightText: 2012, 2013, 2014, 2015, 2017, 2018, 2019, 2020, 2021, 2022 Michael Pyne # SPDX-FileCopyrightText: 2020 Johan Ouwerkerk # # SPDX-License-Identifier: GPL-2.0-or-later package ksb::Updater::Git 0.15; # Module which is responsible for updating git-based source code modules. Can # have some features overridden by subclassing (see ksb::Updater::KDEProject # for an example). use ksb; use parent qw(ksb::Updater); use ksb::BuildException; use ksb::Debug; use ksb::IPC::Null; use ksb::Util qw(:DEFAULT :await run_logged_p); use ksb::Util::LoggedSubprocess; use Mojo::File; use File::Basename; # basename use File::Spec; # tmpdir use POSIX qw(strftime); use List::Util qw(first); use IPC::Cmd qw(run_forked); use constant { DEFAULT_GIT_REMOTE => 'origin', }; # scm-specific update procedure. # May change the current directory as necessary. sub updateInternal ($self, $ipc = ksb::IPC::Null->new()) { $self->{ipc} = $ipc; my $err; my $promise = $self->updateCheckout() ->catch(sub ($_err) { $err = $_err; }) ->finally(sub { delete $self->{ipc}; }); my $numCommits = await_result($promise); die $err if $err; return $numCommits; } sub name { return 'git'; } sub currentRevisionInternal { my $self = assert_isa(shift, 'ksb::Updater::Git'); return $self->commit_id('HEAD'); } # Returns the current sha1 of the given git "commit-ish". sub commit_id { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $commit = shift or croak_internal("Must specify git-commit to retrieve id for"); my $module = $self->module(); my $gitdir = $module->fullpath('source') . '/.git'; # Note that the --git-dir must come before the git command itself. my ($id, undef) = filter_program_output( undef, # No filter qw/git --git-dir/, $gitdir, 'rev-parse', $commit, ); chomp $id if $id; return $id; } sub _verifyRefPresent { my ($self, $module, $repo) = @_; my ($ref, $commitType) = $self->_determinePreferredCheckoutSource($module); return 1 if pretending(); $ref = 'HEAD' if $commitType eq 'none'; my $hashref = run_forked("git ls-remote --exit-code $repo $ref", { timeout => 10, discard_output => 1, terminate_on_parent_sudden_death => 1}); my $result = $hashref->{exit_code}; return 0 if ($result == 2); # Connection successful, but ref not found return 1 if ($result == 0); # Ref is present croak_runtime("git had error exit $result when verifying $ref present in repository at $repo"); } # Perform a git clone to checkout the latest branch of a given git module # # First parameter is the repository (typically URL) to use. # # Returns a promise that resolves to 1, or rejects if an error occurs. sub _clone { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $git_repo = shift; my $module = $self->module(); my $srcdir = $module->fullpath('source'); my @args = ('--', $git_repo, $srcdir); my $ipc = $self->{ipc} // croak_internal ('Missing IPC object'); note ("Cloning g[$module]"); p_chdir($module->getSourceDir()); my ($commitId, $commitType) = $self->_determinePreferredCheckoutSource($module); if ($commitType ne 'none') { $commitId =~ s,^refs/tags/,,; # git-clone -b doesn't like refs/tags/ unshift @args, '-b', $commitId; # Checkout branch right away } my $promise = run_logged_p($module, 'git-clone', $module->getSourceDir(), ['git', 'clone', '--recursive', @args] ); $promise = $promise->then(sub ($exitcode) { croak_runtime("Failed to make initial clone of $module") unless $exitcode == 0; $ipc->notifyPersistentOptionChange( $module->name(), 'git-cloned-repository', $git_repo); p_chdir($srcdir); # Setup user configuration if (my $name = $module->getOption('git-user')) { my ($username, $email) = ($name =~ /^([^<]+) +<([^>]+)>$/); if (!$username || !$email) { croak_runtime("Invalid username or email for git-user option: $name". " (should be in format 'User Name '"); } whisper ("\tAdding git identity $name for new git module $module"); my $result = (safe_system(qw(git config --local user.name), $username) >> 8) == 0; $result = (safe_system(qw(git config --local user.email), $email) >> 8 == 0) || $result; warning ("Unable to set user.name and user.email git config for y[b[$module]!") unless $result; } return 1; # success }); return $promise; } # Checks that the required source dir is either not already present or is empty. # Throws an exception if that's not true. sub _verifySafeToCloneIntoSourceDir { my ($module, $srcdir) = @_; if (-e "$srcdir" && !is_dir_empty($srcdir)) { if ($module->getOption('#delete-my-patches')) { warning ("\tRemoving conflicting source directory " . "as allowed by --delete-my-patches"); warning ("\tRemoving b[$srcdir]"); safe_rmtree($srcdir) or croak_internal("Unable to delete $srcdir!"); } else { error (<module(); my $srcdir = $module->fullpath('source'); my $promise; # While .git is usually a directory, it can also be a file in case of a # worktree checkout (https://git-scm.com/docs/gitrepository-layout) if (-e "$srcdir/.git") { # Note that this function will throw an exception on failure. $promise = $self->updateExistingClone(); } else { $promise = Mojo::Promise->new(sub ($resolve, $reject) { _verifySafeToCloneIntoSourceDir($module, $srcdir); my $git_repo = $module->getOption('repository'); croak_internal("Unable to checkout $module, you must specify a repository to use.") unless $git_repo; if (!$self->_verifyRefPresent($module, $git_repo)) { croak_runtime( $self->_moduleIsNeeded() ? "$module build was requested, but it has no source code at the requested git branch" : "The required git branch does not exist at the source repository" ); } $resolve->($self->_clone($git_repo)->then(sub ($result) { return 1 if pretending(); return count_command_output('git', '--git-dir', "$srcdir/.git", 'ls-files'); })); }); } return $promise; } # Intended to be reimplemented sub _moduleIsNeeded { return 1; } # # Determine whether or not _setupRemote should manage the configuration of the git push URL for the repo. # # Return value: boolean indicating whether or not _setupRemote should assume control over the push URL. # sub isPushUrlManaged { return 0; } # Ensures the given remote is pre-configured for the module's git repository. # The remote is either set up from scratch or its URLs are updated. # # Param $remote name (alias) of the remote to configure # # Returns a promise that resolves to 1, or rejects with an error. sub _setupRemote ($self, $remote) { my $module = $self->module(); my $repo = $module->getOption('repository'); my $hasOldRemote = $self->hasRemote($remote); return Mojo::Promise->new(sub ($resolve, $reject) { if ($hasOldRemote) { whisper("\tUpdating the URL for git remote $remote of $module ($repo)"); $resolve->(run_logged_p($module, 'git-fix-remote', undef, ['git', 'remote', 'set-url', $remote, $repo] )->then(sub ($exitcode) { croak_runtime("Unable to update the URL for git remote $remote of $module ($repo)") unless $exitcode == 0; })); } else { whisper("\tAdding new git remote $remote of $module ($repo)"); $resolve->(run_logged_p($module, 'git-add-remote', undef, ['git', 'remote', 'add', $remote, $repo] )->then(sub ($exitcode) { croak_runtime("Unable to add new git remote $remote of $module ($repo)") unless $exitcode == 0; })); } })->then(sub { # If we make it here, no exceptions were thrown return 1 unless $self->isPushUrlManaged(); # pushInsteadOf does not work nicely with git remote set-url --push # The result would be that the pushInsteadOf kde: prefix gets ignored. # # The next best thing is to remove any preconfigured pushurl and # restore the kde: prefix mapping that way. This is effectively the # same as updating the push URL directly because of the remote set-url # executed previously by this function for the fetch URL. chomp (my $existingPushUrl = qx"git config --get remote.$remote.pushurl"); return 1 unless $existingPushUrl; info("\tRemoving preconfigured push URL for git remote $remote of $module: $existingPushUrl"); return run_logged_p($module, 'git-fix-remote', undef, ['git', 'config', '--unset', "remote.$remote.pushurl"] )->then(sub ($exitcode) { croak_runtime("Unable to remove preconfigured push URL for $module!") unless $exitcode == 0; return 1; # overall success }); }); } # Selects a git remote for the user's selected repository (preferring a # defined remote if available, using 'origin' otherwise). # # Assumes the current directory is already set to the source directory. # # Returns a promise that resolves to the name of the remote (which will be # setup by kdesrc-build) to use for updates, or rejects with an error. # # See also the 'repository' module option. sub _setupBestRemote { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $cur_repo = $module->getOption('repository'); my $ipc = $self->{ipc} // croak_internal ('Missing IPC object'); # Search for an existing remote name first. If none, add our alias. my @remoteNames = $self->bestRemoteName(); my $chosenRemote = @remoteNames ? $remoteNames[0] : DEFAULT_GIT_REMOTE; return $self->_setupRemote($chosenRemote)->then(sub { # Make a notice if the repository we're using has moved. my $old_repo = $module->getPersistentOption('git-cloned-repository'); if ($old_repo and ($cur_repo ne $old_repo)) { note (" y[b[*]\ty[$module]'s selected repository has changed"); note (" y[b[*]\tfrom y[$old_repo]"); note (" y[b[*]\tto b[$cur_repo]"); note (" y[b[*]\tThe git remote named b[", DEFAULT_GIT_REMOTE, "] has been updated"); # Update what we think is the current repository on-disk. $ipc->notifyPersistentOptionChange( $module->name(), 'git-cloned-repository', $cur_repo); } return $chosenRemote; }); } # Returns true if there is a git stash active from the wrong branch, so we # don't mistakenly try to apply the stash after we switch branch. sub _warnIfStashedFromWrongBranch ($self, $remoteName, $branch, $branchName) { my $module = $self->module(); # Check if this branchName we want was already the branch we were on. If # not, and if we stashed local changes, then we might dump a bunch of # conflicts in the repo if we un-stash those changes after a branch switch. # See issue #67. my ($existingBranch) = filter_program_output(undef, qw(git branch --show-current)); chomp $existingBranch if defined ($existingBranch); # The result is empty if in 'detached HEAD' state where we should also # clearly not switch branches if there are local changes. if ($module->getOption('#git-was-stashed') && (!$existingBranch || ($existingBranch ne $branchName))) { # Make error message make more sense $existingBranch ||= 'Detached HEAD'; $branchName ||= "New branch to point to $remoteName/$branch"; info (<_notifyPostBuildMessage( " y[b[*] b[$module] was not updated as it had local changes against an unexpected branch."); return 1; } return; } # Completes the steps needed to update a git checkout to be checked-out to # a given remote-tracking branch. Any existing local branch with the given # branch set as upstream will be used if one exists, otherwise one will be # created. The given branch will be rebased into the local branch. # # No checkout is done, this should be performed first. # Assumes we're already in the needed source dir. # Assumes we're in a clean working directory (use git-stash to achieve # if necessary). # # First parameter is the remote to use. # Second parameter is the branch to update to. # # Returns a promise resolving to a boolean success flag. # Exception may be thrown if unable to create a local branch. sub _updateToRemoteHead ($self, $remoteName, $branch) { my $module = $self->module(); return Mojo::Promise->new(sub ($resolve, $reject) { # 'branch' option requests a given remote head in the user's selected # repository. The local branch with 'branch' as upstream might have a # different name. If there's no local branch this method creates one. my $branchName = $self->getRemoteBranchName($remoteName, $branch); if ($self->_warnIfStashedFromWrongBranch($remoteName, $branch, $branchName)) { $resolve->(0); return; } my $croak_reason; my $promise; my $cmd = ksb::Util::LoggedSubprocess->new ->module ($module) ->chdir_to($module->fullpath('source')); if (!$branchName) { my $newName = $self->makeBranchname($remoteName, $branch); $cmd->log_to('git-checkout-branch') ->set_command(['git', 'checkout', '-b', $newName, "$remoteName/$branch"]) ->announcer(sub { whisper ("\tUpdating g[$module] with new remote-tracking branch y[$newName]"); }) ; $croak_reason = "Unable to perform a git checkout of $remoteName/$branch to a local branch of $newName"; $promise = $cmd->start; } else { $cmd->log_to('git-checkout-update') ->set_command(['git', 'checkout', $branchName]) ->announcer(sub { whisper ("\tUpdating g[$module] using existing branch g[$branchName]"); }) ; $croak_reason = "Unable to perform a git checkout to existing branch $branchName"; $promise = $cmd->start->then(sub ($exitcode) { return $exitcode unless $exitcode == 0; $croak_reason = "$module: Unable to reset to remote development branch $branch"; # Given that we're starting with a 'clean' checkout, it's now simply a fast-forward # to the remote HEAD (previously we pulled, incurring additional network I/O). return run_logged_p($module, 'git-rebase', undef, ['git', 'reset', '--hard', "$remoteName/$branch"]); }); } $promise = $promise->then(sub ($exitcode) { croak_runtime($croak_reason) unless $exitcode == 0; return 1; # success }); $resolve->($promise); }); } # Completes the steps needed to update a git checkout to be checked-out to # a given commit. The local checkout is left in a detached HEAD state, # even if there is a local branch which happens to be pointed to the # desired commit. Based the given commit is used directly, no rebase/merge # is performed. # # No checkout is done, this should be performed first. # Assumes we're already in the needed source dir. # Assumes we're in a clean working directory (use git-stash to achieve # if necessary). # # First parameter is the commit to update to. This can be in pretty # much any format that git itself will respect (e.g. tag, sha1, etc.). # It is recommended to use refs/$foo/$bar syntax for specificity. # # Returns a promise resolving to a boolean success flag. sub _updateToDetachedHead ($self, $commit) { my $module = $self->module(); my $srcdir = $module->fullpath('source'); return Mojo::Promise->new(sub ($resolve, $reject) { info ("\tDetaching head to b[$commit]"); my $promise = run_logged_p($module, 'git-checkout-commit', $srcdir, ['git', 'checkout', $commit]) ->then(sub ($exitcode) { # need to adapt to boolean success flag return $exitcode == 0; }); $resolve->($promise); }); } # Updates an already existing git checkout by running git pull. # # Throws an exception on error. # # Return parameter is a promise resolving to the number of affected *commits*. sub updateExistingClone { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $cur_repo = $module->getOption('repository'); my $result; p_chdir($module->fullpath('source')); # Try to save the user if they are doing a merge or rebase if (-e '.git/MERGE_HEAD' || -e '.git/rebase-merge' || -e '.git/rebase-apply') { croak_runtime ("Aborting git update for $module, you appear to have a rebase or merge in progress!"); } my $remoteName; my $remoteNamePromise = $self->_setupBestRemote(); # Download updated objects. This also updates remote heads so do this # before we start comparing branches and such. return $remoteNamePromise->then(sub ($_remoteName) { $remoteName = $_remoteName; # save for later info ("Fetching remote changes to g[$module]"); return run_logged_p($module, 'git-fetch', undef, ['git', 'fetch', '-f', '--tags', $remoteName]); })->then(sub ($exitcode) { croak_runtime ("Unable to perform git fetch for $remoteName ($cur_repo)") unless $exitcode == 0; # Now we need to figure out if we should update a branch, or simply # checkout a specific tag/SHA1/etc. my ($commitId, $commitType) = $self->_determinePreferredCheckoutSource($module); if ($commitType eq 'none') { $commitType = 'branch'; $commitId = $self->_detectDefaultRemoteHead($remoteName); } note ("Merging g[$module] changes from $commitType b[$commitId]"); my $start_commit = $self->commit_id('HEAD'); my $updateSub = ($commitType eq 'branch') ? sub { $self->_updateToRemoteHead($remoteName, $commitId); } : sub { $self->_updateToDetachedHead($commitId); } ; return $self->stashAndUpdate($updateSub)->then(sub ($isOk) { return count_command_output('git', 'rev-list', "$start_commit..HEAD"); }); }); } # Tries to determine the best remote branch name to use as a default if the # user hasn't selected one, by resolving the remote symbolic ref "HEAD" from # its entry in the .git dir. This can also be found by introspecting the # output of "git remote show $REMOTE_NAME" or "git branch -r" but these are # incredibly slow. sub _detectDefaultRemoteHead ($self, $remoteName) { croak_internal ("Run " . __SUB__ . " from git repo!") unless -d '.git'; my $data = Mojo::File->new(".git/refs/remotes/$remoteName/HEAD")->slurp; my ($head) = ($data // '') =~ m,^ref: *refs/remotes/[^/]+/([^/]+)$,; croak_runtime ("Can't find HEAD for remote $remoteName") unless $head; chomp($head); return $head; } # Goes through all the various combination of git checkout selection options in # various orders of priority. # # Returns a *list* containing: (the resultant symbolic ref/or SHA1,'branch' or # 'tag' (to determine if something like git-pull would be suitable or whether # you have a detached HEAD)). Since the sym-ref is returned first that should # be what you get in a scalar context, if that's all you want. sub _determinePreferredCheckoutSource { my ($self, $module) = @_; $module //= $self->module(); my @priorityOrderedSources = ( # option-name type getOption-inheritance-flag [qw(commit tag module)], [qw(revision tag module)], [qw(tag tag module)], [qw(branch branch module)], [qw(branch-group branch module)], # commit/rev/tag don't make sense for git as globals [qw(branch branch allow-inherit)], [qw(branch-group branch allow-inherit)], ); # For modules that are not actually a 'proj' module we skip branch-group # entirely to allow for global/module branch selection # options to be selected... kind of complicated, but more DWIMy if (!$module->scm()->isa('ksb::Updater::KDEProject')) { @priorityOrderedSources = grep { $_->[0] ne 'branch-group' } @priorityOrderedSources; } my $checkoutSource; # Sorry about the !!, easiest way to be clear that bool context is intended my $sourceTypeRef = first { !!($checkoutSource = ($module->getOption($_->[0], $_->[2]) // '')) } @priorityOrderedSources; # The user has no clear desire here (either set for the module or globally. # Note that the default config doesn't generate a global 'branch' setting). # In this case it's unclear which convention source modules will use between # 'master', 'main', or something entirely different. So just don't guess... if (!$sourceTypeRef) { whisper ("No branch specified for $module, will use whatever git gives us"); return qw(none none); } # Likewise branch-group requires special handling. checkoutSource is # currently the branch-group to be resolved. if ($sourceTypeRef->[0] eq 'branch-group') { assert_isa($self, 'ksb::Updater::KDEProject'); $checkoutSource = $self->_resolveBranchGroup($checkoutSource); if (!$checkoutSource) { my $branchGroup = $module->getOption('branch-group'); whisper ("No specific branch set for $module and $branchGroup, using master!"); $checkoutSource = 'master'; } } if ($sourceTypeRef->[0] eq 'tag' && $checkoutSource !~ m{^refs/tags/}) { $checkoutSource = "refs/tags/$checkoutSource"; } return ($checkoutSource, $sourceTypeRef->[1]); } # Tries to check whether the git module is using submodules or not. Currently # we just check the .git/config file (using git-config) to determine whether # there are any 'active' submodules. # # MUST BE RUN FROM THE SOURCE DIR sub _hasSubmodules { # The git-config line shows all option names of the form submodule.foo.active, # filtering down to options for which the option is set to 'true' my @configLines = filter_program_output(undef, # accept all lines qw(git config --local --get-regexp ^submodule\..*\.active true)); return scalar @configLines > 0; } # Splits a URI up into its component parts. Taken from # http://search.cpan.org/~ether/URI-1.67/lib/URI.pm # Copyright Gisle Aas under the following terms: # "This program is free software; you can redistribute it and/or modify it # under the same terms as Perl itself." sub _splitUri { my($scheme, $authority, $path, $query, $fragment) = $_[0] =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|; return ($scheme, $authority, $path, $query, $fragment); } sub countStash { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $description = shift; if (-e '.git/refs/stash') { my $count = qx"git rev-list --walk-reflogs --count refs/stash"; chomp $count if $count; debug("\tNumber of stashes found for b[$module] is: b[$count]"); return $count; } else { debug("\tIt appears there is no stash for b[$module]"); return 0; } } # # Wrapper to send a post-build (warning) message via the IPC object. # This just takes care of the boilerplate to forward its arguments as message. # sub _notifyPostBuildMessage { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); $self->{ipc}->notifyNewPostBuildMessage($module->name(), @_); } # This stashes existing changes if necessary, and then runs a provided # update routine in order to advance the given module to the desired head. # Finally, if changes were stashed, they are applied and the stash stack is # popped. # # It is assumed that the required remote has been setup already, that we # are on the right branch, and that we are already in the correct # directory. # # First parameter is a reference to the subroutine to run. This subroutine # should need no parameters and return a boolean success indicator. It may # throw exceptions. # # Returns a promise that resolves to 1, or rejects with an exception. sub stashAndUpdate ($self, $updateSub) { my $module = $self->module(); my $date = strftime ("%F-%R", gmtime()); # ISO Date, hh:mm time my $stashName = "kdesrc-build auto-stash at $date"; # first, log the git status prior to kdesrc-build taking over the reins in the repo my $promise = run_logged_p($module, 'git-status-before-update', undef, [qw(git status)]); my ($oldStashCount, $newStashCount); # Used in promises below return $promise->then(sub ($exitcode) { $oldStashCount = $self->countStash(); # always stash: # - also stash untracked files because what if upstream started to track them # - we do not stash .gitignore'd files because they may be needed for builds? # on the other hand that leaves a slight risk if upstream altered those # (i.e. no longer truly .gitignore'd) whisper ("\tStashing local changes if any..."); return Mojo::Promise->resolve(0) if pretending(); # probably best not to do anything if pretending return run_logged_p($module, 'git-stash-push', undef, [ qw(git stash push -u --quiet --message), $stashName ]); })->then(sub ($exitcode) { return $exitcode if $exitcode == 0; # Might happen if the repo is already in merge conflict state. # We could mark everything as resolved using git add . before stashing, # but that might not always be appreciated by people having to figure # out what the original merge conflicts were afterwards. $self->_notifyPostBuildMessage( "b[$module] may have local changes that we couldn't handle, so the module was left alone." ); return run_logged_p($module, 'git-status-after-error', undef, [qw(git status)]) ->then(sub { croak_runtime("Unable to stash local changes (if any) for $module, aborting update."); }); })->then(sub ($exitcode) { # next: check if the stash was truly necessary. # compare counts (not just testing if there is *any* stash) because there # might have been a genuine user's stash already prior to kdesrc-build # taking over the reins in the repo. $newStashCount = $self->countStash(); # mark that we applied a stash so that $updateSub (_updateToRemoteHead or # _updateToDetachedHead) can know not to do dumb things $module->setOption('#git-was-stashed', 1) if $newStashCount != $oldStashCount; # finally, update to remote head return $updateSub->(); })->then(sub ($updateOk) { return 1 if $updateOk; return run_logged_p($module, 'git-status-after-error', undef, [qw(git status)]) ->then(sub { croak_runtime("Unable to update source code for $module"); }); })->then(sub ($exitcode) { # we ignore git-status exit code deliberately, it's a debugging aid return 1 # success if $newStashCount == $oldStashCount; # If the stash had been needed then try to re-apply it before we build, so # that KDE developers working on changes do not have to manually re-apply. return run_logged_p($module, 'git-stash-pop', undef, [qw(git stash pop)]) ->then(sub ($exitcode) { if ($exitcode != 0) { my $message = "r[b[*] Unable to restore local changes for b[$module]! " . "You should manually inspect the new stash: b[$stashName]"; warning ("\t$message"); $self->_notifyPostBuildMessage($message); } else { info ("\tb[*] You had local changes to b[$module], which have been re-applied."); } return 1; # success }); }); } # This subroutine finds an existing remote-tracking branch name for the # given repository's named remote. For instance if the user was using the # local remote-tracking branch called 'qt-stable' to track kde-qt's master # branch, this subroutine would return the branchname 'qt-stable' when # passed kde-qt and 'master'. # # The current directory must be the source directory of the git module. # # First parameter : The git remote to use (normally origin). # Second parameter: The remote head name to find a local branch for. # Returns: Empty string if no match is found, or the name of the local # remote-tracking branch if one exists. sub getRemoteBranchName { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $remoteName = shift; my $branchName = shift; # We'll parse git config output to search for branches that have a # remote of $remoteName and a 'merge' of refs/heads/$branchName. # TODO: Replace with git for-each-ref refs/heads and the %(upstream) # format. my @branches = slurp_git_config_output( qw/git config --null --get-regexp branch\..*\.remote/, $remoteName ); foreach my $gitBranch (@branches) { # The key/value is \n separated, we just want the key. my ($keyName) = split(/\n/, $gitBranch); my ($thisBranch) = ($keyName =~ m/^branch\.(.*)\.remote$/); # We have the local branch name, see if it points to the remote # branch we want. my @configOutput = slurp_git_config_output( qw/git config --null/, "branch.$thisBranch.merge" ); if (@configOutput && $configOutput[0] eq "refs/heads/$branchName") { # We have a winner return $thisBranch; } } return ''; } # # Filter for bestRemoteName to determine if a given remote name and url looks # like a plausible prior existing remote for a given configured repository URL. # # Note that the actual repository fetch URL is not necessarily the same as the # configured (expected) fetch URL: an upstream might have moved, or kdesrc-build # configuration might have been updated to the same effect. # # Arguments: # - name : name of the remote found # - url : the configured (fetch) URL # - configuredURL : the configured URL for the module (the expected fetch URL). # # Return value: whether the remote will be conisdered for bestRemoteName # sub _isPlausibleExistingRemote { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $name = shift; # not used, subclasses might want to filter on remote name my $url = shift; my $configuredUrl = shift; return $url eq $configuredUrl; } # 99% of the time the 'origin' remote will be what we want anyways, and # 0.5% of the rest the user will have manually added a remote, which we # should try to utilize when doing checkouts for instance. To aid in this, # this subroutine returns a list of all remote aliased matching the # supplied repository (besides the internal alias that is). # # Assumes that we are already in the proper source directory. # # First parameter: Repository URL to match. # Returns: A list of matching remote names (list in case the user hates us # and has aliased more than one remote to the same repo). Obviously the list # will be empty if no remote names were found. sub bestRemoteName { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $module = $self->module(); my $configuredUrl = $module->getOption('repository'); my @outputs; # The Repo URL isn't much good, let's find a remote name to use it with. # We'd have to escape the repo URL to pass it to Git, which I don't trust, # so we just look for all remotes and make sure the URL matches afterwards. eval { @outputs = slurp_git_config_output( qw/git config --null --get-regexp remote\..*\.url ./ ); }; if ($@) { error ("\tUnable to run git config, is there a setup error?"); return; } my @results; foreach my $output (@outputs) { # git config output between key/val is divided by newline. my ($remoteName, $url) = split(/\n/, $output); $remoteName =~ s/^remote\.//; $remoteName =~ s/\.url$//; # remove the cruft # Skip other remotes next unless $self->_isPlausibleExistingRemote($remoteName, $url, $configuredUrl); # Try to avoid "weird" remote names. next if $remoteName !~ /^[\w-]*$/; # A winner is this one. push @results, $remoteName; } return @results; } # Generates a potential new branch name for the case where we have to setup # a new remote-tracking branch for a repository/branch. There are several # criteria that go into this: # * The local branch name will be equal to the remote branch name to match usual # Git convention. # * The name chosen must not already exist. This methods tests for that. # * The repo name chosen should be (ideally) a remote name that the user has # added. If not, we'll try to autogenerate a repo name (but not add a # remote!) based on the repository.git part of the URI. # # As with nearly all git support functions, we should be running in the # source directory of the git module. Don't call this function unless # you've already checked that a suitable remote-tracking branch doesn't # exist. # # First parameter: The name of a git remote to use. # Second parameter: The name of the remote head we need to make a branch name # of. # Returns: A useful branch name that doesn't already exist, or '' if no # name can be generated. sub makeBranchname { my $self = assert_isa(shift, 'ksb::Updater::Git'); my $remoteName = shift || 'origin'; my $branch = shift; my $module = $self->module(); my $chosenName; # Use "$branch" directly if not already used, otherwise try to prefix # with the remote name. for my $possibleBranch ($branch, "$remoteName-$branch", "ksdc-$remoteName-$branch") { my $result = system('git', 'show-ref', '--quiet', '--verify', '--', "refs/heads/$possibleBranch") >> 8; return $possibleBranch if $result == 1; } croak_runtime("Unable to find good branch name for $module branch name $branch"); } # Returns the number of lines in the output of the given command. The command # and all required arguments should be passed as a normal list, and the current # directory should already be set as appropriate. # # Return value is the number of lines of output. # Exceptions are raised if the command could not be run. sub count_command_output { # Don't call with $self->, all args are passed to filter_program_output my @args = @_; my $count = 0; filter_program_output(sub { $count++ if $_ }, @args); return $count; } # A simple wrapper that is used to split the output of 'git config --null' # correctly. All parameters are then passed to filter_program_output (so look # there for help on usage). sub slurp_git_config_output { # Don't call with $self->, all args are passed to filter_program_output local $/ = "\000"; # Split on null # This gets rid of the trailing nulls for single-line output. (chomp uses # $/ instead of hardcoding newline chomp(my @output = filter_program_output(undef, @_)); # No filter return @output; } # Returns true if the git module in the current directory has a remote of the # name given by the first parameter. sub hasRemote { my ($self, $remote) = @_; my $hasRemote = 0; eval { filter_program_output(sub { $hasRemote ||= ($_ && /^$remote/) }, 'git', 'remote'); }; return $hasRemote; } # Subroutine to add the 'kde:' alias to the user's git config if it's not # already set. # # Call this as a static class function, not as an object method # (i.e. ksb::Updater::Git::verifyGitConfig, not $foo->verifyGitConfig) # # Returns false on failure of any sort, true otherwise. sub verifyGitConfig { my $contextOptions = shift; my $protocol = $contextOptions->getOption('git-desired-protocol') || 'git'; my $pushUrlPrefix = ''; my $otherPushUrlPrefix = ''; if ($protocol eq 'git' || $protocol eq 'https') { $pushUrlPrefix = $protocol eq 'git' ? 'ssh://git@invent.kde.org/' : 'https://invent.kde.org/'; $otherPushUrlPrefix = $protocol eq 'git' ? 'https://invent.kde.org/' : 'ssh://git@invent.kde.org/'; } else { error(" b[y[*] Invalid b[git-desired-protocol] $protocol"); error(" b[y[*] Try setting this option to 'git' if you're not using a proxy"); croak_runtime("Invalid git-desired-protocol: $protocol"); } my $configOutput = qx'git config --global --includes --get url.https://invent.kde.org/.insteadOf kde:'; # 0 means no error, 1 means no such section exists -- which is OK if ((my $errNum = $? >> 8) >= 2) { my $error = "Code $errNum"; my %errors = ( 3 => 'Invalid config file (~/.gitconfig)', 4 => 'Could not write to ~/.gitconfig', 2 => 'No section was provided to git-config', 1 => 'Invalid section or key', 5 => 'Tried to set option that had no (or multiple) values', 6 => 'Invalid regexp with git-config', 128 => 'HOME environment variable is not set (?)', ); $error = $errors{$errNum} if exists $errors{$errNum}; error (" r[*] Unable to run b[git] command:\n\t$error"); return 0; } # If we make it here, I'm just going to assume git works from here on out # on this simple task. if ($configOutput !~ /^kde:\s*$/) { whisper ("\tAdding git download kde: alias (fetch: https://invent.kde.org/)"); my $result = safe_system( qw(git config --global --add url.https://invent.kde.org/.insteadOf kde:) ) >> 8; return 0 if $result != 0; } $configOutput = qx"git config --global --includes --get url.$pushUrlPrefix.pushInsteadOf kde:"; if ($configOutput !~ /^kde:\s*$/) { whisper ("\tAdding git upload kde: alias (push: $pushUrlPrefix)"); my $result = safe_system('git', 'config', '--global', '--add', "url.$pushUrlPrefix.pushInsteadOf", 'kde:') >> 8; return 0 if $result != 0; } # Remove old kdesrc-build installed aliases (kde: -> git://anongit.kde.org/) $configOutput = qx'git config --global --get url.git://anongit.kde.org/.insteadOf kde:'; if ($configOutput =~ /^kde:\s*$/) { whisper ("\tRemoving outdated kde: alias (fetch: git://anongit.kde.org/)"); my $result = safe_system( qw(git config --global --unset-all url.git://anongit.kde.org/.insteadOf kde:) ) >> 8; return 0 if $result != 0; } $configOutput = qx'git config --global --get url.https://anongit.kde.org/.insteadOf kde:'; if ($configOutput =~ /^kde:\s*$/) { whisper ("\tRemoving outdated kde: alias (fetch: https://anongit.kde.org/)"); my $result = safe_system( qw(git config --global --unset-all url.https://anongit.kde.org/.insteadOf kde:) ) >> 8; return 0 if $result != 0; } $configOutput = qx'git config --global --get url.git@git.kde.org:.pushInsteadOf kde:'; if ($configOutput =~ /^kde:\s*$/) { whisper ("\tRemoving outdated kde: alias (push: git\@git.kde.org)"); my $result = safe_system( qw(git config --global --unset-all url.git@git.kde.org:.pushInsteadOf kde:) ) >> 8; return 0 if $result != 0; } # remove outdated alias if git-desired-protocol gets flipped $configOutput = qx"git config --global --get url.$otherPushUrlPrefix.pushInsteadOf kde:"; if ($configOutput =~ /^kde:\s*$/) { whisper ("\tRemoving outdated kde: alias (push: $otherPushUrlPrefix)"); my $result = safe_system('git', 'config', '--global', '--unset-all', "url.$otherPushUrlPrefix.pushInsteadOf", 'kde:') >> 8; return 0 if $result != 0; } return 1; } 1;