# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

# Returns the git version.
function(qt_ir_get_git_version out_var)
    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)

    set(extra_args "")
    if(perl_identical_output_for_tests)
        set(extra_args FORCE_QUIET)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git version ${extra_args}
        OUT_OUTPUT_VAR git_output
        ERROR_MESSAGE "Failed to retrieve git version")

    string(REGEX REPLACE "^git version ([0-9]+)\\.([0-9]+)\\.([0-9]+).*$" "\\1.\\2.\\3"
        version "${git_output}")
    if(NOT version)
        message(FATAL_ERROR "Failed to parse git version: ${git_output}, expected [d]+.[d]+.[d]+")
    endif()

    set(${out_var} "${version}" PARENT_SCOPE)
endfunction()

# Returns the git version, but caches the result in a global property.
function(qt_ir_get_git_version_cached out_var)
    get_property(version GLOBAL PROPERTY _qt_git_version)
    if(NOT version)
        qt_ir_get_git_version(version)
    endif()

    set_property(GLOBAL PROPERTY _qt_git_version "${version}")

    set(${out_var} "${version}" PARENT_SCOPE)
endfunction()

# Returns whether git supports the git submodule --progress option.
function(qt_ir_is_git_progress_supported out_var)
    qt_ir_get_git_version_cached(version)
    if(version VERSION_GREATER_EQUAL "2.11")
        set(${out_var} TRUE PARENT_SCOPE)
    else()
        set(${out_var} FALSE PARENT_SCOPE)
    endif()
endfunction()

# Get the mirror with trailing slashes removed.
function(qt_ir_get_mirror out_var)
    qt_ir_get_option_value(mirror mirror)
    qt_ir_get_option_value(berlin berlin)
    qt_ir_get_option_value(oslo oslo)

    if(berlin)
        set(mirror "git://hegel/qt/")
    elseif(oslo)
        set(mirror "git://qilin/qt/")
    endif()

    # Replace any double trailing slashes from end of mirror
    string(REGEX REPLACE "//+$" "/" mirror "${mirror}")

    set(${out_var} "${mirror}" PARENT_SCOPE)
endfunction()

# Sets up the commit template for a submodule.
function(qt_ir_setup_commit_template commit_template_dir working_directory)
    set(template "${commit_template_dir}/.commit-template")
    if(NOT EXISTS "${template}")
        return()
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git config commit.template "${template}"
        ERROR_MESSAGE "Failed to setup commit template"
        WORKING_DIRECTORY "${working_directory}")
endfunction()

# Initializes a list of submodules. This does not them, but just
# sets up the .git/config file submodule.$submodule_name.url based on the .gitmodules template file.
function(qt_ir_run_git_submodule_init submodules working_directory)
    set(submodule_dirs "")
    foreach(submodule_name IN LISTS submodules)
        set(submodule_path "${${prefix}_${submodule_name}_path}")
        list(APPEND submodule_dirs "${submodule_name}")
    endforeach()
    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git submodule init ${submodule_dirs}
        ERROR_MESSAGE "Failed to git submodule init ${submodule_dirs}"
        WORKING_DIRECTORY "${working_directory}")

    qt_ir_setup_commit_template("${working_directory}" "${working_directory}")
endfunction()

# Add gerrit remotes to the repository located in the working_directory.
# repo_relative_url is the relative URL of the repository.
# Examples:
# - qt5
# - qttools.git
# - ../playground/qlitehtml.git
# - ../qt/qttools-litehtml.git
function(qt_ir_add_git_remotes repo_relative_url working_directory)
    set(gerrit_ssh_base "ssh://@USER@codereview.qt-project.org@PORT@/")
    set(gerrit_repo_url "${gerrit_ssh_base}")

    qt_ir_get_option_value(codereview-username username)

    # If given a username, make a "verbose" remote.
    # Otherwise, rely on proper SSH configuration.
    if(username)
        string(REPLACE "@USER@" "${username}@" gerrit_repo_url "${gerrit_repo_url}")
        string(REPLACE "@PORT@" ":29418" gerrit_repo_url "${gerrit_repo_url}")
    else()
        string(REPLACE "@USER@" "" gerrit_repo_url "${gerrit_repo_url}")
        string(REPLACE "@PORT@" "" gerrit_repo_url "${gerrit_repo_url}")
    endif()

    set(namespace "qt")
    set(repo_relative_url_with_namespace "${namespace}/${repo_relative_url}")
    qt_ir_normalize_git_url("${repo_relative_url_with_namespace}" normalized_url)
    string(APPEND gerrit_repo_url "${normalized_url}")

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git config remote.gerrit.url "${gerrit_repo_url}"
        ERROR_MESSAGE "Failed to set gerrit repo url"
        WORKING_DIRECTORY "${working_directory}")

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS
            git config remote.gerrit.fetch "+refs/heads/*:refs/remotes/gerrit/*" "/heads/"
        ERROR_MESSAGE "Failed to set gerrit repo fetch refspec"
        WORKING_DIRECTORY "${working_directory}")
endfunction()

# Handles the copy-objects option, which is used to detach alternates.
# A copy of all git objects are made from the alternate repository to the current repository.
# Then the alternates reference is removed.
function(qt_ir_handle_detach_alternates working_directory)
    qt_ir_get_option_value(copy-objects should_detach)
    if(NOT should_detach)
        return()
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git repack -a
        ERROR_MESSAGE "Failed to repack objects to detach alternates"
        WORKING_DIRECTORY "${working_directory}")

    set(alternates_path "${working_directory}/.git/objects/info/alternates")
    if(EXISTS "${alternates_path}")
        file(REMOVE "${alternates_path}")
        if(EXISTS "${alternates_path}")
            message(FATAL_ERROR "Failed to remove alternates file: ${alternates_path}")
        endif()
    endif()
