Skip to content

Latest commit

 

History

History
499 lines (434 loc) · 21.1 KB

import-custom.asc

File metadata and controls

499 lines (434 loc) · 21.1 KB

직접 Importer 만들기

사용하는 VCS가 앞서 살펴본 시스템이 아니면 인터넷에서 적당한 Importer를 찾아봐야 한다. CVS, Clear Case, Visual Source Safe 같은 시스템용 Importer가 좋은게 많다. 심지어 단순히 디렉토리 아카이브용 Importer에도 좋은게 있다. 사람들이 잘 안쓰는 시스템을 사용하고 있는데 적당한 Importer를 못 찾았거나 부족해서 좀 더 고쳐야 한다면 git fast-import 를 사용한다. 이 명령은 표준입력으로 데이터를 입력받는다. ch10-git-internals.asc 에서 배우는 저수준 명령어와 내부 객체를 직접 다루는 것보다 훨씬 쉽다. 먼저 사용하는 VCS에서 필요한 정보를 수집해서 표준출력으로 출력하는 스크립트를 만든다. 그리고 그 결과를 git fast-import 의 표준입력으로 보낸다.

간단한 Importer를 작성해보자. current 디렉토리에서 작업하고 back_YYYY_MM_DD 이라는 디렉토리에 백업하면서 진행했던 프로젝트를 살펴 보자. Importer를 만들 때 디렉토리 상태는 아래와 같다.

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Importer를 만들기 전에 우선 Git이 어떻게 데이터를 저장하는지 알아야 한다. 이미 알고 있듯이 Git은 기본적으로 스냅샷을 가리키는 커밋 개체가 연결된 리스트이다. 스냅샷이 뭐고, 그걸 가리키는 커밋은 또 뭐고, 그 커밋의 순서가 어떻게 되는지 fast-import 에 알려 줘야 한다. 이 것이 해야할 일의 전부다. 그러면 디렉토리마다 스냅샷을 만들고, 그 스냅샷을 가리키는 커밋 개체를 만들고, 이전 커밋과 연결 시킨다.

ch08-customizing-git.asc 절에서 했던 것 처럼 Ruby로 스크립트를 작성한다. 책에서 계속 스크립트를 작성할 때 Ruby로 해왔고, 읽기도 쉽기에 Ruby를 쓴다. 하지만 자신에게 익숙한 것을 사용해서 표준출력으로 적절한 정보만 출력할 수 있으면 된다. 그리고 Windows에서는 라인 바꿈 문자에 CR(Carriage Return) 문자가 들어가지 않도록 주의해야 한다. git fast-import 명령은 Windows에서도 라인 바꿈 문자로 CRLF 문자가 아니라 LF(Line Feed) 문자만 허용한다.

우선 해당 디렉토리로 이동해서 어떤 디렉토리가 있는지 살펴본다. 하위 디렉토리마다 스냅샷 하나가 되고 커밋 하나가 된다. 하위 디렉토리를 이동하면서 필요한 정보를 출력한다. 기본적인 로직은 아래와 같다.

last_mark = nil

# loop through the directories
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # move into the target directory
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

각 디렉토리에서 print_export 를 호출하는데 이 함수는 아규먼트로 디렉토리와 이전 스냅샷 Mark를 전달받고 현 스냅샷 Mark를 반환한다. 그래서 적절히 연결 시킬 수 있다. fast-import 에서 “Mark” 는 커밋의 식별자를 말한다. 커밋을 하나 만들면 “Mark” 도 같이 만들어 이 “Mark” 로 다른 커밋과 연결 시킨다. 그래서 print_export 에서 우선 해야 하는 일은 각 디렉토리 이름으로 “Mark” 를 생성하는 것이다.

mark = convert_dir_to_mark(dir)

Mark는 정수 값을 사용해야 하기 때문에 디렉토리를 배열에 담고 그 Index를 Mark로 사용한다. 아래와 같이 작성한다.

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

각 커밋을 가리키는 정수 Mark를 만들었고 다음에는 커밋 메타데이터에 넣을 날짜 정보가 필요하다. 이 날짜는 디렉토리 이름에 있는 것을 가져다 사용한다. print_export 의 두 번째 라인은 아래와 같다.

date = convert_dir_to_date(dir)

convert_dir_to_date 는 아래와 같이 정의한다.

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

시간는 정수 형태로 반환한다. 마지막으로 메타정보에 필요한 것은 Author인데 이 것은 전역 변수 하나로 설정해서 사용한다.

$author = 'John Doe <[email protected]>'

