vcs2json 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cli/main.rb +8 -6
- data/lib/vcs2json/git.rb +189 -89
- data/lib/vcs2json/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7320d73c75a5f2989baaf2789bd5809d1262da6e
|
4
|
+
data.tar.gz: 4e2a78f7370a848f1bf6d590d3e3b31516dab4d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c85a8e3d1aa5177349eeb3dfb0e72f0bf155d4fd9202ffdb39c630f99701d97cad527611ad1d8357e9db9cf1af6cf654b187f5a805ef5ee23f76a7a9485cbf3a
|
7
|
+
data.tar.gz: cb97b86a43a3006d229c8221aaabd4930c0dd9b650335926b523b313b2b2f958427f14bb2d3d3d0a5ac257fc0c409b91a4f9de41d23667b3f9094bba3015a107
|
data/lib/cli/main.rb
CHANGED
@@ -2,13 +2,15 @@ require_relative 'cli_helper'
|
|
2
2
|
|
3
3
|
module Vcs2JsonCLI
|
4
4
|
class Main < Thor
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
method_option :ignore, type: :string, desc: "Specify location of .evocignore file"
|
6
|
+
method_option :case_id, type: :string, desc: "Specify case identifier. Used by .evocignore etc"
|
7
|
+
method_option :issue, :aliases => '-i', :type => :boolean, :default => false, :desc => "Attempt to extract issue ids from commit messages"
|
8
|
+
method_option :after, :aliases => '-a', :desc => "Only include commits after this date"
|
9
|
+
method_option :before, :aliases => '-b', :desc => "Only include commits before this date"
|
10
|
+
method_option :number, :aliases => '-n', type: :numeric, default: 10000, :desc => "The number of commits to dump"
|
11
|
+
desc "git [options]","Make a dump of the change-history of system using git, output on stdout"
|
10
12
|
def git
|
11
|
-
|
13
|
+
Vcs2Json::Git.new(options).execute
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
data/lib/vcs2json/git.rb
CHANGED
@@ -1,109 +1,209 @@
|
|
1
1
|
require_relative '../vcs2json_helper'
|
2
2
|
|
3
3
|
module Vcs2Json
|
4
|
-
|
4
|
+
class Git
|
5
|
+
# Generate separators between fields and commits
|
6
|
+
FIELD_SEP = Digest::SHA256.hexdigest Time.new.to_s + "field_sep"
|
7
|
+
COMMIT_SEP = Digest::SHA256.hexdigest Time.new.to_s + "commit_sep"
|
8
|
+
|
5
9
|
def initialize(opts)
|
6
|
-
|
10
|
+
@opts = opts
|
11
|
+
self.ignore = @opts[:ignore]
|
12
|
+
# Create a commit hash that defaults to creating new hashes given hash[:key]
|
13
|
+
# so we can do 'commit[:commit][:author][:name] = .. ' without creating the :commit and :author hashes first
|
14
|
+
@commits = Hash.new {|h,k| h[k] = Hash.new(&h.default_proc) }
|
15
|
+
# place to stare empty commit ids if they are encountered
|
16
|
+
@empty_commits = []
|
17
|
+
# used to decide if we should try to search for more commits
|
18
|
+
@oldest_commit_in_previous_search = Time.now
|
7
19
|
end
|
8
20
|
|
21
|
+
def execute
|
22
|
+
# recursively add commits as long as we have less than :number and there are still more commits to search
|
23
|
+
add_commits(@opts)
|
24
|
+
add_integer_mapping
|
9
25
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
26
|
+
# sort on date and prune excessive commits
|
27
|
+
sorted_and_pruned = @commits.sort_by {|id,commit| commit[:date]}.reverse.map {|(_,commit)| commit}.first(@opts[:number])
|
28
|
+
|
29
|
+
# print commits to stdout as json
|
30
|
+
$stdout.puts JSON.pretty_generate(sorted_and_pruned)
|
31
|
+
|
32
|
+
# print ids of empty commits to stderr
|
33
|
+
if !@empty_commits.empty?
|
34
|
+
STDERR.puts "EMPTY COMMITS"
|
35
|
+
STDERR.puts @empty_commits
|
36
|
+
end
|
37
|
+
# print additional info to stderr
|
38
|
+
STDERR.puts "\n\nExtracted #{sorted_and_pruned.size} commits."
|
20
39
|
end
|
21
40
|
|
41
|
+
def ignore
|
42
|
+
@ignore
|
43
|
+
end
|
22
44
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
45
|
+
def ignore= path
|
46
|
+
default_locations = ["#{Dir.pwd}/.evocignore","~/.evocignore"]
|
47
|
+
paths = (path.nil? ? default_locations : [path] + default_locations)
|
48
|
+
file = nil
|
49
|
+
ignore = []
|
50
|
+
paths.each do |p|
|
51
|
+
if File.exist?(p)
|
52
|
+
file = File.open(p)
|
53
|
+
STDERR.puts "Loading files to ignore from #{file.path}"
|
54
|
+
# return first match
|
55
|
+
break
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if file.nil?
|
59
|
+
STDERR.puts ".evocignore not found. Tried #{paths}. All files will be used."
|
60
|
+
else
|
61
|
+
if @opts[:case_id].nil?
|
62
|
+
STDERR.puts "Id in .evocignore not specified, not ignoring any files."
|
63
|
+
else
|
64
|
+
ignore_file = YAML.load(file)
|
65
|
+
if ignore_file.key?(@opts[:case_id])
|
66
|
+
ignore = ignore_file[@opts[:case_id]]
|
67
|
+
if !ignore.nil?
|
68
|
+
STDERR.puts "Ignoring #{ignore.size} files"
|
69
|
+
end
|
70
|
+
else
|
71
|
+
STDERR.puts "The id: '#{@opts[:case_id]}' not found in #{file.path}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@ignore = (ignore.nil? ? [] : ignore)
|
76
|
+
return @ignore
|
77
|
+
end
|
78
|
+
private
|
79
|
+
|
80
|
+
def add_commits(opts)
|
81
|
+
add_meta_information(opts)
|
82
|
+
add_change_information(opts)
|
83
|
+
|
84
|
+
if @commits.size < @opts[:number]
|
85
|
+
oldest_commit_in_this_search = get_oldest_commit
|
86
|
+
if oldest_commit_in_this_search != @oldest_commit_in_previous_search
|
87
|
+
# we found new commits in this search but still need more
|
88
|
+
@oldest_commit_in_previous_search = oldest_commit_in_this_search
|
89
|
+
add_commits(before: oldest_commit_in_this_search, number: (@opts[:number] - @commits.size)*2)
|
90
|
+
else
|
91
|
+
STDERR.puts "\nAsked for #{@opts[:number]} commits, only found #{@commits.size} non-empty commits. Searched all the way back to #{oldest_commit_in_this_search}."
|
92
|
+
end
|
93
|
+
end
|
30
94
|
end
|
31
95
|
|
96
|
+
def get_oldest_commit
|
97
|
+
oldest = nil
|
98
|
+
if !@commits.empty?
|
99
|
+
oldest = @commits.first[1][:date]
|
100
|
+
@commits.each do |sha,info|
|
101
|
+
if info[:date] < oldest
|
102
|
+
oldest = info[:date]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
return oldest
|
107
|
+
end
|
32
108
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
commits[sha][:author_date] = Time.parse fields[4]
|
59
|
-
commits[sha][:message] = fields[5]
|
60
|
-
|
61
|
-
# attempt to parse an issue id from the commit message
|
62
|
-
if @opts.issue?
|
63
|
-
commits[commit[0]][:issue] = parse_issue(commits[sha][:message])
|
109
|
+
def hash_2_gitoptions(opts)
|
110
|
+
before = opts[:before].nil? ? '' : "--before=\"#{opts[:before]}\""
|
111
|
+
after = opts[:after].nil? ? '' : "--after=\"#{opts[:after]}\""
|
112
|
+
number = opts[:number].nil? ? '' : "-n #{opts[:number]}"
|
113
|
+
return "#{before} #{after} #{number} --no-merges"
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_meta_information(opts)
|
117
|
+
raw_commits = `git log #{hash_2_gitoptions(opts)} --pretty=format:'%H#{FIELD_SEP}%cn#{FIELD_SEP}%ce#{FIELD_SEP}%cd#{FIELD_SEP}%ad#{FIELD_SEP}%B#{COMMIT_SEP}'`
|
118
|
+
commits_info = raw_commits.encode('UTF-8', :invalid => :replace).split(COMMIT_SEP)
|
119
|
+
|
120
|
+
commits_info.each do |commit|
|
121
|
+
fields = commit.split(FIELD_SEP)
|
122
|
+
sha = fields[0].delete("\n") #remove astray newlines
|
123
|
+
@commits[sha][:sha] = sha
|
124
|
+
@commits[sha][:name] = fields[1]
|
125
|
+
@commits[sha][:email] = fields[2]
|
126
|
+
@commits[sha][:date] = Time.parse fields[3]
|
127
|
+
@commits[sha][:author_date] = Time.parse fields[4]
|
128
|
+
@commits[sha][:message] = fields[5]
|
129
|
+
|
130
|
+
# attempt to parse an issue id from the commit message
|
131
|
+
if @opts[:issue]
|
132
|
+
@commits[commit[0]][:issue] = parse_issue(@commits[sha][:message])
|
133
|
+
end
|
64
134
|
end
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
135
|
+
end
|
136
|
+
|
137
|
+
def add_change_information(opts)
|
138
|
+
commits_changes_type = `git log --pretty=format:'#{FIELD_SEP}%H' --name-status #{hash_2_gitoptions(opts)}`.split(FIELD_SEP)
|
139
|
+
commits_changes_type.each do |commit|
|
140
|
+
if !commit.empty?
|
141
|
+
lines = commit.split("\n")
|
142
|
+
sha = lines[0]
|
143
|
+
@commits[sha][:changes][:all] = []
|
144
|
+
if lines.size > 1
|
145
|
+
lines[1..-1].each do |line|
|
146
|
+
if !line.empty?
|
147
|
+
file_info = line.split("\t")
|
148
|
+
file_name = file_info[1]
|
149
|
+
status = file_info[0]
|
150
|
+
@commits[sha][:changes][:all] << file_name
|
151
|
+
@commits[sha][:changes][:details][file_name][:filename] = file_name
|
152
|
+
@commits[sha][:changes][:details][file_name][:status] = parse_status(status)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
# filter out ignored files
|
157
|
+
if !self.ignore.nil?
|
158
|
+
@commits[sha][:changes][:all].reject! {|i| self.ignore.include?(i)}
|
159
|
+
end
|
160
|
+
if @commits[sha][:changes][:all].empty?
|
161
|
+
@empty_commits << sha
|
162
|
+
@commits.delete(sha)
|
163
|
+
end
|
84
164
|
end
|
85
|
-
end
|
86
165
|
end
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_integer_mapping
|
169
|
+
# create file_name -> integer mapping
|
170
|
+
mapping = Hash.new
|
171
|
+
index_counter = 0
|
172
|
+
@commits.each do |sha,info|
|
173
|
+
integer_representation = []
|
174
|
+
info[:changes][:all].each do |file|
|
175
|
+
if mapping[file].nil?
|
176
|
+
mapping[file] = index_counter
|
177
|
+
index_counter += 1
|
178
|
+
end
|
179
|
+
integer_representation << mapping[file]
|
180
|
+
info[:changes][:details][file][:id] = mapping[file]
|
181
|
+
end
|
182
|
+
info[:changes][:all].clear
|
183
|
+
info[:changes][:all] = integer_representation
|
101
184
|
end
|
102
|
-
|
103
|
-
info[:changes][:all] = integer_representation
|
104
|
-
end
|
185
|
+
end
|
105
186
|
|
106
|
-
|
187
|
+
# simply un-abbreviates the status code given by --name-status
|
188
|
+
def parse_status(abbreviated_status)
|
189
|
+
case abbreviated_status
|
190
|
+
when "A"
|
191
|
+
"added"
|
192
|
+
when "M"
|
193
|
+
"modified"
|
194
|
+
when "D"
|
195
|
+
"deleted"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
# attempts to parse an issue/bug id from the given commit message
|
201
|
+
def parse_issue(message)
|
202
|
+
if match = /(bug|issue) (?<id>\d+)/i.match(message)
|
203
|
+
return match[:id]
|
204
|
+
else
|
205
|
+
return ""
|
206
|
+
end
|
107
207
|
end
|
108
|
-
|
208
|
+
end
|
109
209
|
end
|
data/lib/vcs2json/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vcs2json
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Rolfsnes
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-06-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -159,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
159
|
version: '0'
|
160
160
|
requirements: []
|
161
161
|
rubyforge_project:
|
162
|
-
rubygems_version: 2.
|
162
|
+
rubygems_version: 2.5.1
|
163
163
|
signing_key:
|
164
164
|
specification_version: 4
|
165
165
|
summary: Create JSON dumps of change history from git etc.
|