endfunction()

# Clones a submodule, unless it was previously cloned.
# When cloning, checks out a specific branch if requested, otherwise does not
# checkout any files yet, mimicking a bare repo.
# Sets up an alternates link if requested.
# Detaches alternates if requested.
# Fetches refs if requested.
# Adds a gerrit git remote.
# Sets up the commit template for the submodule.
function(qt_ir_clone_one_submodule submodule_name)
    set(options
        CHECKOUT_BRANCH
        FETCH
    )
    set(oneValueArgs
        ALTERNATES
        BASE_URL
        WORKING_DIRECTORY
    )
    set(multiValueArgs "")
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    qt_ir_get_working_directory_from_arg(working_directory)

    set(clone_args "")
    set(submodule_path "${${prefix}_${submodule_name}_path}")

    if(arg_ALTERNATES)
        # alternates is a qt5 repo, so the submodule will be under that.
        set(alternates_dir "${arg_ALTERNATES}/${submodule_path}/.git")
        if(EXISTS "${alternates_dir}")
            list(APPEND clone_args --reference "${arg_ALTERNATES}/${submodule_path}")
        else()
            message(WARNING "'${arg_ALTERNATES}/${submodule_path}' not found, "
                "ignoring alternate for this submodule")
        endif()
    endif()

    if(NOT EXISTS "${working_directory}/${submodule_path}/.git")
        set(should_clone TRUE)
    else()
        set(should_clone FALSE)
    endif()

    set(submodule_base_git_path "${${prefix}_${submodule_name}_base_git_path}")

    set(submodule_url "${submodule_base_git_path}")
    qt_ir_parse_git_url(
        URL "${submodule_url}"
        OUT_VAR_HAS_URL_SCHEME has_url_scheme
    )

    if(NOT has_url_scheme AND arg_BASE_URL)
        set(submodule_url "${arg_BASE_URL}${submodule_url}")
        qt_ir_normalize_git_url("${submodule_url}" submodule_url)
    endif()

    qt_ir_get_mirror(mirror_url)
    set(mirror "")
    if(NOT has_url_scheme AND mirror_url AND (should_clone OR arg_FETCH))
        set(mirror "${mirror_url}${submodule_base_git_path}")
        qt_ir_normalize_git_url("${mirror}" mirror)
    endif()

    set(mirror_or_original_url "${submodule_url}")
    if(mirror)
        # Only use the mirror if it can be reached.
        # Access a non-existing ref so no output is shown. It should still
        # succeed if the mirror is accessible.
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git ls-remote "${mirror}" "test/if/mirror/exists"
            WORKING_DIRECTORY "${working_directory}"
            NO_HANDLE_ERROR
            OUT_RESULT_VAR proc_result)
        if(NOT proc_result EQUAL 0)
            message("mirror [${mirror}] is not accessible; ${submodule_url} will be used")
            set(mirror "")
        else()
            set(mirror_or_original_url "${mirror}")
        endif()
    endif()

    set(submodule_branch "${${prefix}_${submodule_name}_branch}")

    qt_ir_is_git_progress_supported(is_git_progress_supported)
    qt_ir_get_option_value(quiet quiet)
    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)

    set(progress_args "")
    if(is_git_progress_supported AND NOT quiet AND NOT perl_identical_output_for_tests)
        set(progress_args --progress)
    endif()

    if(should_clone)
        if(arg_CHECKOUT_BRANCH)
            list(APPEND clone_args --branch "${submodule_branch}")
        else()
            list(APPEND clone_args --no-checkout)
        endif()
        list(APPEND clone_args ${progress_args})
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git clone ${clone_args} "${mirror_or_original_url}" "${submodule_path}"
            ERROR_MESSAGE "Failed to clone submodule '${submodule_name}'"
            WORKING_DIRECTORY "${working_directory}")
    endif()

    set(submodule_working_dir "${working_directory}/${submodule_path}")

    if(mirror)
        # This is only for the user's convenience - we make no use of it.
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git config "remote.mirror.url" "${mirror}"
            ERROR_MESSAGE "Failed to set git config remote.mirror.url to ${mirror}"
            WORKING_DIRECTORY "${submodule_working_dir}")
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git config "remote.mirror.fetch" "+refs/heads/*:refs/remotes/mirror/*"
            ERROR_MESSAGE "Failed to set git config remote.mirror.fetch"
            WORKING_DIRECTORY "${submodule_working_dir}")
    endif()

    if(NOT should_clone AND arg_FETCH)
        # If we didn't clone, fetch from the right location. We always update
        # the origin remote, so that submodule update --remote works.
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git config remote.origin.url "${mirror_or_original_url}"
            ERROR_MESSAGE "Failed to set remote origin url"
            WORKING_DIRECTORY "${submodule_working_dir}")
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git fetch origin ${progress_args}
            ERROR_MESSAGE "Failed to fetch origin"
            WORKING_DIRECTORY "${submodule_working_dir}")
    endif()

    if(NOT (should_clone OR arg_FETCH) OR mirror)
        # Leave the origin configured to the canonical URL. It's already correct
        # if we cloned/fetched without a mirror; otherwise it may be anything.
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git config remote.origin.url "${submodule_url}"
            ERROR_MESSAGE "Failed to set remote origin url"
            WORKING_DIRECTORY "${submodule_working_dir}")
endif()

    set(commit_template_dir "${working_directory}")
    qt_ir_setup_commit_template("${commit_template_dir}" "${submodule_working_dir}")

    if(NOT has_url_scheme)
        qt_ir_add_git_remotes("${submodule_base_git_path}" "${submodule_working_dir}")
    endif()

    qt_ir_handle_detach_alternates("${submodule_working_dir}")
