1

我有一个提交的每个文件的差异统计列表(在 Git 中使用 diff --numstat),我需要将其解析为树结构作为哈希,以便我可以将其用作 JSON。原始数据的格式如下:

1       1       app/assets/javascripts/foo.js.coffee
2       1       app/assets/javascripts/bar.js
16      25      app/assets/javascripts/baz.js.coffee
11      0       app/controllers/foo_controller.rb
3       2       db/schema.rb
41      1       lib/foobar.rb

我需要将其解析为嵌套哈希格式,如下所示:

{ name: "app", children: [
  { name: "assets", children: [
    { name: "javascripts", children: [
      { name: "foo.js.coffee", add: 1, del: 1 },
      { name: "bar.js", add: 2, del: 1 }
      { name: "baz.js.coffee", add: 16, del: 25 }
    ], add: 19, del: 27 },
    ...
  ] } 
] }

树的每一层都由它的名称表示,子节点作为哈希值以及该树的添加和删除总数。

有没有一种有效的方法可以在 Ruby 中构造这样的哈希?

4

2 回答 2

3

完整来源:https ://gist.github.com/dimitko/5541709 。您可以下载并直接运行它而没有任何麻烦(只要确保拥有awesome_printgem;它以更易于阅读的格式向您显示对象层次结构)。

我稍微丰富了你的测试输入,以确保算法不会犯愚蠢的错误。

鉴于此输入:

input = <<TEXT
2       1       app/assets/javascripts/bar.js
16      25      app/assets/javascripts/baz.js.coffee
1       1       app/assets/javascripts/foo.js.coffee
4       9       app/controllers/bar_controller.rb
3       2       app/controllers/baz_controller.rb
11      0       app/controllers/foo_controller.rb
3       2       db/schema.rb
41      1       lib/foobar.rb
12      7       lib/tasks/cache.rake
5       13      lib/tasks/import.rake
TEXT

而这个预期的结果:

[{:name=>"app", :add=>37, :del=>38, :children=>[{:name=>"assets", :add=>19, :del=>27, :children=>[{:name=>"javascripts", :add=>19, :del=>27, :children=>[{:name=>"bar.js", :add=>2, :del=>1}, {:name=>"baz.js.coffee", :add=>16, :del=>25}, {:name=>"foo.js.coffee", :add=>1, :del=>1}]}]}, {:name=>"controllers", :add=>18, :del=>11, :children=>[{:name=>"bar_controller.rb", :add=>4, :del=>9}, {:name=>"baz_controller.rb", :add=>3, :del=>2}, {:name=>"foo_controller.rb", :add=>11, :del=>0}]}]}, {:add=>3, :del=>2, :name=>"db", :children=>[{:name=>"schema.rb", :add=>3, :del=>2}]}, {:add=>58, :del=>21, :name=>"lib", :children=>[{:name=>"foobar.rb", :add=>41, :del=>1}, {:name=>"tasks", :add=>17, :del=>20, :children=>[{:name=>"cache.rake", :add=>12, :del=>7}, {:name=>"import.rake", :add=>5, :del=>13}]}]}]

而这段代码:

def git_diffnum_parse_paths(list, depth, out)
  to = 1
  base = list.first[:name][depth]
  while list[to] and list[to][:name][depth] == base do
    to += 1
  end

  if list.first[:name][depth+1]
    out << {name: base, add: 0, del: 0, children: []}

    # Common directory found for the first N records; recurse deeper.
    git_diffnum_parse_paths(list[0..to-1], depth + 1, out.last[:children])

    add = del = 0
    out.last[:children].each do |x| add += x[:add].to_i; del += x[:del].to_i; end
    out.last[:add] = add
    out.last[:del] = del
  else
    # It's a file, we can't go any deeper.
    out << {name: list.first[:name].last, add: list.first[:add].to_i, del:     list.first[:del].to_i}
  end

  if list[to]
    # Recurse in to try find common directories for the deeper records.
    git_diffnum_parse_paths(list[to..-1], depth, out)
  end

  nil
