在“git log”中包含子模块提交信息

22

假设我的仓库中有两个版本...每个版本都已被标记如下:

  • Tag1
  • Tag2

现在假设一个提交更新了子模块引用,使其指向位于Tag1和Tag2之间的新的提交。 我运行以下命令,并得到以下结果:

# show commits between these two tags
git log Tag1..Tag2


commit be3d0357b93322f472e8f03285cb3e1e0592eabd
Author: James Johnston <snip>
Date:   Wed Jan 25 19:42:56 2012 +0000

    Updated submodule references.
在这种情况下,唯一的更改是子模块的更新。如何使子模块提交与父存储库提交交错?
具体来说,在此示例中,假设父存储库指向子模块中的SubTag5标记。在子模块中两个提交后是一个SubTag6标记。所显示的提交将更新子模块指针,使其指向SubTag6而不是SubTag5。我想要的是让git log除了已经打印的提交外,还打印出使子模块从SubTag5到SubTag6转换的两个子模块提交。
4个回答

20
这里有一个简单的bash命令,可以创建一个ASCII提交图(类似于gitk),当子模块在超级项目中改变时,它会交错显示相关的子模块提交。它会打印出每个提交的完整补丁,然后使用grep过滤掉补丁内容,只保留摘要行和子模块更改。
git log --graph --oneline -U0 --submodule Tag1..Tag2 | grep -E '^[*| /\\]+([0-9a-f]{7} |Submodule |> |$)'

它会产生类似这样的输出:

* 854407e Update submodule
| Submodule SUB 8ebf7c8..521fc49:
|   > Commit C
* 99df57c Commit B
* 79e4075 Commit A

把这个加入到gitk里面会很棒! - Ciprian Tomoiagă
4
问题在于这里列出的子模块提交记录不完整。如果子模块中存在合并操作,则合并后的提交记录不会被列出,只有合并提交记录会被列出。有没有办法也列出那些丢失的提交记录? - Thomas Lauria
这个能用来列出暂存区(索引)和HEAD~之间的差异吗?这样它就可以为主仓库提交提供提交信息。 - Mr_and_Mrs_D

18

你可以显示子模块的更改,但仅在使用git log -p时。下面的命令会显示每个提交和子模块更改的完整差异。

git log -p --submodule=log

子模块提交消息将会以这种方式列出:

Submodule <submodule-name> <starting-commit>..<ending-commit>:
> Commit message 1
> Commit message 2
...
> Commit message n

如果您不想阅读每个提交的完整差异,您可以匹配和过滤掉那些部分:

git log -p --submodule=log | awk '
/^commit/ { add=1 } # Start of commit message
/^diff --git/ { add=0 } # Start of diff snippet
{ if (add) { buf = buf "\n" $0 } } # Add lines if part of commit message
END { print buf }
'

2
功能上还算正常,但是否有一种方法可以获得子模块的更清晰输出,类似于主存储库,而无需浏览完整的差异?例如,git log在主存储库上打印作者、日期和完整的提交消息。希望在主存储库中列出子模块的条目,就像提交下面的条目一样,没有补丁差异,这将是很好的。 - James Johnston
据我所知,目前没有办法。但是你仍然可以在输出上运行一些正则表达式,过滤掉所有你不想看到的东西。 - iblue

2
如果您使用的是bash,您可以使用以下脚本将子模块提交日志嵌入到超级项目日志中进行显示。
#!/bin/bash 

# regular expressions related to git log output
# when using options -U0 and --submodule=log
kREGEXP_ADD_SUBMODLE='0+\.\.\.[0-9a-f]+'
kREGEXP_REM_SUBMODLE='[0-9a-f]+\.\.\.0+'

