Monday, September 03, 2007

CVS (Concurrent Versions System)協作版本系統

在寫了一系列的WinCVS 2.x 的文章後, 有人問我, 什麼是CVS, 為什麼要用CVS, 諷刺的是, 回答這個問題的答案, 就是當初我寫這一系列文章的原因, 我以為介紹完WinCVS的使用方法, 就可以讓人知道版本控制器是怎麼一回事, 不過在寫了這麼多篇後,突然被問到這個問題, 讓我不得不停下來想一下是否應該先做個解釋了

CVS 主要為了幾個功能

1. 程式碼備份 :

程式設計者都知道原始程式碼得備份,雖然常常是硬碟壞掉才知道, 因為不像作家, 他們的作品發行後會有人幫他們保存, 而程式設計者只發行執行檔, 而CVS是一個Server , 可以讓你保存不同時期的程式碼, 所以程式碼備份對於CVS是一項重要的功能, 而且他通常是Client/Server 架構(因為它也可以在local作業,只是那會失去開發協同的功能,較少人使用), 意味著備份資料不在本機, 也算是一種異地備份吧

2. 程式碼版本控制:

在一直commit程式碼進CVS 時, 也就一直在做程式不同版本的備份, 因此,也擁有讀取出不同版本的功能, 這有點幾好處,

a. 查核舊版本的程式碼 :

過去就是是過去了, 為什麼要查, 大部份都是因為發行過的舊版程式出了某些問題, 需要查明問題的真正原因, 並確認問題是否有延續到其它版本或是正在開發的版本, 所以我們得取出那個版本的程式碼出來除錯

b. 比較不同版本的改變 :

這個功能我特別在Diff 時展現過他的威力, 他可以快速比較某兩個版本間的差別 ,我常用他來查核同事上傳的程式碼, 與自己改爛的程式碼, 或是為了確認某個Commit時說明寫的不夠清楚, 以至於我必需確定看到修改的程式碼

3. 產品協同開發 :

一個產品的開發, 可能同時由多人同步開發不同功能, 而程式碼會互相影響, 有時甚至同一個cpp 檔案, 會同時有兩個人動到, 所以確認多人開發最後會匯到同一份程式碼或是確定正在開發的是最新的程式碼的動作是協同開發最重要的工作, 而CVS正是為了處理這個問題而來, 也是所有協同程式版本控制器的最主要功能

以上這三點就是CVS這類版本控制器的重要特色, 如果你有這些需求(我還沒看過哪個程式設計者沒有的), 一定要來試試CVS .

不過有趣的是, 眾多這類程式, 為何唯獨CVS這麼多人用,這是一個很有趣的問題, CVS並不是最好用的程式, 這點可以由Linux的老大linus所使用的版本控制器不是CVS就可以看出端倪, 我試著做些解答

CVS 功能還算完整, 但真正的主流原因應該在於它是Open Source, 在SourceForge裡可以看出原因, 它一開始就採用CVS, 因為Open Source 界比一般公司更需要協同開發環境, 因為Open Soruce的專案, 通常多人開發, 而開發人員四散各地, 重點是, 他們是Open Source, 對於程式碼讀取權限的要求並不特別看重,而且因為他們是Open Source, 所以需要找一套也是Open Source的程式, 所以CVS 正是首選, 他免費也Open Source

不過CVS 隨時間增加, 還是漸漸露出疲態, 較鬆散的權限控制, unicode的支援不足(CVSNT有加強,但原始CVS還是沒有動), 對於Binary File的支援不足,所以SVN正是因此產生, 一群人重寫, 企圖取代CVS世界, 事實上由於多年的開發, SVN現在已相當穩定, 像是SourceForge都已經開始有SVN,我所有在注意的幾個Open Source 專案也開始有改到SVN的情況, 這是好現象, 只是在這樣的情況, 為什麼我還要介紹CVS呢!!

其實CVS還是個好東西, 簡單易懂, 而且現存很多專案與公司內部還都是CVS系統, 但是新加入的程式設計者還是需要學習一套版本控制, SVN因為較新,很多人開始觀注, 資料較多, 所以我還是留在CVS, 雖然我用SVN一陣子了, 但是對於CVS 的心得還是大於SVN, 我除了WinCVS 2.x系列外, 還會有其它使用CVS的工具會再持續介紹的

