# Distributed under the OSI-approved BSD 3-Clause License. See accompanying # file Copyright.txt or https://cmake.org/licensing for details. cmake_minimum_required(VERSION 3.5) function(get_hash_for_ref ref out_var err_var) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git rev-parse "${ref}^0" WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code OUTPUT_VARIABLE ref_hash ERROR_VARIABLE error_msg OUTPUT_STRIP_TRAILING_WHITESPACE ) if(error_code) set(${out_var} "" PARENT_SCOPE) else() set(${out_var} "${ref_hash}" PARENT_SCOPE) endif() set(${err_var} "${error_msg}" PARENT_SCOPE) endfunction() get_hash_for_ref(HEAD head_sha error_msg) if(head_sha STREQUAL "") message(FATAL_ERROR "Failed to get the hash for HEAD:\n${error_msg}") endif() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git show-ref "@git_tag@" WORKING_DIRECTORY "@work_dir@" OUTPUT_VARIABLE show_ref_output ) if(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/remotes/") # Given a full remote/branch-name and we know about it already. Since # branches can move around, we always have to fetch. set(fetch_required YES) set(checkout_name "@git_tag@") elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/tags/") # Given a tag name that we already know about. We don't know if the tag we # have matches the remote though (tags can move), so we should fetch. set(fetch_required YES) set(checkout_name "@git_tag@") # Special case to preserve backward compatibility: if we are already at the # same commit as the tag we hold locally, don't do a fetch and assume the tag # hasn't moved on the remote. # FIXME: We should provide an option to always fetch for this case get_hash_for_ref("@git_tag@" tag_sha error_msg) if(tag_sha STREQUAL head_sha) message(VERBOSE "Already at requested tag: ${tag_sha}") return() endif() elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/heads/") # Given a branch name without any remote and we already have a branch by that # name. We might already have that branch checked out or it might be a # different branch. It isn't safe to use a bare branch name without the # remote, so do a fetch and replace the ref with one that includes the remote. set(fetch_required YES) set(checkout_name "@git_remote_name@/@git_tag@") else() get_hash_for_ref("@git_tag@" tag_sha error_msg) if(tag_sha STREQUAL head_sha) # Have the right commit checked out already message(VERBOSE "Already at requested ref: ${tag_sha}") return() elseif(tag_sha STREQUAL "") # We don't know about this ref yet, so we have no choice but to fetch. # We deliberately swallow any error message at the default log level # because it can be confusing for users to see a failed git command. # That failure is being handled here, so it isn't an error. set(fetch_required YES) set(checkout_name "@git_tag@") if(NOT error_msg STREQUAL "") message(VERBOSE "${error_msg}") endif() else() # We have the commit, so we know we were asked to find a commit hash # (otherwise it would have been handled further above), but we don't # have that commit checked out yet set(fetch_required NO) set(checkout_name "@git_tag@") if(NOT error_msg STREQUAL "") message(WARNING "${error_msg}") endif() endif() endif() if(fetch_required) message(VERBOSE "Fetching latest from the remote @git_remote_name@") execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git fetch --tags --force "@git_remote_name@" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) endif() set(git_update_strategy "@git_update_strategy@") if(git_update_strategy STREQUAL "") # Backward compatibility requires REBASE as the default behavior set(git_update_strategy REBASE) endif() if(git_update_strategy MATCHES "^REBASE(_CHECKOUT)?$") # Asked to potentially try to rebase first, maybe with fallback to checkout. # We can't if we aren't already on a branch and we shouldn't if that local # branch isn't tracking the one we want to checkout. execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git symbolic-ref -q HEAD WORKING_DIRECTORY "@work_dir@" OUTPUT_VARIABLE current_branch OUTPUT_STRIP_TRAILING_WHITESPACE # Don't test for an error. If this isn't a branch, we get a non-zero error # code but empty output. ) if(current_branch STREQUAL "") # Not on a branch, checkout is the only sensible option since any rebase # would always fail (and backward compatibility requires us to checkout in # this situation) set(git_update_strategy CHECKOUT) else() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git for-each-ref "--format=%(upstream:short)" "${current_branch}" WORKING_DIRECTORY "@work_dir@" OUTPUT_VARIABLE upstream_branch OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY # There is no error if no upstream is set ) if(NOT upstream_branch STREQUAL checkout_name) # Not safe to rebase when asked to checkout a different branch to the one # we are tracking. If we did rebase, we could end up with arbitrary # commits added to the ref we were asked to checkout if the current local # branch happens to be able to rebase onto the target branch. There would # be no error message and the user wouldn't know this was occurring. set(git_update_strategy CHECKOUT) endif() endif() elseif(NOT git_update_strategy STREQUAL "CHECKOUT") message(FATAL_ERROR "Unsupported git update strategy: ${git_update_strategy}") endif() # Check if stash is needed execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git status --porcelain WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code OUTPUT_VARIABLE repo_status ) if(error_code) message(FATAL_ERROR "Failed to get the status") endif() string(LENGTH "${repo_status}" need_stash) # If not in clean state, stash changes in order to be able to perform a # rebase or checkout without losing those changes permanently if(need_stash) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash save @git_stash_save_options@ WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) endif() if(git_update_strategy STREQUAL "CHECKOUT") execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git checkout "${checkout_name}" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) else() execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git rebase "${checkout_name}" WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code OUTPUT_VARIABLE rebase_output ERROR_VARIABLE rebase_output ) if(error_code) # Rebase failed, undo the rebase attempt before continuing execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git rebase --abort WORKING_DIRECTORY "@work_dir@" ) if(NOT git_update_strategy STREQUAL "REBASE_CHECKOUT") # Not allowed to do a checkout as a fallback, so cannot proceed if(need_stash) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" ) endif() message(FATAL_ERROR "\nFailed to rebase in: '@work_dir@'." "\nOutput from the attempted rebase follows:" "\n${rebase_output}" "\n\nYou will have to resolve the conflicts manually") endif() # Fall back to checkout. We create an annotated tag so that the user # can manually inspect the situation and revert if required. # We can't log the failed rebase output because MSVC sees it and # intervenes, causing the build to fail even though it completes. # Write it to a file instead. string(TIMESTAMP tag_timestamp "%Y%m%dT%H%M%S" UTC) set(tag_name _cmake_ExternalProject_moved_from_here_${tag_timestamp}Z) set(error_log_file ${CMAKE_CURRENT_LIST_DIR}/rebase_error_${tag_timestamp}Z.log) file(WRITE ${error_log_file} "${rebase_output}") message(WARNING "Rebase failed, output has been saved to ${error_log_file}" "\nFalling back to checkout, previous commit tagged as ${tag_name}") execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git tag -a -m "ExternalProject attempting to move from here to ${checkout_name}" ${tag_name} WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git checkout "${checkout_name}" WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) endif() endif() if(need_stash) # Put back the stashed changes execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code ) if(error_code) # Stash pop --index failed: Try again dropping the index execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git reset --hard --quiet WORKING_DIRECTORY "@work_dir@" ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --quiet WORKING_DIRECTORY "@work_dir@" RESULT_VARIABLE error_code ) if(error_code) # Stash pop failed: Restore previous state. execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git reset --hard --quiet ${head_sha} WORKING_DIRECTORY "@work_dir@" ) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git stash pop --index --quiet WORKING_DIRECTORY "@work_dir@" ) message(FATAL_ERROR "\nFailed to unstash changes in: '@work_dir@'." "\nYou will have to resolve the conflicts manually") endif() endif() endif() set(init_submodules "@init_submodules@") if(init_submodules) execute_process( COMMAND "@git_EXECUTABLE@" --git-dir=.git submodule update @git_submodules_recurse@ --init @git_submodules@ WORKING_DIRECTORY "@work_dir@" COMMAND_ERROR_IS_FATAL ANY ) endif()