Ruby で git リポジトリを操作する rugged を使ってみた

[rugged](https://github.com/libgit2/rugged) は libgit2 を ruby にバインドした gem です。

gitの細かい操作が出来て便利。

自分が前に使っていた時のメモです。（1年前のなのでちょっと古いかも）

https://github.com/siman-man/rugged-examples

[colorize](https://github.com/fazibear/colorize) は見栄えを良くするために使っています。


## git clone

```rb
require 'rugged'

url = 'https://github.com/siman-man/rugged-examples'
local_path = File.expand_path('repos/rugged-examples', Dir.pwd)

repo = Rugged::Repository.clone_at(url, local_path)
```

private なリポジトリを取ってくる時は access_token を発行して、credentials 情報を追加してあげる。

```rb
require 'rugged'

url = 'https://github.com/siman-man/rugged-examples'
local_path = File.expand_path('repos/rugged-examples', Dir.pwd)

credentials = Rugged::Credentials::UserPassword.new(
  username: "access_token",
  password: ""
)

repo = Rugged::Repository.clone_at(url, local_path, credentials: credentials)
```

## git reflog

```rb
require 'colorize'
require 'rugged'

repo = Rugged::Repository.new('.')

ref = repo.head

ref.log.reverse_each do |entry|
  sha = entry[:id_new]
  message = entry[:message]
  committer = entry[:committer]
  author = committer[:name]
  email = committer[:email]
  time = committer[:time]

  puts <<-GITLOG
#{"commit\t#{sha}".yellow}
Author:\t#{author} <#{email}>
Date:\t#{time}"
    #{message}"
  GITLOG
end
```

## git branch

```rb
require 'colorize'
require 'rugged'

repo = Rugged::Repository.new('.')

repo.branches.each do |branch|
  name = branch.name
  remote_name = branch.remote_name

  next if name.include?('/')

  if branch.head?
    puts "* #{name.green}"
  else
    puts "  #{name}"
  end
end
```


## git branch [name]
ブランチの作成

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

repo.create_branch('test')
```

## git log

```rb
require 'colorize'
require 'rugged'

repo = Rugged::Repository.new('.')

repo.walk(repo.last_commit).take(5).each do |entry|
  sha = entry.oid
  message = entry.message.chomp
  committer = entry.author
  author = committer[:name]
  email = committer[:email]
  time = committer[:time]

  puts "commit #{sha}".yellow
  puts "Author:\t#{author} <#{email}>"
  puts "Date:\t#{time}"
  puts ""
  puts "#{message}"
  puts ""
end
```


# git ls-tree

```rb
require 'rugged'

repo = Rugged::Repository.new('.')
ref = repo.head
commit = ref.target
tree = commit.tree

tree.each do |obj|
  puts "%06o #{obj[:type]} #{obj[:oid]}\t#{obj[:name]}" % [obj[:filemode]]
end
```


## git diff

```rb
require 'colorize'
require 'rugged'

def color_diff_message(message)
  message.split("\n").map {|line|
    case line
    when /^\+/
      line.green
    when /^\-/
      line.red
    when /^@@.*@@$/
      line.cyan
    else
      line
    end
  }.join("\n")
end

repo = Rugged::Repository.new('.')
diff = repo.diff_workdir(repo.last_commit)

diff.each_patch do |patch|
  puts color_diff_message(patch.to_s)
end
```

# git status [WIP]

結講実装が面倒

```rb
require 'rugged'
require 'colorize'

# TODO: not yet implemented
repo = Rugged::Repository.new('.')


repo_status = Hash.new{|h,k| h[k] = []}

index_files = []
worktree_files = []

repo.status {|file, status_data|
  repo_status[status_data.first] << file
  status = status_data.first

  if %i(index_new index_modified index_deleted).include?(status_data.first)
    index_files << [file, status]
  elsif %i(worktree_new worktree_modified worktree_deleted).include?(status_data.first)
    worktree_files << [file, status]
  end
}

puts "On branch master"
puts "Your branch is up-to-date with 'origin/master'."
puts '  Changes to be committed:'
puts '    (use "git reset HEAD <file>..." to unstage)'
puts ''

index_files.each do |file, status|
  status = status.to_s.gsub('index_', '')
  puts "\t#{status} file:  #{file}".green
end

puts ''
puts 'Untracked files:'
puts '  (use "git add <file>..." to include in what will be committed)'
puts ''

worktree_files.each do |file, status|
  status = status.to_s.gsub('worktree_', '')
  puts "\t#{file}".red
end

puts ''
```

## 一番最初のコミットを取得

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

first_commit = repo.walk(repo.last_commit).to_a.last

puts first_commit.oid
puts first_commit.message
```

## 一番最後のコミットを取得

`Repository#last_commit` で取得可能

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

commit = repo.last_commit

puts commit.oid
puts commit.message
```

## 指定した範囲のコミットオブジェクトを取得

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

def range_log(repo, from, to)
  logs = []
  commits = repo.walk(from)

  commits.each do |commit|
    break if to.oid == commit.oid
    logs << commit
  end

  if commits.size == logs.size
    raise 'Object not found' if commits.size == logs.size
  else
    logs
  end
end

commits = repo.walk(repo.last_commit).to_a

logs = range_log(repo, commits[0], commits[5])

p logs.size
```

## 最新コミットからある特定の時間までのコミットを取得

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

def get_timerange_log(repo, time)
  repo.walk(repo.last_commit).take_while do |commit|
    commit.time > time
  end
end

# 1 hour's commits
hour_logs = get_timerange_log(repo, Time.now - 3600)

# 1 day's commits
day_logs = get_timerange_log(repo, Time.now - 3600 * 24)

# 1 week's commits
week_logs = get_timerange_log(repo, Time.now - 3600 * 24 * 7)

puts "#{hour_logs.size} commits 1 hour ago"
puts "#{day_logs.size} commits 1 day ago"
puts "#{week_logs.size} commits 1 week ago"
```

## ファイルの中身を閲覧
`git cat-file -p <object_id>` に相当するやつ、事前に対象のobject_idが判明している場合は `Rugged::Blob.lookup(repo, object_id)` が良さそう

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

revision = repo.last_commit.oid
path = 'Gemfile'

blob = repo.blob_at(revision, path)

puts blob.text

puts ""

puts blob.content
```

`Blob#content` でも同様の処理が可能、`#text` と `#content` には引数が渡せて、`#text` の方では「最大n行まで」、`#content` では「最大n bytes」までの取得を行う操作が可能

## ブランチ属性でフィルタリング

ローカルブランチとリモートブランチのフィルタリングが可能

```rb
require 'colorize'
require 'rugged'

repo = Rugged::Repository.new('.')

repo.branches.each(:local) do |local_branch|
  puts local_branch.name
end

repo.branches.each(:remote) do |remote_branch|
  puts remote_branch.name
end

puts "\nlocal branche list"

repo.branches.each_name(:local) do |name|
  puts "  #{name}"
end

puts "\nremote branch list"

repo.branches.each_name(:remote) do |name|
  puts "  #{name}"
end
```

## ブランチを削除する

強制削除

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

branch_name = 'sample_branch'

# git branch -D branch_name
repo.branches.delete(branch_name)
```

## ブランチオブジェクトを取得する

`BranchCollection#[]` にブランチ名を渡して上げると取れる

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

branch_name = 'master'
branches = repo.branches

branch = branches[branch_name]

puts branch.name
```

## ブランチが存在するかどうかを調べる

`BranchCollection#exist` と `BranchCollection#exists` が使える

```rb
require 'rugged'

repo = Rugged::Repository.new('.')

branches = repo.branches

p branches.exist?('hoge') #=> false
p branches.exist?('master') #=> true

p branches.exists?('hoge') #=> false
p branches.exists?('master') #=> true
```

## コミット情報の出力
git log コマンドに parent_id と Committer 情報を足したぐらい

```rb
require 'rugged'
require 'colorize'

repo = Rugged::Repository.new('.')

commit = repo.last_commit

author = commit.author
committer = commit.committer

puts <<-COMMIT_LOG
parents: #{commit.parent_ids.join(', ').blue}
commit #{commit.oid.yellow}
Author: #{author[:name]} <#{author[:email]}>
Committer: #{committer[:name]} <#{committer[:email]}>
Date: #{committer[:time]}
    #{commit.message}
COMMIT_LOG
```

## 最新コミットのファイル一覧表示

最後の表示部分は `puts [root, entry[:name]].join` でもOK

```rb
require 'rugged'
require 'pathname'

repo = Rugged::Repository.new('.')

repo.last_commit.tree.walk_blobs do |root, entry|
  puts Pathname.new(root).join(entry[:name])
end
```