講了這麼多還是不了解CVS嗎? 如果如此, 留一下留言吧, 我在來看看要怎麼說明才能更完整

相關連結

WinCVS 2.x 的快速入門 安裝篇
WinCVS 2.x 的快速入門 設定篇
WinCVS 2.x 快速入門(二) Login for pserver (登錄)
WinCVS 2.x 快速入門(三) Checkout module (下載)
WinCVS 2.x 快速入門(四) Import module (載入程式)
WinCVS 2.x 快速入門 (五) Diff (程式差值查詢)
WinCVS 2.x 快速入門 (六) Commit (提交)
WinCVS 2.x 快速入門(七) Add (加入新檔)
WinCVS 2.x 快速入門(八) Remove, Erase(刪除檔案)
CVS
CVSNT
SVN
WINCVS

38 comments:

Anonymous said...

想請問您個問題
最近我們因為開始導入cmmi
於是乎開始使用cvs
不過我這樣看
cvs似乎管理原始碼版本比較多
能夠管理一般文件版本嗎?

另...您聽過vesta這東西嗎?

DreamMan said...

一樣可以的, 不過如果你是ansi 的這類txt file, 就完全可以像是source code這樣做比較

但如果您的文件是屬於doc 這類的, 它還是可以做版本控制, 但就無法做差值比對了

而關於vesta 恐怕我才殊學淺沒有聽過

Anonymous said...

那我想再請問一下...
cvs是怎麼作權限控管的呢?
您有提到他這方面比較鬆散
why?

DreamMan said...

cvs 是採用作業系統的權限控制, 所以其對專案的權限其實是OS對目錄的權限, 用起來是沒啥大問題, 只是有些情況比較麻煩
以linux 為例, 是以group 來控制所屬的專案, 所以負責A專案的人, 都設A Group, B 專案的人都設B Group, 問題來了, 如果某個user同時負責A與B 問題就麻煩了, Linux 無法映對到兩個group,我的解法就是開兩個account 摟,分別A,B都開
不過您的問題倒是都問重底, 我WinCVS還有大概4篇還沒發, 我想, 等我寫完WinCVS應該來寫一下CVS Server 才是

Anonymous said...

呃...那我有個笨問題
如果我的環境是架在windows平台
那我是不是該用個iis之類的?

另...
在windows的平台下
權限跟帳戶的控管是在cvs裡面設定還是windows裡面設定呢?

不好意思因為初次使用這東西很多稿不清楚><

不知道方便跟您要即時通之類的嗎?

DreamMan said...

其實在您安裝WinCVS 時, 會提示安裝CVSNT就是一套windows 版本的CVS Server

不過單獨的安裝, 您可以在
http://www.march-hare.com/cvspro/
這裡找到.!

CVSNT 是以原cvs server 改出來的, 加入了一些新功能, 安裝也很簡單,我剛google了一下, 很多中文的部落格以介紹, 您可以參考.!

而CVSNT for windows 好像也是使用windows的使用者帳號管理, 不過因為我一直使用Linux版本, 所以對於Windows版的UI設定比較不熟

Anonymous said...

日安:
你好我在請問個問題
我在創建repositories時
他會自動格式化那個資料夾
目錄中多一個cvsroot的子目錄
也就是說
會呈現
mycvs
|______CVSROOT
降子的結構

然後我如果要在裡面進行版本的控制

我應該用
mycvs
|______CVSROOT
|______work1
|______work2

的方式還是

mycvs
|______CVSROOT
|______work1
|______work2
呢?

另外好像說不能直接創建子目錄
而是應該在cvs或是wincvs下創建?

Anonymous said...

sorry下面的結應該是這樣
就是將專案的區域放到cvsroot下

mycvs
|______CVSROOT
|______work1
|______work2

我的意思是...module下的各個區域劃(work1...work2)分是不是一定要在cvsroot下?

DreamMan said...

不一定的, 您可以建個
\shit 的目錄
然後import module 當\shit 目錄下

所以就會變成

\shit
shitwork1
shitwork2

Anonymous said...

不好意思喔又來問您個問題了
就是想請問說
cvs的數據可以在windows平台跟linux平台間流動嗎?
我的意思是說
假設我的cvs serve放在red hat上
我可以用wincvs去存取嗎?

