Git 에서 지원하는 기능 중의 한 가지가 바로 hooks 이다.
다른 형상관리 도구 또는 변경관리 도구에서는 trigger 기능이라고도 부른다.

예를 들어 개발자가 Local에서 commit을 한 것을 Remote로 push를 할 때에
어떤 작업을 실행하고 싶은 것이 있을 때 사용할 수 있는 기능이다.

Git의 어떤 행위를 수행 했을 때 특정 작업을 하도록 연결을 하고 싶다면 이 기능을 사용하면 되는 것이다.
 


Redmine에서는 Git을 저장소에 연결할 수 있다.


gitweb에 비해서 기능적으로는 조금 부족할 수도 있지만,
그에 비해서 훨씬 편리하고 예쁜 인터페이스를 자랑한다. 개인적으로 너무나 좋아한다.


그리고 Redmine은 Git의 commit을 작업으로 관리할 수도 있다.



그런데, 이렇게 아름다운 Redmine과 Git의 관계에 몇 가지 문제가 존재한다.
문제일 수도 있고, 그냥 이슈일 수도 있겠다.

1. Redmine과 Git은 같은 위치에 존재해야 한다.
   - Remote repository를 지원하지 않는 것은 정말 아쉽다.
   - mirroring 등의 방법으로 해결할 수는 있지만 문제는 문제다.

2. 대용량 repository 처리 시간에 따른 오류 발생 가능성이 있다.
   - Git 정보를 Web으로 처리하기 위해서 Redmine은 별도 가공을 해야 하는데 이 때 많은 시간이 소요된다.
   - Redmine은 웹으로 처리가 되다보니 지나치게 많은 소요시간이 걸리게 되면 Timeout이 되게 되는 것이다.
   - Apache의 Timeout 시간을 늘리는 것으로 임시조치는 할 수 있지만, 역시 문제는 문제다.

3. Git에 새로운 commit이 push 되었어도 Redmine에 실시간으로 반영되지 않는다.
   - 저장소 탭을 누군가 열어보기 전까지는 Redmine은 Git의 새로운 변경 내역을 알지 못한다.
   - 이 부분은 마땅히 해결할 방법이 없다. 관리자가 정기적으로 저장소 탭을 실행하곤 해야 하는 것이다.
   - 그런데, 이 때 Git의 hooks 기능을 사용하면 된다 !!!



정리해보면,
Redmine에서 형상관리 도구로 Git을 지원을 해주고 있지만 몇 가지 제약사항이 있다.
그 중 한 가지는 Git 저장소의 변경 사항이 실시간으로 Redmine에 반영이 되지 않는다는 것이다.
그래서 Git의 hooks 기능을 이용해 저장소의 변경 사항이 바로 바로 Redmine에 반영이 되도록 해보겠다.


원하는 결과를 얻기 위해서는 고려해야 할 사항들이 있다.
   - 우리가 원하는 기능을 수행할 시기는 Git Repository에 commit이 들어온 다음이다.
   - 우리가 원하는 기능은 Redmine의 저장소 페이지를 자동으로 여는 것이다.
   - Redmine의 저장소 페이지를 열도록 하기 위해서는 페이지를 열 수 있는 권한이 필요하다.


그런데, 이 부분을 처리하기 위해 알아 보던 중 또 하나 새로운 사항을 알게 되었다.

 


Redmine에서 설정 부분의 저장소 부분에 대한 것들을 살펴보자.

아래 적힌 내용은 저자가 테스트해본 결과에 기반한 것이다.
저자가 잘못 알고 있는 부분이 있다면 언제든 알려주기 바란다 !!!!


□ 커밋(commit)된 변경묶음을 자동으로 가져오기
   - Redmine 탭에서 '저장소'를 클릭하여 페이지를 열 때 저장소의 변경 사항을 받아오게 한다.
   - 선택하지 않으면 변경된 사항을 가져오지 않는다. 이런 경우 별도로 cron 작업을 통해서 변경 사항을 적용한다.

□ 저장소 관리에 WS를 사용
   - Redmine 공식적인 설명을 보면, SVN 저장소를 생성하기 위한 스크립트를 설치했을 때 활성화 시켜야 한다고 한다.
   - 그런데, Git refresh를 할 때에도 활성화 되어야 한다. 이유는 아직 모르겠다.