endfunction()

# Get list of submodules that were previously initialized, by looking at the .git/config file.
function(qt_ir_get_already_initialized_submodules prefix
    out_var_already_initialized_submodules
    parent_repo_base_git_path
    working_directory
    )

    qt_ir_parse_git_config_file_contents("${prefix}"
        READ_GIT_CONFIG
        PARENT_REPO_BASE_GIT_PATH "${parent_repo_base_git_path}"
        WORKING_DIRECTORY "${working_directory}"
    )

    set(${out_var_already_initialized_submodules} "${${prefix}_submodules}" PARENT_SCOPE)
endfunction()

# If init-repository --force is called with a different subset, remove
# previously initialized submodules from the .git/config file.
# Also mark submodules as ignored if requested.
function(qt_ir_handle_submodule_removal_and_ignoring prefix
    included_submodules
    parent_repo_base_git_path
    working_directory
    )

    qt_ir_get_option_value(ignore-submodules ignore_submodules)

    qt_ir_get_already_initialized_submodules("${prefix}"
        already_initialized_submodules
        "${parent_repo_base_git_path}"
        "${working_directory}")

    foreach(submodule_name IN LISTS already_initialized_submodules)
        if(NOT submodule_name IN_LIST included_submodules)
            # If a submodule is not included in the list of submodules to be initialized,
            # and it was previously initialized, then remove it from the config.
            qt_ir_execute_process_and_log_and_handle_error(
                COMMAND_ARGS git config --remove-section "submodule.${submodule_name}"
                ERROR_MESSAGE "Failed to deinit submodule '${submodule_name}'"
                WORKING_DIRECTORY "${working_directory}")
            continue()
        endif()
        if(ignore_submodules)
            qt_ir_execute_process_and_log_and_handle_error(
                    COMMAND_ARGS git config "submodule.${submodule_name}.ignore" all
                    ERROR_MESSAGE "Failed to ignore submodule '${submodule_name}'"
                    WORKING_DIRECTORY "${working_directory}")
        endif()
    endforeach()
endfunction()

# Checks if the submodule is dirty (has uncommited changes).
function(qt_ir_check_if_dirty_submodule submodule_name working_directory out_is_dirty)
    set(submodule_path "${working_directory}/${${prefix}_${submodule_name}_path}")
    if(NOT EXISTS "${submodule_path}/.git")
        return()
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        FORCE_QUIET
        COMMAND_ARGS git status --porcelain --untracked=no --ignore-submodules=all
        WORKING_DIRECTORY "${submodule_path}"
        ERROR_MESSAGE "Failed to get dirty status for '${submodule_name}'"
        OUT_OUTPUT_VAR git_output)

    string(STRIP "${git_output}" git_output)
    string(REPLACE "\n" ";" git_lines "${git_output}")

    # After a git clone --no-checkout, git status reports all files as
    # staged for deletion, but we still want to update the submodule.
    # It's unlikely that a genuinely dirty index would have _only_ this
    # type of modifications, and it doesn't seem like a horribly big deal
    # to lose them anyway, so ignore them.
    # @sts = grep(!/^D  /, @sts);
    # Filter list that starts with the regex
    list(FILTER git_lines EXCLUDE REGEX "^D  ")

    if(git_lines)
        message(STATUS "${submodule_name} is dirty.")
        set(is_dirty TRUE)
    else()
        set(is_dirty FALSE)
    endif()

    set(${out_is_dirty} "${is_dirty}" PARENT_SCOPE)
endfunction()

# Checks if any submodules are dirty and exits early if any are.
function(qt_ir_handle_dirty_submodule submodules working_directory)
    set(any_is_dirty FALSE)
    foreach(submodule_name IN LISTS submodules)
        qt_ir_check_if_dirty_submodule("${submodule_name}" "${working_directory}" is_dirty)
        if(is_dirty)
            set(any_is_dirty TRUE)
        endif()
    endforeach()

    if(any_is_dirty)
        message(FATAL_ERROR "Dirty submodule(s) present; cannot proceed.")
    endif()
endfunction()

# If the branch option is set, checkout the branch specified in the .gitmodules file.
function(qt_ir_handle_branch_option prefix submodule_name working_directory)
    set(branch_name "${${prefix}_${submodule_name}_branch}")
    if(NOT branch_name)
        message(FATAL_ERROR "No branch defined for submodule '${submodule_name}'")
    endif()

    set(repo_dir "${working_directory}/${${prefix}_${submodule_name}_path}")
    qt_ir_execute_process_and_log_and_handle_error(
        FORCE_QUIET
        COMMAND_ARGS git rev-parse -q --verify ${branch_name}
        WORKING_DIRECTORY "${repo_dir}"
        NO_HANDLE_ERROR
        OUT_RESULT_VAR proc_result)

    # If the branch exists locally, check it out.
    # Otherwise check it out from origin and create a local branch.
    if(proc_result EQUAL 0)
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git checkout ${branch_name}
            WORKING_DIRECTORY "${repo_dir}"
            ERROR_MESSAGE
            "Failed to checkout branch '${branch_name}' in submodule '${submodule_name}'")
    else()
        qt_ir_execute_process_and_log_and_handle_error(
            COMMAND_ARGS git checkout -b ${branch_name} origin/${branch_name}
            WORKING_DIRECTORY "${repo_dir}"
            ERROR_MESSAGE
            "Failed to checkout branch '${branch_name}' in submodule '${submodule_name}'")
    endif()
endfunction()

# If the update option is set, update the submodules, without fetching.
function(qt_ir_handle_update_option will_checkout_branch working_directory)
    set(extra_args "")
    if(will_checkout_branch)
        list(APPEND extra_args --remote --rebase)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git submodule update --force --no-fetch ${extra_args}
        ERROR_MESSAGE "Failed to update submodule '${submodule_name}'"
        WORKING_DIRECTORY "${working_directory}")
