2012年12月29日 星期六

Git : 動手做看看 - 基本觀念

版本控制在管理源始碼的時候非常重要,可以讓開發者了解目前進度,共同開發時候也可以幫助開發者整合不同的源始碼。
日前因為 GitHub 的火紅帶領了 Git 這個工具的流行,而大部分的開發 IDE 也都支援 Git 的功能,這篇文章透過筆者的觀點來解釋 git 的運作方式,進而利用 Xcode 來說明圖形介面的操作和其相對應底層的機制是如何。

首先是

安裝 Git 


  1. 可以從最新的 Xcode 安裝或
  2. 到這裡下載 http://code.google.com/p/git-osx-installer/ (雖然檔案寫的是 snow leopard 但是 lion, mountain lion 都也可以用。)

1. 用 Xcode 安裝非常簡單,打開 Xcode 的 Preferences。如下

看到視窗後選 Downloads 的 Tab 然後安裝 Command Line Tools 如下。
這樣就好了在終端機輸入
git --version
會看到如下,那就是安裝成功了
git version 1.7.10.2 (Apple Git-33)
可以直接跳過下面,到 Git - 基本操作。

用 2. 的方法安裝的話 Mountain Lion 的朋友要注意一下,不是從 Apple Mac App 下載的安裝程式會被預設的安全機制給拒絕安裝,這時候就要打開,系統偏好設定。從左上方的蘋果圖示按下。

接著選擇安全性與隱私
看到如下畫面如果左下的鎖是鎖起來,點選,然後輸入密碼打開它
然後允許任何來源
這樣就可以安裝 Git。安裝好之後,把終端機(Terminal),打開輸入
git --version
一樣要看到如下
git version 1.7.10.2 (Apple Git-33)
安裝完成後,就來操作一下 git。
在終端機輸入
mkdir gitpractice

Git - 基本操作


來產生一個資料夾,這只是一個資料夾沒有什麼特別的,
再輸入
cd gitpractice
進入剛剛產生的 gitpractice 之內
這個裡面就是空的,沒有任何東西輸入
ls -al
來確認一下,得到

total 0
drwxr-xr-x   2 chronoer  staff    68 12 29 16:30 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
這樣的結果。現在要把這個資料夾轉成 git 可以處理的資料夾,輸入
git init
會得到如下
Initialized empty Git repository in /Users/chronoer/gitpractice/.git/
再用
ls -al
來看看這個資料夾有什麼變化,得到如下
total 0
drwxr-xr-x   3 chronoer  staff   102 12 29 16:31 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
drwxr-xr-x  10 chronoer  staff   340 12 29 16:31 .git
多了一個 .git 這個資料夾,這個 .git 就是存放和 git 的操作相關所有的在這個 gitpractice 底下所有檔案改成的歷史資料都存在這。而這個資料夾的存在,就代表,gitpractice 這個資料夾是受到 git 控管的,任何的檔案結構的改變都可以記錄下來,存放在 .git 這個資料夾裡面。有兩點要注意
Git 控管的資料夾只有根目錄會有 .git 不是每個子目錄都有。這個和 svn 做法不同
比如說 gitpractice 底下還有個 src 和 doc,架構如下
gitpractice/
                src/
                doc/
只有 gitpractice 裡面會有 .git/ 這個資料夾,src 和 doc 裡不會有,雖然 src 和 doc 這兩個也都受 git 控管,裡面檔案的變化也會記錄在其 git 根目錄 gitparctice 的 .git 之下。換言之如果把 .git 整個移除,那麼就無法得知 gitpractice 底下所有檔案變化,gitpractice 就回到一般目錄的身份。
另一個要注意的是
Git 不會自動控管檔案,而要手動加入。用 git add <fileName> 來把 <fileName> 加入 git 管理的隊伍中。
那我們就來動手加入一個檔案試。首先新增一個文字檔在 gitpractice/ 裡。輸入
echo "first line" > sample.txt
然後用
ls -al
看是不是多了一個文字檔如下

total 8
drwxr-xr-x   4 chronoer  staff   136 12 29 16:57 .
drwxr-xr-x+ 52 chronoer  staff  1768 12 29 16:30 ..
drwxr-xr-x  10 chronoer  staff   340 12 29 16:31 .git
-rw-r--r--   1 chronoer  staff    11 12 29 16:57 sample.txt