□ API 키
   - 실행 권한을 받기 위해서는 '키 생성'을 하고 이를 사용하면 된다.




자~ 서론이 길었다. 이제 시작해보자.

우리가 원하는 것은 현재 Redmine에 있는 하나의 프로젝트에서 사용하고 있는 Git repository에
새로운 commit이 들어오면 Redmine에게 정보를 리프레쉬 하라고 명령을 보내는 것이다.


1. Redmine 준비하기
   - 위 스크린샷과 같이,
   - [ 관리 → 설정 → 저장소 ] 메뉴에서 설정을 하자.
      ▷ 커밋(commit)된 변경묶음을 자동으로 가져오기 : 활성화
      ▷ 저장소 관리에 WS를 사용 : 활성화
      ▷ API 키 : 키생성


2. 저장소 경로 확인하기
   - 어떤 저장소인지 확인을 하자.



3. Git repository에 hook 설정하기
   - [ post-receive ] hook을 작성하자.

$ cd /srv/repository/barerepo.git
$ cd hooks
$ nano ./post-receive

   - [ _apikey ] 값은 위에서 생성한 값을 이용하면 된다.
   - [ _projectid ] 값은 프로젝트의 식별자를 사용한다.

#!/bin/bash

_apikey=UDcdoUtTm4a15xycCH52
_projectid=test

curl "http://127.0.0.1/redmine/sys/fetch_changesets?key=$_apikey&id=$_projectid"&

$ chmod +x ./post-receive

이걸로 끝이다.

그런데, 실제 push 작업을 해보면 무언가 좀 이상한 것이 보일 것이다.

$ git push
Counting objects: 3, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 233 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
remote:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
remote:                                                  Dload  Upload   Total    Spent     Left    Speed
remote: 100     1       0     1          0     0       0           0     --:--:--  0:00:07 --:--:--     0
remote:  To /srv/repository/barerepo.git
   bbde6a2..1107f45  master -> master

push를 하게 되면 Remote repository 입장에서 commit을 받아들인 후 post-receive 명령까지 수행을 해야 한다.
push 작업이 마무리 되는 순간은 post-receive 스크립트가 모두 완료가 되는 때이다.

그래서, 이렇게 하는 것이 문제가 될 수도 있다. 개발자 입장에서는 commit만 push가 되면 되는데,
(개발자 입장에서) 알지 못할 작업 때문에 push가 완료되는데에 시간이 더 소요되기 때문이다.



