2012/08/30

使用脚本分析git index文件内容

昨晚读了git index文件的格式说明,于是尝试的写了一个简单的ruby脚本用来解析index文件。部分代码如下:

#!/usr/bin/ruby

# Author: loveky <ylzcylx#gmail.com>
# Blog  : http://loveky2012.blogspot.com

# This script will parse the given index file and output all entries in it.
# run 'parse_index_file.rb -h' for help message

$: << File.dirname(__FILE__) 
require 'bindata'
require 'optparse'
require 'IndexEntry'

STANDARD_SIGNATURE  = 'DIRC'
entry_count         = 1

begin
    options = {}
    opts = OptionParser.new do |opts|
        opts.banner = "Parse the index file and show you what is there"
        opts.on('-f FILE ','--file FILE', 'Path to the index file') do |value|
            options[:file] = value
        end
    
        opts.on_tail("-h", "--help", "Show this message") do
            puts opts
            exit
        end
    
    end.parse!

    raise ArgumentError, "No index file given!" if options[:file] == nil

    File.open(options[:file], "rb") do |fd|
        SIGNATURE = fd.read(4)
        raise RuntimeError, "Not an index file!" if SIGNATURE != STANDARD_SIGNATURE
    
        VERSION         = fd.read(4).unpack("N")[0]
        ENTRIES_NUMBER  = fd.read(4).unpack("N")[0]
    
        puts "Index version : #{VERSION}"
        puts "Entries number: #{ENTRIES_NUMBER}"
    
        ENTRIES_NUMBER.times do
            entry = IndexEntry.read(fd)
            puts "Found file: " + entry.path 
            puts "  SHA1    : " + entry.sha1.unpack("H40").join
            puts "  stage   : #{entry.flags << 2 >> 14}"
            puts "  ctime   : " + Time.at(entry.ctime_s + entry.ctime_ns * 10**-9).strftime('%Y-%m-%d %H:%M:%S.%9N')
            puts "  mtime   : " + Time.at(entry.mtime_s + entry.mtime_ns * 10**-9).strftime('%Y-%m-%d %H:%M:%S.%9N')
            puts "  size    : #{entry.file_size}"
        
            if (fd.pos - 12) % 8 != 0
                fd.seek(8 - ((fd.pos - 12) % 8), IO::SEEK_CUR)
            end
        end
    end
rescue => ex
    puts "#{ex.class}: #{ex.message}"
end

使用起来很简单,只需要ruby parse_index_file.rb --file [index文件路径]就好了。

要查看最新的,完整脚本内容请猛击我的github

最后附上index文件的格式说明,E文的,不过写的很简洁,就先不翻译了
GIT index format
================

= The git index file has the following format

All binary numbers are in network byte order. Version 2 is described
here unless stated otherwise.

- A 12-byte header consisting of

4-byte signature:
The signature is { 'D', 'I', 'R', 'C' } (stands for "dircache")

4-byte version number:
The current supported versions are 2 and 3.

32-bit number of index entries.

- A number of sorted index entries (see below).

- Extensions

Extensions are identified by signature. Optional extensions can
be ignored if GIT does not understand them.

GIT currently supports cached tree and resolve undo extensions.

4-byte extension signature. If the first byte is 'A'..'Z' the
extension is optional and can be ignored.

32-bit size of the extension

Extension data

- 160-bit SHA-1 over the content of the index file before this
checksum.

== Index entry

Index entries are sorted in ascending order on the name field,
interpreted as a string of unsigned bytes (i.e. memcmp() order, no
localization, no special casing of directory separator '/'). Entries
with the same name are sorted by their stage field.

32-bit ctime seconds, the last time a file's metadata changed
this is stat(2) data

32-bit ctime nanosecond fractions
this is stat(2) data

32-bit mtime seconds, the last time a file's data changed
this is stat(2) data

32-bit mtime nanosecond fractions
this is stat(2) data

32-bit dev
this is stat(2) data

32-bit ino
this is stat(2) data

32-bit mode, split into (high to low bits)

4-bit object type
valid values ina binary are 1000 (regular file), 1010 (symbolic link)
and 1110 (gitlink)

3-bit unused

9-bit unix permission. Only 0755 and 0644 are valid for regular files.
Symbolic links and gitlinks have value 0 in this field.

32-bit uid
this is stat(2) data

32-bit gid
this is stat(2) data

32-bit file size
This is the on-disk size from stat(2), truncated to 32-bit.

160-bit SHA-1 for the represented object

A 16-bit 'flags' field split into (high to low bits)

1-bit assume-valid flag

1-bit extended flag (must be zero in version 2)

2-bit stage (during merge)

12-bit name length if the length is less than 0xFFF; otherwise 0xFFF
is stored in this field.

(Version 3) A 16-bit field, only applicable if the "extended flag"
above is 1, split into (high to low bits).

1-bit reserved for future

1-bit skip-worktree flag (used by sparse checkout)

1-bit intent-to-add flag (used by "git add -N")

13-bit unused, must be zero

Entry path name (variable length) relative to top level directory
(without leading slash). '/' is used as path separator. The special
path components ".", ".." and ".git" (without quotes) are disallowed.
Trailing slash is also disallowed.

The exact encoding is undefined, but the '.' and '/' characters
are encoded in 7-bit ASCII and the encoding cannot contain a NUL
byte (iow, this is a UNIX pathname).

(Version 4) In version 4, the entry path name is prefix-compressed
relative to the path name for the previous entry (the very first
entry is encoded as if the path name for the previous entry is an
empty string). At the beginning of an entry, an integer N in the
variable width encoding (the same encoding as the offset is encoded
for OFS_DELTA pack entries; see pack-format.txt) is stored, followed
by a NUL-terminated string S. Removing N bytes from the end of the
path name for the previous entry, and replacing it with the string S
yields the path name for this entry.

1-8 nul bytes as necessary to pad the entry to a multiple of eight bytes
while keeping the name NUL-terminated.

(Version 4) In version 4, the padding after the pathname does not
exist.

== Extensions

=== Cached tree