endfunction()

# Looks for the 'default' and 'existing' keys, and replaces them with appropriate
# values, while making sure to prepend '-' to the values if the original key had it.
function(qt_ir_handle_dash_in_module_subset_expansion out_var
    module_subset already_initialized_submodules)

    set(expanded_module_subset "")
    foreach(submodule_name IN LISTS module_subset)
        set(has_dash FALSE)
        string(REGEX REPLACE "^(-)" "" submodule_name "${submodule_name}")
        if(CMAKE_MATCH_1)
            set(has_dash TRUE)
        endif()

        # Replace the default keyword in the input, with the the list of default submodules types,
        # which will be further replaced.
        if(submodule_name STREQUAL "default")
            set(replacement "preview;essential;addon;deprecated")
        # Replace the existing keyword, with the list of already initialized submodules
        # from a previous run.
        elseif(submodule_name STREQUAL "existing")
            set(replacement "${already_initialized_submodules}")

            if(has_dash)
                # We can't properly support this with the existing algorithm, because we will
                # then exclude it also after dependency resolution, and it can cause an empty list
                # of submodules in certain situations.
                message(FATAL_ERROR "Excluding existing submodules with '-existing' "
                    "is not supported, just don't include them.")
            endif()
        else()
            set(replacement "${submodule_name}")
        endif()

        # Prepend dash to all expanded values
        if(has_dash)
            list(TRANSFORM replacement PREPEND "-")
        endif()

        list(APPEND expanded_module_subset "${replacement}")
    endforeach()

    set(${out_var} "${expanded_module_subset}" PARENT_SCOPE)
endfunction()

# Processes the given module subset using values that were set by parsing the .gitmodules file.
#
# The module subset is a comma-separated list of module names, with an optional '-' at the start.
# If a - is present, the module (or special expanded keyword) is excluded from the subset.
# If the value is empty, the default subset is used on initial runs, or the previously
#   existing submodules are used on subsequent runs.
# If the value is "all", all known submodules are included.
# If the value is a status like 'addon' or 'essential', only submodules with that status are
#   included.
# If the value is 'existing', only submodules that were previously initialized are included.
#   This evaluates to an empty list for the first script run.
# If the value is a module name, only that module is included.
# The modules to exclude are also set separately, so they can be excluded even after dependency
# resolution which is done later.
function(qt_ir_process_module_subset_values prefix)
    set(options
        PREVIOUSLY_INITIALIZED
    )
    set(oneValueArgs
        OUT_VAR_INCLUDE
        OUT_VAR_EXCLUDE
    )
    set(multiValueArgs
        ALREADY_INITIALIZED_SUBMODULES
        EXTRA_IMPLICIT_SUBMODULES
        MODULE_SUBSET
    )
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    string(REPLACE "," ";" module_subset "${arg_MODULE_SUBSET}")

    # If a module subset is not specified, either use the default list for the very first run,
    # or use the previously initialized submodules for a subsequent run.
    #
    # If the are no previously initialized submodules, and 'existing' is specified, default
    # to 'default'. This handles the case when someone runs git submodule deinit --all --force,
    # where git initrepository.initialized config key is still true, and then runs
    # configure -init-submodules. Without defaulting to default, we would end up with an empty
    # subset and configure would fail.
    if(NOT module_subset)
        if(arg_PREVIOUSLY_INITIALIZED)
            if(arg_ALREADY_INITIALIZED_SUBMODULES)
                set(module_subset "existing")
            else()
                message(DEBUG "No previously initialized submodules detected even though "
                        "'existing' was specified, defaulting to 'default'")
                set(module_subset "default")
            endif()
        else()
            set(module_subset "default")
        endif()
    endif()


    qt_ir_handle_dash_in_module_subset_expansion(
        expanded_module_subset "${module_subset}" "${arg_ALREADY_INITIALIZED_SUBMODULES}")

    set(include_modules "")
    set(exclude_modules "")

    if(arg_EXTRA_IMPLICIT_SUBMODULES)
        list(APPEND include_modules ${arg_EXTRA_IMPLICIT_SUBMODULES})
    endif()

    foreach(value IN LISTS expanded_module_subset ${prefix}_submodules_to_remove)
        # An '-' at the start means we should exclude those modules.
        string(REGEX REPLACE "^(-)" "" value "${value}")
        set(list_op "APPEND")
        if(CMAKE_MATCH_1)
            set(list_op "REMOVE_ITEM")
        endif()

        if(value STREQUAL "all")
            list(${list_op} include_modules "${${prefix}_submodules}")
            if("${list_op}" STREQUAL "REMOVE_ITEM")
                list(APPEND exclude_modules "${${prefix}_submodules}")
            endif()
        elseif(value IN_LIST ${prefix}_statuses)
            list(${list_op} include_modules "${${prefix}_status_${value}_submodules}")
            if("${list_op}" STREQUAL "REMOVE_ITEM")
                list(APPEND exclude_modules "${${prefix}_status_${value}_submodules}")
            endif()
        elseif(NOT "${${prefix}_${value}_path}" STREQUAL "")
            list(${list_op} include_modules "${value}")
            if("${list_op}" STREQUAL "REMOVE_ITEM")
                list(APPEND exclude_modules "${value}")
            endif()
        else()
            if(list_op STREQUAL "REMOVE_ITEM")
                message(WARNING "Excluding non-existent module: ${value}")
            else()
                message(FATAL_ERROR
                    "Invalid module subset specified, module name is non-existent: ${value}")
            endif()
        endif()
    endforeach()

    set(${arg_OUT_VAR_INCLUDE} "${include_modules}" PARENT_SCOPE)
    set(${arg_OUT_VAR_EXCLUDE} "${exclude_modules}" PARENT_SCOPE)