再用
cat sample.txt 
把這個文字檔的內容呈現在螢幕上
first line
這樣子我們新增一個檔案在 gitpractice/ 裡面,那這個檔案有被 git 所管理了嗎?NO 當然沒有,我們可以下
git status
來看 git 管理的檔案目前的狀態。如下
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
# sample.txt
出現 untracked files: 然後有一個檔案就是剛剛新增的叫 sample.txt。
先插開一下話題,設定一下 git,如果要讓 git 的一些常用的輸出有顏色出現比較好看的話,可以做如下設定。可以參考這個網頁
git config --global color.diff auto # git diff 要顯示顏色
git config --global color.status auto # git status 要顯示顏色
git config --global color.branch auto
git config --global color.log auto
OK 回到正題,我們新增一個檔案但是不被 git 所管理,那這種檔案叫 untracked file 。那怎麼變成 tracked 呢?就要如下輸入
git add sample.txt
然後再輸入
git status
就會得到如下
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
# new file:   sample.txt
#
沒有出現 untracked 字樣,這樣就是表示這個檔案 sample.txt 已為 git 所管理。
接下來我們要把這個檔案目前的樣子,記錄在 git 的歷史裡面,做為一個歷史的截圖,要輸入以下指令
git commit sample.txt -m "add first line"
其中用了 -m 這個 option 指的是對這個歷史節點,一定要留下註解,就是寫在 -m 之後的字串。然後會得到如下回應
[master (root-commit) 56f3cf3] add first line
 1 file changed, 1 insertion(+)
 create mode 100644 sample.txt
然後再用 
git log 
把歷史記錄呈現出來,如下
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
記得 git log 只會呈現 git commit 之後的狀態,沒有 commit 的檔案是不會出現在 git log 列表裡的。
稍微看一下歷史記錄的內容,第一個 row 看到一堆奇怪的編碼如下
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
這個代表 git 把 sample.txt 的整份檔案做一個 HASH 的動作,然後存成一個 40 character 的字串,就代表了那個時候的檔案狀態 (讀者產生的 40 字元的字串會因內容不同而產生不同的字串)。接下來看到的是
Author: chronoer <scentsome@gmail.com>
這個就是作者和 e-mail ,可以從這兩個指令設定

git config --global user.name "chronoer"
git config --global user.email "scentsome@gmail.com"
接下來就是記錄的日期
Date:   Sat Dec 29 21:56:25 2012 +0800
和當初 commit 時候的註解

add first line
接著我們再用
git status
來看一下git 的狀態
會得到
# On branch master
nothing to commit (working directory clean) 
表明目前工作的目錄沒什麼改變是一個常態。
那我們就來改變一下 sample.txt 的內容
輸入
echo "second line" >> sample.txt  
注意是兩個">" 意思是把 "second line" 接到 sample.txt 的最後一行後面一行。用
cat sample.txt 
就會看到如下 sample.txt 的內容

first line
second line
此時我們改變了 sample.txt 然後用
git status
來查看目前 git 所管理的檔案的狀態,得到如下反應
# On branch master
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   sample.txt#
任何被 git add 加入的檔案有改變,而還沒 commit 之前,都會如上表示,為 modified 代表有被修改。
這邊還有一個 Changes not staged for commit 的敘述,是什麼意思呢,之後再解釋,現在我們要把 sample.txt 變成 staged 或是 index 如下輸入
git add sample.txt
再一次把修改中或是 unstaged 的檔案加入到 git 系統
然後我再用
git status
來看檔案的狀態會得到如下的回應

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt
#

就不是 unstaged 狀態了。
為了把目前修改的 sample.txt 內容當成一個歷史的節點存下來,要用
git commit sample.txt -m "second line"
就會得到如下的回應

[master c1c905f] second line
 1 file changed, 1 insertion(+)
再次用
git status 
看一下目前的狀態,會得到

# On branch master
nothing to commit (working directory clean)
把剛剛修改的 sample.txt 再一次加入到了歷史的節點,工作目錄底下也就回到常態。到目前為止我們 commit 了兩次,用
git log
就可以看到兩個 commit 的記錄,就有兩個 HASH 之後的 40 個字元的字串。如下

commit c1c905f0d0f19a3ba750c3571d845bcc967c9ae6
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 23:33:35 2012 +0800
    second line