이제 Importer에서 출력할 커밋 데이터는 다 준비했다. 이제 출력해보자. 사용할 브랜치, 해당 커밋과 관련된 Mark, 커미터 정보, 커밋 메시지, 이전 커밋을 출력한다. 코드로 만들면 아래와 같다.

# print the import information
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

우선 시간대(-0700) 정보는 편의상 하드코딩으로 처리했다. 각자의 시간대에 맞는 오프셋을 설정해야 한다. 커밋 메시지는 아래와 같은 형식을 따라야 한다.

data (size)\n(contents)

이 형식은 “data” 라는 단어, 읽을 데이터의 크기, 라인 바꿈 문자, 실 데이터로 구성된다. 이 형식을 여러 곳에서 사용해야 하므로 export_data 라는 Helper 메소드로 만들어 놓는게 좋다.

def export_data(string)
  print "data #{string.size}\n#{string}"
end

이제 남은 것은 스냅샷에 파일 내용를 포함시키는 것 뿐이다. 디렉토리로 구분돼 있기 때문에 어렵지 않다. 우선 deleteall 이라는 명령을 출력하고 그 뒤에 모든 파일의 내용을 출력한다. 그러면 Git은 스냅샷을 잘 저장한다.

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

Note: 대부분의 VCS는 리비전을 커밋간의 변화로 생각하기 때문에 fast-import에 추가/삭제/변경된 부분만 입력할 수도 있다. 스냅샷 사이의 차이를 구해서 fast-import에 넘길 수도 있지만 훨씬 복잡하다. 줄 수 있는 데이터는 전부 Git에 줘서 Git이 계산하게 해야 한다. 꼭 이렇게 해야 한다면 어떻게 데이터를 전달해야 하는지 fast-import 의 ManPage를 참고하라.

파일 정보와 내용은 아래와 같은 형식으로 출력한다.

M 644 inline path/to/file
data (size)
(file contents)

644는 파일의 모드를 나타낸다(실행파일이라면 755로 지정해줘야 한다). inline 다음 라인 부터는 파일 내용이라 말하는 것이다. inline_data 메소드는 아래와 같다.

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

파일 내용은 커밋 메시지랑 같은 방법을 사용하기 때문에 앞서 만들어 놓은 export_data 메소드를 다시 이용한다.

마지막으로 다음에 커밋할 현 Mark 값을 반환한다.

return mark
Note

Windows에서 실행할 때는 추가 작업이 하나 더 필요하다. 앞에서 얘기했지만 Windows는 CRLF를 사용하지만 git fast-import 는 LF를 사용한다. 이 문제를 해결 하려면 Ruby가 CRLF 대신 LF를 사용하도록 알려 줘야 한다.

$stdout.binmode

모든게 끝났다. 스크립트 코드는 아래와 같다:

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <[email protected]>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end

def convert_dir_to_date(dir)
    if dir == 'current'
        return Time.now().to_i
    else
        dir = dir.gsub('back_', '')
        (year, month, day) = dir.split('_')
        return Time.local(year, month, day).to_i
    end
end

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    puts 'commit refs/heads/master'
    puts "mark :#{mark}"
    puts "committer #{$author} #{date} -0700"
    export_data("imported from #{dir}")
    puts "from :#{last_mark}" if last_mark

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end

# Loop through the directories
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # move into the target directory
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

스크립트를 실행하면 아래와 같이 출력된다.

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <[email protected]> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <[email protected]> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

디렉토리를 하나 만들고 git init 명령을 실행해서 옮길 Git 프로젝트를 만든다. 그리고 그 프로젝트 디렉토리로 이동해서 이 명령의 표준출력을 git fast-import 명령의 표준입력으로 연결한다(pipe).

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

성공적으로 끝나면 여기서 보여주는 것처럼 어떻게 됐는지 통계를 보여준다. 이 경우엔 브랜치 1개와 커밋 4개 그리고 개체 13개가 임포트됐다. 이제 git log 명령으로 히스토리 조회가 가능하다.

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <[email protected]>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <[email protected]>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

깔끔하게 Git 저장소가 완성됐다. 이 시점에서는 아무것도 Checkout 하지 않았기 때문에 워킹 디렉토리에 아직 아무 파일도 없다. master 브랜치로 Reset 해서 파일을 Checkout 한다.

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

fast-import 명령으로 많은 일을 할 수 있다. 모드를 설정하고, 바이너리 데이터를 다루고, 브랜치를 여러 개 다루고, Merge 하고, 태그를 달고, 진행상황을 보여 주고, 등등 무수히 많은 일을 할 수 있다. Git 소스의 contrib/fast-import 디렉토리에 복잡한 상황을 다루는 예제가 많다.