endfunction()

# Sort the modules and add dependencies if dependency resolving is enabled.
function(qt_ir_get_module_subset_including_deps prefix out_var initial_modules)
    qt_ir_get_option_value(resolve-deps resolve_deps)
    qt_ir_get_option_value(optional-deps include_optional_deps)
    if(resolve_deps)
        set(exclude_optional_deps "")
        if(NOT include_optional_deps)
            set(exclude_optional_deps EXCLUDE_OPTIONAL_DEPS)
        endif()

        qt_internal_sort_module_dependencies("${initial_modules}" out_repos
            ${exclude_optional_deps}
            PARSE_GITMODULES
            GITMODULES_PREFIX_VAR "${prefix}"
        )
    else()
        set(out_repos "${initial_modules}")
    endif()

    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)
    if(NOT perl_identical_output_for_tests)
        message(DEBUG "repos that will be initialized after dependency handling: ${out_repos}")
    endif()

    set(${out_var} "${out_repos}" PARENT_SCOPE)
endfunction()

# Check whether init-repository has been run before, perl style.
# We assume that if the submodule qtbase has been initialized, then init-repository has been run.
function(qt_ir_check_if_already_initialized_perl_style out_var_is_initialized working_directory)
    set(cmd git config --get submodule.qtbase.url)

    set(extra_args "")
    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)
    if(perl_identical_output_for_tests)
        list(APPEND extra_args FORCE_QUIET)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS ${cmd}
        OUT_RESULT_VAR git_result
        OUT_OUTPUT_VAR git_output
        OUT_ERROR_VAR git_error
        ${extra_args}
        NO_HANDLE_ERROR
        WORKING_DIRECTORY "${working_directory}")

    if(git_result EQUAL 1 AND NOT git_output)
        set(is_initialized FALSE)
    elseif(git_result EQUAL 0 AND git_output)
        set(is_initialized TRUE)
    else()
        message(FATAL_ERROR "Failed to get result of ${cmd}: ${git_output}")
    endif()

    set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE)
endfunction()

# Check whether init-repository has been run before, cmake style.
# Check for the presence of the initrepository.initialized git config key.
function(qt_ir_check_if_already_initialized_cmake_style out_var_is_initialized working_directory)
    set(options
        FORCE_QUIET
    )
    set(oneValueArgs "")
    set(multiValueArgs "")
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    set(cmd git config --get initrepository.initialized)

    set(extra_args "")
    if(arg_FORCE_QUIET)
        list(APPEND extra_args FORCE_QUIET)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS ${cmd}
        OUT_RESULT_VAR git_result
        OUT_OUTPUT_VAR git_output
        OUT_ERROR_VAR git_error
        ${extra_args}
        NO_HANDLE_ERROR
        WORKING_DIRECTORY "${working_directory}")

    if(git_result EQUAL 1 AND NOT git_output)
        set(is_initialized FALSE)
    elseif(git_result EQUAL 0 AND git_output)
        set(is_initialized TRUE)
    else()
        message(FATAL_ERROR "Failed to get result of ${cmd}: ${git_output}")
    endif()

    set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE)
endfunction()

# Check whether init-repository has been run before.
# The CMake and perl script do it differently, choose which way to do it based
# on the active options.
function(qt_ir_check_if_already_initialized out_var_is_initialized working_directory)
    qt_ir_get_option_value(perl-init-check perl_init_check)
    if(perl_init_check)
        qt_ir_check_if_already_initialized_perl_style(is_initialized "${working_directory}")
    else()
        qt_ir_check_if_already_initialized_cmake_style(is_initialized "${working_directory}")
    endif()

    set(${out_var_is_initialized} "${is_initialized}" PARENT_SCOPE)
endfunction()

# Marks the repository as initialized.
# The perl script used to determine this by checking whether the qtbase submodule was initialized.
# In the CMake script, we instead opt to set an explicit marker in the repository.
function(qt_ir_set_is_initialized working_directory)
    # If emulating perl style initialization check, don't set the marker and exit early.
    qt_ir_get_option_value(perl-init-check perl_init_check)
    if(perl_init_check)
        return()
    endif()

    set(cmd git config initrepository.initialized true)

    set(extra_args "")
    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)
    if(perl_identical_output_for_tests)
        list(APPEND extra_args FORCE_QUIET)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS ${cmd}
        ERROR_MESSAGE "Failed to mark repository as initialized"
        ${extra_args}
        WORKING_DIRECTORY "${working_directory}")
endfunction()

# If the repository has already been initialized, exit early.
function(qt_ir_handle_if_already_initialized out_var_should_exit working_directory)
    set(should_exit FALSE)

    qt_ir_check_if_already_initialized(is_initialized "${working_directory}")
    qt_ir_get_option_value(force force)
    qt_ir_get_option_value(quiet quiet)
    qt_ir_is_called_from_configure(is_called_from_configure)

    if(is_initialized)
        if(NOT force)
            set(should_exit TRUE)
            if(NOT quiet AND NOT is_called_from_configure)
                message(
                    "Will not reinitialize already initialized repository (use -f to force)!")
            endif()
        endif()
    endif()

    set(${out_var_should_exit} ${should_exit} PARENT_SCOPE)
endfunction()

