标签归档:Git

git completion

git源码包提供了命令行下git自动补全的脚本git-completion.bash,下载地址: github.comkernel.org

它能补全的内容包括:
*) git子命令,git subcmd。
*) git长选项参数,形如git subcmd --long-options。
*) 目录和文件名。
*) 本地和远程分支名。
*) 本地和远程tag名。
*) .git/remotes目录的文件名称。

安装流程:

1. 将git-completion.bash文件cp到某个目录并重命名,例如~/.git-completion.bash,加点变成隐藏文件可以减少视觉干扰。
2. 打开~/.bash_profile或者~/.bashrc,添加:

1
source ~/.git-completion.bash

3. 重启或新建Shell。OK。

--EOF--

git hooks

git允许用户在执行一条git命令的前后通过脚本的方式进行一些预处理或善后工作,这些脚本称为hook,也就是钩子。钩子文件统一存放在在.git/hooks目录下,一些常用的钩子介绍在这里可以找到。git hooks配合git config,可以构建一个工作流,非常方便。

比如有个场景,一个仓库下有多个分支,每个分支依赖这不同的配置文件,里面的配置项在不同的开发机上不同(IP,MAC地址等),当有多个开发人员协作开发时,会因为别人的一次提交覆盖自己本地仓库里的配置(加.gitignore也解决不了,git clone时会缺失这个文件)。这时候就可以利用post-checkout这个钩子,每次执行git checkout命令后,post-checkout中的shell脚本会读取本机配置,写入项目配置文件。

一个简单的钩子例子:

1
2
3
#!/bin/sh
 