Cached tree extension contains pre-computed hashes for trees that can
be derived from the index. It helps speed up tree object generation
from index for a new commit.

When a path is updated in index, the path must be invalidated and
removed from tree cache.

The signature for this extension is { 'T', 'R', 'E', 'E' }.

A series of entries fill the entire extension; each of which
consists of:

- NUL-terminated path component (relative to its parent directory);

- ASCII decimal number of entries in the index that is covered by the
tree this entry represents (entry_count);

- A space (ASCII 32);

- ASCII decimal number that represents the number of subtrees this
tree has;

- A newline (ASCII 10); and

- 160-bit object name for the object that would result from writing
this span of index as a tree.

An entry can be in an invalidated state and is represented by having
-1 in the entry_count field. In this case, there is no object name
and the next entry starts immediately after the newline.

The entries are written out in the top-down, depth-first order. The
first entry represents the root level of the repository, followed by the
first subtree---let's call this A---of the root level (with its name
relative to the root level), followed by the first subtree of A (with
its name relative to A), ...

=== Resolve undo

A conflict is represented in the index as a set of higher stage entries.
When a conflict is resolved (e.g. with "git add path"), these higher
stage entries will be removed and a stage-0 entry with proper resoluton
is added.

When these higher stage entries are removed, they are saved in the
resolve undo extension, so that conflicts can be recreated (e.g. with
"git checkout -m"), in case users want to redo a conflict resolution
from scratch.

The signature for this extension is { 'R', 'E', 'U', 'C' }.

A series of entries fill the entire extension; each of which
consists of:

- NUL-terminated pathname the entry describes (relative to the root of
the repository, i.e. full pathname);

- Three NUL-terminated ASCII octal numbers, entry mode of entries in
stage 1 to 3 (a missing stage is represented by "0" in this field);
and

- At most three 160-bit object names of the entry in stages from 1 to 3
(nothing is written for a missing stage).


2012/08/29

git diff之diff两个分支

常见的diff用法是比较工作区,index,HEAD或者是diff两个提交。除了这些用法之外diff还可以比较2个分支,用法如下:
git diff topic master     (1)
git diff topic..master    (2)
git diff topic...master   (3)

用法1,直接跟两个使用空格分隔的分支名会直接将两个分支上最新的提交做diff,相当于diff了两个commit。
用法2,用两个点号分隔的分支名,作用同用法1(装酷利器)
用法3,用三个点号分隔的分支名会输出自topic与master分别开发以来,master分支上的change。

需要注意的是这里的..和...不能与git rev-list中的..和...混淆。

2012/08/27

[GIT] 根据文件内容快速定位该文件在哪次历史提交中引入

今天一位同事找到我说到,如何能快速的定位一个文件的某个版本是在哪一次历史提交里引入的。

解决方案如下:
首先根据文件内容确定该文件对应的blob对象的object ID
git hash-object path/to/file
或者直接使用几个shell命令生成:
(echo -e -n "blob $(wc -c path/to/file | awk '{print $1}')\0" ; cat path/to/file) | shasum

得到对应blob对象的object ID以后,使用whatchanged查看该文件的变更历史,默认情况下whatchanged不会输出merge中修改的文件,为了避免遗漏,此处还是用-m来告诉whatchanged还要输出merge的结果:
git whatchanged -m -- path/to/file

此时可以搜索刚刚得到的blob的object ID。需要注意的是由于whatchanged默认显示blob变化信息时只使用blob ID的前7位,因此搜索时也要注意不能使用完整的object ID,也应该使用object ID的前几位。

ruby写了个脚本实现查找:

注:你看到此文章时,该脚本可能已经不是最新版本,需要最新的code请移步loveky's GITHUB

#!/usr/bin/ruby

# Author: loveky <ylzcylx@gmail.com>
# Blog  : http://loveky2012.blogspot.com

#Usage: find_it_in_history.rb --file <file> --repo <path/to/repo> --path_in_repo <file/path/in/repo>

require 'digest/sha1'
require 'optparse'

found   = false
commit  = nil
options = {}
OptionParser.new do |opts|
    opts.banner = "Find the commit who first introduced the specified version of a file"
    opts.on('--file FILE', 'Which file to check') do |value|
        options[:file] = value
    end
    opts.on('--repo REPO', 'which repo to check') do |value|
        options[:repo] = value
    end
    opts.on('--path_in_repo PATH', 'the path of the file in repo') do |value|
        options[:path_in_repo] = value
    end
end.parse!

file_content = File.open(options[:file]) {|f| f.read}
file_sha1    = Digest::SHA1.hexdigest("blob #{file_content.length}\0" + file_content)

puts("#{options[:file]} => #{file_sha1}")

Dir.chdir(options[:repo])

IO.popen("git whatchanged --oneline -m -- #{options[:path_in_repo]}") do |git_whatchanged|
    git_whatchanged.each_line do |line|
        if line[0,1] == ':'
            new_blob = (/\.\.\. ([0-9a-fA-F]{7})\.\.\./.match(line))[1]
            
            if new_blob == file_sha1[0,7]
                puts "Found blob(#{file_sha1[0,7]}) in commit " + commit
                found = true
            elsif found == true
                exit 0
            end
        else
            commit = line[0,7]   
        end
    end
end

2012/08/15

git命令之git stash

在日常的开发过程中,我们会常常会遇到这种情况:当前再分支1上进行开发,开发到一半,突然有需要去修改其他branch上的文件,这时可能会让你感到很尴尬,因为当前开发到一半,工作尚未完成,要么匆忙的进行一个“半成品”提交,要么就要reset工作区,否则当你checkout到分支2时,git会提示你:
error: Your local changes to the following files would be overwritten by checkout:
 b
Please, commit your changes or stash them before you can switch branches.
Aborting
组织你切换分支。

这时就可以用到git stash命令。git stash可以保存当前的工作进度,并reset工作区,这样在不丢失当前工作进度的前提下,你可以切换分支继续工作。
该命令的基本使用方法如下:

保存当前工作进度:
git stath save [message]

查看已经保存的工作进度列表:
git stash list