end

def to_git_diffnum_tree(txt)
  items = []
  txt.split("\n").each do |line|
    m = line.match(/(\d+)\s+(\d+)\s+(.+)/).to_a[1..3]
    items << {add: m[0], del: m[1], name: m[2]}
  end

  items.sort! { |a,b|
    a[:name] <=> b[:name]
  }

  items.each do |item|
    item[:name] = item[:name].split("/")
  end

  out = []
  git_diffnum_parse_paths(items, 0, out)
  out
end

而这段代码,它正在使用它:

require 'awesome_print'
out = to_git_diffnum_tree(input)
puts; ap out; puts
puts; puts "Expected result:"; puts expected.inspect
puts; puts "Actual result: ";  puts out.inspect
puts; puts "Are expected and actual results identical: #{expected == out}"

它似乎产生了你想要的东西。

笔记:

  • 我正在按目录/文件名对已解析条目的数组进行排序。这样做是为了避免遍历整个列表来搜索公共目录;相反,该算法可以扫描列表直到第一个不匹配。
  • 我远非认为这是最佳解决方案,但这是我在空闲时间想出的。
  • puts在要点中留下了一些 [未] 注释的陈述,以防你想大致了解算法是如何工作的。
  • 如果您想对其进行更可靠的测试,请尝试以下操作:

git diff --numstat `git rev-list --max-parents=0 HEAD | 头 -n 1`头

这将为您提供自初始提交以来的添加和删除数量(前提是您的 Git 版本 >=1.7.4.2),这是一个更大的输入,您可以在其中对算法进行更严格的测试。

希望我有所帮助。

于 2013-05-08T16:50:22.693 回答
0

定义“高效”。如果您的问题是“性能”,那么您的解决方案不是红宝石。

除非你真的在 Linux 源代码上运行这个脚本,否则我不会担心性能,只担心意图的清晰性。

我从@dimitko 的解决方案中获得灵感,并最小化了使用的代码。

https://gist.github.com/x1024/3d0f9ad61fcb4b189be3

def git_group lines, root = 'root'
  if lines.count == 1 and lines[0][:name].empty? then
    return {
      name: root,
      add: lines.map { |l| l[:add] }.reduce(0, :+),
      del: lines.map { |l| l[:del] }.reduce(0, :+),
    }
  end

  lines = lines.group_by { |line| line[:name].shift }
               .map { |key, value| git_group(value, key) }

  return {
    name: root,
    add: lines.map { |l| l[:add] }.reduce(0, :+),
    del: lines.map { |l| l[:del] }.reduce(0, :+),
    children: lines
  }
end

def to_git_diffnum_tree(txt)
  data = txt.split("\n")
    .map { |line| line.split() }
    .map { |line| {add: line[0].to_i, del: line[1].to_i, name: line[2].split('/')} }
    .sort_by { |item| item[:name] }

  git_group(data)[:children]
end

如果你愿意妥协你的数据格式(即返回相同的数据但结构不同),你可以用更少的代码做到这一点:

https://gist.github.com/x1024/5ecfdfe886e31f8b5ab9

def git_group lines
  dirs = lines.select { |line| line[:name].count > 1 }
  files = (lines - dirs).map! { |file| [file.delete(:name).shift, file] }
  dirs_processed = dirs.group_by { |dir| dir[:name].shift }
                    .map { |key, value| [key, git_group(value)] }
  data = dirs_processed.concat(files)

  return {
    add: data.map { |k,l| l[:add] }.reduce(0, :+),
    del: data.map { |k,l| l[:del] }.reduce(0, :+),
    children: Hash[data]
  }
end

def to_git_diffnum_tree(txt)
  data = txt.split("\n")
    .map { |line| line.split() }
    .map { |line| {add: line[0].to_i, del: line[1].to_i, name: line[2].split('/')} }
    .sort_by { |item| item[:name] }

  git_group(data)[:children]
end

记住孩子们,用 Ruby 编写 C++ 是不好的。

于 2013-05-09T10:13:53.570 回答