或者是說
我是用cvsnt..那我可以在red hat上存取cvsnt的東西嗎?

ps.這邊的存取是指進行專暗的一版版協同作業...checkin checkout etc.

Anonymous said...

奇怪我的輸入好像會秀斗><
打出來的自跟放上去的字都不一樣
ps是這樣才對

------我是正確的ps-------------
ps.這邊的存取是指進行專案的協同作業...checkin, checkout, etc.

DreamMan said...

可以的... 這麼說好了..

CVS Server (包含CVSNT)

CVS Client 是獨立的, 但CVS Server 與CVS Client 都是多個平台的, 所以您可以用任何平台的CVS Client 讀取 Linux 版的CVS Server, 反之亦然

不過WinCVS主要是在windows上, 但在linux上也有其它的 cvs client, 不過我在linux上我都是直接下cvs command

如果到WinCVS 的網站去, 就可以發現很多其它平台的cvs client

Unknown said...

瞭解~
感謝您解答︿︿
原來我有google的帳號...哈...終於不用匿名了

Unknown said...
This comment has been removed by the author.
DreamMan said...

感謝您..

我這blog 冷到爆, 有您的留言讓我感覺的有點人氣

Unknown said...

哈囉夢大我又來打擾你了~
就是阿最近一玩然後有幾個問題
一個就是說...
CVS版本控制都是1.1...1.2...1.3這樣的格式嗎?
我可以自訂初始的格式
讓他從0.1 0.2或是abc_01_1.0這樣的格式開始嗎?

然後版本跑到某個地方的時候
我想讓他直接變成另一個編號可以嗎?
比如說...弄到1.4的時候有重大變革
我們希望讓他直接就變成2.0版
cvs可以做到這樣嗎?

感恩

DreamMan said...

我解釋一下它的運作方式

第一次commit 就會為
1.1
接下來只要你一直commit 就會一直往上長, 當你長到某個版本了, 突然想做個branch(我已經開始動工寫了), 例如你在1.20 時想到要分岐, 就會重算

1.20.1 , 1.20.2 等, 但如果你回到原來的線上, 還是可以繼續 1.21, 1.22, 1.23也就是現在分兩條線跑

OK 會到你的問題, 可不可以自己爽就增加number 或移位呢..答案是不行, 這不應該算是CVS的限制, 是因為CVS 是以檔案為標地的檔案控制系統, 但是像我們在放source code, 是不會一次只有一個檔的, 可能一個projects 有30個檔, 但也不是每一次更新版本都會動到30個檔, 有可能這一次只動5個, 下一次動10個, 這樣一來每個檔案的版本就不一, 所以控制真正對外的版本, 我們不會使用CVS上的編號, 而是使用自己的版本控制編號, 那又怎麼控管我們自己的控制編號呢??是使用tag,我..我..我會盡快寫出來的....!!

Unknown said...

日安:
你好~又來請教您問題了
我們導cmmi的過程中將cvs訂成幾個區域
其中有基準區(baseline)與工作區
基準區的東西不能任意chexkin & checkout
工作區的東西則是可以任意取出或更動...

請問您有規劃這些區域經驗可以給我參考一下嗎?
我現在比較confuse的地方是
一般在用cvs
就是規劃幾個資料夾(或是磁碟),然後設定各自的權限就好了嗎?還需要在做其他的事情嗎?

另外就是
cvs的版本軌跡的回碩可以設定權限嗎?
我的意思是說
可以只有某部份的人才能作回溯的動作而
其他人雖然能夠可以看到版本的演進卻無權回到(或是檢視)之前的版本(這邊是以主機上的資料為主...不管本機他怎麼留存)

然後還有個問題是
CVS在產生歷史軌跡的時候
是把原來的檔案存到別的地方
然後把一個最新的檔案放到上面
還是說
一直是用同一個檔案
只是紀錄了變動而已?
會這樣問是因為想知道會不會因為一直變動而造成專案的肥大?
舉個例
當一個專案有10分文件
每份文件到結案前都被更動了10次以上的話
那前者就會多100個以上的檔案出來
越後面會用多的空間
不過後者大概就會少很多
請問Cvs是哪種機制呢?

我本來沒想過這個問題
因為我看cvs的主機上並沒有像前者那樣變動一次就多一個檔
不過我也不確定cvs是不是有另外的地方去存放那些歷史軌跡
所以想跟您確認一下