# Parses git remote.origin.url and extracts the base url and the repository name.
#
# base_url example: git://code.qt.io/qt
# repo name example: qt5 or tqtc-qt5
function(qt_ir_get_qt5_repo_name_and_base_url)
    set(options "")
    set(oneValueArgs
        OUT_VAR_QT5_REPO_NAME
        OUT_VAR_BASE_URL
        WORKING_DIRECTORY
    )
    set(multiValueArgs "")
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    if(NOT arg_WORKING_DIRECTORY)
        message(FATAL_ERROR "qt_ir_get_qt5_repo_name_and_base_url: No working directory specified")
    endif()
    set(working_directory "${arg_WORKING_DIRECTORY}")

    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)

    set(extra_args "")
    if(perl_identical_output_for_tests)
        set(extra_args FORCE_QUIET)
    endif()

    qt_ir_execute_process_and_log_and_handle_error(
        COMMAND_ARGS git config remote.origin.url ${extra_args}
        ERROR_MESSAGE "No origin remote found for qt5 repository"
        OUT_OUTPUT_VAR git_output
        WORKING_DIRECTORY "${working_directory}")

    string(STRIP "${git_output}" git_output)

    # Remove the .git at the end, with an optional slash
    string(REGEX REPLACE ".git/?$" "" qt5_repo_name "${git_output}")

    # Remove the tqtc- prefix, if it exists, and the qt5 suffix and that will be the base_url
    # The qt5_repo_name is qt5 or tqtc-qt5.
    string(REGEX REPLACE "((tqtc-)?qt5)$" "" base_url "${qt5_repo_name}")
    set(qt5_repo_name "${CMAKE_MATCH_1}")

    if(NOT qt5_repo_name)
        set(qt5_repo_name "qt5")
    endif()

    if(NOT base_url)
        message(FATAL_ERROR "Failed to parse base url from origin remote: ${git_output}")
    endif()

    set(${arg_OUT_VAR_QT5_REPO_NAME} "${qt5_repo_name}" PARENT_SCOPE)
    set(${arg_OUT_VAR_BASE_URL} "${base_url}" PARENT_SCOPE)
endfunction()

# Creates a symlink or a forwarding script to the target path.
# Use for setting up git hooks.
function(qt_ir_ensure_link source_path target_path)
    qt_ir_get_option_value(force-hooks force_hooks)
    if(EXISTS "${target_path}" AND NOT force_hooks)
        return()
    endif()

    # In case we have a dead symlink or pre-existing hook
    file(REMOVE "${target_path}")

    qt_ir_get_option_value(quiet quiet)
    if(NOT quiet)
        message("Aliasing ${source_path}\n      as ${target_path} ...")
    endif()

    if(NOT CMAKE_HOST_WIN32)
        file(CREATE_LINK "${source_path}" "${target_path}" RESULT result SYMBOLIC)
        # Don't continue upon success. If symlinking failed, fallthrough to creating
        # a forwarding script.
        if(result EQUAL 0)
            return()
        endif()
    endif()

    # Windows doesn't do (proper) symlinks. As the post_commit script needs
    # them to locate itself, we write a forwarding script instead.

    # Make the path palatable for MSYS.
    string(REGEX REPLACE "^(.):/" "/\\1/" source_path "${source_path}")

    set(contents "#!/bin/sh\nexec ${source_path} \"$@\"\n")
    file(WRITE "${target_path}" "${contents}")
endfunction()

# Installs the git hooks from the qtrepotools module.
function(qt_ir_install_git_hooks)
    set(options "")
    set(oneValueArgs
        PARENT_REPO_BASE_GIT_PATH
        TOP_LEVEL_SRC_PATH
        WORKING_DIRECTORY
    )
    set(multiValueArgs "")
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    if(NOT arg_WORKING_DIRECTORY)
        message(FATAL_ERROR "qt_ir_install_git_hooks: No working directory specified")
    endif()
    set(working_directory "${arg_WORKING_DIRECTORY}")

    if(NOT arg_PARENT_REPO_BASE_GIT_PATH)
        message(FATAL_ERROR "qt_ir_install_git_hooks: No PARENT_REPO_BASE_GIT_PATH specified")
    endif()
    set(parent_repo_base_git_path "${arg_PARENT_REPO_BASE_GIT_PATH}")

    if(NOT arg_TOP_LEVEL_SRC_PATH)
        message(FATAL_ERROR "qt_ir_install_git_hooks: No TOP_LEVEL_SRC_PATH specified")
    endif()
    set(top_level_src_path "${arg_TOP_LEVEL_SRC_PATH}")

    set(hooks_dir "${top_level_src_path}/qtrepotools/git-hooks")
    if(NOT EXISTS "${hooks_dir}")
        message("Warning: cannot find Git hooks, qtrepotools module might be absent")
        return()
    endif()

    set(prefix ir_hooks)
    qt_ir_parse_git_config_file_contents("${prefix}"
        READ_GIT_CONFIG_LOCAL
        PARENT_REPO_BASE_GIT_PATH "${parent_repo_base_git_path}"
        WORKING_DIRECTORY "${working_directory}"
    )

    foreach(submodule_name IN LISTS ${prefix}_submodules)
        set(submodule_git_dir "${working_directory}/${submodule_name}/.git")
        if(NOT IS_DIRECTORY "${submodule_git_dir}")
            # Get first line
            file(STRINGS "${submodule_git_dir}" submodule_git_dir_contents LIMIT_COUNT 1)

            # Remove the gitdir: prefix
            string(REGEX REPLACE "^(gitdir: )" "" submodule_git_dir
                "${submodule_git_dir_contents}")
            if("${CMAKE_MATCH_1}" STREQUAL "")
                message(FATAL_ERROR "Malformed .git file ${submodule_git_dir}")
            endif()

            # Make it an absolute path, because gitdir: is usually relative to the submodule
            get_filename_component(submodule_git_dir "${submodule_git_dir}"
                ABSOLUTE BASE_DIR "${working_directory}/${submodule_name}")

            # Untested
            set(common_dir "${submodule_git_dir}/commondir")
            if(EXISTS "${common_dir}")
                file(STRINGS "${common_dir}" common_dir_contents LIMIT_COUNT 1)
                string(STRIP "${common_dir_contents}" common_dir_path)
                set(submodule_git_dir "${submodule_git_dir}/${common_dir_path}")
                get_filename_component(submodule_git_dir "${submodule_git_dir}" ABSOLUTE)
            endif()
        endif()
        qt_ir_ensure_link("${hooks_dir}/gerrit_commit_msg_hook"
            "${submodule_git_dir}/hooks/commit-msg")
        qt_ir_ensure_link("${hooks_dir}/git_post_commit_hook"
            "${submodule_git_dir}/hooks/post-commit")
        qt_ir_ensure_link("${hooks_dir}/clang-format-pre-commit"
            "${submodule_git_dir}/hooks/pre-commit")
    endforeach()