단지 어떻게 하는지만 알고 싶은 사람은 여기까지면 충분~!!
조금 더 알고 싶은 사람을 위해 설명을 추가하면...

   - [ http://<redmine url>/sys/fetch_changesets?key=<key값> ]과 같이 사용해도 충분하다.
   - 다만, 위와 같이 사용할 경우 사용하고 있는 redmine 전체에 대해서 refresh 하게 된다.

   - 전체가 아니라 특정 프로젝트만 refresh 하고 싶은 경우 위와 같이 [ &id=<식별자> ]를 붙이면 된다.

   - redmine 가이드를 보면 Git repository 이름을 사용하게 되어있는데, 그렇게 하면 실행이 안되었다.
   - 기능 변경이 있던지, 아니면 뭔가의 오류로 보인다.

   - [ /sys/fetch_changesets ] 주소는 그냥 사용하면 된다.

   - 개발자의 개발 과정에 지장을 주기 싫다면 (push 명령의 완료 시점) 위와 같이 이벤트 드리븐 방식으로 하면 안된다.
   - cron 등의 방법을 사용해서 개발자 부담을 줄여주는 것도 고려해볼만 하다.
   - [ ruby /path_to_redmine/redmine/script/rails runner "Repository.fetch_changesets" -e production > /dev/null 2>&1 & ]


Redmine 공식 가이드는 아래와 같다.

http://www.redmine.org/projects/redmine/wiki/HowTo_setup_automatic_refresh_of_repositories_in_Redmine_on_commit

반응형

Redmine의 장점 중 하나가 바로 형상관리 도구와의 연계이다.
Git 역시 Redmine과 잘 연동이 된다.

하지만 환경 설정을 좀 해줘야 한다.


1. config/configuration.yml

     - git을 사용하고 싶으면 실행파일의 경로를 알려줘야 한다.


$ cd /srv/www/redmine.whatwant.com/redmine
$ sudo nano ./configuration.yml

# = Redmine configuration file
#

# ==== SMTP server at using TLS (GMail)
#
default:
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      tls: true
      enable_starttls_auto: true
      address: "smtp.gmail.com"
      port: 587
      domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
      authentication: :plain
      user_name: xxx@gmail.com
      password: "xxx"

  # Absolute path to the directory where attachments are stored.
  #
  attachments_storage_path: /srv/www/redmine.whatwant.com/files

  # Configuration of SCM executable command.
  #
#  scm_subversion_command:
#  scm_mercurial_command:
  scm_git_command: /usr/local/bin/git
#  scm_cvs_command:
#  scm_bazaar_command:
#  scm_darcs_command:

     - 윗 부분은 이메일 셋팅이고, 아랫부분이 형상관리 도구 경로를 명시해주는 부분이다.
     - git을 사용하기 위해서 [ scm_git_command: /usr/local/bin/git ] 부분을 명시해주면 되는 것이다.



2. 전체 설정 - 저장소

     - 위와 같이 설정을 해주면 이제 사용할 수 있다.


     - 관리자 계정 (admin)으로 로그인해서 관리 메뉴를 고르고 저장소 항목을 보면 위와 같은 화면을 볼 수 있다.
     - 설정이 안된 도구들은 사용할 수가 없다는 것을 알 수 있을 것이다.



3. Project 저장소

     - 그러면 이제 실제 프로젝트에서의 설정을 살펴보자.


     - 위와 같이 간편하게 설정할 수 있다.
     - "Main repository"와 "식별자" 부분이 있는 것은 하나의 프로젝트에 복수의 저장소를 사용하는 경우 때문이다.



4. Remote Repository

     - 그런데, Redmine과 Repository는 기본적으로 같은 Local에 있어야지만 된다.
     - 만약 별도의 서버에 존재한다면 mirror repository를 만든다던지 해서 결국 같은 local에 위치시켜야 한다.

     - 일반적으로 Redmine이 있는 서버에 다른 서버에 위치하고 있는 repository를 clone을 한다.
     - 그리고, crontab 등을 이용해서 동기화를 하여 사용하는 형식을 취한다.

$ git clone --mirror git@xxx.xxx.xxx.xxx:myrepository.git

$ git remote update

     - 위와 같이 하면 된다고 하지만, 실제 위와 같이 운용해보지는 않았다.



5. Authority

     - local에 위치한 repository를 위 스크린샷과 같이 경로만 적어주면 Redmine이 알아서 이해를 해야하는데...
     - 그게 생각처럼 바로 잘 되는 경우가 없다. 그 이유는 바로 권한 !!!


     - 지금 현재 필자가 구동하고 있는 Redmine은 ubuntu 패키지 설치로 apache2로 하고 있다.
     - 그래서 웹서버가 www-data 계정으로 구동되고 있다.
     - 따라서 Redmine에서 저장소를 보기 위해 접근하고 있는 것은 www-data 계정이다.

$ cd /srv
$ sudo chown -R git.www-data ./repositories

     - 여기에선 이 글을 보고 계신 분의 설정 상황에 많이 좌우된다.
     - 즉, 여기에 쓰여져 있는 내용을 참조해서 본인의 상황에 맞춰 잘 판단해서 적용해야 한다.

     - 현재 repository는 git 계정으로 gitolite를 활용하여 운용이 되고 있고,
     - 그래서 사용자가 접근하여 사용하는 모든 행위는 git 계정의 권한으로 이루어지고 있다.
     - 그리고 앞에서 설명한 바와 같이 redmine은 apache2의 www-data 계정으로 활동을 한다.

     - 그래서 위와 같이 repository 들의 소유권 중에서 그룹을 www-data로 전부 변경을 해버리자.
     - 기본적으로 그룹에게 읽기 권한은 주어져 있는 상태이기 때문에 redmine에서 저장소 접근이 된다.

     - 그런데, 여기에서 또 하나 고려해야 하는 것이 있다.
     - 지금 현재 상태에서는 접근이 잘 되지만,
     - 사용자가 push를 하게 되면 그 때 생성되거나 수정된 파일의 권한이 바뀌면서 redmine에서 저장소를 못 읽는다.

     - 그렇다면, 그냥 git 계정의 정체를 바꿔버리면 간단히 해결이 된다.
     - 즉, git 계정의 소속 그룹을 그냥 www-data로 바꿔버리는 것이다.

$ sudo usermod -g www-data git


     - 최신 버전은 commit의 흐름도 제일 왼쪽에 visual하게 보여주어서 더더욱 마음에 든다!!!!!



Redmine 사용자 분들에게 많은 도움이 되길 기원하며....
반응형

어이없게도 Gitolite의 설치나 설정만 신경썼지,
사용자를 추가하는 것과 repository를 추가하는 것은 신경도 안썼다.

소사~ 소사~ 맙소사~!!!

     - http://sitaramc.github.com/gitolite/users.html


물론 앞에서 설명한 것들을 잘 조합하면 사용자 추가하는 방법과 repository 추가 등록하는 방법을 알 수는 있지만,
나름 정리하는 차원에서 다시 한 번 살펴보자.



1. 사용자 추가하기

     - Gitolite는 SSH 공개키 (public key) 파일로 사용자 등록을 한다.


$ ssh gitolite@localhost
$ cd ./repositories/gitolite-admin
$ cd ./keydir
$ scp chani@localhost:/home/chani/.ssh/id_rsa.pub ./chani.pub

     - Gitolite 관리자 계정으로 들어가서 clone 해놓은 [ gitolite-admin ] repository 에 위치하자.
     - [ gitolite-admin ] repository를 살펴보면 [ keydir ] 디렉토리가 있다. 이곳이 바로 공개키를 넣을 곳이다.

     - 내 경우는 "scp"를 이용해서 'chani' 계정의 공개키를 복사해왔다.

     - 여기에서 중요한 점은 가져온 공개키의 파일명이다. chani 계정의 경우 [ chani.pub ]라고 이름을 지어줬다.
     - 그러면 다르게 하는 경우도 있지않을까?라는 의문점이 드는데... 불행하게도(?) 그렇다.

     - 사용자 1명이 1개의 공개키를 사용하면 무관하지만, 복수개의 공개키를 사용할 때에는 어떻게할까?!
     - [ chani@gmail.pub, chani@whatwant.pub ] 처럼 파일이름을 해도 되고,
     - [ chani@gmail.com.pub, chani@whatwant.com.pub ] 처럼 파일이름을 해도 된다.



2. Error

     - 그런데, 여기에서 에러 상황이 발생했다.


$ git add ./keydir/chani.pub
$ git commit -a -m "add user 'chani'"
$ git push

     - 여기에서 경고 메시지를 발견했나요?

remote: WARNING: keydir/chani.pub duplicates a non-gitolite key, sshd will ignore it

     - 어?! gitolite가 아닌 키가 중복된다고?! sshd가 이걸 무시할거라고? 이게 뭔말이지?
     - 매뉴얼 등에서 이런 말은 못들었는데~!!!

     - 그래서 뭔가 이상해서 chani의 SSH 키 파일을 새로 생성해서 gitolite-admin/keydir/ 에 다시 등록해보았다.
     - 그랬더니 이번에는 push가 잘 되었는데...

     - chani 계정에서 ssh로 git-repo@localhost 에 접근을 할 수가 없는 것이다.
     - 새로 생성한 공개키를 git-repo@localhost 에 등록하지 않았으니,
     - 즉, 예전 공개키가 등록이 되어있으니 당연한 것이다.

     - 이 때 번뜩 떠오른 생각이 있으니...


     - "chani"라는 계정의 공개키를 지금 현재 2가지의 목적으로 사용하려고 하고 있는 것이다.
     - 물론 내부적으로는 SSH를 이용하고 있다는 점은 같지만,
     - SSH Console 로 접속을 하는 한가지와 Gitolite로 접속하는 한가지...
     - 즉, 2가지 접근 방법을 하나의 통로로 사용하려는 것이다.

     - 그럼 내부적으로 이걸 어떻게 확인할 수 있을까?


$ nano ./.ssh/authorized_keys


     - Gitolite 관리자 계정에서 [ ./.ssh/authorized_keys ] 파일을 확인해보자.


     - 어?! 이상한 내용이 추가되어 있다!!! [ # gitolite start ~ # gitolite end ] 라는 문장들이다.

     - 정확한 내부 로직은 분석을 해보지 않았지만 (솔직히 좀 귀찮다.... 흑... 분석해보면 도움이 될거라는 것은 알지만)
       일단 기본적인 SSH Login 흐름을 Gitolite가 가로채서 자신의 공개키로 인증하게 하는 것으로 추정이 된다.

     - 그러다보니 여기에서 문제가 발생하는 것이다. 같은 공개키를 사용하려하면 이 놈이 햇갈려하는 것 같다.

     - 결론은 같은 공개키로 SSH와 Gitolite를 사용하는 것은 안된다.
     - 어떻게 하면 할 수는 있겠지만, 문제의 소지가 있는 것은 원천적인 해결책을 찾기 전엔 하지 않는 것이 좋다!


     - 일단, 위와 같은 과정을 통해서 사용자 추가는 되었다!



3. Repository 추가하기

     - Gitolite로 관리하는 repository로 만들기에 대해서 알아봐야 하는데, 실은 엄청 쉽다.
     - 지금까지 Gitolite의 환경 파일 [ ./conf/gitolite.conf ] 파일에 해당 repository 정보만 적어주면 되는 것이다.

     - 문제는 이렇게만 알고 있다고 끝이 아니다!!! 이게 생각보다 복잡하다.



4. New Repository 생성하기

     - 신규로 repository를 생성하는 방법은 어떻게 될까?!


repo bare2repo
             RW+          = @whatwant

     - 실제로 존재하지 않는 [ bare2repo ]라는 이름의 repository에 대해서 권한 설정을 해준다.
     - 당연히 commit 하고 push 하는 것은 잊지 않기를...


     - 이제 어떻게 repository가 생기는 것인지 해보자.



     - 위와 같이 repository를 clone을 해서 사용할 수 있다.
     - 여기에서 포인트는 Server에 [ bare2repo.git ] repository가 없는데도 clone을 할 수가 있다는 점이다.

     - 그럼 여기에서 드는 궁금증. Gitolite의 권한 설정 없이 그냥 아무 이름이나 사용해서 가능할까?


     - 위 스크린샷과 같이 에러가 발생한다.


     - 결론!! 새로운 repository를 만들고 싶으면 [ ./conf/gitolite.conf ] 파일에 권한 설정을 해주면 된다!!!



5. Existing repository

     - 이번 포스팅에서 개인적으로 가장 큰 관심이 있는 부분은 바로 이것이다!
     - 기존에 작업하던 repository를 셋팅한 여기 이 Server에 넣어서 관리를 하고 싶을 때 어떻게 하면 될까?!


     - "git-repo" 계정으로 접속해서 가져오고 싶은 repository를 가져오자.
     - [ scp -r ] 명령으로 repository를 전송 받아오면 된다. (가져오는 방법은 각자 다양하게 있을 것이다)

     - 차례대로 따라왔으면 눈치챘겠지만, 가져왔으면 당연히 [ gitolite-admin/conf/gitolite.conf ] 파일을 손봐야한다.


repo bare1repo
             RW+          = @whatwant

     - "bare1repo" repository에 대한 설정을 넣어주면 된다. (commit & push 는 이제 더 이상 말하지 않아도...)

     - [ RW+    personal/USER/ = @whatwant ] 줄은 무시하시길....^^ 다음 포스팅 때 알게될것이니....

     - bare1repo.git repository를 위치에 놓고 계정 권한 설정을 했으니 이제 OK?????


     - 바로 해당 repository를 clone을 하면 위와 같이 error가 발생을 한다.
     - [ gl-conf ]를 못찾는다고 하는데, 대체 [ gl-conf ]가 뭐길래?


     - 혹시나하고 [ gitolite-admin.git ] repository에서 살펴보니 위와 같이 존재한다.
     - 내용은 계정에 대한 정보이다. 아마도 'GitoLite-CONFig' 파일인가보다.


     - 그래서 복사해 놓은 "bare1repo.git " repository의 파일을 살펴보니 당연하게도 [ gl-conf ] 파일은 없다.

     - 그러면, 이걸 어떻게 만들어 넣을 수 있을까?
     - 이 질문을 바꾸면 Gitolite에게 이 repository의 존재에 대해서 어떻게 알려줄 수 있을까?


$ ~/gitolite/src/gitolite setup

     - [ ~/gitolite/src/gitolite setup ] 명령을 실행시키면 [ gl-conf ] 파일이 생성이 된다.
     - 아무런 메시지를 보여주지 않아서 쫌 썰렁하지만, 너무나 간편하다.


     - 다시 clone을 해보면..... 짠~!!! 성공~!!!!



너무 길게 포스팅을 한 것 같지만...
그래도 매뉴얼 같은 곳에서 제대로 된 설명을 찾아보기 힘든 부분이니 잘 참고하세요~^^

반응형

'SCM > Git-GitHub' 카테고리의 다른 글

Rebase - 또 하나의 merge  (0) 2012.05.30
Gitolite - Personal Branches  (4) 2012.05.28
Git 계정 관리 - Gitolite's Repository  (4) 2012.05.20
Git 계정 관리 - Gitolite 설정하기  (3) 2012.05.19
Git 계정 관리 - Gitolite 설치하기  (23) 2012.05.15

내 컴퓨터에 Repository를 만드는 방법은 "Initializing"하는 방법과 "Cloning"하는 방법의 2가지가 있다.

1. Initializing
     - 새롭게 비어있는 저장소를 생성하는 경우이다.


     ① 저장소로 사용할 디렉토리를 하나 생성한다.
     ② 해당 디렉토리로 경로를 변경한다.
     ③ "git init"을 실행한다.

     - 이렇게 생성된 Repository는 work (non-bare) 타입의 저장소이다.
     - bare 타입과 work (non-bare) 타입의 차이는 다음 기회에 설명을 다시 하겠다.


2. Cloning
     - 기존에 이미 만들어진 Repository를 복사해서 나의 Repository를 만드는 경우이다.


     ① 저장소를 만들고자 하는 상위 디렉토리로 이동한다.
     ② "git clone"을 실행한다.

     - 마찬가지로 이렇게 만들어진 Repository는 work (non-bare) 타입의 저장소이다.


'git init'과 'git clone' 이 두가지만 알면 Git Repository를 생성할 수 있다.
반응형

'SCM > Git-GitHub' 카테고리의 다른 글

Tracking file - add, status, commit  (0) 2011.11.30
File Status Lifecycle in GIT  (1) 2011.11.22
저장소 - Repository  (0) 2011.11.17
Git에게 주인님 알려주기 (in Windows)  (0) 2011.11.14
Git에게 주인님 알려주기 (in Ubuntu)  (0) 2011.10.30

http://www.terms.co.kr/repository.htm
     - 데이터 집합체가 보관되고 조직적인 방식으로 유지되는 저장 장소

Git에서의 Repository는 Visual Studio 등에서 프로젝트와 비슷한 개념이다.

형상관리를 하려면 Repository를 생성하고,
그곳에 소스 코드를 넣고 히스토리를 기록하고 브랜치를 저장하면 된다.



이러한 Repository를 만드는 방법은 크게 2가지가 있다.

1. Initializing
     - 새로 Repository를 생성하는 방법

2. Cloning
     - 기존 Repository를 복제하는 방법



이런 Repository에는 2가지 타입이 있다.

1. Bare Repository
     - 순수한 의미에서의 Repository이다.
     - 형상관리 서버로 사용하기 위해서는 꼭 bare 타입이어야 한다.

2. Work Repository (non-bare repository)
     - 작업을 하기 위한 Repository이다.
     - 소스 코드 수정은 work repository에서만 할 수 있다.



Repository에 대한 간단한 소개는 이와 같고,
각각에 대해서는 앞으로 실제 사용 방법을 설명하면 이해가 될 것이다.

반응형

'SCM > Git-GitHub' 카테고리의 다른 글

File Status Lifecycle in GIT  (1) 2011.11.22
Repository 생성하기  (1) 2011.11.20
Git에게 주인님 알려주기 (in Windows)  (0) 2011.11.14
Git에게 주인님 알려주기 (in Ubuntu)  (0) 2011.10.30
Install GIt (in Windows)  (0) 2011.10.24

+ Recent posts