大概是這樣
感恩︿︿

DreamMan said...

不好意思, 這兩天工作超忙, 回的有點慢

1. 我是規化不同的group, a,b 四個user 為group A c,d 為 group B,group A 只能read 目錄 AA, 而groupb 可以read/write 目錄BB,所以將需要only read 專案的, 都放在AA, 要read/write 的都專案放在BB

2. 關於cvs 版本的history 是否可以設權限, 這是有趣的問題, 我自己沒用過, 不過我找了一下, 應該是沒有這樣的功能, 至少cvs server 沒有 cvsnt 我就不確定了

3. cvs 在做history 使用兩種方式, text file(cvsnt 可以support UTF8/16的檔案), 與binary, text file 是只有一個檔案, 每次只加入差值的部份, 而binary,就一定是上傳一次, 就存一個檔

不論哪一種檔案, 一定是記錄越多, 所耗的儲存空間越大, 只不是text file 長大的比較慢, binary 長大比較快

您如果要實際看這問題, 您只要到cvs server 的目錄去看, 就可以知道結果了

4. 不好意思, 我覺的您的問題都問的很好, 可以讓我跟您的對話post 成一篇blog嗎, 一方面幫到其它人, 另一方面撐一下BLOG篇數..

Unknown said...

日安:
當然可以啊~
其實本來是想說是不是我問的東西太奇怪
所以讓您不想回了說...
希望未來的日子能夠繼續跟您請教
謝謝^^

Unknown said...

日安:
不好意思又來請教您問題
不知道您在CVS中是如何規劃專案的模板?
(就是一個固定的專案版型或是專案的初始狀態)
以我現在來說
我們被要求要製作一個專案的template
裡面包含了該有的區域(資料夾)與範本文件
然後一有專案進來就把它複製過去
(或許改改名字或作一個符合該專案的變更這樣)

我本來的作法是(管理員的身份下)
在我自己的電腦裡面放一份專案的基本模版
有專案啟動的時候
我就把這個模版複製到CVS對應的目錄下然後創建(改個名)並CHECKIN這個模組
不過這樣的問題是
裡面的文件都要一個一個在add contents一次
然後可能我必須再進到主機裡對資料夾或是帳號作一些權限的設定

所以我現在想說
1.一開始在CVS裡面產生一個Module並且規劃好權限等當作template..
2.然後把它複製起來放到一個資料夾存放
3.以後專案啟動時
就把這個模版複製到CVS裡面然後改名字讓人使用
降子應該會保留CVS應該有的結構
權限之類的我也不需重新規劃
不知道這樣可行嗎?
有一種不安的感覺...卻又說不出哪邊不對...
(其實一開始我是沒做1那個步驟,後來發現雖然可以看到我複製過去之後的MODULE...卻沒辦法CHECKOUT...取出的結果是空的ORZ..所以我想必須要用CVS自己的結構吧?
)

還是說您有有其他的方式呢?
畢竟專案如果很多又很大的話
不太可能每次都手工建立專案吧?

BTW
一方面我也在試著用SVN
聽說有比CVS強大...
不過一開始的配置有點小機車><

DreamMan said...

真是不好意思, 最近真的是忙翻了, 回應有點慢

您的問題倒是有趣, 我真的沒有這樣的模版需求, 以Soruce Code來說, 每個專案的程式重覆性都被要求盡量低, 所以我們都會盡量以呼叫另一個專案的方式來降低不同模組的維護成本

不過我倒是想到一個方法您可以試試, 在CVS甚至是您新用的SVN都一樣, 有一個分岐的功能, 所以您可以將您所謂的範本建在root, 然後每一個專案, 都從root 建分岐出來, 這樣就會有一個故地的基底(您稱為範本)的共同初始資料了

SVN是好東西, 我一直想將公司的程式碼管理改為SVN,不過影響太多人, 怕被大力幹譙 , 事實上我私人的專案已經都用SVN管理了,

Unknown said...

日安:
夢大您好
最近系統快上線了好緊張喔@@
還要教育訓練...對CVS還不是很有把握說><
有幾個疑惑想請教您一下

創建新模組(Make New Module)跟
我直接在MODULE下開資料夾有什麼不同?
我的意思是說...
像我們再用CVS開專案做版本控管的時候
應該是開一個ROOT Module
然後管理員(或是PM)直接連到這個ROOT自己建立自己的專案資料夾