恢复指定的工作进度:
git stash apply [--index] [stash]

恢复指定的工作进度,并将其从进度列表中删除:
git stash pop [--index] [stash]

删除一个存储的进度:
git stash drop [stash]

删除所有存储的进度:
git stash clear

2012/08/14

说说git中tag的实现

今天下午跟Yi姐聊到git中tag的实现,整理成文如下。

tag的实现
在git中,常见的tag有两种,一种是轻量级的tag,一种是带说明的tag。

要创建一个新的tag是一个很简单的操作,创建轻量级tag只需git tag [tagname] 创建带说明的tag只需git tag [tagname] -m [message]。看似两者差别很小,但背后,在git内部,二者的实现却差别很大。

首先通过创建两个tag来开始今天的讨论:
gewang@LM-SHC-00355679@17:01:40:~/test/TryOurInternalGithub
=> git log --oneline -1 # 查看当前所在commit
ab2d6ee 2nd commit
gewang@LM-SHC-00355679@16:34:14:~/test/TryOurInternalGithub
=> find .git/objects/  
.git/objects/
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.idx
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.pack
gewang@LM-SHC-00355679@16:34:17:~/test/TryOurInternalGithub
=> git tag tag1 # 创建轻量级的tag1
gewang@LM-SHC-00355679@16:34:23:~/test/TryOurInternalGithub
=> find .git/objects/ # 没有新的object生成
.git/objects/
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.idx
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.pack
gewang@LM-SHC-00355679@16:34:26:~/test/TryOurInternalGithub
=> git tag tag2 -m "tag2" # 创建一个带说明的提交
gewang@LM-SHC-00355679@16:34:42:~/test/TryOurInternalGithub
=> find .git/objects/ # 生成了一个新的object 84458d
.git/objects/
.git/objects//84
.git/objects//84/458d8d078307134b8f6712d84c07f438ba6657
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.idx
.git/objects//pack/pack-37b8c10ba135fb7e9166a0ea483ab9597b15d039.pack

可以看到,二者深层次的差别在于是否会创建一个object来记录这个tag。到git repo中的tag命名空间查看:
gewang@LM-SHC-00355679@16:59:23:~/test/TryOurInternalGithub
=> ls .git/refs/tags/
tag1 tag2
接着查看两者的内容:
gewang@LM-SHC-00355679@17:00:34:~/test/TryOurInternalGithub
=> cat .git/refs/tags/tag1
ab2d6eea3cde6b1fac1784a6afb19a552a78689e
gewang@LM-SHC-00355679@17:01:14:~/test/TryOurInternalGithub
=> cat .git/refs/tags/tag2
84458d8d078307134b8f6712d84c07f438ba6657

可以看到轻量级的tag1的内容直接指向了当前的commit id。而带提交的tag2则指向了那个新生成的object。让我们来看看这个新object的真面目。
gewang@LM-SHC-00355679@17:04:17:~/test/TryOurInternalGithub
=> git cat-file -t 84458d8d078307134b8f6712d84c07f438ba6657
tag
gewang@LM-SHC-00355679@17:04:22:~/test/TryOurInternalGithub
=> git cat-file -p 84458d8d078307134b8f6712d84c07f438ba6657
object ab2d6eea3cde6b1fac1784a6afb19a552a78689e
type commit
tag tag2
tagger Michael Wang  Tue Aug 14 16:34:42 2012 +0800

tag2
可以看到新生成的object的类型是tag(这是git四个基本object类型中的一种,其他三种是commit,tree,blob)。可以看到这个object记录的5条信息,它指向了一个id为ab2d6eea3的对象,这个对象的类型为commit,这个tag的名字叫tag2,打tag的username是Michael,当天给的message是“tag2”。

至此,基本可以看出git中tag的实现方式,轻量级的tag直接通过一个tag文件指向一个commit id。带说明的tag会生成一个特定的tag对象,记录一系列和tag相关的信息。

那么实际操作中我们该使用哪种tag呢?一般情况下推荐使用带提交的tag,因为它除了基本的信息外,还记录了谁在什么时间创建这个tag,以及一些说明,这对以后查阅历史提供了更详尽的信息。

与server端交互
现在尝试将新创建的tag推送到server:
gewang@LM-SHC-00355679@17:04:25:~/test/TryOurInternalGithub
=> git push
Everything up-to-date
git提示我们所有的refs都已经up-to-date,表示git默认不会推送本地tag到server端,必须明确指定要推送的tag:
gewang@LM-SHC-00355679@17:16:24:~/test/TryOurInternalGithub
=> git push origin tag1
Total 0 (delta 0), reused 0 (delta 0)
To git@github.scm.corp.ebay.com:gewang/TryOurInternalGithub.git
 * [new tag]         tag1 -> tag1
gewang@LM-SHC-00355679@17:17:38:~/test/TryOurInternalGithub
=> git push origin tag2
Counting objects: 1, done.
Writing objects: 100% (1/1), 158 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.scm.corp.ebay.com:gewang/TryOurInternalGithub.git
 * [new tag]         tag2 -> tag2
在上边的例子中可以看到推送tag2时,向server端写了1个object,而tag1则没有,这也印证了2者的区别。

那么要如何才能从server获得tag呢?我们删除本地tag,执行git pull看看:
gewang@LM-SHC-00355679@17:20:45:~/test/TryOurInternalGithub
=> git tag -d tag1
Deleted tag 'tag1' (was ab2d6ee)
gewang@LM-SHC-00355679@17:20:54:~/test/TryOurInternalGithub
=> git tag -d tag2
Deleted tag 'tag2' (was 84458d8)
gewang@LM-SHC-00355679@17:20:54:~/test/TryOurInternalGithub
=> git pull
remote: Counting objects: 1, done.
remote: Total 1 (delta 0), reused 1 (delta 0)
Unpacking objects: 100% (1/1), done.
From github.scm.corp.ebay.com:gewang/TryOurInternalGithub
 * [new tag]         tag1       -> tag1
 * [new tag]         tag2       -> tag2
可以看到默认清空下,git pull会将server端的tag获取到本地。