commit 56f3cf35c041a80f71393649cab87bbcab5d40bb
Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
比較新的放在上方。就有兩筆 commit log。
到目前為止的動作整理一下

  1. 新增資料夾 - mkdir gitpractice
  2. 進入資料夾 - cd gitpractice
  3. 把資料夾轉成 git 可以管理的 - git init 
  4. 新增檔案 - echo "first line" > sample.txt
  5. 加入 git 管理 - git add sample.txt
  6. 記錄歷史節點 - git commit sample.txt -m "first line"
  7. 修改檔案 - echo "second line" >> sample.txt
  8. 變成 staged 檔案 - git add sample.txt 
  9. 記錄歷史節點 - git commit sample.txt -m "second line" (若沒做 8. 會一起執行)
以上就是基本用 git 來做版本控管的方式,每一個 commit log 都可以視為一個版本。
如何檢視各個版本就是用 
git log
會得到如下的結果
commit c1c905f0d0f19a3ba750c3571d845bcc967c9ae6Author: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 23:33:35 2012 +0800
    second line
commit 56f3cf35c041a80f71393649cab87bbcab5d40bbAuthor: chronoer <scentsome@gmail.com>
Date:   Sat Dec 29 21:56:25 2012 +0800
    add first line
把各個 commit 版本呈現出來。每個版本的代號就是 40 個字的字串。如果想要比較兩個不同版本的差異可以用
git diff <commit id> <commit id>
其中<commit id> 可以用原本的 40 字的前 7 個來代表比如上面的例子,要比較
c1c905f0d0f19a3ba750c3571d845bcc967c9ae656f3cf35c041a80f71393649cab87bbcab5d40bb
可以用  c1c905f  56f3cf3 寫成
git diff c1c905f 56f3cf3
得到如下結果
diff --git a/sample.txt b/sample.txt
index 06fcdd7..08fe272 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1,2 +1 @@
 first line
-second line 
也就是  a/sample.txt, c1c905f 比 b/sample.txt 56f3cf3 少了一行 second line
如果把比較的檔案位子交換成
git diff 56f3cf3 c1c905f
就會得到

diff --git a/sample.txt b/sample.txt
index 08fe272..06fcdd7 100644
--- a/sample.txt
+++ b/sample.txt
@@ -1 +1,2 @@
 first line
+second line
也就是 56f3cf3 比 c1c905f 多了一行 second line
有了版本,就會想到要之前的版本,在 git 可以這麼做
git checkout <commit id> <file name>
<file name> 就會回到 <commit id> 的狀態,比如在這個例子,我們這麼做
git checkout 56f3cf3 sample.txt
然後再用
cat sample.txt
看看 sample.txt 的內容得到
first line
回到了 56f3cf3 的狀態了
很方便的版本管理可以任意回到之前的版本。
最後筆者來整理一下檔案在 git 資料夾裡的狀態。
剛剛我們用 git checkout <commit id> <file name> 之後會回到之前 commit 狀態,此時如果用
git status 
來看狀態會是如下

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt
#

是回到一個 staged 的 sample.txt,如果再對這個檔案修改。比如用 vi 存完檔
就會發生一個很有趣的狀況

git status 
來看會看到

# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   sample.txt#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified:   sample.txt#
同一個檔案有兩個狀態,一個是 staged 另一個是 unstaged 可以直接 commit 或是先把 unstaged 用 git add 變成 staged
之後如果 commit 就會再多出一個 commit log。
以上是 git 的簡介,下一篇會來探討Xcode 的 GUI 和 git 運作的相關功能。








2012年12月2日 星期日

Objective-C naming convention 的重要性 - ARC class 呼叫 non-ARC Class method

Michael 上課的時候都會和學員提到命名的習慣,每個語言都有其命名的習慣。了解習慣可以幫助初學者很快的進入這個環境想要傳答的意圖是什麼。
命名的範圍很廣,從 Source Code 的檔案名稱到 Class 的名稱,Variable 的命稱,Method 的名稱等等都有其命名的習慣。 打個比方,在寫 Java 時,Java 的 Class 如下定義
class Car {
}
產生一個 Car class 其第一個字大寫是 Java 的命名習慣,而相對應這個 Source code 的檔案,也常命名為 Car.java,習慣就是 Java 的檔案名稱和其內容的第一個 Class 命稱一樣,大寫字開頭,這個是 Java 語言中的習慣。
習慣指的是,即使語法沒有強制說不行,但是大家都這樣做。
而 Objective-C 沒有這樣的習慣。

