1# Distributed under the OSI-approved BSD 3-Clause License. See accompanying 2# file Copyright.txt or https://cmake.org/licensing for details. 3 4cmake_minimum_required(VERSION 3.5) 5 6function(get_hash_for_ref ref out_var err_var) 7 execute_process( 8 COMMAND "@git_EXECUTABLE@" rev-parse "${ref}^0" 9 WORKING_DIRECTORY "@work_dir@" 10 RESULT_VARIABLE error_code 11 OUTPUT_VARIABLE ref_hash 12 ERROR_VARIABLE error_msg 13 OUTPUT_STRIP_TRAILING_WHITESPACE 14 ) 15 if(error_code) 16 set(${out_var} "" PARENT_SCOPE) 17 else() 18 set(${out_var} "${ref_hash}" PARENT_SCOPE) 19 endif() 20 set(${err_var} "${error_msg}" PARENT_SCOPE) 21endfunction() 22 23get_hash_for_ref(HEAD head_sha error_msg) 24if(head_sha STREQUAL "") 25 message(FATAL_ERROR "Failed to get the hash for HEAD:\n${error_msg}") 26endif() 27 28 29execute_process( 30 COMMAND "@git_EXECUTABLE@" show-ref "@git_tag@" 31 WORKING_DIRECTORY "@work_dir@" 32 OUTPUT_VARIABLE show_ref_output 33) 34if(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/remotes/") 35 # Given a full remote/branch-name and we know about it already. Since 36 # branches can move around, we always have to fetch. 37 set(fetch_required YES) 38 set(checkout_name "@git_tag@") 39 40elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/tags/") 41 # Given a tag name that we already know about. We don't know if the tag we 42 # have matches the remote though (tags can move), so we should fetch. 43 set(fetch_required YES) 44 set(checkout_name "@git_tag@") 45 46 # Special case to preserve backward compatibility: if we are already at the 47 # same commit as the tag we hold locally, don't do a fetch and assume the tag 48 # hasn't moved on the remote. 49 # FIXME: We should provide an option to always fetch for this case 50 get_hash_for_ref("@git_tag@" tag_sha error_msg) 51 if(tag_sha STREQUAL head_sha) 52 message(VERBOSE "Already at requested tag: ${tag_sha}") 53 return() 54 endif() 55 56elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/heads/") 57 # Given a branch name without any remote and we already have a branch by that 58 # name. We might already have that branch checked out or it might be a 59 # different branch. It isn't safe to use a bare branch name without the 60 # remote, so do a fetch and replace the ref with one that includes the remote. 61 set(fetch_required YES) 62 set(checkout_name "@git_remote_name@/@git_tag@") 63 64else() 65 get_hash_for_ref("@git_tag@" tag_sha error_msg) 66 if(tag_sha STREQUAL head_sha) 67 # Have the right commit checked out already 68 message(VERBOSE "Already at requested ref: ${tag_sha}") 69 return() 70 71 elseif(tag_sha STREQUAL "") 72 # We don't know about this ref yet, so we have no choice but to fetch. 73 # We deliberately swallow any error message at the default log level 74 # because it can be confusing for users to see a failed git command. 75 # That failure is being handled here, so it isn't an error. 76 set(fetch_required YES) 77 set(checkout_name "@git_tag@") 78 if(NOT error_msg STREQUAL "") 79 message(VERBOSE "${error_msg}") 80 endif() 81 82 else() 83 # We have the commit, so we know we were asked to find a commit hash 84 # (otherwise it would have been handled further above), but we don't 85 # have that commit checked out yet 86 set(fetch_required NO) 87 set(checkout_name "@git_tag@") 88 if(NOT error_msg STREQUAL "") 89 message(WARNING "${error_msg}") 90 endif() 91 92 endif() 93endif() 94 95if(fetch_required) 96 message(VERBOSE "Fetching latest from the remote @git_remote_name@") 97 execute_process( 98 COMMAND "@git_EXECUTABLE@" fetch --tags --force "@git_remote_name@" 99 WORKING_DIRECTORY "@work_dir@" 100 COMMAND_ERROR_IS_FATAL ANY 101 ) 102endif() 103 104set(git_update_strategy "@git_update_strategy@") 105if(git_update_strategy STREQUAL "") 106 # Backward compatibility requires REBASE as the default behavior 107 set(git_update_strategy REBASE) 108endif() 109 110if(git_update_strategy MATCHES "^REBASE(_CHECKOUT)?$") 111 # Asked to potentially try to rebase first, maybe with fallback to checkout. 112 # We can't if we aren't already on a branch and we shouldn't if that local 113 # branch isn't tracking the one we want to checkout. 114 execute_process( 115 COMMAND "@git_EXECUTABLE@" symbolic-ref -q HEAD 116 WORKING_DIRECTORY "@work_dir@" 117 OUTPUT_VARIABLE current_branch 118 OUTPUT_STRIP_TRAILING_WHITESPACE 119 # Don't test for an error. If this isn't a branch, we get a non-zero error 120 # code but empty output. 121 ) 122 123 if(current_branch STREQUAL "") 124 # Not on a branch, checkout is the only sensible option since any rebase 125 # would always fail (and backward compatibility requires us to checkout in 126 # this situation) 127 set(git_update_strategy CHECKOUT) 128 129 else() 130 execute_process( 131 COMMAND "@git_EXECUTABLE@" for-each-ref "--format='%(upstream:short)'" "${current_branch}" 132 WORKING_DIRECTORY "@work_dir@" 133 OUTPUT_VARIABLE upstream_branch 134 OUTPUT_STRIP_TRAILING_WHITESPACE 135 COMMAND_ERROR_IS_FATAL ANY # There is no error if no upstream is set 136 ) 137 if(NOT upstream_branch STREQUAL checkout_name) 138 # Not safe to rebase when asked to checkout a different branch to the one 139 # we are tracking. If we did rebase, we could end up with arbitrary 140 # commits added to the ref we were asked to checkout if the current local 141 # branch happens to be able to rebase onto the target branch. There would 142 # be no error message and the user wouldn't know this was occurring. 143 set(git_update_strategy CHECKOUT) 144 endif() 145 146 endif() 147elseif(NOT git_update_strategy STREQUAL "CHECKOUT") 148 message(FATAL_ERROR "Unsupported git update strategy: ${git_update_strategy}") 149endif() 150 151 152# Check if stash is needed 153execute_process( 154 COMMAND "@git_EXECUTABLE@" status --porcelain 155 WORKING_DIRECTORY "@work_dir@" 156 RESULT_VARIABLE error_code 157 OUTPUT_VARIABLE repo_status 158) 159if(error_code) 160 message(FATAL_ERROR "Failed to get the status") 161endif() 162string(LENGTH "${repo_status}" need_stash) 163 164# If not in clean state, stash changes in order to be able to perform a 165# rebase or checkout without losing those changes permanently 166if(need_stash) 167 execute_process( 168 COMMAND "@git_EXECUTABLE@" stash save @git_stash_save_options@ 169 WORKING_DIRECTORY "@work_dir@" 170 COMMAND_ERROR_IS_FATAL ANY 171 ) 172endif() 173 174if(git_update_strategy STREQUAL "CHECKOUT") 175 execute_process( 176 COMMAND "@git_EXECUTABLE@" checkout "${checkout_name}" 177 WORKING_DIRECTORY "@work_dir@" 178 COMMAND_ERROR_IS_FATAL ANY 179 ) 180else() 181 execute_process( 182 COMMAND "@git_EXECUTABLE@" rebase "${checkout_name}" 183 WORKING_DIRECTORY "@work_dir@" 184 RESULT_VARIABLE error_code 185 OUTPUT_VARIABLE rebase_output 186 ERROR_VARIABLE rebase_output 187 ) 188 if(error_code) 189 # Rebase failed, undo the rebase attempt before continuing 190 execute_process( 191 COMMAND "@git_EXECUTABLE@" rebase --abort 192 WORKING_DIRECTORY "@work_dir@" 193 ) 194 195 if(NOT git_update_strategy STREQUAL "REBASE_CHECKOUT") 196 # Not allowed to do a checkout as a fallback, so cannot proceed 197 if(need_stash) 198 execute_process( 199 COMMAND "@git_EXECUTABLE@" stash pop --index --quiet 200 WORKING_DIRECTORY "@work_dir@" 201 ) 202 endif() 203 message(FATAL_ERROR "\nFailed to rebase in: '@work_dir@'." 204 "\nOutput from the attempted rebase follows:" 205 "\n${rebase_output}" 206 "\n\nYou will have to resolve the conflicts manually") 207 endif() 208 209 # Fall back to checkout. We create an annotated tag so that the user 210 # can manually inspect the situation and revert if required. 211 # We can't log the failed rebase output because MSVC sees it and 212 # intervenes, causing the build to fail even though it completes. 213 # Write it to a file instead. 214 string(TIMESTAMP tag_timestamp "%Y%m%dT%H%M%S" UTC) 215 set(tag_name _cmake_ExternalProject_moved_from_here_${tag_timestamp}Z) 216 set(error_log_file ${CMAKE_CURRENT_LIST_DIR}/rebase_error_${tag_timestamp}Z.log) 217 file(WRITE ${error_log_file} "${rebase_output}") 218 message(WARNING "Rebase failed, output has been saved to ${error_log_file}" 219 "\nFalling back to checkout, previous commit tagged as ${tag_name}") 220 execute_process( 221 COMMAND "@git_EXECUTABLE@" tag -a 222 -m "ExternalProject attempting to move from here to ${checkout_name}" 223 ${tag_name} 224 WORKING_DIRECTORY "@work_dir@" 225 COMMAND_ERROR_IS_FATAL ANY 226 ) 227 228 execute_process( 229 COMMAND "@git_EXECUTABLE@" checkout "${checkout_name}" 230 WORKING_DIRECTORY "@work_dir@" 231 COMMAND_ERROR_IS_FATAL ANY 232 ) 233 endif() 234endif() 235 236if(need_stash) 237 # Put back the stashed changes 238 execute_process( 239 COMMAND "@git_EXECUTABLE@" stash pop --index --quiet 240 WORKING_DIRECTORY "@work_dir@" 241 RESULT_VARIABLE error_code 242 ) 243 if(error_code) 244 # Stash pop --index failed: Try again dropping the index 245 execute_process( 246 COMMAND "@git_EXECUTABLE@" reset --hard --quiet 247 WORKING_DIRECTORY "@work_dir@" 248 ) 249 execute_process( 250 COMMAND "@git_EXECUTABLE@" stash pop --quiet 251 WORKING_DIRECTORY "@work_dir@" 252 RESULT_VARIABLE error_code 253 ) 254 if(error_code) 255 # Stash pop failed: Restore previous state. 256 execute_process( 257 COMMAND "@git_EXECUTABLE@" reset --hard --quiet ${head_sha} 258 WORKING_DIRECTORY "@work_dir@" 259 ) 260 execute_process( 261 COMMAND "@git_EXECUTABLE@" stash pop --index --quiet 262 WORKING_DIRECTORY "@work_dir@" 263 ) 264 message(FATAL_ERROR "\nFailed to unstash changes in: '@work_dir@'." 265 "\nYou will have to resolve the conflicts manually") 266 endif() 267 endif() 268endif() 269 270set(init_submodules "@init_submodules@") 271if(init_submodules) 272 execute_process( 273 COMMAND "@git_EXECUTABLE@" submodule update @git_submodules_recurse@ --init @git_submodules@ 274 WORKING_DIRECTORY "@work_dir@" 275 COMMAND_ERROR_IS_FATAL ANY 276 ) 277endif() 278