以上就是关于git中tag的简要分析,有问题的朋友可以留言说明,我会尽快update。

git内部原理图及简要分析

今天早上画了一幅git的工作原理图,对应的repository在我的Github上,有兴趣的话可以clone下来对照着看。

 简单解释一下这张图吧:
1. git里commit,tree,blob是不同类型的object(第四种object是tag,这里不涉及)。
2. 每个object,不论类型,都有一个唯一ID,是一个SHA-1值。
3. tree对象用来记录一个目录的内容(包含的文件,子目录,以及他们的object id),blob对象用来某个文件特定版本的内容(不记录文件名,仅有文件内容)。
4. 每一个commit(除第一个外)都会记录parent commit(父提交),这样就可以从提交历史中的任何一个commit追溯到整个repository的第一个commit,形成一个完整的提交历史。
5. 每一个commit都会记录一个tree,这个tree就是本次提交的目录树中的顶层目录。
6. git会通过tree对象递归的(上一层tree对象会记录子目录的引用)找出所有子目录及 文件。
7. 文件名和blob对象的映射被记录在tree对象中,而不是blob对象中。
8. 多个不同文件名但是内容相同的文件只在repository中存储1份,不同的文件名在不同的tree对象中会引用同一个blob对象。



2012/08/13

git使用之如何撤销某个操作

本文聊聊在使用git的过程中,如何撤销一个操作,这个操作可能是git add,git commit,或是git merge,或是其他的。接下来分情况讨论:

情景一 文件被修改,还没有git add。
这时候文件还没有被添加到object database中,可以使用index中的版本来覆盖工作区。
git checkout -- [filename]

情景二 文件被修改,且已经git add。
此时修改过的文件已经作为一个新的object添加到object databse中,可以使用当前HEAD中的版本来覆盖index。
这时候文件还没有被添加到object database中,可以使用index中的版本来覆盖工作区。
git reset HEAD -- [filename]
如果还想重置工作区,可以继续:
这时候文件还没有被添加到object database中,可以使用index中的版本来覆盖工作区。
git checkout -- [filename]

情景三 已经提交commit,需要撤销commit。
这种情况又分为多种子情景,详细讨论,首先给出一个模拟的提交历史,从左到右一次提交,master执行提交H。
A---B---C---D---E---F---G---H
                            |
                          master
子情形1. 要撤销最近1个commit,要撤销H。
可以使用2个命令:
git reset --hard G

git revert H
二者区别在于git reset不会产生新的提交,而是将分支指向指定的commit,生成的提交历史为:
A---B---C---D---E---F---G---H
                        |  
                      master
而git revert会产生一个新的提交,生成的提交历史为:
A---B---C---D---E---F---G---H---I
                                |  
                              master

子情形2. 要撤销的提交不是最新的提交,即在错误提交之后又有若干个提交。
比如我们想撤销E,F两个提交,此时git reset已经无法胜任,因为我们在E,F后又有多个正常提交。这时可以使用git revert,但是要操作两次。这种情况下,推荐使用git rebase:
git rebase --onto D G^ H

情形四 执行merge操作,但是遇到conflict,想要撤销merge操作。
此时并为生成新的提交,当前分支头并未移动,因此可以简单的:
git reset --hard HEAD

情形五 执行merge操作,且已生成新的提交,要撤销merge操作。
这是简单的git revert 已经不能胜任。因为merge生成的提交有2个父提交,git不知道该使用哪个作为主线。这时需要添加一个额外的参数:
git revert -m [parent-number] [refs] 
-m指定要采用哪条主线(编号从1开始)。

如果还有其他情形本文未考虑到,请留言写出,大家一起讨论。

git命令之git fsck, git prune 和 git repack

这次说几个平时出场率不高的git命令。git fsck,git prune已经git repack。

首先说说fsck这个命令。该命令用来检查object database的完整性。例如是否所有commit均存在在object database中,是否有某个tree对象所引用到的blob对象丢失了,是否存在不被任何branch,tag所引用的"悬空"的commit。
几个常见参数有:
--root 报告当前repo的根节点,也即当前repo的第一个commit。
--tags 报告检查过的tags。
--no-reflog 将那些仅被reflog引用的commit视为unreachable。
--lost-found 根据对象的类型,将"悬空"对象写入到.git/lost-found/commit/ 或 .git/lost-found/other/目录中。
git fsck
dangling blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4
#有一个blob对象,没有被任何commit引用到。但是依旧占用磁盘空间,需要清理。

git fsck --root --tags
tagged commit b40c14f880c04fefcdce1328a2d6a537ed64b182 (test_tag1) in 00311b3f18af6306df2e5843d13a901681b677ef #一个tag
tagged commit f62a94635be5b72c9778a04aea116f6b4349eb61 (test_tag2) in 4de836025102abda82c0fd544a8725c82c46752f #另一个tag
root 63a763ead83edabcadac4f62a08b0d39b0d719b4 #当前repo的历史中第一次commit。
dangling blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4

上个示例中看到了有一个dangling blog,这种对象不被任何commit中的tree引用,但是却存在于object database中浪费磁盘空间,需要被清除。要实现类似object的清除,就需要用到第2个命令,git prune。
此命令没有太多的参数,直接git prune就可以了。如果在实际删除之前想要查看一下哪些object会被影响,可以使用git prune --dry-run来查看,该参数仅会输出将要被删除的object,并不会实际删除。
关于此命令,有一点需要注意:git prune只会删除unpacked object,如果一个对象已经被pack,那么git prune不会删除。

如果想要删除那些被pack的dangling文件要怎么样呢?第三个命令git repack就要出场了。
git repack -a -d
git repack -A -d
两者的区别在于:
-a 会直接删掉pack文件中的dangling对象。
-A 并不会立即删除,而是将pack文件中的dangling object重新以松散对象的形式存储在database中。也就是把dangling object从pack文件中"吐出来"。

关于3个命令的综合示例如下:

示例一
=> git log --oneline -3
b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
ec55218 1 on test
c1b13cb dd
=> touch test
=> echo test >> test
=> git add test
=> git commit -m "test git fsck"
[master d2d0bfa] test git fsck
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 test
=> git reset --hard HEAD^ #rollback到之前一个commit,制造一个dangling commit。
HEAD is now at b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
=> git fsck #因为刚刚的commit现在还可以通过reflog引用到,所以fsck并不认为它是unreachable。
=> git fsck --no-reflog #忽略reflog就可以看到
dangling commit d2d0bfa2ee4dfe767862cb441595db9748d8beab
=> git reflog expire --expire=now --all #删除reflog
=> git fsck #成功制造了一个dangling commit。
dangling commit d2d0bfa2ee4dfe767862cb441595db9748d8beab
=> git prune #删掉它!
=> git fsck

示例二
=> git log --oneline -3
b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
ec55218 1 on test
c1b13cb dd
=> touch test
=> echo test >> test
=> git add test
=> git commit -m "test fsck, prune and repack"
[master 09f3015] test fsck, prune and repack
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 test
=> git repack  #首先将新创建的commit打包。
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), done.
Total 3 (delta 0), reused 0 (delta 0)
=> git reset --hard HEAD^
HEAD is now at b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
=> git fsck
=> git fsck --no-reflog
dangling commit 09f3015743b00805e100259b9b4b0c230236a891
=> git reflog expire --expire=now --all
=> git fsck #制造了一个dangling commit。
dangling commit 09f3015743b00805e100259b9b4b0c230236a891
=> git prune
=> git fsck #prune无法删除已经pack的object。
dangling commit 09f3015743b00805e100259b9b4b0c230236a891
=> git repack -a -d #reapck -a -d 直接删除。
Counting objects: 173, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (94/94), done.
Writing objects: 100% (173/173), done.
Total 173 (delta 59), reused 173 (delta 59)
=> git fsck

示例三
=> git log --oneline -3
b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
ec55218 1 on test
c1b13cb dd
=> touch test
=> echo test >> test
=> git add test
=> git commit -m "test fsck, prune and repack -A"
[master 8df4fa5] test fsck, prune and repack -A
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 test
=> git repack
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), done.
Total 3 (delta 0), reused 0 (delta 0)
=> find .git/objects/ # 当前database中的文件。
.git/objects/
.git/objects//8d
.git/objects//8d/f4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
.git/objects//9d
.git/objects//9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4
.git/objects//e6
.git/objects//e6/a38484a68f1d818090516266a1fe5115bedb2d
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-f82243c549f6e21d59eb4b3cd90c1f7ea4bc2b2f.idx
.git/objects//pack/pack-f82243c549f6e21d59eb4b3cd90c1f7ea4bc2b2f.pack
.git/objects//pack/pack-ff6a31c9b76b7a762bf76eb2500048a55e0e076d.idx
.git/objects//pack/pack-ff6a31c9b76b7a762bf76eb2500048a55e0e076d.pack
=> git gc # 清理一下。
Counting objects: 176, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (96/96), done.
Writing objects: 100% (176/176), done.
Total 176 (delta 60), reused 175 (delta 59)
=> find .git/objects/ #现在干净多了。
.git/objects/
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-25320b3227fb4b00d05585542643651019b1a041.idx
.git/objects//pack/pack-25320b3227fb4b00d05585542643651019b1a041.pack
=> git reset --hard HEAD^
HEAD is now at b841691 Merge branch 'test' of github.scm.corp.ebay.com:gewang/TryOurInternalGithub
=> git fsck
=> git fsck --no-reflog
dangling commit 8df4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
=> git reflog expire --expire=now --all
=> git fsck #制造dangling commit。
dangling commit 8df4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
=> git prune
=> git fsck
dangling commit 8df4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
=> git repack -A -d #这次使用-A
Counting objects: 173, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (94/94), done.
Writing objects: 100% (173/173), done.
Total 173 (delta 59), reused 172 (delta 59)
=> git fsck #这货还在
dangling commit 8df4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
=> find .git/objects/ #对应的松散对象也出现啦。
.git/objects/
.git/objects//8d
.git/objects//8d/f4fa5b683a6c2614d0bb5f6a9bbd0df6e76ebb
.git/objects//9d
.git/objects//9d/aeafb9864cf43055ae93beb0afd6c7d144bfa4
.git/objects//e6
.git/objects//e6/a38484a68f1d818090516266a1fe5115bedb2d
.git/objects//info
.git/objects//info/packs
.git/objects//pack
.git/objects//pack/pack-ff6a31c9b76b7a762bf76eb2500048a55e0e076d.idx
.git/objects//pack/pack-ff6a31c9b76b7a762bf76eb2500048a55e0e076d.pack

2012/08/10

git知识测试题—答案及解析

首先给出答案:ABCD BCDA CDAB DABC ABCD

解答如下:

题目1
这种情况往往是修改了工作区的文件,但是没有运行git add来将修改的文件添加到index中。正确方法为git add需要commit的文件,再执行git commit。
git commit --allow-empty只会创建一个空提交,工作区的内容仍旧未提交至git repo里。
git commit -a 会直接提交所有修改/删除的文件,通常不推荐此操作,因为很可能会将尚未ready的改变一并提交至git仓库。
git commit --amend 会进行一次修补提交,即不会产生一个新的commit,而是将此次commit的change直接apply到最近一次提交,提交相关信息不变。

题目2
git reset -- hello.c 只能恢复index中的文件,工作区的文件并不会被重置。
git checkout HEAD -- hello.c 同时恢复index与工作区中的文件。
git revert hello.c 非法操作
git update hello.c 非法操作

题目3
每次执行git add时,git会将修改过的文件以一个新object的形式加入到git仓库中,因此,如果此时执行git reset --hard,则这些新加入的object已经不被任何commit引用(包括index),此时运行git fsck会发现,每一次git add操作添加的文件版本都会显示为一个dangling object。

题目4
git add -A 添加所有文件,包括新添加的文件。
git add -p 通过逐一检查工作区文件与index文件diff输出来决定是否添加文件。
git add -i 交互式的选择要git add的文件。
git add -u 添加所有修改的文件,不包括新添加的文件。