上述的 class 在 Objective-C 是這樣命名


@interface Car
@end

@implementation Car
@end

Class 的定義在 Objective-C 是分開的分為

@interface Car
@end


@implementation Car
@end
兩個部分。
再來看看另一個語言  C# 是如下定義
 
class Car{
} 
看起來和 Java 有點像,也沒有檔案名稱要和 class 命稱一樣命名的習慣。
我們再來看看 Class 的內部命名習慣。
把剛剛的 class 加上 ivar 和 method.
Java 部分
 
class Car {
  String name;
  String getName(){
    return name;
  }
  void setName(String newName){
    name = newName;
  }
}
新增了 name 這個變數 和 getName 還有 setName 這兩個 method。 getName 稱為 getter 而 setName 則為 setter 。
Java 的命名習慣就是,第一個字小寫,不論 variable 或是 method 的名稱。名詞和名詞之間後面接的名詞第一個字大寫,這種命西習慣稱為 CamelCase 駝峰式大小寫
再來看看 Objective-C 的命名習慣。

@interface Car:NSObject{
  NSString * name;
}
@end

@implementation Car
-(NSString *) name{
  return name;
}

-(void) setName:(NSString *) newName{
  name = newName;
}
@end
 
Objective-C 的命名習慣和 Java 差不多,但是 getter 的地方不一樣,在 Objective-C getter 的名稱和要讀的變數名稱一樣。在這個例子兩者都叫 name。
我們再來看看類似的 class 在 C# 如何表現?
 
class Car{
  string name;
  public string GetName(){
    return name;
  }
  public void SetName(string newName){
    name = newName;
  }
} 
在 C# 中新增了 name 這個變數和 GetName 還有 SetName 這兩個 method。在這邊可以注意到 C# 的變數名稱是小寫開頭而 method 的名稱習慣用大寫字開頭。
三種語言都有其個自的習慣,雖然不按照這個習慣,大部分情況下程式也是可以執行,但是在 Objective-C 如果不照者命名的習慣會造成一些奇怪的問題產生。
就讓我們來進入這篇文章的主題,開啟一個 ARC enable 的 Project 再加入一個 none ARC 的 class 就用上述的 Car 再加上兩個 method,讓我們一步一步來。
首先開啟一個 Single View Application 的專案如下
記得要勾選 Automatic Reference Counting
新增一個 Class 名為 Car 繼承 NSObject 如下是 Car.h
 
@interface Car : NSObject
-(id) newName;
-(id) newname;
@end
Car.m 如下
 
@implementation Car
-(id) newName{
    return [NSString stringWithFormat:@"Honda"];
}

-(id) newname{
    return [NSString stringWithFormat:@"Honda"];
}
@end
新增兩個 method 這兩個 method 的名稱很像,一個叫 newName 一個叫 newname 差別在於一個 name 有大寫的 N 另一個沒有。換句話說,newName 是有依照習慣命名,newname 是沒有的。 接下我們要把這個 Car.m 設定為不使用 ARC。
如下設定。
再新增一個 Button 在 畫面上,然後 Button 按下去 action 如下
 
- (IBAction)testCar:(id)sender {
    Car * car = [Car new];
    NSLog(@"Car name %@", [car newName]);
}
我們先使用這個 newName method。一執行,發現馬上就 crash。 但是如果換成 newname 這個 method 就是正常執行。 為什麼呢? 因為對於 non-ARC 的 method,
ARC 會看其 method 的開頭是不是 alloc, new 或 copy (所謂開頭指的是符合 CamelCase 的第一個詞)
或是
第一個是 mutable 第二個是 copy ,也就是寫成 mutableCopy 。
此時 ARC 會自動認為這個 method 是會產生一個沒有加到 autorelease pool 的物件,然後就自行決定加上 release 在這個例子使用 newName 就 crash 是因為 newName 回傳的是
[NSString stringWithFormat:@"Honda"] 
這個有加到 autorelease pool 的物件。而 ARC 自動多 release 一次了。這個例子是要提示大家,在寫 method 的時候要注意 CamelCase 第一個字,這個就是 ARC 的習慣。