還是說,應該是對每個專案都Make New Module?

或是說...無所謂...大家用的爽就好?
權限...功能...控管上差別大嗎?

另外...我不知道您有沒有用過TortoiseCVS
他UPDATE有兩種(斯斯有兩種@@)
一個是很普通的UPDATE...就只是將目前的文件更新到最新的版本
另一個是在選項裡面
他是翻譯成更新到指定的版本
這個意思是說...我可以強制他回到某一個版本
嗎?
假設跑到1.6了...我更新他回到1.2...那他就又從1.2開始跑這樣嗎?
不太懂這個的用法@@

Unknown said...

日安:
然後...
TAG還是不太會用Orz...

Unknown said...

日安:
那個~
再問一個問題
我們公司的AD在A電腦
CVS我架在B電腦
接著我把DOMAIN_USER加到B電腦這邊
然後用SSPI的協定來使用CVS...
結果~
它它它~~~不給用><
這樣正常嗎@@?(保安~~~

DreamMan said...

不好意思, 近來太忙了, 在公司忙工作,在家忙著看傑森包恩, 總算把神鬼系列三大本都看完了, 所以回的有點晚...

不過我覺的你的問題的很棒, 真的是有看過用過想過才發出來的問題, 我很願意回答您的問題

1. 老實說 Create new module, 與在一個大module 裡面在create 一個目錄, 再上傳, 其實只是指令不同, 其實CVS Server 內部的處理方式是一樣的,你可以實驗一下就知道了..

a. 在D:\ Import 一個shit1 module
b. update 後在d:\shit1\ create 一個 shit2
c. 在shit2裡面放個 shit.txt
d. 整個連同shit2 的目錄與shit.txt commit
d. 不要用update 的, 用checkout module shit1\shit2
你就會發覺, 與你直接Update 整個shit1 的目錄是一樣的結果,也就是這兩者, 存在的差別就是使用的方式不同, 我常交換著用, 例如同事在\Projects\底下新增了一個shit5 的目錄, 但我又不想更新整個Projects, 但透過update又不能指定某個還未存在你的電腦利的目錄,我就會使用這個方法, 所以不用特別拘泥於哪一種用法

2.TortoiseCVS 是好東西, 它一開始即用WinCVS 改來的, 不過不管他的故事, 你的問題問的好, 它有兩種update, 如果你直接update, 就跟你直接在WinCVS 按下右鍵點入update後, 不加任何參數直接按Ok的結果是一樣的, 但另一種update spexx, 我有點忘了他的拼法, 反正中文的意思就是"指定更新",它就是可以設定你要update 的tag 日期或版本,也就是說, 一個簡單版的update,一個專業版的update, 不過我到現在還找不到reset tag 的指令, 所以你的問題, 是否可以由 1.2轉成1.6 再轉成1.3 , 當然可以, 但問題是,以我所了解, 無法取消tag,也就是不管版本, 我就是要最新的, 所以這個動作我通常會轉到WinCVS來做

3. Sorry. 對於您的這個SSPI我一點概念都沒有, 可能幫不上忙


對於您要上線, 真的是很高興, 總算這些文章有幫點人, 如果用的到, 把我的文章拿去印出來也沒關係, 事實上我也做的差不多, 等我最後兩章寫完後, 我會弄個完整的文件讓人可以download, 我第一次搞cvs是被逼的, 所以我也不知道要怎麼緊張, 所以也沒啥給你建議的, 祝你一切順利摟..

Unknown said...

日安:
關於那個SSPI我終於找出解答了
(翻了一下咕狗官於SSPI的問題)
必須要在帳號前面加上DOMAIN
(也有人說要加上MACHINE NAME...不過我是加上網域名稱就OK了!)

例如你的DOMAIN叫做ABC(實驗過之後發現大小寫好像沒差)
使用者名稱那邊就要變成
ABC\user
所以整串指令會變成
CVSROOT:
sspi:ABC\user@XXX.XXX.XXX.XXX:2401/測試區

如果以後您或其他人要用到SSPI
這可以參考一下^^

Unknown said...

日安:
大哥請問一下
CVS會產生LOG檔嗎?
比如說我今天增加了某的檔案或是提交什麼
會在伺服器留下紀錄嗎?

Unknown said...

日安:
哈囉好久沒來跟您請教問題囉
因為最近已經把管理員的工作交接出去了
不過最近因為又開始出現很怪的問題
所以又來打擾您一下

有些檔案(真的是有些,有的有,有的就不會)在我提交的時候
會出現
cvs server: user 'xxxxx' is not a valid editor of the file 'ooooo'

cvs [server aborted]: correct above errors first!

我去咕狗找一下
他說是沒將檔案設為編輯狀態
所以我在都會先讓他的狀態是可編輯
才修改,但是只要一提交,就是這個錯誤,實在是很火大,最後只有去伺服器直接砍掉這個檔

請問您有遇過這樣的問題嗎?
想說是不是跟檔名有關係....

DreamMan said...

我也想把管理員的工作交出去....>< ..

對了, 您之前問的LOG問題,我發了一篇文做回答, 希望您有看到

CVS有個根本性的問題, 如我兩個人同時edit 同一個版本, 如 1.8, 當其中一個人要commit時, 會變成1.9, 但當另一個人要update時就會錯誤, 因為你已經不是edit最新版本, 所以你需要先update成1.9再重1.9改,或是進行merge ..

而您用到的"編輯狀態"就是這個用途, 當你要開始編, 你就設為"編輯狀態", 別人就會知道而無法上傳, 所以當這個檔案被人設為"編輯狀態",你就會出現這樣的訊息, 解決方法就是看誰設的狀態請他先解除就可以了..

我的建議是, 這個功能不是很實用,因為縱使如我剛講的1.8被改為1.9你要上傳還是會被警告, 所以不會出錯, 所以如果可以, 根本不必使用狀態設定...

Unknown said...

日安:
收到嚕!!!
感恩~
關於那個log阿...
感覺起來好像wincvs功能比較強呢
烏龜大概就是因為整合了ie所以比較方便一點,
我看很多地方都是用wincvs...
(嘖...是不是壓錯寶了><)

DreamMan said...

嘻...我不壓寶, 兩個都用, 我電腦上有WinCVS與tortoisecvs, 隨時切換使用

其實最主要是WinCVS可以寫外掛, 所以功能無限延伸, 不過不知道為啥tortoisecvs還沒有

Unknown said...

咦~~~
可以並存嗎~~~?
不會衝突嗎@@?
因為我看烏龜會建立一個cvs的隱藏目錄來坐cvs的控管(or紀錄)
wincvs也是用同一個嗎?
還是他自己用自己的?

可以並存的話我倒是想用wincvs,
畢竟烏龜簡單歸簡單,
但是還是適合一般使用者
對於管理者來說,似乎還是wincvs比較好!

DreamMan said...

其實不論WinCVS 或 tortoisecvs, 底層控制CVS的動作都是由CVSNT 來執行, 所以兩者當然可以共用

CVS 的隱藏目錄, 是由cvsnt所建的, 我建議兩個程式交錯使用,因為其各有優缺點

對了TAG的部份我已經Post上了, 有機會參考一下吧, 接下來就剩下branch的部份了,WinCVS入門的文章就快要結束了

Unknown said...

日安:
當然阿~
您那篇一po出來我馬上就去收看囉!
然後再把您的文章從頭看一下,
再溫習一下wincvs的使用!

DreamMan said...

感恩, 好在有您的回應,至少還寫的有點價值

Unknown said...

哈囉~
自從將CVS交接出去之後
好久沒跟您請教囉!

哈~這次遇到的是備份的問題
我本來以為將他的整個檔案庫整個拷貝起來就好

但是很明顯的這樣會失敗
一旦CHECKOUT就會出現錯誤跟衝突
我想是因為檔案的一些資訊都不一樣了
所以他沒辦法MATCH
想請問您都是怎麼備份CVS的東西的呢?

另外我知道CVS沒辦法自己決定版本號
必須透過標籤的方式來決定
SVN也是如此嗎?

DreamMan said...

關於備份, 我是真的將整個zip起來後用ftp傳到別台主機, 這樣應該都不會有問題, 所以你可不可以把你的error message 寫出來

關於CVS 版本編號部份, SVN也是一樣的, 不過SVN可以記錄一批而不是單檔, 這部份SVN做的比較好