echo $(git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* .*/\1/')

将上述脚本保存为post-checkout,放在.git/hooks目录下,今后每次执行git-checkout命令都会在控制台显示当前分支名称。

--EOF--

批量git-cherry-pick

Git从1.7.2版本开始支持批量cherry-pick,就是一次可以cherry-pick一个区间的commit。

用法如下:

1
2
3
$ git cherry-pick <start-commit-id>..<end-commit-id>
或
$ git cherry-pick <start-commit-id>^..<end-commit-id>

前者表示把<start-commit-id>到<end-commit-id>之间(左开右闭,不包含start-commit-id)的提交cherry-pick到当前分支;后者表示把<start-commit-id>到<end-commit-id>之间(闭区间,包含start-commit-id)的提交cherry-pick到当前分支。其中,<start-commit-id>到<end-commit-id>只需要commit-id的前6位即可,并且<start-commit-id>在时间上必须早于<end-commit-id>。

下面这个场景可以使用批量cherry-pick:
有两个分支,master和develop,develop有一部分功能比较紧急,需要优先上线,所以develop的一部分提交要合并到master分支。通过git-merge命令,忽略一些commit也可以临时满足要求,说“临时”是因为develop分支上剩下的提交迟早也要合到master上去,所以通过git-merge忽略分支的做法不大可行。这时候使用git-chrry-pick进行批量操作比较方便。

举个栗子:
1. master分支下只有1次提交。

1
2
(master)$ git log --pretty=oneline
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

2. develop分支下有10次提交。

1
2
3
4
5
6
7
8
9
10
11
(develop)$ git log --pretty=oneline
91b8696560ce3c2fe4b282d4fa814447c0d93cd1 10
e97cc3c92634c8278e1c60b9053d735033db956a 9
58febd437b20391611f90ebeedea0d024345655f 8
562130942862e44a11009b60bb9bddbaa23dc21f 7
dd627ee763bdc3190f6ebd199f74b9a67978b30c 6
9b68ddf74e50573e4cdc6542fb17c182c12e3e07 5
b1d63e7181bb9ef996a90368a9c5a840f201ae80 4
793cd10cbab4ba147b0011d4bdf882d050ff6435 3
1739e414db9709821ae253187a7ce38c56bf6f2d 2
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

3. 现在要把develop分支上的3-4,7-10次提交合并到master分支,2、5和6暂时不合。

1
2
(master)$ git cherry-pick 1739e4..b1d63e //(2, 4]
(master)$ git cherry-pick 562130^..91b869 //[7,10]

4. 检查master分支log,所需提交已经合并完成。

1
2
3
4
5
6
7
8
(master)$ git log --pretty=oneline
b195b40627f333f2e8f2fa0429d099364b50dd65 10
66ca86797023d05eda47804b325c575ee976d2f0 9
e677961d84dc77771086e3cca0b04fe35bd00046 8
e0dcac8a3786e4b96455a5d1ddac1e30631ee8af 7
87fad31564e801d143ea0a88329a174127a9b477 4
1dbd37e4728b8a92117f2070d057aaff426ce3fa 3
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

--EOF--

git回滚

git-reset命令可以让本地仓库回滚到之前的任意版本。例如:

1
git reset commit-id

如果加--hard参数,会抹掉当前工作区、暂存区乃至仓库中所有在命令中commit-id之后提交的修改。这样,在本地仓库通过git-log命令就看不见被回滚掉的commit了。

假如一份代码需要多人协作,托管在远程服务器(例如GitHub)上,本地仓库回滚后还要push到服务器上,如果直接执行:

1
$ git push origin

服务器会返回:

1
2
3
4
5
6
To https://github.com/fengchj/test.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/fengchj/test.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.

这时候要给git-push命令加-f参数,表示强制push。如:

1
2
3
4
$ git push -f origin
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/fengchj/test.git
 + d1e6f05...087898e master -> master (forced update)

服务器上的HEAD指针也被回滚到之前的某个版本。如果此时客户端git-clone一个仓库,便看不到被回滚掉的commit。

但是假如在强制push之前,有客户端已经克隆了一个仓库,看到了本来应该被回滚掉的commit,此时即使他执行git-fetch或git-pull命令,也无法将本地仓库的HEAD指针与服务器上的同步起来,本地HEAD会领先服务器HEAD好几个commit。这时需要这个客户端执行git-reset命令,同步本地仓库的HEAD指针与服务器HEAD,使他们指向同一个commit。如:

1
2
3
$ git reset origin/master
或
$ git reset --hard origin/master

执行过后git-log命令就看不到被其他客户端回滚掉的commit了。

--EOF--

虾米收藏歌曲下载器

使用虾米的时间不是很长,但多少也积累了一些收藏歌曲,很想把它们都下下来,趁这两天有空,就开始付诸实践,顺便可以巩固下Python。现在基本功能已经实现,代码已上传到GitHub:https://github.com/fengchj/xiami-favsong-downloader

虾米收藏歌曲下载器的基本功能包括:
1. 下载指定用户收藏的歌曲。
2. 自定义歌曲下载目录。
3. 格式化歌曲ID3信息。

后续会在本地曲库中维护一个元数据文件,加入判断当前歌曲是否已经在本地曲库中的功能,避免每次运行程序都从头开始下载。另外可能会加入下载收藏专辑、收藏歌手的Top100歌曲的功能。

用法:
1. 启动程序: python downmusic.py。
2. 输入用户在虾米网的userid。userid是一串数字,进入用户首页,如http://www.xiami.com/u/3270716, 3270716就是userid。
3. 输入歌曲的下载目录。默认是程序所在目录。

下载完成后,会分别显示下载成功和失败的次数:

这里先说说虾米收藏歌曲下载器的基本思路:
1. 获取用户收藏的歌曲列表。获取用户收藏的歌曲列表目的是拿到歌曲ID列表。虾米无需登陆就能查看任意用户收藏的歌曲列表,查看地址为:

1
http://www.xiami.com/space/lib-song/u/{userid}/page/{pageno}

{userid}表示用户id,{pageno}表示指定页的收藏歌曲列表。这个地址返回的是个HTML文件,其中每首歌都以"<a title="Set Fire to the Rain" href="/song/1769915737">Set Fire to the Rain</a>"的格式呈现,我们目的是拿到歌曲ID,所以只需正则匹配出"/song/1769915737"部分就可以了,其他信息可以从歌曲的元信息XML中拿到。遍历用户的所有收藏歌曲页面,匹配"/song/\d{1,20}",直到匹配结果为空结束,得到用户收藏的歌曲ID列表。

2. 获取歌曲元信息(包含歌曲名、歌手和链接地址等)。在试听一首歌时,虾米先到http://www.xiami.com/song/playlist/id/{song_id}/object_name/default/object_id/0请求一个包含歌曲元信息(歌名、歌手、专辑名和加密链接地址等)的XML文件,这个地址对所有歌曲有效,用的时候只需把{song_id}替换成歌曲的ID即可。

比如,Adele的『Set Fire to the Rain』歌曲页面为:http://www.xiami.com/song/1769915737,歌曲ID为1769915737,元信息XML地址为:

1
http://www.xiami.com/song/playlist/id/1769915737/object_name/default/object_id/0

在浏览器打开它,返回的XML信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track>
<title><![CDATA[Set Fire to the Rain]]></title>
<song_id>1769915737</song_id>
<album_id>253320</album_id>
<album_name><![CDATA[21]]></album_name>
<object_id>1</object_id>
<object_name>default</object_name>
<insert_type>1</insert_type>
<background>http://img.xiami.com/res/player/bimg/bg-5.bak.jpg</background>
<grade>-1</grade>
<artist><![CDATA[Adele]]></artist>
<location>
4h%2Fxit2522%519383.t3Ffi.%3%5%2E7179%mtA%1an24235F565_25pp%2.meF8F3E%_9724E3
</location>
<ms></ms>
<lyric>http://img.xiami.com/./lyric/upload/37/1769915737_1356124042.lrc</lyric>
<pic>http://img.xiami.com/images/album/img85/23485/2533201341353998_1.jpg</pic>
</track>
</trackList>
<type>default</type>
<type_id>1</type_id>
<clearlist></clearlist>
</playlist>

用正则匹配找出歌曲名(title节点),歌手(artist节点),歌曲链接(location节点)。

3. 获取歌曲的真实链接地址。从XML中location节点得到的地址是加过密的,不过虾米只对歌曲链接进行了简单的加密,网上已有大把分析文章,很容易破解。歌曲链接采用的是栅栏加密算法,将明文以列顺序写为几行,然后按照行的顺序读出来生成密文。
比如明文"attack-at-once"的加密过程为:

1
2
3
4
5
1) 将attack-at-once以列序写成三行。
aa--e
tcao
tktc
2) 按行读取生成密文"aa--etcaotktc"。

