├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── git.c.in ├── git.h ├── git_watcher.cmake └── tests ├── assert.sh ├── hello-world ├── CMakeLists.txt └── main.cc ├── interfaces ├── c99 │ ├── CMakeLists.txt │ └── main.c └── cxx98_cxx17 │ ├── CMakeLists.txt │ └── main.cc ├── run_all.sh ├── test_branch_name.sh ├── test_commit_attributes.sh ├── test_dirty_head.sh ├── test_dirty_head_untracked.sh ├── test_git_created_post_configure.sh ├── test_git_describe_with_tag.sh ├── test_interfaces.sh ├── test_make_clean.sh ├── test_modified_preconfig_file.sh ├── test_multiline_messages.sh ├── test_new_commits.sh ├── test_no_change_means_no_rebuild.sh ├── test_no_git_history.sh ├── test_quote_messages.sh ├── test_semicolon_multiline_commit_message.sh └── util.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh linguist-detectable=false 2 | *.cmake linguist-detectable=true 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Regression Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | run-tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v2 15 | - name: get cmake and ninja 16 | uses: lukka/get-cmake@latest 17 | - name: run tests 18 | run: | 19 | git config --global user.email "ci@example.com" 20 | git config --global user.name "CI" 21 | git config --global init.defaultBranch master 22 | ./tests/run_all.sh 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2...3.27) 2 | project(cmake_git_version_tracking 3 | LANGUAGES C) 4 | 5 | # Define the two required variables before including 6 | # the source code for watching a git repository. 7 | set(PRE_CONFIGURE_FILE "git.c.in") 8 | set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/git.c") 9 | include(git_watcher.cmake) 10 | 11 | # Create a library out of the compiled post-configure file. 12 | # 13 | # Note that the include is a system include. This was done 14 | # so downstream projects don't suffer from warnings on a 15 | # 3rdparty library. 16 | add_library(${PROJECT_NAME} STATIC ${POST_CONFIGURE_FILE}) 17 | target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 18 | add_dependencies(${PROJECT_NAME} check_git) 19 | 20 | # The C99 standard is only required because we're using . 21 | # This could be removed if it's a problem for users, but would require the 22 | # cmake configure() commands to translate true/false literals to 1/0. 23 | set_property(TARGET ${PROJECT_NAME} PROPERTY C_STANDARD 99) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrew Hardin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Regression Tests](https://github.com/andrew-hardin/cmake-git-version-tracking/actions/workflows/main.yml/badge.svg)](https://github.com/andrew-hardin/cmake-git-version-tracking/actions/workflows/main.yml) 2 | # Embed Git metadata in C/C++ projects via CMake 3 | This project embeds up-to-date git metadata in a standalone C/C++ static library via CMake. 4 | It's written responsibly to only trigger rebuilds if git metadata changes (e.g. a new commit is added). 5 | The core capability is baked into single self-contained 6 | [script](git_watcher.cmake). 7 | 8 | ## Requirements 9 | - CMake >= 3.2 10 | - C Compiler (with C99 standard support) 11 | - Git 12 | 13 | ## Quickstart via FetchContent 14 | You can use CMake's `FetchContent` module to build the static library `cmake_git_version_tracking`: 15 | ```cmake 16 | include(FetchContent) 17 | FetchContent_Declare(cmake_git_version_tracking 18 | GIT_REPOSITORY https://github.com/andrew-hardin/cmake-git-version-tracking.git 19 | GIT_TAG 904dbda1336ba4b9a1415a68d5f203f576b696bb 20 | ) 21 | FetchContent_MakeAvailable(cmake_git_version_tracking) 22 | 23 | target_link_libraries(your_target 24 | cmake_git_version_tracking 25 | ) 26 | ``` 27 | Then [`#include git.h`](./git.h) and use the provided functions to retrieve git metadata. 28 | 29 | ## Intended use case 30 | You're continuously shipping prebuilt binaries for an 31 | application. A user discovers a bug and files a bug report. 32 | By embedding up-to-date versioning information, the user 33 | can include this information in their report, e.g.: 34 | 35 | ``` 36 | Commit SHA1: 46a396e (46a396e6c1eb3d) 37 | Dirty: false (there were no uncommitted changes at time of build) 38 | ``` 39 | 40 | This allows you to investigate the _precise_ version of the 41 | application that the bug was reported in. 42 | 43 | ## Q: What if I want to track `$special_git_field`? 44 | Fork the project and modify [git_watcher.cmake](git_watcher.cmake) 45 | to track new additional fields (e.g. kernel version or build hostname). 46 | Sections that need to be modified are marked with `>>>`. 47 | 48 | ## Q: Doesn't this already exist? 49 | It depends on your specific requirements. Before writing this, I 50 | found two categories of existing solutions: 51 | 52 | - Write the commit ID to the header at configure time (e.g. `cmake `). 53 | This works well for automated build processes (e.g. check-in code and build artifacts). 54 | However, any changes made after running `cmake` 55 | (e.g. `git commit -am "Changed X"`) aren't reflected in the header. 56 | 57 | - Every time a build is started (e.g. `make`), write the commit ID to a header. 58 | The major drawback of this method is that any object file that includes the new 59 | header will be recompiled -- _even if the state of the git repo hasn't changed_. 60 | 61 | ## Q: What's the better solution? 62 | We check Git every time a build is started (e.g. `make`) to see if anything has changed, 63 | like a new commit to the current branch. If nothing has changed, then we don't 64 | touch anything- _no recompiling or linking is triggered_. If something has changed, then we 65 | reconfigure the header and CMake rebuilds any downstream dependencies. 66 | -------------------------------------------------------------------------------- /git.c.in: -------------------------------------------------------------------------------- 1 | #include "git.h" 2 | 3 | bool git_IsPopulated() { 4 | return @GIT_RETRIEVED_STATE@; 5 | } 6 | bool git_AnyUncommittedChanges() { 7 | return @GIT_IS_DIRTY@; 8 | } 9 | const char* git_AuthorName() { 10 | return "@GIT_AUTHOR_NAME@"; 11 | } 12 | const char* git_AuthorEmail() { 13 | return "@GIT_AUTHOR_EMAIL@"; 14 | } 15 | const char* git_CommitSHA1() { 16 | return "@GIT_HEAD_SHA1@"; 17 | } 18 | const char* git_CommitDate() { 19 | return "@GIT_COMMIT_DATE_ISO8601@"; 20 | } 21 | const char* git_CommitSubject() { 22 | return "@GIT_COMMIT_SUBJECT@"; 23 | } 24 | const char* git_CommitBody() { 25 | return "@GIT_COMMIT_BODY@"; 26 | } 27 | const char* git_Describe() { 28 | return "@GIT_DESCRIBE@"; 29 | } 30 | const char* git_Branch() { 31 | return "@GIT_BRANCH@"; 32 | } 33 | -------------------------------------------------------------------------------- /git.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | // git.h 3 | // https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git.h 4 | // 5 | // Released under the MIT License. 6 | // https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE 7 | 8 | #include 9 | 10 | #ifdef __cplusplus 11 | #define GIT_VERSION_TRACKING_EXTERN_C_BEGIN extern "C" { 12 | #define GIT_VERSION_TRACKING_EXTERN_C_END } 13 | #else 14 | #define GIT_VERSION_TRACKING_EXTERN_C_BEGIN 15 | #define GIT_VERSION_TRACKING_EXTERN_C_END 16 | #endif 17 | 18 | // Don't mangle the C function names if included in a CXX file. 19 | GIT_VERSION_TRACKING_EXTERN_C_BEGIN 20 | 21 | /// Is the metadata populated? 22 | // 23 | /// We may not have metadata if there wasn't a .git directory 24 | /// (e.g. downloaded source code without revision history). 25 | bool git_IsPopulated(); 26 | 27 | /// Were there any uncommitted changes that won't be reflected 28 | /// in the CommitID? 29 | bool git_AnyUncommittedChanges(); 30 | 31 | /// The commit author's name. 32 | const char* git_AuthorName(); 33 | 34 | /// The commit author's email. 35 | const char* git_AuthorEmail(); 36 | 37 | /// The commit SHA1. 38 | const char* git_CommitSHA1(); 39 | 40 | /// The ISO8601 commit date. 41 | const char* git_CommitDate(); 42 | 43 | /// The commit subject. 44 | const char* git_CommitSubject(); 45 | 46 | /// The commit body. 47 | const char* git_CommitBody(); 48 | 49 | /// The commit describe. 50 | const char* git_Describe(); 51 | 52 | /// The symbolic reference tied to HEAD. 53 | const char* git_Branch(); 54 | 55 | GIT_VERSION_TRACKING_EXTERN_C_END 56 | #undef GIT_VERSION_TRACKING_EXTERN_C_BEGIN 57 | #undef GIT_VERSION_TRACKING_EXTERN_C_END 58 | 59 | #ifdef __cplusplus 60 | 61 | /// This is a utility extension for C++ projects. 62 | /// It provides a "git" namespace that wraps the 63 | /// C methods in more(?) ergonomic types. 64 | /// 65 | /// This is header-only in an effort to keep the 66 | /// underlying static library C99 compliant. 67 | 68 | 69 | // We really want to use std::string_view if it appears 70 | // that the compiler will support it. If that fails, 71 | // revert back to std::string. 72 | #define GIT_VERSION_TRACKING_CPP_17_STANDARD 201703L 73 | #if __cplusplus >= GIT_VERSION_TRACKING_CPP_17_STANDARD 74 | #define GIT_VERSION_USE_STRING_VIEW 1 75 | #else 76 | #define GIT_VERSION_USE_STRING_VIEW 0 77 | #endif 78 | 79 | 80 | #if GIT_VERSION_USE_STRING_VIEW 81 | #include 82 | #include 83 | #else 84 | #include 85 | #endif 86 | 87 | namespace git { 88 | 89 | #if GIT_VERSION_USE_STRING_VIEW 90 | using StringOrView = std::string_view; 91 | #else 92 | typedef std::string StringOrView; 93 | #endif 94 | 95 | namespace internal { 96 | 97 | /// Short-hand method for initializing a std::string or std::string_view given a C-style const char*. 98 | inline const StringOrView InitString(const char* from_c_interface) { 99 | #if GIT_VERSION_USE_STRING_VIEW 100 | return StringOrView { from_c_interface, std::strlen(from_c_interface) }; 101 | #else 102 | return std::string(from_c_interface); 103 | #endif 104 | } 105 | 106 | } // namespace internal 107 | 108 | inline bool IsPopulated() { 109 | return git_IsPopulated(); 110 | } 111 | inline bool AnyUncommittedChanges() { 112 | return git_AnyUncommittedChanges(); 113 | } 114 | inline const StringOrView& AuthorName() { 115 | static const StringOrView kValue = internal::InitString(git_AuthorName()); 116 | return kValue; 117 | } 118 | inline const StringOrView AuthorEmail() { 119 | static const StringOrView kValue = internal::InitString(git_AuthorEmail()); 120 | return kValue; 121 | } 122 | inline const StringOrView CommitSHA1() { 123 | static const StringOrView kValue = internal::InitString(git_CommitSHA1()); 124 | return kValue; 125 | } 126 | inline const StringOrView CommitDate() { 127 | static const StringOrView kValue = internal::InitString(git_CommitDate()); 128 | return kValue; 129 | } 130 | inline const StringOrView CommitSubject() { 131 | static const StringOrView kValue = internal::InitString(git_CommitSubject()); 132 | return kValue; 133 | } 134 | inline const StringOrView CommitBody() { 135 | static const StringOrView kValue = internal::InitString(git_CommitBody()); 136 | return kValue; 137 | } 138 | inline const StringOrView Describe() { 139 | static const StringOrView kValue = internal::InitString(git_Describe()); 140 | return kValue; 141 | } 142 | inline const StringOrView Branch() { 143 | static const StringOrView kValue = internal::InitString(git_Branch()); 144 | return kValue; 145 | } 146 | 147 | } // namespace git 148 | 149 | 150 | // Cleanup our defines to avoid polluting. 151 | #undef GIT_VERSION_USE_STRING_VIEW 152 | #undef GIT_VERSION_TRACKING_CPP_17_STANDARD 153 | 154 | #endif // __cplusplus 155 | -------------------------------------------------------------------------------- /git_watcher.cmake: -------------------------------------------------------------------------------- 1 | # git_watcher.cmake 2 | # https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake 3 | # 4 | # Released under the MIT License. 5 | # https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE 6 | 7 | 8 | # This file defines a target that monitors the state of a git repo. 9 | # If the state changes (e.g. a commit is made), then a file gets reconfigured. 10 | # Here are the primary variables that control script behavior: 11 | # 12 | # PRE_CONFIGURE_FILE (REQUIRED) 13 | # -- The path to the file that'll be configured. 14 | # 15 | # POST_CONFIGURE_FILE (REQUIRED) 16 | # -- The path to the configured PRE_CONFIGURE_FILE. 17 | # 18 | # GIT_STATE_FILE (OPTIONAL) 19 | # -- The path to the file used to store the previous build's git state. 20 | # Defaults to the current binary directory. 21 | # 22 | # GIT_WORKING_DIR (OPTIONAL) 23 | # -- The directory from which git commands will be run. 24 | # Defaults to the directory with the top level CMakeLists.txt. 25 | # 26 | # GIT_EXECUTABLE (OPTIONAL) 27 | # -- The path to the git executable. It'll automatically be set if the 28 | # user doesn't supply a path. 29 | # 30 | # GIT_FAIL_IF_NONZERO_EXIT (OPTIONAL) 31 | # -- Raise a FATAL_ERROR if any of the git commands return a non-zero 32 | # exit code. This is set to TRUE by default. You can set this to FALSE 33 | # if you'd like the build to continue even if a git command fails. 34 | # 35 | # GIT_IGNORE_UNTRACKED (OPTIONAL) 36 | # -- Ignore the presence of untracked files when detecting if the 37 | # working tree is dirty. This is set to FALSE by default. 38 | # 39 | # DESIGN 40 | # - This script was designed similar to a Python application 41 | # with a Main() function. I wanted to keep it compact to 42 | # simplify "copy + paste" usage. 43 | # 44 | # - This script is invoked under two CMake contexts: 45 | # 1. Configure time (when build files are created). 46 | # 2. Build time (called via CMake -P). 47 | # The first invocation is what registers the script to 48 | # be executed at build time. 49 | # 50 | # MODIFICATIONS 51 | # You may wish to track other git properties like when the last 52 | # commit was made. There are two sections you need to modify, 53 | # and they're tagged with a ">>>" header. 54 | 55 | # Short hand for converting paths to absolute. 56 | macro(PATH_TO_ABSOLUTE var_name) 57 | get_filename_component(${var_name} "${${var_name}}" ABSOLUTE) 58 | endmacro() 59 | 60 | # Check that a required variable is set. 61 | macro(CHECK_REQUIRED_VARIABLE var_name) 62 | if(NOT DEFINED ${var_name}) 63 | message(FATAL_ERROR "The \"${var_name}\" variable must be defined.") 64 | endif() 65 | PATH_TO_ABSOLUTE(${var_name}) 66 | endmacro() 67 | 68 | # Check that an optional variable is set, or, set it to a default value. 69 | macro(CHECK_OPTIONAL_VARIABLE_NOPATH var_name default_value) 70 | if(NOT DEFINED ${var_name}) 71 | set(${var_name} ${default_value}) 72 | endif() 73 | endmacro() 74 | 75 | # Check that an optional variable is set, or, set it to a default value. 76 | # Also converts that path to an abspath. 77 | macro(CHECK_OPTIONAL_VARIABLE var_name default_value) 78 | CHECK_OPTIONAL_VARIABLE_NOPATH(${var_name} ${default_value}) 79 | PATH_TO_ABSOLUTE(${var_name}) 80 | endmacro() 81 | 82 | CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_FILE) 83 | CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_FILE) 84 | CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${CMAKE_CURRENT_BINARY_DIR}/git-state-hash") 85 | CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${CMAKE_SOURCE_DIR}") 86 | CHECK_OPTIONAL_VARIABLE_NOPATH(GIT_FAIL_IF_NONZERO_EXIT TRUE) 87 | CHECK_OPTIONAL_VARIABLE_NOPATH(GIT_IGNORE_UNTRACKED FALSE) 88 | 89 | # Check the optional git variable. 90 | # If it's not set, we'll try to find it using the CMake packaging system. 91 | if(NOT DEFINED GIT_EXECUTABLE) 92 | find_package(Git QUIET REQUIRED) 93 | endif() 94 | CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE) 95 | 96 | 97 | set(_state_variable_names 98 | GIT_RETRIEVED_STATE 99 | GIT_HEAD_SHA1 100 | GIT_IS_DIRTY 101 | GIT_AUTHOR_NAME 102 | GIT_AUTHOR_EMAIL 103 | GIT_COMMIT_DATE_ISO8601 104 | GIT_COMMIT_SUBJECT 105 | GIT_COMMIT_BODY 106 | GIT_DESCRIBE 107 | GIT_BRANCH 108 | # >>> 109 | # 1. Add the name of the additional git variable you're interested in monitoring 110 | # to this list. 111 | ) 112 | 113 | 114 | 115 | # Macro: RunGitCommand 116 | # Description: short-hand macro for calling a git function. Outputs are the 117 | # "exit_code" and "output" variables. The "_permit_git_failure" 118 | # variable can locally override the exit code checking- use it 119 | # with caution. 120 | macro(RunGitCommand) 121 | execute_process(COMMAND 122 | "${GIT_EXECUTABLE}" ${ARGV} 123 | WORKING_DIRECTORY "${_working_dir}" 124 | RESULT_VARIABLE exit_code 125 | OUTPUT_VARIABLE output 126 | ERROR_VARIABLE stderr 127 | OUTPUT_STRIP_TRAILING_WHITESPACE) 128 | if(NOT exit_code EQUAL 0 AND NOT _permit_git_failure) 129 | set(ENV{GIT_RETRIEVED_STATE} "false") 130 | 131 | # Issue 26: git info not properly set 132 | # 133 | # Check if we should fail if any of the exit codes are non-zero. 134 | # Most methods have a fall-back default value that's used in case of non-zero 135 | # exit codes. If you're feeling risky, disable this safety check and use 136 | # those default values. 137 | if(GIT_FAIL_IF_NONZERO_EXIT ) 138 | string(REPLACE ";" " " args_with_spaces "${ARGV}") 139 | message(FATAL_ERROR "${stderr} (${GIT_EXECUTABLE} ${args_with_spaces})") 140 | endif() 141 | endif() 142 | endmacro() 143 | 144 | 145 | 146 | # Function: GetGitState 147 | # Description: gets the current state of the git repo. 148 | # Args: 149 | # _working_dir (in) string; the directory from which git commands will be executed. 150 | function(GetGitState _working_dir) 151 | 152 | # This is an error code that'll be set to FALSE if the 153 | # RunGitCommand ever returns a non-zero exit code. 154 | set(ENV{GIT_RETRIEVED_STATE} "true") 155 | 156 | # Get whether or not the working tree is dirty. 157 | if (GIT_IGNORE_UNTRACKED) 158 | set(untracked_flag "-uno") 159 | else() 160 | set(untracked_flag "-unormal") 161 | endif() 162 | RunGitCommand(status --porcelain ${untracked_flag}) 163 | if(NOT exit_code EQUAL 0) 164 | set(ENV{GIT_IS_DIRTY} "false") 165 | else() 166 | if(NOT "${output}" STREQUAL "") 167 | set(ENV{GIT_IS_DIRTY} "true") 168 | else() 169 | set(ENV{GIT_IS_DIRTY} "false") 170 | endif() 171 | endif() 172 | 173 | # There's a long list of attributes grabbed from git show. 174 | set(object HEAD) 175 | RunGitCommand(show -s "--format=%H" ${object}) 176 | if(exit_code EQUAL 0) 177 | set(ENV{GIT_HEAD_SHA1} ${output}) 178 | endif() 179 | 180 | RunGitCommand(show -s "--format=%an" ${object}) 181 | if(exit_code EQUAL 0) 182 | set(ENV{GIT_AUTHOR_NAME} "${output}") 183 | endif() 184 | 185 | RunGitCommand(show -s "--format=%ae" ${object}) 186 | if(exit_code EQUAL 0) 187 | set(ENV{GIT_AUTHOR_EMAIL} "${output}") 188 | endif() 189 | 190 | RunGitCommand(show -s "--format=%ci" ${object}) 191 | if(exit_code EQUAL 0) 192 | set(ENV{GIT_COMMIT_DATE_ISO8601} "${output}") 193 | endif() 194 | 195 | RunGitCommand(show -s "--format=%s" ${object}) 196 | if(exit_code EQUAL 0) 197 | # Escape \ 198 | string(REPLACE "\\" "\\\\" output "${output}") 199 | # Escape quotes 200 | string(REPLACE "\"" "\\\"" output "${output}") 201 | set(ENV{GIT_COMMIT_SUBJECT} "${output}") 202 | endif() 203 | 204 | RunGitCommand(show -s "--format=%b" ${object}) 205 | if(exit_code EQUAL 0) 206 | if(output) 207 | # Escape \ 208 | string(REPLACE "\\" "\\\\" output "${output}") 209 | # Escape quotes 210 | string(REPLACE "\"" "\\\"" output "${output}") 211 | # Escape line breaks in the commit message. 212 | string(REPLACE "\r\n" "\\r\\n\\\r\n" safe "${output}") 213 | if(safe STREQUAL output) 214 | # Didn't have windows lines - try unix lines. 215 | string(REPLACE "\n" "\\n\\\n" safe "${output}") 216 | endif() 217 | else() 218 | # There was no commit body - set the safe string to empty. 219 | set(safe "") 220 | endif() 221 | set(ENV{GIT_COMMIT_BODY} "${safe}") 222 | else() 223 | set(ENV{GIT_COMMIT_BODY} "") # empty string. 224 | endif() 225 | 226 | # Get output of git describe 227 | RunGitCommand(describe --always ${object}) 228 | if(NOT exit_code EQUAL 0) 229 | set(ENV{GIT_DESCRIBE} "unknown") 230 | else() 231 | set(ENV{GIT_DESCRIBE} "${output}") 232 | endif() 233 | 234 | # Convert HEAD to a symbolic ref. This can fail, in which case we just 235 | # set that variable to HEAD. 236 | set(_permit_git_failure ON) 237 | RunGitCommand(symbolic-ref --short -q ${object}) 238 | unset(_permit_git_failure) 239 | if(NOT exit_code EQUAL 0) 240 | set(ENV{GIT_BRANCH} "${object}") 241 | else() 242 | set(ENV{GIT_BRANCH} "${output}") 243 | endif() 244 | 245 | # >>> 246 | # 2. Additional git properties can be added here via the 247 | # "execute_process()" command. Be sure to set them in 248 | # the environment using the same variable name you added 249 | # to the "_state_variable_names" list. 250 | 251 | endfunction() 252 | 253 | 254 | 255 | # Function: GitStateChangedAction 256 | # Description: this function is executed when the state of the git 257 | # repository changes (e.g. a commit is made). 258 | function(GitStateChangedAction) 259 | foreach(var_name ${_state_variable_names}) 260 | set(${var_name} $ENV{${var_name}}) 261 | endforeach() 262 | configure_file("${PRE_CONFIGURE_FILE}" "${POST_CONFIGURE_FILE}" @ONLY) 263 | endfunction() 264 | 265 | 266 | 267 | # Function: HashGitState 268 | # Description: loop through the git state variables and compute a unique hash. 269 | # Args: 270 | # _state (out) string; a hash computed from the current git state. 271 | function(HashGitState _state) 272 | set(ans "") 273 | foreach(var_name ${_state_variable_names}) 274 | string(SHA256 ans "${ans}$ENV{${var_name}}") 275 | endforeach() 276 | set(${_state} ${ans} PARENT_SCOPE) 277 | endfunction() 278 | 279 | 280 | 281 | # Function: CheckGit 282 | # Description: check if the git repo has changed. If so, update the state file. 283 | # Args: 284 | # _working_dir (in) string; the directory from which git commands will be ran. 285 | # _state_changed (out) bool; whether or no the state of the repo has changed. 286 | function(CheckGit _working_dir _state_changed) 287 | 288 | # Get the current state of the repo. 289 | GetGitState("${_working_dir}") 290 | 291 | # Convert that state into a hash that we can compare against 292 | # the hash stored on-disk. 293 | HashGitState(state) 294 | 295 | # Issue 14: post-configure file isn't being regenerated. 296 | # 297 | # Update the state to include the SHA256 for the pre-configure file. 298 | # This forces the post-configure file to be regenerated if the 299 | # pre-configure file has changed. 300 | file(SHA256 ${PRE_CONFIGURE_FILE} preconfig_hash) 301 | string(SHA256 state "${preconfig_hash}${state}") 302 | 303 | # Check if the state has changed compared to the backup on disk. 304 | if(EXISTS "${GIT_STATE_FILE}") 305 | file(READ "${GIT_STATE_FILE}" OLD_HEAD_CONTENTS) 306 | if(OLD_HEAD_CONTENTS STREQUAL "${state}") 307 | # State didn't change. 308 | set(${_state_changed} "false" PARENT_SCOPE) 309 | return() 310 | endif() 311 | endif() 312 | 313 | # The state has changed. 314 | # We need to update the state file on disk. 315 | # Future builds will compare their state to this file. 316 | file(WRITE "${GIT_STATE_FILE}" "${state}") 317 | set(${_state_changed} "true" PARENT_SCOPE) 318 | endfunction() 319 | 320 | 321 | 322 | # Function: SetupGitMonitoring 323 | # Description: this function sets up custom commands that make the build system 324 | # check the state of git before every build. If the state has 325 | # changed, then a file is configured. 326 | function(SetupGitMonitoring) 327 | add_custom_target(check_git 328 | ALL 329 | DEPENDS ${PRE_CONFIGURE_FILE} 330 | BYPRODUCTS 331 | ${POST_CONFIGURE_FILE} 332 | ${GIT_STATE_FILE} 333 | COMMENT "Checking the git repository for changes..." 334 | COMMAND 335 | ${CMAKE_COMMAND} 336 | -D_BUILD_TIME_CHECK_GIT=TRUE 337 | -DGIT_WORKING_DIR=${GIT_WORKING_DIR} 338 | -DGIT_EXECUTABLE=${GIT_EXECUTABLE} 339 | -DGIT_STATE_FILE=${GIT_STATE_FILE} 340 | -DPRE_CONFIGURE_FILE=${PRE_CONFIGURE_FILE} 341 | -DPOST_CONFIGURE_FILE=${POST_CONFIGURE_FILE} 342 | -DGIT_FAIL_IF_NONZERO_EXIT=${GIT_FAIL_IF_NONZERO_EXIT} 343 | -DGIT_IGNORE_UNTRACKED=${GIT_IGNORE_UNTRACKED} 344 | -P "${CMAKE_CURRENT_LIST_FILE}") 345 | endfunction() 346 | 347 | 348 | 349 | # Function: Main 350 | # Description: primary entry-point to the script. Functions are selected based 351 | # on whether it's configure or build time. 352 | function(Main) 353 | if(_BUILD_TIME_CHECK_GIT) 354 | # Check if the repo has changed. 355 | # If so, run the change action. 356 | CheckGit("${GIT_WORKING_DIR}" changed) 357 | if(changed OR NOT EXISTS "${POST_CONFIGURE_FILE}") 358 | GitStateChangedAction() 359 | endif() 360 | else() 361 | # >> Executes at configure time. 362 | SetupGitMonitoring() 363 | endif() 364 | endfunction() 365 | 366 | # And off we go... 367 | Main() 368 | -------------------------------------------------------------------------------- /tests/assert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # assert.sh 3 | 4 | # Pulled from the "Advanced Bash-Scripting Guide". 5 | # http://tldp.org/LDP/abs/html/debugging.html#ASSERT 6 | 7 | ####################################################################### 8 | assert () # If condition false, 9 | { #+ exit from script 10 | #+ with appropriate error message. 11 | E_PARAM_ERR=98 12 | E_ASSERT_FAILED=99 13 | 14 | if [ -z "$2" ] # Not enough parameters passed 15 | then #+ to assert() function. 16 | return $E_PARAM_ERR # No damage done. 17 | fi 18 | 19 | lineno=$2 20 | 21 | if [ ! $1 ] 22 | then 23 | echo "Assertion failed: \"$1\"" 24 | echo "File \"$0\", line $lineno" # Give name of file and line number. 25 | exit $E_ASSERT_FAILED 26 | # else 27 | # return 28 | # and continue executing the script. 29 | fi 30 | } # Insert a similar assert() function into a script you need to debug. 31 | ####################################################################### 32 | -------------------------------------------------------------------------------- /tests/hello-world/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | project(hello-world) 3 | 4 | set(VERSION_TRACKING_MODULE_PATH "" CACHE STRING "The location of the cmake-git-version-tracking repository") 5 | 6 | include(FetchContent) 7 | set(FETCHCONTENT_QUIET OFF) 8 | FetchContent_Declare(cmake_git_version_tracking 9 | URL ${VERSION_TRACKING_MODULE_PATH} 10 | ) 11 | FetchContent_MakeAvailable(cmake_git_version_tracking) 12 | 13 | # Setup a demo executable that links to the git library. 14 | add_executable(demo main.cc) 15 | target_link_libraries(demo cmake_git_version_tracking) 16 | -------------------------------------------------------------------------------- /tests/hello-world/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "git.h" 5 | 6 | int main() { 7 | if(git::IsPopulated()) { 8 | if(git::AnyUncommittedChanges()) { 9 | std::cerr << "WARN: there were uncommitted changes at build-time." << std::endl; 10 | } 11 | std::cout << "commit " << git::CommitSHA1() << " (" << git::Branch() << ")\n" 12 | << "describe " << git::Describe() << "\n" 13 | << "Author: " << git::AuthorName() << " <" << git::AuthorEmail() << ">\n" 14 | << "Date: " << git::CommitDate() << "\n\n" 15 | << git::CommitSubject() << "\n" << git::CommitBody() << std::endl; 16 | return EXIT_SUCCESS; 17 | } 18 | else { 19 | std::cerr << "WARN: failed to get the current git state. Is this a git repo?" << std::endl; 20 | return EXIT_FAILURE; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/interfaces/c99/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(TestCInterface 3 | LANGUAGES C) 4 | set(VERSION_TRACKING_MODULE_PATH "" CACHE STRING "The location of the cmake-git-version-tracking repository") 5 | 6 | include(FetchContent) 7 | FetchContent_Declare(cmake_git_version_tracking 8 | URL ${VERSION_TRACKING_MODULE_PATH} 9 | ) 10 | FetchContent_MakeAvailable(cmake_git_version_tracking) 11 | 12 | add_executable(demo main.c) 13 | set_property(TARGET demo PROPERTY C_STANDARD 99) 14 | target_link_libraries(demo cmake_git_version_tracking) 15 | -------------------------------------------------------------------------------- /tests/interfaces/c99/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | printf("%s\n", git_CommitSHA1()); 6 | return 0; 7 | } -------------------------------------------------------------------------------- /tests/interfaces/cxx98_cxx17/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(TestCXXInterface 3 | LANGUAGES C CXX) 4 | set(CXX_STANDARD 98 CACHE STRING "Which CXX standard to compile against.") 5 | set(VERSION_TRACKING_MODULE_PATH "" CACHE STRING "The location of the cmake-git-version-tracking repository") 6 | 7 | include(FetchContent) 8 | FetchContent_Declare(cmake_git_version_tracking 9 | URL ${VERSION_TRACKING_MODULE_PATH} 10 | ) 11 | FetchContent_MakeAvailable(cmake_git_version_tracking) 12 | 13 | add_executable(demo main.cc) 14 | set_property(TARGET demo PROPERTY CXX_STANDARD ${CXX_STANDARD}) 15 | target_link_libraries(demo cmake_git_version_tracking) 16 | -------------------------------------------------------------------------------- /tests/interfaces/cxx98_cxx17/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main() { 5 | std::cout << git::CommitSHA1() << std::endl; 6 | return 0; 7 | } -------------------------------------------------------------------------------- /tests/run_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Super simple - just run every script that starts with test_*. 4 | # Print exit and success depending on the script exit code. 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | for file in $DIR/test_*; do 7 | $file 8 | if [ $? -ne 0 ]; then 9 | echo "TEST: failed $file" 10 | exit 1 11 | else 12 | echo "TEST: success $file" 13 | fi 14 | echo 15 | done 16 | echo "All tests passed." 17 | exit 0 18 | -------------------------------------------------------------------------------- /tests/test_branch_name.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a new commit is made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am "Initial commit." 16 | git checkout -b true-branch-name 17 | 18 | # Build the project 19 | set -e 20 | cd $build 21 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module -DGIT_FAIL_IF_NONZERO_EXIT=FALSE 22 | cmake --build . --target demo 23 | 24 | # Run the demo. 25 | # It should report EXIT_SUCCESS because git history was found. 26 | set +e 27 | ./demo &> output.txt 28 | assert "$? -eq 0" $LINENO 29 | 30 | # Check that we picked up the true branch name. 31 | set -e 32 | truth=true-branch-name 33 | if ! grep -q $truth output.txt; then 34 | echo "Demo didn't pickup the branch name" 35 | assert "1 -eq 0" $LINENO 36 | fi 37 | 38 | # Got to a detached head state. 39 | cd $src 40 | touch thing.txt 41 | git add thing.txt 42 | git commit -am "Second commit" 43 | git checkout HEAD~1 44 | 45 | # Verify that we show HEAD when there isn't a symbolic ref. 46 | cd $build 47 | cmake --build . --target demo 48 | ./demo &> output.txt 49 | if ! grep -q HEAD output.txt; then 50 | echo "We didn't output HEAD as the branch name." 51 | assert "1 -eq 0" $LINENO 52 | fi -------------------------------------------------------------------------------- /tests/test_commit_attributes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test that commit attributes are being tracked. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit --author="Author Name " -am "Initial commit." -m "Commit body." 16 | 17 | # Build the project 18 | set -e 19 | cd $build 20 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 21 | cmake --build . --target demo 22 | 23 | # Run the demo. 24 | # It should report EXIT_SUCCESS because git history was found. 25 | set +e 26 | ./demo &> output.txt 27 | assert "$? -eq 0" $LINENO 28 | 29 | set -e 30 | # Check name. 31 | if ! grep -q "Author Name" output.txt; then 32 | echo "Missing author name" 33 | assert "1 -eq 0" $LINENO 34 | fi 35 | # Check address. 36 | if ! grep -q "author@address.com" output.txt; then 37 | echo "Missing author email address" 38 | assert "1 -eq 0" $LINENO 39 | fi 40 | # Check subject. 41 | if ! grep -q "Initial commit." output.txt; then 42 | echo "Missing commit subject line" 43 | assert "1 -eq 0" $LINENO 44 | fi 45 | # Check body. 46 | if ! grep -q "Commit body." output.txt; then 47 | echo "Missing commit body" 48 | assert "1 -eq 0" $LINENO 49 | fi -------------------------------------------------------------------------------- /tests/test_dirty_head.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when uncommitted changes are made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | touch README.md 15 | git add . 16 | git commit -am "Initial commit." 17 | 18 | # Build the project 19 | set -e 20 | cd $build 21 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 22 | cmake --build . --target demo 23 | 24 | # Run the demo. 25 | # It should report EXIT_SUCCESS because git history was found. 26 | set +e 27 | ./demo &> output.txt 28 | assert "$? -eq 0" $LINENO 29 | 30 | # Check that the head isn't dirty. 31 | set -e 32 | if grep -q "uncommitted" output.txt; then 33 | echo "Demo reported a dirty head." 34 | assert "1 -eq 0" $LINENO 35 | fi 36 | 37 | # Modify a file then rebuild. 38 | cd $src 39 | echo "this is a cool edit" >> README.md 40 | cd $build 41 | cmake --build . --target demo 42 | 43 | # Run the demo. 44 | set +e 45 | ./demo &> output.txt 46 | assert "$? -eq 0" $LINENO 47 | 48 | # Check that demo reported that there were uncommitted changes. 49 | set -e 50 | if ! grep -q "uncommitted" output.txt; then 51 | echo "Demo didn't report a dirty head." 52 | assert "1 -eq 0" $LINENO 53 | fi 54 | -------------------------------------------------------------------------------- /tests/test_dirty_head_untracked.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test if the demo detects DIRTY in the presence of 5 | # untracked files. (PR #29) 6 | 7 | # Load utilities. 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | source $DIR/util.sh 10 | 11 | # Create git history. 12 | # Note that there's an untracked file. 13 | set -e 14 | cd $src 15 | git init 16 | git add . 17 | git commit -am "Initial commit." 18 | echo "This is an untracked file!" >> untracked.txt 19 | 20 | # Build the project 21 | set -e 22 | cd $build 23 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 24 | cmake --build . --target demo 25 | 26 | # Run the demo. 27 | # It should report EXIT_SUCCESS because git history was found. 28 | set +e 29 | ./demo &> output.txt 30 | assert "$? -eq 0" $LINENO 31 | 32 | # Check that the head is dirty (we have an untracked file). 33 | set -e 34 | if ! grep -q "uncommitted" output.txt; then 35 | echo "Demo didn't reported a dirty head." 36 | assert "1 -eq 0" $LINENO 37 | fi 38 | 39 | # Regenerate the build system but this time ignore 40 | # untracked changes. 41 | cmake -G "$TEST_GENERATOR" $src -DGIT_IGNORE_UNTRACKED=TRUE 42 | cmake --build . --target demo 43 | 44 | # Run the demo. 45 | set +e 46 | ./demo &> output.txt 47 | assert "$? -eq 0" $LINENO 48 | 49 | # Check that demo didn't report uncommitted changes- we're ignoring 50 | # the one untracked file. 51 | set -e 52 | if grep -q "uncommitted" output.txt; then 53 | echo "Demo reported a dirty head, but we should have ignored untracked changes." 54 | assert "1 -eq 0" $LINENO 55 | fi 56 | -------------------------------------------------------------------------------- /tests/test_git_created_post_configure.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when the .git directory is created 5 | # after the project has been configure. 6 | 7 | # Load utilities. 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | source $DIR/util.sh 10 | 11 | # Configure and build the project. 12 | # Should fail because the git commands can't run. 13 | set -e 14 | cd build 15 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 16 | set +e 17 | cmake --build . --target demo 18 | assert "$? -ne 0" $LINENO 19 | set -e 20 | 21 | # Regenerate, but this time allow git commands to fail. 22 | # The build should pass. 23 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module -DGIT_FAIL_IF_NONZERO_EXIT=FALSE 24 | cmake --build . --target demo 25 | assert "$? -eq 0" $LINENO 26 | 27 | # Run the demo. 28 | # It should report EXIT_FAILURE because no git history was found. 29 | set +e 30 | ./demo 31 | assert "$? -eq 1" $LINENO 32 | 33 | # Create git history 34 | set -e 35 | cd $src 36 | git init 37 | git add . 38 | git commit -am "Initial commit." 39 | 40 | # Build again. 41 | cd $build 42 | cmake --build . --target demo 43 | 44 | # Run the demo. 45 | # It should report EXIT_SUCCESS because git history was found. 46 | set +e 47 | ./demo 48 | assert "$? -eq 0" $LINENO 49 | -------------------------------------------------------------------------------- /tests/test_git_describe_with_tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test that git --describe reports a tag. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am "Initial commit." 16 | echo "this is a new file" > some_file 17 | git add . 18 | git commit -am "Second commit." 19 | git tag -a v1.2 -m "version 1.2" 20 | echo "another file after the tag" > some_file_two 21 | git add . 22 | git commit -am "Third commit." 23 | 24 | 25 | # Build the project 26 | set -e 27 | cd $build 28 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 29 | cmake --build . --target demo 30 | 31 | # Run the demo. 32 | # It should report EXIT_SUCCESS because git history was found. 33 | set +e 34 | ./demo &> output.txt 35 | assert "$? -eq 0" $LINENO 36 | 37 | # Check that the tag was printed 38 | set -e 39 | if ! grep -q "v1.2" output.txt; then 40 | echo "Missing the version from describe." 41 | assert "1 -eq 0" $LINENO 42 | fi 43 | -------------------------------------------------------------------------------- /tests/test_interfaces.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a new commit is made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Clean up the default unit test setup. 11 | # We'll be testing the interface projects instead. 12 | rm -rf $src 13 | cp -r $interface_src $src 14 | 15 | # Create git history 16 | set -e 17 | cd $src 18 | git init 19 | touch README.md 20 | git add . 21 | git commit -am "Initial commit." 22 | 23 | function check_interface() { 24 | 25 | configure_flags=$1 26 | source_dir=$2 27 | 28 | # Build the project 29 | set -e 30 | cd $build 31 | rm -rf ./* 32 | cmake -G "$TEST_GENERATOR" $source_dir $version_tracking_module $configure_flags 33 | cmake --build . --target demo 34 | 35 | # Run the demo. 36 | # It should report EXIT_SUCCESS because git history was found. 37 | set +e 38 | ./demo &> output.txt 39 | assert "$? -eq 0" $LINENO 40 | 41 | # Check that the git commit matches what git reports. 42 | set -e 43 | truth=$(cd $src && git rev-parse --verify HEAD) 44 | if ! grep -q $truth output.txt; then 45 | # Commit ID wasn't found. 46 | echo "Demo didn't print the correct commit." 47 | assert "1 -eq 0" $LINENO 48 | fi 49 | } 50 | 51 | check_interface "" $src/cxx98_cxx17 52 | check_interface "-DCXX_STANDARD=17" $src/cxx98_cxx17 53 | check_interface "" $src/c99 54 | -------------------------------------------------------------------------------- /tests/test_make_clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Issue #9: 4 | # Verify that the header is regenerated after a make clean. 5 | # https://github.com/andrew-hardin/cmake-git-version-tracking/issues/9 6 | # 7 | # For additional context, see this issue: 8 | # https://gitlab.kitware.com/cmake/cmake/issues/18300 9 | # 10 | # Depending on which version of CMake you're using, the makefile 11 | # generator may or may-not remove byproducts of custom targets. 12 | 13 | # Load utilities. 14 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 15 | source $DIR/util.sh 16 | 17 | # Don't run the test if we're pre-3.13. 18 | version=$(cmake --version | head -n 1) 19 | version=${version##* } 20 | major=${version%%.*} 21 | patch=${version##*.} 22 | minor=${version##$major.} 23 | minor=${minor%%.$patch} 24 | if [[ $major -lt 3 || ( $major -eq 3 && $minor -lt 13 ) ]]; then 25 | echo "CMake $version doesn't remove makefile byproducts- skipping test" 26 | exit 0 27 | fi 28 | 29 | # Create git history 30 | set -e 31 | cd $src 32 | git init 33 | git add . 34 | git commit -am "Initial commit." 35 | 36 | # Build the project 37 | set -e 38 | cd $build 39 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 40 | cmake --build . --target demo 41 | 42 | # The configured source file should exist. 43 | # The git-state file should exist. 44 | configured_src=./_deps/cmake_git_version_tracking-build/git.c 45 | git_state=./_deps/cmake_git_version_tracking-build/git-state-hash 46 | assert "-f $configured_src" $LINENO 47 | assert "-f $git_state" $LINENO 48 | 49 | # Make followed by clean should scrub both these files. 50 | cmake --build . 51 | cmake --build . --target clean 52 | assert "! -f $configured_src" $LINENO 53 | assert "! -f $git_state" $LINENO 54 | 55 | # We should generate them again after calling make. 56 | cmake --build . 57 | assert "-f $configured_src" $LINENO 58 | assert "-f $git_state" $LINENO 59 | -------------------------------------------------------------------------------- /tests/test_modified_preconfig_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test that items are regenerated after modifying the pre-configure 5 | # file. See issue 14. 6 | 7 | # Load utilities. 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | source $DIR/util.sh 10 | 11 | # Create git history 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am "Initial commit." 16 | 17 | # Make the state dirty, so that when we modify the 18 | # preconfigure file it doesn't regenerate just 19 | # because the git-state changed. 20 | echo "hello world" > dirty 21 | 22 | # Configure and build the project. 23 | set -e 24 | cd $build 25 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 26 | cmake --build . --target demo 27 | 28 | # Record the date on the post-configure file, then 29 | # modify the pre-configure file. 30 | file_to_check=./_deps/cmake_git_version_tracking-build/git.c 31 | file_to_modify=./_deps/cmake_git_version_tracking-src/git.c.in 32 | before=$(stat -c %y $file_to_check) 33 | echo "// this is a modification" >> "$file_to_modify" 34 | 35 | # Make the project again. Verify that it regenerated 36 | # our git file. 37 | cmake --build . --target demo 38 | after=$(stat -c %y $file_to_check) 39 | 40 | # Modified stamps need to be different. 41 | if [ "$before" == "$after" ]; then 42 | assert "1 -eq 0" $LINENO 43 | fi 44 | -------------------------------------------------------------------------------- /tests/test_multiline_messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a commit is made with \ and newlines. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am 'Oh no!\ 16 | 17 | This spans multiple lines. 18 | Can our script handle it? 19 | 20 | There should be a skipped above and after this line. 21 | 22 | This also includes a bad \ character at the very end. 23 | 24 | The End!\' 25 | git log 26 | 27 | # Build the project 28 | set -e 29 | cd $build 30 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 31 | cmake --build . --target demo 32 | 33 | # Run the demo. 34 | # It should report EXIT_SUCCESS because git history was found. 35 | set +e 36 | ./demo &> output.txt 37 | assert "$? -eq 0" $LINENO 38 | 39 | if ! grep -q 'The End!\\' output.txt; then 40 | echo "Dropped the last line" 41 | assert "1 -eq 0" $LINENO 42 | fi 43 | 44 | -------------------------------------------------------------------------------- /tests/test_new_commits.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a new commit is made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | touch README.md 15 | git add . 16 | git commit -am "Initial commit." 17 | 18 | # Build the project 19 | set -e 20 | cd $build 21 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 22 | cmake --build . --target demo 23 | 24 | # Run the demo. 25 | # It should report EXIT_SUCCESS because git history was found. 26 | set +e 27 | ./demo &> output.txt 28 | assert "$? -eq 0" $LINENO 29 | 30 | # Check that the git commit matches what git reports. 31 | set -e 32 | truth=$(cd $src && git rev-parse --verify HEAD) 33 | if ! grep -q $truth output.txt; then 34 | # Commit ID wasn't found. 35 | echo "Demo didn't print the correct commit." 36 | assert "1 -eq 0" $LINENO 37 | fi 38 | 39 | # Modify a file and commit the changes. 40 | # Then rebuild. 41 | cd $src 42 | echo "this is a cool edit" >> README.md 43 | git commit -am "A new commit with a new ID." 44 | cd $build 45 | cmake --build . --target demo 46 | 47 | # Repeat the prior two tests by running the demo and checking 48 | # the commit ID. 49 | set +e 50 | ./demo &> output.txt 51 | assert "$? -eq 0" $LINENO 52 | 53 | # Check that the git commit matches what git reports. 54 | set -e 55 | truth_2=$(cd $src && git rev-parse --verify HEAD) 56 | assert "! "$truth" = "$truth_2"" $LINENO 57 | if ! grep -q $truth_2 output.txt; then 58 | # Commit ID wasn't found. 59 | echo "Demo didn't print the correct commit." 60 | assert "1 -eq 0" $LINENO 61 | fi 62 | -------------------------------------------------------------------------------- /tests/test_no_change_means_no_rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Confirm that the project doesn't update the git-state file when 5 | # no changes have been made. 6 | 7 | # Load utilities. 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | source $DIR/util.sh 10 | 11 | # Create git history 12 | set -e 13 | cd $src 14 | git init 15 | git add . 16 | git commit -am "Initial commit." 17 | 18 | # Build the project 19 | set -e 20 | cd $build 21 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 22 | cmake --build . --target demo 23 | 24 | # Run the demo. 25 | ./demo &> output.txt 26 | 27 | # Check that the git commit matches what git reports. 28 | truth=$(cd $src && git rev-parse --verify HEAD) 29 | if ! grep -q $truth output.txt; then 30 | # Commit ID wasn't found. 31 | echo "Demo didn't print the correct commit." 32 | assert "1 -eq 0" $LINENO 33 | fi 34 | 35 | # Watch the git-state file and make sure it doesn't change 36 | # when we try to rebuild the project. 37 | state_file=$build/_deps/cmake_git_version_tracking-build/git-state-hash 38 | demo_file="$build/demo" 39 | last_touched_state="$(stat -c %y $state_file)" 40 | last_touched_exe="$(stat -c %y $demo_file)" 41 | cmake --build . --target demo 42 | new_touched_state="$(stat -c %y $state_file)" 43 | new_touched_exe="$(stat -c %y $demo_file)" 44 | 45 | if [ "$last_touched_state" != "$new_touched_state" ]; then 46 | assert "0 -eq 1" $LINENO 47 | fi 48 | if [ "$last_touched_exe" != "$new_touched_exe" ]; then 49 | assert "0 -eq 1" $LINENO 50 | fi 51 | -------------------------------------------------------------------------------- /tests/test_no_git_history.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env bash 3 | # 4 | # Purpose: 5 | # Test how the demo performs when no .git directory was found. 6 | 7 | # Load utilities. 8 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 9 | source $DIR/util.sh 10 | 11 | # Configure and build the project. 12 | # The build should fail because the git repo is missing. 13 | set -e 14 | cd build 15 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 16 | set +e 17 | cmake --build . --target demo 18 | assert "$? -ne 0" $LINENO 19 | -------------------------------------------------------------------------------- /tests/test_quote_messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a new commit is made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am 'I said "hello"! 16 | 17 | This spans multiple lines. 18 | 19 | I say "goodbye"!' 20 | git log 21 | 22 | # Build the project 23 | set -e 24 | cd $build 25 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 26 | cmake --build . --target demo 27 | 28 | # Run the demo. 29 | # It should report EXIT_SUCCESS because git history was found. 30 | set +e 31 | ./demo &> output.txt 32 | assert "$? -eq 0" $LINENO 33 | 34 | if ! grep -q 'I said "hello"!' output.txt; then 35 | echo "Did not properly escape quote in subject" 36 | assert "1 -eq 0" $LINENO 37 | fi 38 | 39 | if ! grep -q 'I say "goodbye"!' output.txt; then 40 | echo "Did not properly escape quote in body" 41 | assert "1 -eq 0" $LINENO 42 | fi 43 | -------------------------------------------------------------------------------- /tests/test_semicolon_multiline_commit_message.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Purpose: 4 | # Test how the demo performs when a new commit is made. 5 | 6 | # Load utilities. 7 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 8 | source $DIR/util.sh 9 | 10 | # Create git history 11 | set -e 12 | cd $src 13 | git init 14 | git add . 15 | git commit -am 'Oh no! 16 | 17 | This spans multiple lines; with a semicolon. 18 | Can our script handle it? 19 | 20 | There should be a skipped above and after this line 21 | 22 | The End!' 23 | git log 24 | 25 | # Build the project 26 | set -e 27 | cd $build 28 | cmake -G "$TEST_GENERATOR" $src $version_tracking_module 29 | cmake --build . --target demo 30 | 31 | # Run the demo. 32 | # It should report EXIT_SUCCESS because git history was found. 33 | set +e 34 | ./demo &> output.txt 35 | assert "$? -eq 0" $LINENO 36 | 37 | if ! grep -q "The End!" output.txt; then 38 | echo "Dropped the last line" 39 | assert "1 -eq 0" $LINENO 40 | fi 41 | 42 | -------------------------------------------------------------------------------- /tests/util.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Purpose: load scripts and variables that are common between tests. 3 | set -e 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | source $DIR/assert.sh 7 | 8 | # Making a temporary directory is tricky depending on the platform. 9 | # Solution pulled from here: 10 | # https://unix.stackexchange.com/questions/30091/fix-or-alternative-for-mktemp-in-os-x 11 | create_temp_directory=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` 12 | demo_src=$DIR/hello-world 13 | interface_src=$DIR/interfaces 14 | version_tracking_module="-DVERSION_TRACKING_MODULE_PATH=$DIR/.." 15 | git_watcher=$DIR/../git_watcher.cmake 16 | 17 | # Create a root directory and copy the demo code to there. 18 | root=$create_temp_directory 19 | src=$root/source 20 | build=$root/build 21 | mkdir -p $build 22 | cp -r $demo_src $src 23 | cp $git_watcher $src/.. 24 | 25 | # Make sure we don't carry along a preconfigured git.h header. 26 | if [ -f src/git.h ]; then 27 | rm $src/git.h 28 | fi 29 | 30 | # Set the default generator if it hasn't been set already. 31 | if [ ! -n "$TEST_GENERATOR" ]; then 32 | TEST_GENERATOR="Ninja" 33 | fi 34 | 35 | function cleanup () { 36 | if [ -d "$root" ]; then 37 | echo "TEST: removing test root directory..." 38 | rm -rf "$root" 39 | fi 40 | } 41 | trap cleanup EXIT 42 | 43 | echo "=========================" 44 | echo "Running test in \"$root\"" 45 | echo "-------------------------" 46 | cd $root --------------------------------------------------------------------------------