endfunction()

# Saves the list of top-level submodules that should be included and excluded.
# Will be used to pass these values to the top-level configure script.
function(qt_ir_set_top_level_submodules included_submodules excluded_submodules)
    set_property(GLOBAL PROPERTY _qt_ir_top_level_included_submodules "${included_submodules}")
    set_property(GLOBAL PROPERTY _qt_ir_top_level_excluded_submodules "${excluded_submodules}")
endfunction()

# Gets the list of top-level submodules that should be included and excluded.
function(qt_ir_get_top_level_submodules out_included_submodules out_excluded_submodules)
    get_property(included GLOBAL PROPERTY _qt_ir_top_level_included_submodules)
    get_property(excluded GLOBAL PROPERTY _qt_ir_top_level_excluded_submodules)

    set(${out_included_submodules} "${included}" PARENT_SCOPE)
    set(${out_excluded_submodules} "${excluded}" PARENT_SCOPE)
endfunction()

# Parses the .gitmodules file and proceses the submodules based on the module-subset option
# or the given SUBMODULES argument.
# Also adds dependencies if requested.
#
# This is a macro because we want the variables set by
# qt_ir_parse_gitmodules_file_contents to be available in the calling scope, because it's
# essentially setting a dictionarty, and we don't want to propagate all the variables manually.
macro(qt_ir_get_submodules prefix out_var_submodules)
    set(options
        PREVIOUSLY_INITIALIZED
        PROCESS_SUBMODULES_FROM_COMMAND_LINE
    )
    set(oneValueArgs
        PARENT_REPO_BASE_GIT_PATH
        WORKING_DIRECTORY
    )
    set(multiValueArgs
        ALREADY_INITIALIZED_SUBMODULES
        SUBMODULES
    )
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    qt_ir_get_working_directory_from_arg(working_directory)

    # Parse the .gitmodules content here, so the parsed data is available downstream
    # in other functions and recursive calls of the same function.
    qt_ir_parse_git_config_file_contents("${prefix}"
        READ_GITMODULES
        PARENT_REPO_BASE_GIT_PATH "${arg_PARENT_REPO_BASE_GIT_PATH}"
        WORKING_DIRECTORY "${working_directory}"
    )

    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)
    set(extra_implict_submodules "")

    # Get which modules should be initialized, based on the module-subset option.
    if(arg_PROCESS_SUBMODULES_FROM_COMMAND_LINE)
        qt_ir_get_option_value(module-subset initial_module_subset)

        # Implicitly add qtrepotools, so we can install git hooks and don't get the
        # missing qtrepotools warning.
        if(NOT perl_identical_output_for_tests)
            list(APPEND extra_implict_submodules "qtrepotools")
            qt_ir_is_verbose(verbose)
            if(verbose)
                message("Implicitly adding qtrepotools to the list of submodules "
                        "to initialize for access to git commit hooks, etc. "
                        "(use --module-subset=<values>,-qtrepotools to exclude it)")
            endif()
        endif()

        if(NOT perl_identical_output_for_tests)
            message(DEBUG "module-subset from command line: ${initial_module_subset}")
        endif()
    elseif(arg_SUBMODULES)
        set(initial_module_subset "${arg_SUBMODULES}")
        if(NOT perl_identical_output_for_tests)
            message(DEBUG "module-subset from args: ${initial_module_subset}")
        endif()
    else()
        message(FATAL_ERROR "No submodules specified")
    endif()

    qt_ir_get_cmake_flag(PREVIOUSLY_INITIALIZED previously_initialized_opt)
    qt_ir_process_module_subset_values("${prefix}"
        ${previously_initialized_opt}
        ${perl_identical_output_opt}
        ALREADY_INITIALIZED_SUBMODULES ${arg_ALREADY_INITIALIZED_SUBMODULES}
        EXTRA_IMPLICIT_SUBMODULES ${extra_implict_submodules}
        MODULE_SUBSET "${initial_module_subset}"
        OUT_VAR_INCLUDE processed_module_subset
        OUT_VAR_EXCLUDE modules_to_exclude
    )
    if(NOT perl_identical_output_for_tests)
        message(DEBUG "Processed module subset: ${processed_module_subset}")
    endif()

    # We only resolve dependencies for the top-level call, not for recursive calls.
    if(arg_PROCESS_SUBMODULES_FROM_COMMAND_LINE)
        # Resolve which submodules should be initialized, including dependencies.
        qt_ir_get_module_subset_including_deps("${prefix}"
            submodules_with_deps "${processed_module_subset}")

        # Then remove any explicitly specified submodules.
        set(submodules_with_deps_and_excluded "${submodules_with_deps}")
        if(modules_to_exclude)
            list(REMOVE_ITEM submodules_with_deps_and_excluded ${modules_to_exclude})
        endif()

        if(NOT perl_identical_output_for_tests AND modules_to_exclude)
            message(DEBUG "Repos that will be excluded after dependency handling: ${modules_to_exclude}")
        endif()

        set(submodules "${submodules_with_deps_and_excluded}")
        qt_ir_set_top_level_submodules("${submodules}" "${modules_to_exclude}")
    else()
        set(submodules "${processed_module_subset}")
    endif()

    # Remove duplicates
    set(submodules_maybe_duplicates "${submodules}")
    list(REMOVE_DUPLICATES submodules)
    if(NOT perl_identical_output_for_tests AND NOT submodules STREQUAL submodules_maybe_duplicates)
            message(DEBUG "Removed duplicates from submodules, final list: ${submodules}")
    endif()

    set(${out_var_submodules} "${submodules}" PARENT_SCOPE)
