-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgitversion.cmake
287 lines (259 loc) · 12.6 KB
/
gitversion.cmake
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
278
279
280
281
282
283
284
285
286
287
# Copyright (C) 2024 Toitware ApS. All rights reserved.
# Use of this source code is governed by an MIT-style license that can be
# found in the LICENSE file.
# Load the git package giving us 'GIT_EXECUTABLE'
find_package(Git QUIET REQUIRED)
function(backtick out_name)
execute_process(
COMMAND ${ARGN}
OUTPUT_VARIABLE result
OUTPUT_STRIP_TRAILING_WHITESPACE
COMMAND_ERROR_IS_FATAL ANY
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
set(${out_name} ${result} PARENT_SCOPE)
endfunction()
# Computes a version from git branches and git tags.
# If a commit is tagged explicitly with a version tag (v1.2.3) then use that one.
# If a commit is on a release-branch (release-v1.2), then we have two cases:
# - there is already a release on this branch (visible by a version tag with the
# same major/minor)
# - there isn't a release yet.
# If there is already a release, then count the commits since that release, and
# mark the current commit as a prerelease of a patch-release. For example,
# if we are on release-v1.2 and there is a tag v1.2.4. Then we count the commits
# since that tag (say 5) and return 'v1.2.5-pre.5+<commit-hash>'
# If there isn't a release yet (no tag), then we count the commits since the previous
# release-branch (say 38 commits since release-v1.1 branch point) and
# return 'v1.2.0-pre.38+<commit-hash>'. We need to count from the previous branch-point
# since 'master' is using the same naming scheme, and we need to ensure monotonically
# increasing version numbers. (See next case).
# If a commit is not on a release-branch, find the newest release-branch (say release-v1.5),
# and count the commits since it (say 22). Calculate the version as v1.6.0-pre.22+<commit-hash>
# if we are on master/main or don't know the branch name.
# At the same time find the most recent tag that is reachable from the current commit. Calculate
# a version number similar to what we do for the release branch.
# Compare the version from the tag with the version from the branch and return the highest one.
#
# Note that "v1.6.0-pre." is also used on a release-v1.6 branch until the release has
# happened. Care must be taken to ensure that the version numbers increase correctly.
# If there isn't any release-branch, then we use the DEFAULT_PRE_VERSION parameter as prefix.
set(DEFAULT_PRE_VERSION "v0.0.1")
function(compute_git_version VERSION)
# Check that we are in a git tree.
backtick(ignored ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree)
backtick(CURRENT_COMMIT ${GIT_EXECUTABLE} rev-parse HEAD)
backtick(CURRENT_COMMIT_SHORT ${GIT_EXECUTABLE} rev-parse --short HEAD)
# Note: the clone of the repository must have access to all tags.
# Buildbots might provide a shallow copy, in which case `git fetch --tags` must be called.
# Check if we are matching a tag directly.
execute_process(
# The '--abbrev=0' ensures that we only get the tag, without the number of intermediate commits.
# Git describe uses globs for matching and not regexps.
COMMAND ${GIT_EXECUTABLE} describe --tags --match "v[0-9]*" --abbrev=0 HEAD
RESULT_VARIABLE result
OUTPUT_VARIABLE LATEST_VERSION_TAG
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
if ("${result}" EQUAL 0 AND NOT "${LATEST_VERSION_TAG}" STREQUAL "")
backtick(VERSION_TAG_COMMIT ${GIT_EXECUTABLE} rev-parse "${LATEST_VERSION_TAG}^{}")
# If we are directly on a tag commit, just use that one.
if ("${VERSION_TAG_COMMIT}" STREQUAL "${CURRENT_COMMIT}")
set(${VERSION} "${LATEST_VERSION_TAG}" PARENT_SCOPE)
return()
endif()
endif()
backtick(CURRENT_BRANCH ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD)
if ("${CURRENT_BRANCH}" STREQUAL master OR "${CURRENT_BRANCH}" STREQUAL main OR "${CURRENT_BRANCH}" STREQUAL HEAD)
# Master branch: v0.5.0-pre.17+9a1fbdb29
set(BRANCH_ID "")
else()
# Other branch: v0.5.0-pre.17+branch-name.9a1fbd29
# Semver requires the dot-separated identifiers to comprise only alphanumerics and hyphens.
string(REGEX REPLACE "[^.0-9A-Za-z-]" "-" SANITIZED_BRANCH ${CURRENT_BRANCH})
set(BRANCH_ID "${SANITIZED_BRANCH}.")
endif()
# Find all release branches.
backtick(ALL_BRANCHES ${GIT_EXECUTABLE} branch -a "--format=%(refname:short)")
# Split lines.
# Assumes there aren't any ';' in the branch names.
STRING(REGEX REPLACE "\n" ";" ALL_BRANCHES "${ALL_BRANCHES}")
# Find all remote release branches.
# We ignore the local ones.
set(RELEASE_BRANCHES ${ALL_BRANCHES})
list(FILTER RELEASE_BRANCHES INCLUDE REGEX "/release-v[0-9]+\\.[0-9]")
list(SORT RELEASE_BRANCHES COMPARE NATURAL ORDER DESCENDING)
list(LENGTH RELEASE_BRANCHES BRANCHES_LENGTH)
if (${BRANCHES_LENGTH} EQUAL 0)
if ("${LATEST_VERSION_TAG}" STREQUAL "")
backtick(COMMIT_COUNT ${GIT_EXECUTABLE} rev-list --count HEAD)
set(${VERSION} "${DEFAULT_PRE_VERSION}-pre.${COMMIT_COUNT}+${BRANCH_ID}${CURRENT_COMMIT_SHORT}" PARENT_SCOPE)
return()
endif()
endif()
function (version_from_branch BRANCH MAJOR MINOR)
string(REGEX MATCH "/release-v([0-9]+)\\.([0-9]+)" IGNORED "${BRANCH}")
set(${MAJOR} "${CMAKE_MATCH_1}" PARENT_SCOPE)
set(${MINOR} "${CMAKE_MATCH_2}" PARENT_SCOPE)
endfunction()
function (version_from_tag TAG MAJOR MINOR PATCH)
string(REGEX MATCH "v([0-9]+)\\.([0-9]+)\\.([0-9]+)" IGNORED "${TAG}")
set(${MAJOR} "${CMAKE_MATCH_1}" PARENT_SCOPE)
set(${MINOR} "${CMAKE_MATCH_2}" PARENT_SCOPE)
set(${PATCH} "${CMAKE_MATCH_3}" PARENT_SCOPE)
endfunction()
# Returns the common ancestor of COMMIT1 and COMMIT2.
function (common_ancestor COMMIT1 COMMIT2 COMMON_ANCESTOR)
set(DEPTH 128)
while (1)
execute_process(
COMMAND ${GIT_EXECUTABLE} merge-base "${COMMIT1}" "${COMMIT2}"
RESULT_VARIABLE result
OUTPUT_VARIABLE ANCESTOR
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
if ("${result}" EQUAL 0)
break()
endif()
if (${DEPTH} GREATER 10000)
message(FATAL_ERROR "Couldn't determine merge-base for ${COMMIT1} and ${COMMIT2}")
endif()
backtick(ignored ${GIT_EXECUTABLE} fetch --depth=${DEPTH})
math(EXPR DEPTH "${DEPTH} * 2")
endwhile()
set(${COMMON_ANCESTOR} "${ANCESTOR}" PARENT_SCOPE)
endfunction()
# Returns the distance of HEAD to the common ancestor of HEAD and COMMIT.
function (commits_since_common_ancestor COMMIT COMMIT_COUNT)
common_ancestor("${COMMIT}" HEAD COMMON_ANCESTOR)
backtick(COMMITS_IN_BRANCH ${GIT_EXECUTABLE} rev-list --count "HEAD...${COMMON_ANCESTOR}")
set(${COMMIT_COUNT} ${COMMITS_IN_BRANCH} PARENT_SCOPE)
endfunction()
# Run through the release-branches to see if we are on one.
if (NOT ${BRANCHES_LENGTH} EQUAL 0)
math(EXPR LEN_MINUS_1 "${BRANCHES_LENGTH} - 1")
foreach (index RANGE 0 ${LEN_MINUS_1})
list(GET RELEASE_BRANCHES ${index} RELEASE_BRANCH)
common_ancestor("${RELEASE_BRANCH}" origin/master BRANCH_POINT)
# We are looking for commits that are between the branch point and the
# branch-head.
# In other words: BRANCH_POINT <= HEAD <= RELEASE_BRANCH-HEAD
execute_process(
COMMAND ${GIT_EXECUTABLE} merge-base --is-ancestor ${BRANCH_POINT} HEAD
RESULT_VARIABLE RESULT
ERROR_QUIET
OUTPUT_QUIET
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
if (NOT "${RESULT}" EQUAL 0)
continue()
endif()
execute_process(
COMMAND ${GIT_EXECUTABLE} merge-base --is-ancestor HEAD ${RELEASE_BRANCH}
RESULT_VARIABLE RESULT
ERROR_QUIET
OUTPUT_QUIET
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
if (NOT "${RESULT}" EQUAL 0)
continue()
endif()
# This commit is a child branch of a release-branch.
version_from_branch("${RELEASE_BRANCH}" branch_major branch_minor)
# See if there is already a release of this branch.
execute_process(
# The '--abbrev=0' ensures that we only get the tag, without the number of intermediate commits.
# Git describe uses globs for matching and not regexps. This makes this a bit more awkward.
COMMAND ${GIT_EXECUTABLE} describe --tags
--match "v${branch_major}.${branch_minor}.[0-9]"
--match "v${branch_major}.${branch_minor}.[0-9][0-9]"
--match "v${branch_major}.${branch_minor}.[0-9][0-9][0-9]"
--abbrev=0 HEAD
RESULT_VARIABLE result
ERROR_QUIET
OUTPUT_VARIABLE LATEST_VERSION_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
if ("${result}" EQUAL 0 AND NOT "${LATEST_VERSION_TAG}" STREQUAL "")
version_from_tag("${LATEST_VERSION_TAG}" tag_major tag_minor tag_patch)
endif()
if ("${branch_major}" EQUAL "${tag_major}" AND "${branch_minor}" EQUAL "${tag_minor}")
# There is already a release on this branch.
# Use next patch version.
MATH(EXPR patch "${tag_patch}+1")
# Count the distance to the last tag.
backtick(VERSION_TAG_COMMIT ${GIT_EXECUTABLE} rev-parse "${LATEST_VERSION_TAG}^{}")
backtick(CURRENT_COMMIT_NO ${GIT_EXECUTABLE} rev-list --count HEAD "^${VERSION_TAG_COMMIT}")
set(${VERSION} "v${tag_major}.${tag_minor}.${patch}-pre.${CURRENT_COMMIT_NO}+${BRANCH_ID}${CURRENT_COMMIT_SHORT}" PARENT_SCOPE)
return()
endif()
# First release on this major.minor branch.
# We need to find the distance to the previous commit-branch to maintain a consistent
# counting (monotonically increasing).
# We assume the next release-branch in the list is the previous branch-point.
# Remember: the release-branches are sorted in descending order.
math(EXPR next_index "${index}+1")
if (${next_index} EQUAL ${BRANCHES_LENGTH})
# No previous branch-release.
# Count all commits.
backtick(COMMITS_SINCE_LAST_RELEASE ${GIT_EXECUTABLE} rev-list --count HEAD)
else()
list(GET RELEASE_BRANCHES ${next_index} PREVIOUS_RELEASE_BRANCH)
commits_since_common_ancestor(${PREVIOUS_RELEASE_BRANCH} COMMITS_SINCE_LAST_RELEASE)
endif()
set(${VERSION} "v${branch_major}.${branch_minor}.0-pre.${COMMITS_SINCE_LAST_RELEASE}+${BRANCH_ID}${CURRENT_COMMIT_SHORT}" PARENT_SCOPE)
return()
endforeach()
endif()
# We are not on a release branch.
# Use the next highest release since the last release branch, or count up from the most recent tag.
# The first branch is the latest version (by virtue of sorting in natural descending order).
if (${BRANCHES_LENGTH} EQUAL 0)
set(branch_major 0)
set(branch_minor 0)
else()
list(GET RELEASE_BRANCHES 0 NEWEST_BRANCH)
version_from_branch("${NEWEST_BRANCH}" branch_major branch_minor)
endif()
if (NOT "${LATEST_VERSION_TAG}" STREQUAL "")
string(SUBSTRING "${LATEST_VERSION_TAG}" 1 -1 LATEST_VERSION_TAG_NO_V)
if (LATEST_VERSION_TAG_NO_V VERSION_GREATER "${branch_major}.${branch_minor}.0")
# Count the commits since the tag.
commits_since_common_ancestor(${LATEST_VERSION_TAG} COMMITS_SINCE_LAST_TAG)
if (${LATEST_VERSION_TAG} MATCHES "v([0-9]+)\\.([0-9]+)\\.([0-9]+)-")
# The tag is a prerelease.
# According to semver, a larger set of pre-release fields has a higher precedence.
# 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
# Add our commit count at the end.
# For example, if the latest tag is v2.0.0-alpha.39, then we generate v2.0.0-alpha.39.<commit-count>.
set(RESULT_PREFIX "${LATEST_VERSION_TAG}.${COMMITS_SINCE_LAST_TAG}")
else()
# A normal version number.
# Add a '-pre.<commit-count>' suffix.
version_from_tag("${LATEST_VERSION_TAG}" tag_major tag_minor tag_patch)
MATH(EXPR patch "${tag_patch}+1")
set(RESULT_PREFIX "v${tag_major}.${tag_minor}.${patch}-pre.${COMMITS_SINCE_LAST_TAG}")
endif()
endif()
endif()
if ("${RESULT_PREFIX}" STREQUAL "")
# Use the release-branch as base.
# Count the commits since the last release-branch.
commits_since_common_ancestor(${NEWEST_BRANCH} COMMITS_SINCE_LAST_RELEASE)
MATH(EXPR minor "${branch_minor}+1")
set(RESULT_PREFIX "v${branch_major}.${minor}.0-pre.${COMMITS_SINCE_LAST_RELEASE}")
endif()
# Add the branch name to the version.
set(${VERSION} "${RESULT_PREFIX}+${BRANCH_ID}${CURRENT_COMMIT_SHORT}" PARENT_SCOPE)
endfunction()
# Print the git-version on stdout:
# cmake -DPRINT_VERSION=1 -P gitversion.cmake
if (DEFINED PRINT_VERSION)
compute_git_version(VERSION)
execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "${VERSION}")
endif()