2009年8月12日星期三

使用hooks/update脚本控制git-push用户权限

使用hooks/update脚本控制git-push用户权限

作者:志磊先生 <talebook@gmail.com>

概述:
我们使用三个文件$GIT_DIR/hooks/update, $GIT_DIR/info/allowed_users,
$GIT_DIR/info/allowed_groups对GIT的push提交进行控制,进而来实现中心版本库服务器功能。


一、原理
根据相关文献[1],我们可以知道开发者client向版本库服务器push新版本的时候,会执行$GIT_DIR/hooks/中的相关脚本。其中,在每
个ref更新之前,假如$GIT_DIR/hooks/update脚本存在,并且可执行,则会以如下方式调用这个脚本:
$GIT_DIR/hooks/update refname sha1-old sha1-new
因此,可以使用特定脚本对每个client的更新进行过滤处理,以达到控制用户权限的目的。
文献[2]也对本文使用的方案作了简要叙述。

二、适用条件
1、总是使用fast-forward[3]快速提交。例如,从不会执行'git push origin +dev:master'[3]这类命令,并且不会跨
分支提交(如将abc分支提交到xyz分支)。
2、希望控制某些用户对某些分支的更新权利。
3、希望控制标签(tags)的创建/删除,只允许特定用户更新。

三、脚本内容
将以下脚本存储为$GIT_DIR/hook/update,并确定是可执行的。

-- >8 -- 脚本开始 -- >8 --

#!/bin/bash

#file: $GIT_DIR/hooks/update

umask 002

# If you are having trouble with this access control hook script
# you can try setting this to true. It will tell you exactly
# why a user is being allowed/denied access.

verbose=false

# Default shell globbing messes things up downstream
GLOBIGNORE=*

function grant {
$verbose && echo >&2 "-Grant- $1"
echo grant
exit 0
}

function deny {
$verbose && echo >&2 "-Deny- $1"
echo deny
exit 1
}

function info {
$verbose && echo >&2 "-Info- $1"
}