知道了栅栏加密的原理后,只需得到加密时的行数,就能从密文中反解出明文。虾米用location的第一个字符表示行数,因此歌曲『Set Fire to the Rain』的location反解过程分为:

1
2
3
4
5
6
7
8
9
10
11
1) 将密文分行:
密文:4h%2Fxit2522%519383.t3Ffi.%3%5%2E7179%mtA%1an24235F565_25pp%2.meF8F3E%_9724E3
 
4
h%2Fxit2522%519383.
t3Ffi.%3%5%2E7179%m
tA%1an24235F565_25p
p%2.meF8F3E%_9724E3
 
2) 按列读取明文:
http%3A%2F%2Ff1.xiami.net%2F23485%2F25332%5E%2F%5E5_1769915737_289243%5E.mp3

将明文进行URLDecoder后,得到:

1
http://f1.xiami.net/23485/25332^/^5_1769915737_289243^.mp3

将链接中的'^'符号替换成'0'后,就得到了最终的歌曲下载地址:

1
http://f1.xiami.net/23485/253320/05_1769915737_2892430.mp3

4. 格式化歌曲的ID3信息。通过程序下载下来的虾米歌曲ID3信息丢失严重,对于喜欢从手机或者电脑客户端通过Last.FM Scrobbler同步音乐播放记录的人来说,这是个严重问题。解决这个问题的方法是从歌曲元信息XML文件中得到信息较为准确的歌手和歌名信息,再以标准方式写入音频文件。

以上是基本思路,接下是Python代码层面上处理的一些问题:
1. 使用urllib2模块模拟浏览器操作,获取HTTP响应内容,它提供的API可以像读取本地文件一样获取网络上的数据。程序中获取用户收藏歌曲列表和获取歌曲元信息XML都用到这个模块的API。

1
2
3
4
5
6
7
headers = {
    'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) Chrome/24.0'
}
request = urllib2.Request(address, headers = headers)
response = urllib2.urlopen(request)
text = response.read()
print text

HTTP Header中User-Agent信息必不可少,少了它服务器会返回一个503: Service Temporarily Unavailable,估计虾米用它来挡掉一部分无聊的流量吧。

2. 下载歌曲使用了urllib模块的urlretrieve API。这里也能用urllib2 API来实现,两个模块虽有一些区别,但是可以用任何一个实现下载文件功能。选择urllib因为用它代码少:

1
urllib.urlretrieve(location ,path)

其中,location为歌曲的下载地址,path为本地歌曲的存放路径。