题目5
git reset --hard 会重置当前branch
git checkout 只会改变HEAD,不会影响当前branch
git rebase 会嫁接连续的rev list到new base,进而改变历史
git commit --amend 会修改最近一次commit

题目6
这里的主要区别在于使用.gitignore还是.git/info/exclude。两者的主要区别在于.gitignore会被push到server上与其他人共享,而.git/info/exclude完全工作在本地。

题目7
git config --global core.autocrlf true 版本库中使用LF,而检出时始终使用CRLF。
git config --global core.autocrlf input 检出时始终使用LF。
在.gitattributes中设置* text=auto 如果core.eol未设置,则根据操作系统不同,在windows上使用CRLF,在linux上使用LF。

题目8
gitolite的写授权可以针对文件路径,分支。但是只有全局的读权限。

题目9
git revert 不会改变现有历史,而是生成一个新的commit来反转指定commit中的change。

题目10
git reset --hard 适用于要移除的commit为当前branch上最新的commit,否则会移除其他正确的commit。
git checkout 只会改变HEAD头,不会对branch历史修改。
git revert 可以撤销某个commit的change,但是不会在历史中删除该commit。

题目11
git reset -- filename 用于使用HEAD中的filename覆盖index中的版本。

题目12
参考git help describe

题目13
git clone会fetch所有远程branch到本地,但只有远程仓库HEAD指向的branch会在本地checkout。

题目14
使用git push origin :XX可以删除远程仓库中的XX分支。
git branch -D xx 无法删除当前所在分支。
默认执行git fetch,只会fetch远程仓库存在的分支。
默认执行git push,只会push在远程仓库存在对应分支的本地分支。
关于git pull/git push的默认操作,请参考我的另一篇文章 http://loveky2012.blogspot.com/2012/08/default-behaviour-of-git-pull-and-git-push.html

题目15
git rev-list master..maint 会输出所有出现在maint历史中但未出现再master历史中的commit列表。如果列表为空则表示master分支已经包含所有maint上的bugfix。
git log master..maint 原理同上。
在maint分支上执行git merge --ff-only master成功表示在master分支的历史中已经包含了maint分支上的change,因此此时merge master分支可以直接移动maint分支指向的commit进行快进合并。

题目16
当目录merge出现confilct时,可以使用git ls-files -s查看当前staged的object的状态。
263 gewang@LM-SHC-00355679@13:55:04:~/test/show
=> git merge master
Auto-merging test
CONFLICT (content): Merge conflict in test
Automatic merge failed; fix conflicts and then commit the result.
264 gewang@LM-SHC-00355679@13:55:07:~/test/show
=> git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 aaa
100644 388ddf68417917d4c0e0faa74432c1c2e74d572e 1 test
100644 733205e05f85d7b51b96cbef2938d61bf304798b 2 test
100644 23bc1c902ffde28525f215cd2bfc44b6f4222709 3 test
可以看到test文件出现了3个stage的状态,分别为1,2,3。而没有出现冲突的文件的stage状态为0.
针对存在confilict的文件可以:
git show :1:test  # 查看冲突文件的common ancester版本
git show :2:test  # 查看本地branch中的版本
git show :3:test  # 查看远程branch中的版本

题目17
git config --global push.default upstream 不带参数push是仅推送当前branch
git config --global pull.rebase true 在执行git pull时,将远程分支获取到本地后使用rebase,而不是merge来将远程change应用到本地分支。
git config --global receive.denyDeletes true 设置此选项则git-receive-pack不会响应分支删除操作。

题目18
git gc会调用git reflog expire --all,而git reflog expire默认只会删除90天前的记录。如果想即使生效,则应该使用git reflog expire --expire=now --all。

题目19
详情请参考submodule文档。

题目20
用过JIRA,Redmine之类工具的都懂得。。。

git知识测试题

从网上看到的一套测试题,全部单选,稍后放出答案以及详细分析。


1 如果提示提交内容为空、不能提交,则最为合适的处理方式是:_____

a) 执行 git status 查看状态,再执行 git add 命令选择要提交的文件,然后提交。

b) 执行 git commit --allow-empty ,允许空提交。

c) 执行 git commit -a ,提交所有改动。

d) 执行 git commit --amend 进行修补提交。

2 如果把项目中文件 hello.c 的内容破坏了,如何使其还原至原始版本? _____

a) git reset -- hello.c

b) git checkout HEAD -- hello.c

c) git revert hello.c

d) git update hello.c

3 修改的文档 meeting.doc 尚未提交,因为错误地执行了 git reset --hard 导致数据丢失。丢失的数据能找回么? _____

a) 不能。执行硬重置使工作区文件被覆盖,导致数据丢失无法找回。

b) 能。可以通过 git checkout HEAD@{1} -- meeting.doc 找回。

c) 不确定。如果在重置前执行了 git add 命令将 meeting.doc 加入了暂存区,则可以在对象库中处于悬空状态的文件中找到。

d) 不能。因为未提交所以无法找回。

4 仅将工作区中修改的文件添加到暂存区(新增文件不添加),以备提交,用什么命令标记最快? _____

a) git add -A

b) git add -p

c) git add -i

d) git add -u

5 下面哪一个命令 不 会改变提交历史? _____

a) git reset --hard HEAD~1

b) git checkout HEAD^^ .

c) git rebase -i HEAD^^

d) git commit --amend

6 我使用和其他人不一样的IDE软件,总是在目录下生成以 .xx 为后缀的临时文件。如何避免由于自己的误操作导致此类文件被添加到版本库中呢? _____

a) 执行 git clean -f 删除临时性文件。

b) 向版本库中添加一个 .gitignore 文件,其中包含一条内容为 *.xx 的记录。

c) 在文件 .git/info/exclude 中添加一条内容为 *.xx 的记录。

d) 更换另外一款IDE软件。

7 项目跨平台导致文件中的换行符不一致。其中有 Linux 格式换行符(0A),也有DOS格式换行符(0D 0A)。要如何避免此类情况呢? _____