# --------------------------------------------------------------------
# function submodule_log
# --------------------------------------------------------------------
# 
# print a log of submodule changes for a range of commits
#
# arguments : see start of function body for details  
# 
function submodule_log {

    sm_present=$1; # presence 0: no, 1: yes
    sm_status=$2   # status   0: as is, 1: added submodule, 2: removed submodule 
    sm_name=$3     # name
    sm_id_base=$4  # base commit id added changes
    sm_id_now=$5   # final commit id added changes

    cur_dir=`pwd`

    # commits cannot be accessed if sbumodule working tree was removed, 
    # show submodule commits in details only if directory exists
    #
    # note: As of git 1.9, in .git/modules/<submodule-name>
    #       still the entire gitdir is present, just git won't successfully
    #       run something like 'git --git-dir .git/modules/<submodule-name> log f374fbf^!'
    #       from the superproject root dir. It fails as it want's to change directory to
    #       to the submodule working tree at '../../../<submodule-name>' to get the log.
    #       If one just creates it as an empty directory the command succeeds, but
    #       we cannot force the user to leave an empty directory. So just a hint
    #       is output to suggest creation of directory to get full log.

    #echo " $submod_entry"

    if [ -e $sm_name ]  
    then    
        cd $sm_name

        # if submodule not present in current version of superproject
        # can retrieve git log info only by using option '--git-dir'
        # -> use always option --git-dir

        git_dir_opt="--git-dir $cur_dir/.git/modules/$sm_name"
        git_cmd_base="git $git_dir_opt log --format=\"  %Cred%h %s%Creset\""

        if [ $sm_status -eq 0 ]
        then
            # modified module: output info on added commit(s)
            eval "$git_cmd_base ${sm_id_base}..${sm_id_now}"
        fi

        if [ $sm_status -eq 1 ]
        then
            # new module: output only info on base commit    
            eval "$git_cmd_base ${sm_id_now}^!"
        fi

        if [ $sm_status -eq 2 ]
        then
            # removed module: output only info on last commit  
            eval "$git_cmd_base ${sm_id_base}^!"
        fi

        cd $cur_dir 
    else
        echo " Skip info on submodule $sm_name (not present in current working tree)"
        echo " For full log, please add empty directory $sm_name for full log."
    fi 
}

# --------------------------------------------------------------------
# main script 
# --------------------------------------------------------------------

# Get the log of the parent repository (only SHA1 and parent's SHA1), 
# use files as amount of data might be huge in older repos 

# get commit ids as array
readarray -t log_commitids < <(git log --format="%H")

# get commit ids of parent commits 
readarray -t log_parents < <(git log --format="%P")