endmacro()

# Recursively initialize submodules starting from the given current working directory.
# This is the equivalent of the perl script's git_clone_all_submodules function.
function(qt_ir_handle_init_submodules prefix)
    set(options
        CHECKOUT_BRANCH
        PREVIOUSLY_INITIALIZED
        PROCESS_SUBMODULES_FROM_COMMAND_LINE
    )
    set(oneValueArgs
        ALTERNATES
        BASE_URL
        PARENT_REPO_BASE_GIT_PATH
        WORKING_DIRECTORY
    )
    set(multiValueArgs
        ALREADY_INITIALIZED_SUBMODULES
        SUBMODULES
    )
    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    qt_ir_get_working_directory_from_arg(working_directory)

    # Get the submodules that should be initialized.
    qt_ir_get_cmake_flag(PROCESS_SUBMODULES_FROM_COMMAND_LINE
                         process_submodules_from_command_line_opt)
    qt_ir_get_cmake_flag(PREVIOUSLY_INITIALIZED
                         previously_initialized_opt)
    qt_ir_get_submodules(${prefix} submodules
        ${process_submodules_from_command_line_opt}
        ${previously_initialized_opt}
        ALREADY_INITIALIZED_SUBMODULES ${arg_ALREADY_INITIALIZED_SUBMODULES}
        PARENT_REPO_BASE_GIT_PATH "${arg_PARENT_REPO_BASE_GIT_PATH}"
        SUBMODULES "${arg_SUBMODULES}"
        WORKING_DIRECTORY "${working_directory}"
    )

    qt_ir_get_option_value(perl-identical-output perl_identical_output_for_tests)
    if(NOT submodules AND NOT perl_identical_output_for_tests)
        message("No submodules were given to initialize or they were all excluded.")
        return()
    endif()

    # Initialize the submodules, but don't clone them yet.
    qt_ir_run_git_submodule_init("${submodules}" "${working_directory}")

    # Deinit submodules that are not in the list of submodules to be initialized.
    qt_ir_handle_submodule_removal_and_ignoring("${prefix}"
        "${submodules}" "${arg_PARENT_REPO_BASE_GIT_PATH}" "${working_directory}")

    # Check for dirty submodules.
    qt_ir_handle_dirty_submodule("${submodules}" "${working_directory}")

    qt_ir_get_cmake_flag(CHECKOUT_BRANCH branch_flag)
    qt_ir_get_option_as_cmake_flag_option(fetch "FETCH" fetch_flag)

    # Manually clone each submodule if it was not previously cloned, so we can easily
    # use reference (alternates) repos, mirrors, etc.
    # If already cloned, just fetch new data.
    #
    # Note that manually cloning the submodules, as opposed to running git submodule update,
    # places the .git directories inside the submodule directories, but latest git versions
    # expect it in $super_repo/.git/modules.
    # When de-initializing submodules manually, git will absorb the .git directories into the super
    # repo.
    # In case if the super repo already has a copy of the submodule .git dir, git will fail
    # to absorb the .git dir and error out. In that case the already existing .git dir needs to be
    # removed manually, there is no git command to do it afaik.
    foreach(submodule_name IN LISTS submodules)
        qt_ir_clone_one_submodule(${submodule_name}
            ALTERNATES ${arg_ALTERNATES}
            BASE_URL ${arg_BASE_URL}
            WORKING_DIRECTORY "${working_directory}"
            ${branch_flag}
            ${fetch_flag}
        )
    endforeach()

    # Checkout branches instead of the default detached HEAD.
    if(branch_flag)
        foreach(submodule_name IN LISTS submodules)
            qt_ir_handle_branch_option("${prefix}" ${submodule_name} "${working_directory}")
        endforeach()
    endif()

    qt_ir_get_option_value(update will_update)
    if(will_update)

        # Update the checked out refs without fetching.
        qt_ir_handle_update_option("${branch_flag}" "${working_directory}")

        # Recursively initialize submodules of submodules.
        foreach(submodule_name IN LISTS submodules)
            set(submodule_path "${${prefix}_${submodule_name}_path}")
            set(submodule_gitmodules_path "${working_directory}/${submodule_path}/.gitmodules")

            if(EXISTS "${submodule_gitmodules_path}")
                set(alternates_option "")
                if(arg_ALTERNATES)
                    set(alternates_option ALTERNATES "${arg_ALTERNATES}/${submodule_name}")
                endif()

                set(submodule_base_git_path "${${prefix}_${submodule_name}_base_git_path}")

                qt_ir_handle_init_submodules(
                    # Use a different prefix to store new gitmodules data
                    ir_sub_${submodule_name}

                    # Check out all submodules recursively
                    SUBMODULES "all"

                    BASE_URL "${base_url}"
                    PARENT_REPO_BASE_GIT_PATH "${submodule_base_git_path}"
                    WORKING_DIRECTORY "${working_directory}/${submodule_name}"

                    # The CHECKOUT_BRANCH option is not propagated on purpose
                    ${alternates_option}
                )
            endif()
        endforeach()
    endif()
endfunction()