a) 修改 /etc/gitattributes 文件,在其中包含一条内容为 * text=auto 的设置。

b) 执行命令 git config --global core.autocrlf true 。

c) 执行命令 git config --global core.autocrlf input 。

d) 向版本库中添加一个 .gitattributes 文件,在其中包含一条内容为 * text=auto 的设置。

8 下列对于版本库授权说法正确的是:_____

a) 可以为分支或路径设置不同的写入权限,但不能设置不同的读取权限。

b) 除管理员外,版本库的创建者都可以为自己创建的版本库授权。

c) 只要通过授权后,便不能限制所推送的提交的署名作者,可以是任何人。

d) 如果没有向版本库的写入权限,就一定没有读取权限。

9 取消服务器版本库中ID为 a2387 的提交,而且不能引起历史提交的变更,用什么操作? _____

a) git rebase -i a2387^

b) git checkout a2387^ -- .

c) git revert a2387

d) git reset --hard a2387^

10 从版本库中的历史提交中彻底移除ID为 a2387 的提交,用什么操作? _____

a) git reset --hard a2387^

b) git checkout a2387^ -- .

c) git revert a2387

d) git rebase --onto a2387^ a2387 HEAD

11 所有改动的文件都已加入暂存区,若希望将其中的 other.py 文件下次再提交,如何操作? _____

a) git reset -- other.py

b) git checkout -- other.py

c) git checkout HEAD other.py

d) git reset --hard -- other.py

12 若产品的版本号显示为 1.7.10.rc0-33-g9678d-dirty ,可以判断出此版本号是如何生成的么? _____

a) git tag

b) git describe --tags --always --dirty

c) git name-rev HEAD

d) git --version

13 关于 git clone 下面说法 错误 的是:_____

a) 克隆时所有分支均被克隆,但只有HEAD指向的分支被检出。

b) 可以通过 git clone --single-branch 命令实现只克隆一个分支。

c) 克隆出的工作区中执行 git log 、 git status 、 git checkout 、 git commit 等操作不会去访问远程版本库。

d) 克隆时只有远程版本库HEAD指向的分支被克隆。

14 关于删除分支 XX ,下列说法正确的是: _____

a) 执行 git push origin :XX 来删除远程版本库的 XX 分支。

b) 执行 git branch -D XX 删除分支,总是能成功。

c) 远程版本库删除的分支,在执行 git fetch 时本地分支自动删除。

d) 本地删除的分支,执行 git push 时,远程分支亦自动删除。

15 下面的操作中哪一个不能确认维护分支 maint 上所有的 bugfix 提交均已合并至当前分支 master 中。 _____

a) git rev-list ..maint 的输出为空。

b) 在 maint 分支成功地执行 git merge master 。

c) git log ..maint 的输出为空。

d) 新版本发布,在 maint 分支执行 git merge --ff-only master 成功。

16 一个图片文件 logo.png 冲突了,如何取出他人的版本。 _____

a) git show :1:./logo.png > logo.png-theirs

b) git show :2:./logo.png > logo.png-theirs

c) git show :3:./logo.png > logo.png-theirs

d) git show :0:./logo.png > logo.png-theirs

17 工作在特性分支,常常因为执行 git push 默认推送所有本地和远程共有分支,导致非当前分支报告 non-fast-forward 错误。如果设置只推送当前分支可避免此类问题。下面操作正确的是:_____

a) git config --global push.default upstream

b) git config --global pull.rebase true

c) git config --global receive.denyDeletes true

d) git config --global pager.status true

18 于对象库(.git/objects)说法 错误 的是:_____

a) 两个内容相同文件名不同的文件,在对象库中仅有一个拷贝。

b) 对象库执行 git gc 操作后,reflog 会被清空导致其中记录的未跟踪提交及指向的文件被丢弃。

c) 删除文件后,再通过添加相同文件找回,不会造成版本库的冗余。

d) 对象库并非一直保持最优存储,而是通过周期性地执行 git gc 优化版本库。

19 关于子模组 错误 的说法是:_____

a) 克隆父版本库,默认不会克隆子模组版本库。

b) 子模组可以嵌套。执行 git submodule update --recursive 可对嵌套子模组进行更新。

c) 子模组和父版本库的新提交,要先推送父版本库,后推送子模组。

d) 子模组检出处于分离头指针状态(gitlink的指向),在子模组中工作需要手动切换分支。

20 当一个提交说明显示为 souce code refactor (fix #529) ,下面哪个说法是正确的? _____

a) 这个提交只是代码重构,并未修复任何东西,因此没有改变版本库的提交历史。

b) 这个提交修正了第529号提交,没有改变版本库的提交历史。

c) 这个提交撤销了第529号提交,改变了版本库的提交历史。

d) 这个提交和项目的缺陷跟踪平台(如Redmine)关联,并会更新相关问题的状态。

2012/08/09

ruby中case语句的默认行为

通常情况下,大家会这样使用case语句
name = case x
        when 1; "one"
        when 2; "two"
        when 3; "three"
        else "many"
       end

在这种形式的case语句中,ruby会对case后跟的语句进行一次求职,接着按出现的先后顺序逐一和when语句所跟的表达式比较。如果找到匹配项则执行相应操作并返回,否则执行else块的语句并退出。

前边说的这些case的行为很容易理解,接下来说说需要我们注意的地方,即case语句是如何将我们提供的表达式逐一和when语句比较的,比较的标准是什么。事实上,这里的比较是通过===操作符实现的。也就是说之前的示例实际上是按一下逻辑执行的:

name = case
        when 1 === x; "one"
        when 2 === x; "two"
        when 3 === x; "three"
        else "many"
       end

===操作符针对不同class的行为不同,逐一介绍

Fixnum类 ===操作针对Fixnum类的行为与==操作相同,仅是比较操作符两端的数值是否相等。
Class类 ===操作会检查操作符右边的项是否是左边项所指定的class的一个实例。
Regexp类 ===操作会检查操作符右边的项是否匹配左边项所指定的正则表达式。
Range类 ===操作会检查操作符右边的项是否落在左边项所指定的区间内。