for ((c_idx=0; $c_idx<${#log_commitids[@]}; c_idx=$c_idx+1))
do
    # Can only be one commit id, but remove trailing newline and linefeed
    commit="${log_commitids[$c_idx]//[$'\r\n']}"

    # Can be more than one parent if it's a merge
    # remove trailing newline and linefeed
    parents="${log_parents[$c_idx]//[$'\r\n']}"    
    parents_a=($(echo $parents))
    num_parents=${#parents_a[@]}

    # check if merge commit, prefix next commit with M as they are merge
    merge_prefix=""
    if [ $num_parents -ge 2 ] 
    then
        merge_prefix="M$num_parents" 
    fi

    # Print the two-line summary for this commit
    git log --format="%Cgreen%h (%cI %cN)%Creset%n %Cgreen$merge_prefix%Creset %s" $commit^!

    #echo "found $num_parents parents"

    if [ "$parents" = "" ]
    then
       unset parents
    else

        for parent in $parents
        do
            # Find entires like 
            #  "Submodule libA 0000000...f374fbf (new submodule)"      or
            #  "Submodule libA e51c470...0000000 (submodule deleted)"  or 
            #  "Submodule libA f374fbf..af648b2e:"
            # in supermodules history in order to determine submodule's
            # name and commit range describing the changes that 
            # were added to the supermodule. Two regular expressions
            # kREGEXP_ADD_SUBMODLE and kREGEXP_REM_SUBMODLE are used
            # to find added and removed submodules respectively.

            readarray -t submod < <(git log -U0 --submodule=log ${parent}..${commit} \
            | grep -U -P '^Submodule \S+ [0-9a-f]+')

            for ((s_idx=0; $s_idx<${#submod[@]}; s_idx=$s_idx+1))
            do
                # remove trailing newline and linefeed
                submod_entry="${submod[$s_idx]//[$'\r\n']}"

                #echo mainly unfiltered as to show submod name and its
                #commit range stored in repo's log
                echo " $submod_entry"

                # remove preceding info 'Submodule ' as we already know that :-)
                submod_entry="${submod_entry/Submodule }"

                # if viewing repository version for which submodules do not exist
                # they are reported with correct commit ids but trailing text
                # is different, first assume it is present then check submod_entry
                submod_present=1
                if [[ "$submod_entry" =~ "commits not present" ]]
                then
                   submod_present=0

                   # remove trailing info about deleted submodule, if any
                   submod_entry="${submod_entry/'(commits not present)'}"                 
                fi

                # find with submodule got added/modified/removed by this superproject commit
                # assume 'modified' submodule, then check if commit range indicates
                # special cases like added/removed submodule
                sub_status=0                
                if [[ "$submod_entry" =~ $kREGEXP_ADD_SUBMODLE ]]
                then
                   sub_status=1

                   # remove trailing info about new submodule, if any
                   submod_entry="${submod_entry/'(new submodule)'}" 
                fi

                if [[ "$submod_entry" =~ $kREGEXP_REM_SUBMODLE ]]
                then
                   sub_status=2

                   # remove trailing info about deleted submodule, if any
                   submod_entry="${submod_entry/'(submodule deleted)'}"
                fi

                # create log output for submod_entry 
                # - pass contents in submod_entry as separate arguments
                #   by expanding variable and using eval to execute resulting code

                #replace dots by spaces as to split apart source and destination commit id
                submod_entry="${submod_entry//./ }"
                #remove colon behind last commit id, if any
                submod_entry="${submod_entry//:/}"

                eval "submodule_log $submod_present $sub_status $submod_entry"
            done    
        done
    fi
done

该脚本与上面列出的PowerShell脚本类似,但解决了一些问题并以更紧凑的格式输出。它可以处理新的子模块和已删除的子模块。为了正确显示不再是超级项目一部分的子模块的日志信息(已删除的子模块),至少子模块根目录(可以为空)必须保留在存储库中。否则,Git(在Windows上测试版本为2.19.0)将在日志命令中失败(例如在git --git-dir ./.git/modules/libA log --oneline f374fbf^!中),因为它总是更改工作目录到子模块根目录(出于任何原因)。

line 84: readarray: command not found line 87: readarray: command not found - Steph Thirion
@StephThirion,readarray在古老版本的bash中不可用。您可以逐行读取标准输入来替换它,请参考这里 - famoses

1

如果你在使用Windows操作系统,你可以使用以下的PowerShell脚本:

function Parse-SubmoduleDiff($rawDiffLines) {
    $prefix = "Subproject commit "
    $oldCommitLine = $($rawDiffLines | where { $_.StartsWith("-" + $prefix) } | select -First 1)
    $newCommitLine = $($rawDiffLines | where { $_.StartsWith("+" + $prefix) } | select -First 1)

    if ($newCommitLine -eq $null) {
        return $null
    }

    $oldCommit = $null
    if ($oldCommitLine -ne $null) {
        $oldCommit = $oldCommitLine.Substring($prefix.Length + 1)
    }
    $newCommit = $newCommitLine.Substring($prefix.Length + 1)
    return @{ OldCommit = $oldCommit; NewCommit = $newCommit }
}

# Get the paths of all submodules
$submodulePaths = $(git submodule foreach --quiet 'echo $path')
if ($submodulePaths -eq $null) {
    $submodulePaths = @()
}

# Get the log of the parent repository (only SHA1)
$log = $(git log --format="%H %P" $args)
foreach ($line in $log) {

    $parts = $line.Split()
    $commit = $parts[0]
    $parents = $parts[1..$parts.Length]

    # Print the summary for this commit
    git show --format=medium --no-patch $commit
    echo ""

    # Can be more than one parent if it's a merge
    foreach ($parent in $parents) {
        # List the paths that changed in this commit
        $changes = $(git diff --name-only $parent $commit)

        if ([System.String]::IsNullOrWhiteSpace($parent)) {
            continue;
        }

        foreach ($path in $changes) {

            if ($submodulePaths.Contains($path)) {
                # if it's a submodule, the diff should look like this:
                # -Subproject commit 1486adc5c0c37ad3fa2f2e373e125f4000e4235f
                # +Subproject commit a208e767afd0a51c961654d3693893bbb4605902
                # from that we can extract the old and new submodule reference

                $subDiff = $(git diff $parent $commit -- $path)
                $parsed = Parse-SubmoduleDiff($subDiff)
                if ($parsed -eq $null) {
                    continue;
                }

                # Now get the log between the old and new submodule commit
                $oldCommit = $parsed.OldCommit
                $newCommit = $parsed.NewCommit
                echo "Submodule '$path'"
                if ($oldCommit -ne $null) {
                    $range = $($oldCommit + ".." + $newCommit)
                } else {
                    $range = $newCommit
                }

                git --git-dir $path/.git log $range | foreach { "  |  " + $_ }
                echo ""
            }
        }
    }
}

显然可以将其翻译为适用于Linux的bash。其一般原则是:
for each commit in the parent repo
    print the commit summary
    for each submodule that has changed in this commit
        get the old and new commit hashes of the submodule
        print the log of the submodule between those commits
    end for
end for

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接