3. 正则匹配。从网上抓数据少不了正则匹配(re模块)。程序中有两个地方用到正则匹配,一是从歌曲元信息XML中获取感兴趣的信息:歌名、歌手、歌曲链接地址。二是从用户收藏歌曲页面获取歌曲ID列表。

1
2
3
4
5
6
7
8
9
10
11
##1. 匹配歌曲元信息
song_title_reg=r"<title><\!

</span>CDATA<span style=" />

!
(.*)</span><span style=" />></title>"
match = re.search(song_title_reg, text) if match: song_title = match.group(1) print song_title   ##2. 获取用户收藏歌曲ID列表 song_reg=r"\"/song/(\d{1,20})\"" result = re.findall(song_reg, text, re.S) print result

re.findall API返回一个列表,因为匹配模式中只有一个分组,所以返回的result就是歌曲ID列表。正则匹配要仔细考虑贪婪和非贪婪、单行匹配和多行匹配问题,re模块都有相应的解决方案。

4. 写入ID3信息。Python有很多处理音频文件ID3信息的第三方模块,这里用mutagen,因为它支持的文件类型多,接口较丰富。因为同步Last.FM只需要歌名和歌手信息准确就可以,所以也没必要用mutagen那些高级的API,用EasyID3类就足够。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from mutagen.easyid3 import EasyID3
 
def write_id3(path, song_name):
    #write id3 info to song.
    try:
        audio = EasyID3(path)
    except Exception, e:
        #while source track doesn't have ID3 structure. malloc new EasyID3().
        audio = EasyID3()
    song_info = song_name.split(' - ')
    artist = song_info[0]
    title = song_info[1]
    audio["artist"] = unicode(artist, 'utf-8')
    audio["title"] = unicode(title, 'utf-8')
    audio.save(path)

mutagen在构造EasyID3对象时,如果发现音频文件中缺少ID3信息,会抛出一个异常,因此需要在异常处理中new一个EasyID3对象,然后填入歌手和歌名信息,存到文件中。

5. 反解栅栏加密算法。歌曲的真实链接地址被栅栏加密算法加密过,前面已经分析过栅栏加密算法的原理,很容易写出反解算法。在已知行数row的情况下,如果字符串长度能被行数整除,则每行字符数相等;如果字符串长度无法被行数整除,假如余数为r,则前r行的字符数比剩下(row-r)行字符数多1,根据这些规律构造二维数组,按列读取即可。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
##行数已知,计算密文长度、列数、余数等。
encypt_loc_len = len(encypt_location)
column = encypt_loc_len/row 
remainder = encypt_loc_len% row
 
src_list = []
unencypt_loc = ''
iter = 0
##前余数行的二维数组构造方法。
for i in range(0, remainder):
	sub_list = encypt_location[iter:iter + column + 1]
	iter = iter + column + 1
	src_list.append(sub_list)
 
##剩下的(row-余数)行的二维数组构造方法。
for i in range(0, row - remainder):
	sub_list = encypt_location[iter:iter+column]
	iter = iter + column
	src_list.append(sub_list)
 
#二维数组构造出来后,按列读取。
for i in range(0, column + 1):
	for j in range(0, row):
		if i < len(src_list[j]):
			unencypt_loc += src_list[j][i]
print unencypt_loc

6. 下载失败重试。这里的失败主要是指反解出的歌曲真实链接地址不正确。其实能出现这个错误也算幸福的烦恼,在获取歌曲元信息XML文件时,如果间隔太短,虾米服务器返回的信息是无效的(格式仍合法),这样反解出的歌曲真实链接地址就有问题。本来是单线程,大部分时间程序应该阻塞在下载歌曲的API(urllib.urlretrieve)上,但是如果网速太快(=.=!!),就会造成请求歌曲元信息XML间隔太短。现在采用的方法是失败重试,最多重试三次,每次重试之前有个退避时间,退避时间算法为2失败次数秒。失败重试的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if errors ocurs during the download processing, re-try 3 times.
RETRY_TIME_LIMIT  = 3
isFail = True
trytime = 0
while isFail and  trytime < RETRY_TIME_LIMIT:
    try:
        (location, song_title, artist) = parse_xml(song_id)
        target = parse_location(location)
        download_music(target, artist + ' - ' + song_title, base_dir)
        count = count + 1
        isFail = False
    except Exception, e:
        print e
        time.sleep(math.pow(2, trytime))
        isFail = True
        trytime = trytime + 1

--EOF--