示例代码:
puts case x
  when String then "string"
  when Numeric then "number"
  when TrueClass, FalseClass then "boolean"
  else "other"
  end

while line=gets.chomp do
  case line
    when /^\s*#/
      next
    when /^quit$/i
      break
    else
      puts line.reverse
  end
end

git命令之git filter-branch

今天说说git里一个很重量级的命令——git filter-branch。
git filter-branch [--env-filter ] [--tree-filter ]
               [--index-filter ] [--parent-filter ]
               [--msg-filter ] [--commit-filter ]
               [--tag-name-filter ] [--subdirectory-filter ]
               [--prune-empty]
               [--original ] [-d ] [-f | --force]
               [--] [...]
这个命令会按照指定的方式(一系列过滤器,filter)修改给定的commit,几乎可以针对历史commit执行你想要的任何操作。比如,从历史中删除某个文件,修改commit message等。

如果你没有指定任何filter,那么git filter-branch则会原封不动的将所有历史重新commit一次。

改写历史后,所有的object都会有新的object name。因此,你将不能轻易的将改写后的branch推送到server端(non-fastforward issue)。请不要轻易使用该命令,除非你很清楚该命令对git repo的影响。

每次执行git filter-branch操作后,务必检查改写后的历史是否正确。为了避免因操作错误造repo损坏,git会在filter-branch操作实际改写历史时,自动将原refs备份到refs/original/下。

目前,git filter-branch支持以下filter
--env-filter
--tree-filter
--index-filter
--parent-filter
--msg-filter
--commit-filter
--tag-name-filter
--subdirectory-filter
当同时指定多个filter时,会按照上述列表中列出的先后顺序执行相应filter。在执行由filter设置的操作时,git会自动设置以下系统变量:
$GIT_COMMIT
$GIT_AUTHOR_NAME
$GIT_AUTHOR_EMAIL
$GIT_AUTHOR_DATE
$GIT_COMMITTER_NAME
$GIT_COMMITTER_EMAIL
$GIT_COMMITTER_DATE
这些变量包含了改写操作执行前当前commit的相关信息。

下面介绍几个git filter-branch的参数
--original  
使用此参数指定改写历史操作生成的历史备份的存放位置,默认位置为refs/original。 

-d 使用此参数指定要使用的临时目录的路径。默认使用.git-rewrite/。 
-f, --force 默认情况下,如果当前repo已有以refs/original/开头的refs,或者临时目录已经存在,则git filter-branch会拒绝执行。使用此参数以强制执行改写操作。 

示例 
以下两个命令都可以将filename指定的文件从历史中永久删除。
git filter-branch --tree-filter 'rm filename' HEAD
git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD

2012/08/06

git merge命令之--no-ff

今天介绍一个git merge命令的参数,--no-ff。git help merge里对该参数的解释为:
With --no-ff Generate a merge commit even if the merge resolved as a fast-forward.
意思是即使此次merge可以fast-forward,也要创建一个新的commit。

接下来通过两个简单的实验来演示这个参数的效果。实验模拟日常开发中一个最简单的流程,从master上创建test分支,进行开发,将test分支merge回master分支,最后删除test分支。

首先看看不使用--no-ff的情况。
开始操作前通过SmartGit查看log:








现在执行命令
git checkout master
git merge test
此时log如下:








最后 git branch -d test 删除test分支,








可以看到,此时丝毫看不到之前test分支开发的影子。如果想保存之前分支开发的影子我们可在merge时使用--no-ff参数,
git merge --no-ff test
效果如图









即使删除test分支后依然可以看到之前分支的记录:

不带参数执行git pull & git push

首先贴出两段摘自《Git权威指南》中的话。

当不带任何参数执行git push命令时,实际执行过程是:
1. 如果为当前分支设置了<remote>,即由配置branch.<branchname>.remote给出了远程版本库代号,则不带参数执行git push相当于执行了git push <remote>。
2. 如果没有为当前分支设置<remote>,则不带参数执行git push相当于执行了git push origin。
3. 要推送的远程版本库的URL由remote.<remote>.pushurl给出。如果没有配置,则使用remote.<remote>.url配置的URL地址。
4. 如果为注册的远程版本库设置了push参数,即通过remote.<remote>.push配置了一个引用表达式,则使用该引用表达式执行推送。
5. 否则使用“:”作为引用表达式。该表达式的含义是同名分支推送,即对所有在远程版本库中有同名分支的本地分支执行推送。
当不带任何参数执行git pull命令时,实际执行过程是:
1. 如果为当前branch设置了<remote>,即由配置branch.<branchname>.remote出了远程版本库代号,则不带参数执行git pull相当于执行了git pull <remote>。
2. 如果没有为当前分支设置<remote>,则不带参数执行git pull相当于执行了git pull origin
3. 要获取的远程版本库的URL地址由remote.<remote>.url给出。
4. 如果为注册的远程版本库设置了fetch参数,即通过remote.<remote>.fetch配置了一个引用表达式,则使用该引用表达式执行获取操作
5. 接下来要确定合并分支。如果设定了branch.<branchname>.merge,则对其设定的分支执行合并,否则报错退出。
 下面说说自己的理解:
1. 首先说说remote.<remote>.push和remote.<remote>.fetch这两个配置项。remote.<remote>.push是没有缺省值的,而remote.<remote>.fetch有缺省值为"+refs/heads/*:refs/remotes/<remote>/*"。因此当不带参数执行git pull时,将会强制将远程所有branch强制更新到本地refs/remotes/<remote>/下。
2. 当带参数执行git pull时,例如git pull origin test。由于指定的remote以及branch名。因此将直接update本地test分支,但不会更新remotes/origin/test分支。因此当git pull origin test后,再次checkout到test分支时,会出现类似“Your branch is ahead of the tracked remote branch 'origin/test' by 1 commit. ”的提示信息。
3. 前面说到remote.<remote>.push没有缺省值,这时会使用“:”作为引用表达式。即对所有在远程版本库中有同名分支的本地分支执行推送。git push的这个缺省行为可以通过配置选项push.default设置。详细参数可以参考git help config。