# Implement generic branch and tag policies.
# - Tags should not be updated once created.
# - Branches should only be fast-forwarded unless their pattern starts with
'+'
case "$1" in
refs/tags/*)
git rev-parse --verify -q "$1" &&
deny >/dev/null "You can't overwrite an existing tag"
;;
refs/heads/*)
# No rebasing or rewinding
if expr "$2" : '0*$' >/dev/null; then
info "The branch '$1' is new..."
else
# updating -- make sure it is a fast forward
mb=$(git-merge-base "$2" "$3")
case "$mb,$2" in
"$2,$mb") info "Update is fast-forward" ;;
*) noff=y; info "This is not a fast-forward update.";;
esac
fi
;;
*)
deny >/dev/null \
"Branch is not under refs/heads or refs/tags. What are you trying to do?"
;;
esac

# Implement per-branch controls based on username
allowed_users_file=$GIT_DIR/info/allowed-users
username=$(id -u -n)
info "The user is: '$username'"

if test -f "$allowed_users_file"
then
rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' |
while read heads user_patterns
do
# does this rule apply to us?
head_pattern=${heads#+}
matchlen=$(expr "$1" : "${head_pattern#+}")
test "$matchlen" = ${#1} || continue

# if non-ff, $heads must be with the '+' prefix
test -n "$noff" &&
test "$head_pattern" = "$heads" && continue

info "Found matching head pattern: '$head_pattern'"
for user_pattern in $user_patterns; do
info "Checking user: '$username' against pattern: '$user_pattern'"
matchlen=$(expr "$username" : "$user_pattern")
if test "$matchlen" = "${#username}"
then
grant "Allowing user: '$username' with pattern: '$user_pattern'"
fi
done
deny "The user is not in the access list for this branch"
done
)
case "$rc" in
grant) grant >/dev/null "Granting access based on $allowed_users_file" ;;
deny) deny >/dev/null "Denying access based on $allowed_users_file" ;;
*) ;;
esac
fi

allowed_groups_file=$GIT_DIR/info/allowed-groups
groups=$(id -G -n)
info "The user belongs to the following groups:"
info "'$groups'"

if test -f "$allowed_groups_file"
then
rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' |
while read heads group_patterns
do
# does this rule apply to us?
head_pattern=${heads#+}
matchlen=$(expr "$1" : "${head_pattern#+}")
test "$matchlen" = ${#1} || continue

# if non-ff, $heads must be with the '+' prefix
test -n "$noff" &&
test "$head_pattern" = "$heads" && continue

info "Found matching head pattern: '$head_pattern'"
for group_pattern in $group_patterns; do
for groupname in $groups; do
info "Checking group: '$groupname' against pattern:
'$group_pattern'"
matchlen=$(expr "$groupname" : "$group_pattern")
if test "$matchlen" = "${#groupname}"
then
grant "Allowing group: '$groupname' with pattern:
'$group_pattern'"
fi
done
done
deny "None of the user's groups are in the access list for this branch"
done
)
case "$rc" in
grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;;
deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;;
*) ;;
esac
fi

deny >/dev/null "There are no more rules to check. Denying access"

-- >8 -- 脚本结束 -- >8 --

四、配置文件
1、文件配置指南
这个脚本从两个文件中读取配置文件:
$GIT_DIR/info/allowed_users
$GIT_DIR/info/allowed_groups
这两个文件描述了那些用户/用户组对于分支/标签具有更新权限,并且使用相同的格式:
refs/heads/master junio jack
+refs/heads/pu junio
refs/heads/cogito$ pasky
refs/heads/bw/.* linus
refs/heads/tmp/.* .*
refs/tags/v[0-9].* junio
字符的匹配使用正则表达式规则,有多用户时请使用空格将用户名隔开。分支/标签的匹配是按顺序由上至下进行的,请确保出现交集
时,将大范围的匹配规则放置于下方。
对于如上示例,jack将能够更新'master'分支;junio能够更新'master'和'pu',并且创建相关的标签;linus则能够创建/更新
bw/abc, bw/xyz等分支;而任何人都可以创建/更新 tmp/xxx分支。
而pu分支的行首的'+'号,则代表了junio能够对pu分支进行non-fast-forward

2、系统配置指南
多数情况下,我们需要建立一个用户组(起名为 git ),并且将相关的开发者账户添加为该组成员。
当我们导出一个公共版本库后,将git目录的所有者设置为boss:git,其中'boss'是项目负责人的账户名。并且设置组用户有写权限,这
样就能够让同组的开发者对这个目录有写入/更新权利。以命令的形式来描述,则如下:
$ pwd
/home/project.git
$ cd ..
$ chown boss:git project.git -R
$ chmod g+w project.git -R


五、实例
1、配置信息
$ pwd
/home/cvs.git
$ ls -l
总计 52K
drwxrwxr-x 2 tale git 4.0K 08-12 00:03 branches/
-rw-rw-r-- 1 tale git 5 08-12 00:03 COMMIT_EDITMSG
-rw-rw-r-- 1 tale git 92 08-12 00:03 config
-rw-rw-r-- 1 tale git 73 08-12 00:03 description
-rw-rw-r-- 1 tale git 23 08-12 00:03 HEAD
drwxrwxr-x 2 tale git 4.0K 08-12 15:48 hooks/
-rw-rw-r-- 1 tale git 320 08-12 00:03 index
drwxrwxr-x 2 tale git 4.0K 08-12 15:47 info/
drwxrwxr-x 3 tale git 4.0K 08-12 00:03 logs/
drwxrwxr-x 52 tale git 4.0K 08-12 15:38 objects/
drwxrwxr-x 4 tale git 4.0K 08-12 00:03 refs/

# 确保相关的账户已经添加进git组
$ grep git /etc/group
git:x:200:tale,wang,xiao

# 确保这个脚本是本文中提供的脚本,并且具有可执行权限
$ ls -l hooks/update
-r-xrwxr-x 1 tale git 3.8K 08-12 15:48 hooks/update*

# 查看一下当前的用户权限分配
$ cat info/allowed-users
+refs/heads/wang wang tale
+refs/heads/xiao xiao
+refs/heads/tale tale xiao
+refs/heads/master tale
+refs/heads/.* tale
+refs/tags/.* tale


2、某客户端的操作
# 先看看目前是哪个用户(显然,是一个无关的用户)
$id
uid=0(root) gid=0(root) groups=0(root)

# 我们使用wang用户与服务器交流
$git clone wang@222.20.103.115:/home/cvs.git
Initialized empty Git repository in /root/cvs/.git/
wang@222.20.103.115's password:
Receiving objects: 100% (40/40), done.
remote: Counting objects: 40, done.
remote: Compressing objects: 100% (29/29), done.
remote: Total 40 (delta 9), reused 0 (delta 0)
Resolving deltas: 100% (9/9), done.

# 目前的开发进度如下:
$cd cvs/
$git show-branch -a
* [master] root 2
! [origin/HEAD] root 2
! [origin/master] root 2
! [origin/tale] Merge branch 'master' into tale
! [origin/wang] root 2
! [origin/wang2] test
------
*++ + [master] root 2
*++ + [master^] jfakfjklasjf
--- - [master~2] Merge branch 'master' into wang
*++ + [master~3] terer
*++ + [master~4] wang
- [origin/tale] Merge branch 'master' into tale
*++++ [master~2^2] 123
*++++ [master~5] ter
*+++++ [origin/wang2] test

# 从客户本人的分支上开始工作
$git checkout -b local-branch origin/wang
Branch local-branch set up to track remote branch wang from origin.
Switched to a new branch 'local-branch'
22:05:55 cvs $echo "nice to meet you" > nice
22:06:14 cvs $git add nice
22:07:02 cvs $git commit -a -m "nice to meet you"
[local-branch cd9da06] nice to meet you
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 nice

# 将工作进度提交到服务器上
$git push origin local-branch:wang
wang@222.20.103.115's password:
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 297 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To wang@222.20.103.115:/home/cvs.git
27b7142..cd9da06 local-branch -> wang

# 假如提交到别人的分支上,则会提示失败
$git push origin local-branch:wang2
wang@222.20.103.115's password:
Total 0 (delta 0), reused 0 (delta 0)
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/wang2
To wang@222.20.103.115:/home/cvs.git
! [remote rejected] local-branch -> wang2 (hook declined)
error: failed to push some refs to 'wang@222.20.103.115:/home/cvs.git'

六、总结
hooks/update脚本能够提供简陋的用户控制,满足小团队的开发需求。但是,它也有一个很大的缺点:所有控制的用户均是服务器上真
实存在的用户。这会导致两个问题,其一是这些用户可能会登录服务器、并且可能会出现误操作等,造成数据损失(可以将用户的登录
shell设置为/bin/git-shell来解决);其二是随着团队规模的增大,需要在服务器上建立大量的用户。
针对以上缺点,可以使用其他的开源方案(如gitosis[4])作为解决办法。

参考:
[1]Junio C Hamano and Carl Baldwin, control access to branches.
http://www.kernel.org/pub/software/scm/git/docs/howto/update-hook-
example.txt

[2]Git 中文教程 - 管理版本库 - CVS 模式
http://www.linuxsir.org/main/doc/git/gittutorcn.htm
[3]Git manual book: git-push (1)

[4]在 Gentoo 上部署 Git + Gitosis 服务器的笔记
http://py.thonic.org/2008/05/19/git-gitosis-gentoo/

2009年8月11日星期二

祸不单行

即将离校,打算提前将学费冲进银行卡里。
可上周竟然发现常用的银行卡竟然在关键时刻给我掉磁了!于是得花两个星期补办。
昨天翻箱倒柜,找出了学费账户的存折。里面竟然干干净净,一个支取条目都没有,显然是从来就没用过。去中国银行的柜台,竟然被告知
这个存折的信息不对,账号信息无故多了几个零,需要向上级部门反映,还建议我先别往账户里存钱,免得出现问题。
可、我还得回家啊!到时候能不能在开学之前赶来学校都是个问题,这才想着把学费先存进去。。。。
呜呜,看来我得想办法尽早赶来学校了。。。
现在真的体会到什么叫做"祸不单行"了。

archlinux中的无限网卡设置(rc.conf模式)

ARCHLINUX中的无限网卡解决办法很简洁。
首先是网卡的识别问题,参考wiki即可搞定。
其次是无限网络的连接问题,使用WICD更是方便快捷。

可是,倘若我想使用手动模式,或者文本的配置模式(rc.conf),那该如何呢??
我的无线网卡识别为wlan0,网络是开放的,essid是'cap',参考wiki,则应当如下写:

wlan0="wlan0 192.168.1.222 netmask 255.255.255.0 broadcast 192.168.1.255"
wlan_wlan0="wlan0 essid cap"
INTERFACES=(eth0 wlan0)

可是重启网络,却是出现如下问题:

$ sudo /etc/rc.d/network restart
:: Stopping Network [DONE]
:: Starting Network [BUSY]
> Could not associate wlan0 - try increasing WIRELESS_TIMEOUT and check
network is WEP or has no security
SIOCADDRT: No such process
[FAIL]

经过尝试,原来是iwconfig的参数需要添加'ap auto'。于是,现在变成:

wlan0="wlan0 192.168.1.222 netmask 255.255.255.0 broadcast 192.168.1.255"
wlan_wlan0="wlan0 essid cap"
INTERFACES=(eth0 wlan0)

重启解决。


PS:
1、对于部分仍然不能解决的朋友,请再给wlan_wlan0添加参数' txpower auto'。
2、假如手动输入命令能够连接,而network脚本死活也连不上,可以尝试修改/etc/rc.d/network,
在第40行(即/usr/sbin/iwconfig $iwcfg之前),添加一句
/sbin/ifconfig $1 up

2009年6月13日星期六

设计自己的分区方案

引子
通常,由于文件系统效率问题(reiserFS适合处理巨量小文件,XFS适合处理巨量大文件)、文件安全性问题(/home目录数据绝不可丢失,其他目录则随便,反正可以重装),可能会把部分重要目录作为一个单独的分区进行管理。


前备知识
在决定将哪些目录划分为单独分区前,我们来看看根目录下各个文件夹的含义:
。。。。
其中,/bin, /sbin存储基本程序; /dev, /sys, /proc被内核使用,不是一般的文件;/lib, /usr, /opt存放绝大多数程序;/etc保存全局的程序配置;/boot 保存系统启动文件;/media, /mnt通常作为挂载点,是空目录;/var,/srv,/tmp则保存几分钟/小时就变、杂碎的文件;/root, /home则保存用户的数据。
对于个人电脑,由于不关注系统程序的稳定性(允许花上几小时重装、甚至隔几月就重装),关注点只保留在用户数据和性能上,故适合将/, /home, /var作为独立的分区配置不同的文件系统。/boot也有可能被作为一个单独的分区(适合于N个liunx发行版共存的情况);/etc也可以作为一个独立分区,以便重装时方便恢复配置。以我个人为例,我将/, /boot, /home, /var作为了独立的分区。


文件系统类型
让我们来确定各个分区的文件系统类型。
对于/boot,考虑到你的系统引导程序需要能够支持并读取这个分区的数据,并却/boot目录的内容常常是几个星期才变动一次,可以ext2/ext3,注意的是,grub目前(2009/6/10)尚未有支持ext4的版本,故不推荐使用ext4文件系统。
对于/var,考虑到里面的数据会剧烈变动(大量的I/O操作),并且多数为巨量的小文件,故reiserFS最适合作为这个分区文件系统类型。
对于/home,考虑的这些数据文件大小均有,且随意性较大(既有不少几K的文本,也有不少几G的电影),故而不适合使用reiserFS和XFS这些为处理巨量特定大小文件而设计的文件系统,采用通常一些的ext3,ext4等均可。注意避免使用不带日志的文件系统类型(如ext2),否则不小心断电你就哭去吧!
对于根目录/,如果你不把/boot独立作一个分区,那么就得额外考虑系统引导的问题,避免使用ext4等文件系统类型;除此以外,尽量使用XFS文件系统,因为根目录的文件通常不大(没有达到百M级别),XFS没有优势;最后,请根据个人的喜好而选择文件系统类型。


分区大小
确定了目录-分区方案后,来着手研究各个目录(即将来的各个分区)的分配空间大小,以达到空间利用最大效率。
倘若系统已经是独立分区的情况了,使用df命令来快速了解各目录使用情况:
20:08:52 ~ $df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/sda1 9.2G 6.3G 2.5G 72% /
none 1004M 0 1004M 0% /dev/shm
/dev/sda5 1.2G 36M 1.1G 4% /boot
/dev/sda6 3.0G 1.2G 1.8G 40% /var
/dev/sda7 178G 125G 44G 75% /home
如果系统依然是一个分区作为整个系统分区,使用du来统计文件夹的占用大小(记得使用root权限):
20:28:34 / # du --max-depth=1 -h
34M ./boot
5.4G ./usr
8.1M ./etc
5.5M ./bin
15M ./sbin
443M ./opt
1.2G ./var
125G ./home
132G .
(省掉了一些无关目录的大小信息)
注意我的系统已经是使用了许久的日常工作系统,该装的软件都已经装了,各个文件夹大小已经达到了其目标值。故,在此例子中,我可以使用这些值作为参考,设计出我的分区方案。
对于/boot,实际使用了34M,考虑我以后安装其他系统,将这个值提高至10倍(依然还很小,呵呵),故使用340M分区大小。
对于/var,实际使用了1.2G, 增加50%作为备用(空闲的文件系统通常较快),取整,故分配2G。
对于根目录/,主要是/usr目录占用得太多,约5.4G,增加50%作为备用(考虑到以后可能还要装其他大型程序如OpenOffice),故分配8G。
剩下的空间全部分给/home。


结论
故,在我这个空间月190G的系统中,最终的分配结果如下:
目录 大小 类型 备注
/ 8G ext4 为了尝鲜
/boot 340M ext3 为了能让根目录使用EXT4分区,故将/boot独立出来
/home 180G ext3 历史遗留问题,以前就是这个文件系统
/var 2G reiserFS 处理巨量小文件有极大优势


对于一个40G的系统,则可以使用如下的分配方案:
目录 大小 类型 备注
/ 7G ext4 依然要考虑安装巨型程序的问题
/boot 340M ext3 为了能让根目录使用EXT4分区,故将/boot独立出来
/home 31G ext3 历史遗留问题,以前就是这个文件系统
/var 1.5G reiserFS 常常清理的话,/var目录的空间也能剩一些


其他
对于那些极端个性化(比如全能型系统(KDE,GNOME一并装)、精简性系统(连XORG也没有)),只能参考以上的思路自己慢慢设计分区方案了。


参考
Linux各种文件系统(ext3,ReiserFS,jfs,xfs)的性能
http://hi.baidu.com/xuzhi1977/blog/item/c5869758dfafbade9d82040a.html


2009年5月22日星期五

一个简单C单元测试脚本

花絮
今天打算动手重写mystar。于是看了一会儿代码,决定先改写readConfigFromFile函数。于是花了若干时间,写了170行左右代码。恰好前一段时间
看《修改代码的艺术》时,学会了不少单元测试的方法,于是打算拿刚写的这些代码试试手(这些代码的独立性很高,正适合单元测试)。然后又是一番捣鼓,手动写了一个测试
程序。有捣鼓了一个晚上,写了一个自动单元测试脚本UnitTest,命名为ut。
先来看看我的情况吧:


代码略例
刚写了的代码中包含两个文件,分别是option.h和option.c,部分内容如下:
//option.h
#ifndef OPTION_H
#define OPTION_H
extern char * strip(char * str);
extern char * strlow(char * str);
//。。。。此处省略一堆代码

一个简单C单元测试脚本

花絮
今天打算动手重写mystar。于是看了一会儿代码,决定先改写readConfigFromFile函数。于是花了若干时间,写了170行左右代码。恰好前一段时间看《修改代码的艺术》时,学会了不少单元测试的方法,于是打算拿刚写的这些代码试试手(这些代码的独立性很高,正适合单元测试)。然后又是一番捣鼓,手动写了一个测试程序。有捣鼓了一个晚上,写了一个自动单元测试脚本UnitTest,命名为ut。
先来看看我的情况吧:


代码略例
刚写了的代码中包含两个文件,分别是option.h和option.c,部分内容如下:
//option.h
#ifndef OPTION_H
#define OPTION_H
extern char * strip(char * str);
extern char * strlow(char * str);
//。。。。此处省略一堆代码

2009年5月2日星期六

影评《Eternal Sunshine of the Spotless Mind》:那些旧时光,都是花儿

  昨晚恰在白云黄鹤上看到了一篇文章,记下了她记忆中与他之间琐碎又零星的故事回忆。纵使已经逝去5年,两人已天南海北,可是那些旧时光,却都是花儿。
  恰巧今天观看这部电影,视角切换到了男方,可依然是那些回忆起来无比普通细碎却让人不忍忘却的故事。于是把她的文章题目拿于此作为此评论的题目。
  
  原本看到这部电影的名字《美丽心灵的永恒阳光》时,我并没有想到它会是一部会让人感怀忧伤的片子。那个让我误以为是阳光激励片的名字随着电影的开幕、Joel的轻声喘息而不复存在。故事于是在Joel的轻声述说中,在一个阴沉的冬季、在情人节的这天开始。
  
  说实话,直到倒叙Joel的清除记忆经历开始了一段后,我才弄清故事的叙述方式(想起了《记忆碎片》那种诡异的叙事风格)。Joel这个忧郁内向沉稳的帅哥,突发奇想去Montauk,然后在反省自我的光棍生活时碰见了一个身穿桔黄的女子Clem。Clem异常纯朴好动且仿佛不受浊世俗规的约束,敢想敢做地跟Joel聊起了天,直抒胸臆说不喜欢Joel只会用Nice来形容,甚至直接了当地说"I'm gonna marry you。"可把Joel这个遇事习惯掩藏在本子和画本中的孩子吓呆了(我也是)。仿佛一见钟情,他们如相恋多年般迅速熟悉起来。
  
  可实际上,他们确实相恋多年。
  
  镜头的突然切换,出现在我们面前的却是Joel那张纯净的脸,可清澈的眼睛却是强忍着才不会被泪水溢出来。Joel慌乱、不安,让刚刚营造的美好气氛顿时消散,镜头里的明媚阳关已经变作黑夜里的丝丝细雨。发生了什么事?无数人询问。原来已经进入了故事的倒序,开始讲述Clem因为与Joel拌嘴而冲动地去了"忘情诊所"消除记忆,Joel无法接受Clem对他做出的形如陌人的样子,痛苦挣扎中也决定去"忘情诊所"消除记忆。可当Joel开始进行清除时,才蓦然发新,那些曾经,那些旧时光,都是花儿。
  我还记得,那次在市场时你说喜欢小孩,我担心你没准备好做妈妈,我们因此拌嘴;
  我还记得,那次我们温馨地躺在床上,你说希望更加了解我,喜欢和我多说话,让我别孤僻,要更加intimacy;
  我还记得,那次我们在中餐馆,我都已经能够说出你的骂人口头禅了,可我依然喜欢你安静吃饭的样子,喜欢帮你捋起那不安分的发丝;
  我还记得,那次你穿着橙色的运动服,仿佛一个熟透的橘子,于是"柑橘"成了你的昵称;
  我还记得,你说你小时候很怕别人说你丑,还给一个洋娃娃起名叫Clem,总希望她变得漂亮;
  我还记得,我们曾并肩躺在结冰的查尔斯河上,天气很冷,但我却想要就此永远和你躺下去;
  我还记得,我们曾在海边的雪地里玩耍,身上全是细碎的雪花,涛涛浪声也无法淹没你的欢声笑语;
  我还记得,我们第一次相见,就在海边聚会时,你主动过来"搭讪",我们从此相识。
  
  无数细碎珍贵的回忆,如粒粒珍珠般晶莹动人,一如你Clem变幻的发型,不定的发色,散射这七彩的光芒。Joel不愿忘却,但却无可奈何。
  于是花儿凋谢,旧时光烟消云散。
  
  然而深深相爱的两个人不会因此就分开的,冥冥中他们还记念着对方,还会去以前常去的地方,等待着另一个人。这又让我想起了Joel追求Clem时的对话话——
  Clem:I had you pegged, didn't I?(我让你记住我了吧?)
  Joel: You had the whole human race pegged.(你让全人类都记住你了。)
  
  于是这句话终究会如一句魔咒,引领这Joel和Clem重新走在一起。