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