summaryrefslogtreecommitdiff
path: root/Modules/ExternalProject-gitupdate.cmake.in
blob: 461e32354075dc013d944325afacc81d26251f0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# 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@" rev-parse "${ref}^{commit}"
    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@" 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@" 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@" 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@" 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@" 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@" 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@" checkout "${checkout_name}"
    WORKING_DIRECTORY "@work_dir@"
    COMMAND_ERROR_IS_FATAL ANY
  )
else()
  execute_process(
    COMMAND "@git_EXECUTABLE@" 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@" 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@" 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@" 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@" 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@" 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@" reset --hard --quiet
      WORKING_DIRECTORY "@work_dir@"
    )
    execute_process(
      COMMAND "@git_EXECUTABLE@" 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@" reset --hard --quiet ${head_sha}
        WORKING_DIRECTORY "@work_dir@"
      )
      execute_process(
        COMMAND "@git_EXECUTABLE@" 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@" submodule update @git_submodules_recurse@ --init @git_submodules@
    WORKING_DIRECTORY "@work_dir@"
    COMMAND_ERROR_IS_FATAL ANY
  )
endif()