tetra 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. data/.gitignore +27 -0
  2. data/.rubocop.yml +14 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +28 -0
  5. data/MOTIVATION.md +27 -0
  6. data/README.md +106 -0
  7. data/Rakefile +9 -0
  8. data/SPECIAL_CASES.md +130 -0
  9. data/bin/tetra +29 -0
  10. data/integration-tests/commons.sh +55 -0
  11. data/lib/template/gitignore +2 -0
  12. data/lib/template/kit.spec +64 -0
  13. data/lib/template/kit/CONTENTS +8 -0
  14. data/lib/template/kit/jars/CONTENTS +1 -0
  15. data/lib/template/kit/m2/settings.xml +10 -0
  16. data/lib/template/output/CONTENTS +3 -0
  17. data/lib/template/package.spec +65 -0
  18. data/lib/template/src/CONTENTS +6 -0
  19. data/lib/tetra.rb +63 -0
  20. data/lib/tetra/ant_runner.rb +27 -0
  21. data/lib/tetra/archiver.rb +95 -0
  22. data/lib/tetra/commands/ant.rb +23 -0
  23. data/lib/tetra/commands/base.rb +89 -0
  24. data/lib/tetra/commands/download_maven_source_jars.rb +29 -0
  25. data/lib/tetra/commands/dry_run.rb +17 -0
  26. data/lib/tetra/commands/finish.rb +22 -0
  27. data/lib/tetra/commands/generate_all.rb +38 -0
  28. data/lib/tetra/commands/generate_kit_archive.rb +18 -0
  29. data/lib/tetra/commands/generate_kit_spec.rb +16 -0
  30. data/lib/tetra/commands/generate_package_archive.rb +19 -0
  31. data/lib/tetra/commands/generate_package_script.rb +21 -0
  32. data/lib/tetra/commands/generate_package_spec.rb +22 -0
  33. data/lib/tetra/commands/get_pom.rb +33 -0
  34. data/lib/tetra/commands/get_source.rb +30 -0
  35. data/lib/tetra/commands/init.rb +15 -0
  36. data/lib/tetra/commands/list_kit_missing_sources.rb +21 -0
  37. data/lib/tetra/commands/move_jars_to_kit.rb +18 -0
  38. data/lib/tetra/commands/mvn.rb +23 -0
  39. data/lib/tetra/git.rb +140 -0
  40. data/lib/tetra/kit_checker.rb +104 -0
  41. data/lib/tetra/kit_runner.rb +43 -0
  42. data/lib/tetra/kit_spec_adapter.rb +28 -0
  43. data/lib/tetra/logger.rb +28 -0
  44. data/lib/tetra/main.rb +102 -0
  45. data/lib/tetra/maven_runner.rb +47 -0
  46. data/lib/tetra/maven_website.rb +59 -0
  47. data/lib/tetra/package_spec_adapter.rb +59 -0
  48. data/lib/tetra/pom.rb +55 -0
  49. data/lib/tetra/pom_getter.rb +104 -0
  50. data/lib/tetra/project.rb +245 -0
  51. data/lib/tetra/script_generator.rb +57 -0
  52. data/lib/tetra/source_getter.rb +41 -0
  53. data/lib/tetra/spec_generator.rb +60 -0
  54. data/lib/tetra/template_manager.rb +33 -0
  55. data/lib/tetra/version.rb +6 -0
  56. data/lib/tetra/version_matcher.rb +90 -0
  57. data/spec/data/ant-super-simple-code/build.xml +133 -0
  58. data/spec/data/ant-super-simple-code/build/HW.class +0 -0
  59. data/spec/data/ant-super-simple-code/build/mypackage/HW.class +0 -0
  60. data/spec/data/ant-super-simple-code/dist/antsimple-20130618.jar +0 -0
  61. data/spec/data/ant-super-simple-code/lib/junit-4.11.jar +0 -0
  62. data/spec/data/ant-super-simple-code/lib/log4j-1.2.13.jar +0 -0
  63. data/spec/data/ant-super-simple-code/src/mypackage/HW.java +15 -0
  64. data/spec/data/antlr/antlr-2.7.2.jar +0 -0
  65. data/spec/data/antlr/pom.xml +6 -0
  66. data/spec/data/commons-logging/commons-logging-1.1.1.jar +0 -0
  67. data/spec/data/commons-logging/parent_pom.xml +420 -0
  68. data/spec/data/commons-logging/pom.xml +504 -0
  69. data/spec/data/nailgun/nailgun-0.7.1.jar +0 -0
  70. data/spec/data/nailgun/pom.xml +153 -0
  71. data/spec/data/struts-apps/pom.xml +228 -0
  72. data/spec/data/tomcat/pom.xml +33 -0
  73. data/spec/lib/ant_runner_spec.rb +45 -0
  74. data/spec/lib/archiver_spec.rb +106 -0
  75. data/spec/lib/git_spec.rb +105 -0
  76. data/spec/lib/kit_checker_spec.rb +119 -0
  77. data/spec/lib/maven_runner_spec.rb +68 -0
  78. data/spec/lib/maven_website_spec.rb +56 -0
  79. data/spec/lib/pom_getter_spec.rb +36 -0
  80. data/spec/lib/pom_spec.rb +69 -0
  81. data/spec/lib/project_spec.rb +254 -0
  82. data/spec/lib/script_generator_spec.rb +67 -0
  83. data/spec/lib/source_getter_spec.rb +36 -0
  84. data/spec/lib/spec_generator_spec.rb +130 -0
  85. data/spec/lib/template_manager_spec.rb +54 -0
  86. data/spec/lib/version_matcher_spec.rb +64 -0
  87. data/spec/spec_helper.rb +37 -0
  88. data/spec/support/kit_runner_examples.rb +15 -0
  89. data/tetra.gemspec +31 -0
  90. data/utils/delete_nonet_user.sh +8 -0
  91. data/utils/setup_nonet_user.sh +8 -0
  92. metadata +267 -0
@@ -0,0 +1,55 @@
1
+ # encoding: UTF-8
2
+
3
+ module Tetra
4
+ # encapsulates a pom.xml file
5
+ class Pom
6
+ def initialize(filename)
7
+ @doc = Nokogiri::XML(open(filename).read)
8
+ @doc.remove_namespaces!
9
+ end
10
+
11
+ def group_id
12
+ @doc.xpath("project/groupId").text || ""
13
+ end
14
+
15
+ def artifact_id
16
+ @doc.xpath("project/artifactId").text || ""
17
+ end
18
+
19
+ def name
20
+ @doc.xpath("project/name").text || ""
21
+ end
22
+
23
+ def version
24
+ @doc.xpath("project/version").text || ""
25
+ end
26
+
27
+ def description
28
+ @doc.xpath("project/description").text || ""
29
+ end
30
+
31
+ def url
32
+ @doc.xpath("project/url").text || ""
33
+ end
34
+
35
+ def license_name
36
+ @doc.xpath("project/licenses/license/name").text || ""
37
+ end
38
+
39
+ def runtime_dependency_ids
40
+ @doc.xpath("project/dependencies/dependency[\
41
+ not(optional='true') and not(scope='provided') and not(scope='test') and not(scope='system')\
42
+ ]").map do |element|
43
+ [element.xpath("groupId").text, element.xpath("artifactId").text, element.xpath("version").text]
44
+ end
45
+ end
46
+
47
+ def scm_connection
48
+ @doc.xpath("project/scm/connection").text || ""
49
+ end
50
+
51
+ def scm_url
52
+ @doc.xpath("project/scm/url").text || ""
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ # encoding: UTF-8
2
+
3
+ module Tetra
4
+ # attempts to get java projects' pom file
5
+ class PomGetter
6
+ include Logging
7
+
8
+ # saves a jar poms in <jar_filename>.pom
9
+ # returns filename and status if found, else nil
10
+ def get_pom(filename)
11
+ content, status = (get_pom_from_jar(filename) || get_pom_from_sha1(filename) || get_pom_from_heuristic(filename))
12
+ return unless content
13
+
14
+ pom_filename = filename.sub(/(\.jar)?$/, ".pom")
15
+ File.open(pom_filename, "w") { |io| io.write(content) }
16
+ [pom_filename, status]
17
+ end
18
+
19
+ # returns a pom embedded in a jar file
20
+ def get_pom_from_jar(file)
21
+ log.debug("Attempting unpack of #{file} to find a POM")
22
+ begin
23
+ Zip::File.foreach(file) do |entry|
24
+ if entry.name =~ /\/pom.xml$/
25
+ log.info("pom.xml found in #{file}##{entry.name}")
26
+ return entry.get_input_stream.read, :found_in_jar
27
+ end
28
+ end
29
+ rescue Zip::Error
30
+ log.warn("#{file} does not seem to be a valid jar archive, skipping")
31
+ rescue TypeError
32
+ log.warn("#{file} seems to be a valid jar archive but is corrupt, skipping")
33
+ end
34
+ nil
35
+ end
36
+
37
+ # returns a pom from search.maven.org with a jar sha1 search
38
+ def get_pom_from_sha1(file)
39
+ log.debug("Attempting SHA1 POM lookup for #{file}")
40
+ begin
41
+ if File.file?(file)
42
+ site = MavenWebsite.new
43
+ sha1 = Digest::SHA1.hexdigest File.read(file)
44
+ results = site.search_by_sha1(sha1).select { |result| result["ec"].include?(".pom") }
45
+ result = results.first
46
+ unless result.nil?
47
+ log.info("pom.xml for #{file} found on search.maven.org for sha1 #{sha1}\
48
+ (#{result["g"]}:#{result["a"]}:#{result["v"]})"
49
+ )
50
+ group_id, artifact_id, version = site.get_maven_id_from result
51
+ return site.download_pom(group_id, artifact_id, version), :found_via_sha1
52
+ end
53
+ end
54
+ rescue RestClient::ResourceNotFound
55
+ log.warn("Got a 404 error while looking for #{file}'s SHA1 in search.maven.org")
56
+ end
57
+ nil
58
+ end
59
+
60
+ # returns a pom from search.maven.org with a heuristic name search
61
+ def get_pom_from_heuristic(filename)
62
+ begin
63
+ log.debug("Attempting heuristic POM search for #{filename}")
64
+ site = MavenWebsite.new
65
+ filename = cleanup_name(filename)
66
+ version_matcher = VersionMatcher.new
67
+ my_artifact_id, my_version = version_matcher.split_version(filename)
68
+ log.debug("Guessed artifact id: #{my_artifact_id}, version: #{my_version}")
69
+
70
+ result = site.search_by_name(my_artifact_id).first
71
+ log.debug("Artifact id search result: #{result}")
72
+ unless result.nil?
73
+ group_id, artifact_id, _ = site.get_maven_id_from result
74
+ results = site.search_by_group_id_and_artifact_id(group_id, artifact_id)
75
+ log.debug("All versions: #{results}")
76
+ their_versions = results.map { |doc| doc["v"] }
77
+ best_matched_version = (
78
+ if !my_version.nil?
79
+ version_matcher.best_match(my_version, their_versions)
80
+ else
81
+ their_versions.max
82
+ end
83
+ )
84
+ best_matched_result = (results.select { |r| r["v"] == best_matched_version }).first
85
+
86
+ group_id, artifact_id, version = site.get_maven_id_from(best_matched_result)
87
+ log.warn("pom.xml for #{filename} found on search.maven.org with heuristic search\
88
+ (#{group_id}:#{artifact_id}:#{version})"
89
+ )
90
+
91
+ return site.download_pom(group_id, artifact_id, version), :found_via_heuristic
92
+ end
93
+ rescue RestClient::ResourceNotFound
94
+ log.warn("Got a 404 error while looking for #{filename} heuristically in search.maven.org")
95
+ end
96
+ nil
97
+ end
98
+
99
+ # get a heuristic name from a path
100
+ def cleanup_name(path)
101
+ Pathname.new(path).basename.to_s.sub(/.jar$/, "")
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,245 @@
1
+ # encoding: UTF-8
2
+
3
+ module Tetra
4
+ # encapsulates a Tetra project directory
5
+ class Project
6
+ include Logging
7
+
8
+ attr_accessor :full_path
9
+ attr_accessor :git
10
+
11
+ def initialize(path)
12
+ @full_path = Tetra::Project.find_project_dir(File.expand_path(path))
13
+ @git = Tetra::Git.new(@full_path)
14
+ end
15
+
16
+ def name
17
+ File.basename(@full_path)
18
+ end
19
+
20
+ def version
21
+ latest_tag_count(:dry_run_finished)
22
+ end
23
+
24
+ # finds the project directory up in the tree, like git does
25
+ def self.find_project_dir(starting_dir)
26
+ result = starting_dir
27
+ while project?(result) == false && result != "/"
28
+ result = File.expand_path("..", result)
29
+ end
30
+
31
+ fail NoProjectDirectoryError, starting_dir if result == "/"
32
+
33
+ result
34
+ end
35
+
36
+ # returns true if the specified directory is a valid tetra project
37
+ def self.project?(dir)
38
+ File.directory?(File.join(dir, "src")) &&
39
+ File.directory?(File.join(dir, "kit")) &&
40
+ File.directory?(File.join(dir, ".git"))
41
+ end
42
+
43
+ # returns the package name corresponding to the specified dir, if any
44
+ # raises NoPackageDirectoryError if dir is not a (sub)directory of a package
45
+ def get_package_name(dir)
46
+ dir_path = Pathname.new(File.expand_path(dir)).relative_path_from(Pathname.new(@full_path))
47
+ components = dir_path.to_s.split(File::SEPARATOR)
48
+ if components.count >= 2 &&
49
+ components.first == "src" &&
50
+ Dir.exist?(File.join(@full_path, components[0], components[1]))
51
+ components[1]
52
+ else
53
+ fail NoPackageDirectoryError
54
+ end
55
+ rescue ArgumentError, NoProjectDirectoryError
56
+ raise NoPackageDirectoryError, dir
57
+ end
58
+
59
+ # inits a new project directory structure
60
+ def self.init(dir)
61
+ Dir.chdir(dir) do
62
+ Tetra::Git.new(".").init
63
+
64
+ FileUtils.mkdir_p "src"
65
+ FileUtils.mkdir_p "kit"
66
+
67
+ # populate the project with templates and take a snapshot
68
+ project = Project.new(".")
69
+
70
+ template_manager = Tetra::TemplateManager.new
71
+ template_manager.copy "output", "."
72
+ template_manager.copy "kit", "."
73
+ template_manager.copy "src", "."
74
+ template_manager.copy "gitignore", ".gitignore"
75
+
76
+ project.take_snapshot "Template files added", :init
77
+ end
78
+ end
79
+
80
+ # starts a dry running phase: files added to kit/ will be added
81
+ # to the kit package, src/ will be reset at the current state
82
+ # when finished
83
+ def dry_run
84
+ return false if dry_running?
85
+
86
+ current_directory = Pathname.new(Dir.pwd).relative_path_from Pathname.new(@full_path)
87
+
88
+ take_snapshot("Dry-run started", :dry_run_started, current_directory)
89
+ true
90
+ end
91
+
92
+ # returns true iff we are currently dry-running
93
+ def dry_running?
94
+ latest_tag_count(:dry_run_started) > latest_tag_count(:dry_run_finished)
95
+ end
96
+
97
+ # ends a dry-run.
98
+ # if abort is true, reverts the whole directory
99
+ # if abort is false, reverts sources and updates output file lists
100
+ def finish(abort)
101
+ if dry_running?
102
+ if abort
103
+ @git.revert_whole_directory(".", latest_tag(:dry_run_started))
104
+ @git.delete_tag(latest_tag(:dry_run_started))
105
+ else
106
+ take_snapshot "Changes during dry-run", :dry_run_changed
107
+
108
+ @git.revert_whole_directory("src", latest_tag(:dry_run_started))
109
+
110
+ take_snapshot "Dry run finished", :dry_run_finished
111
+ end
112
+ return true
113
+ end
114
+ false
115
+ end
116
+
117
+ # takes a revertable snapshot of this project
118
+ def take_snapshot(message, tag_prefix = nil, tag_message = nil)
119
+ tag = (
120
+ if tag_prefix
121
+ "#{tag_prefix}_#{latest_tag_count(tag_prefix) + 1}"
122
+ else
123
+ nil
124
+ end
125
+ )
126
+
127
+ @git.commit_whole_directory(message, tag, tag_message)
128
+ end
129
+
130
+ # replaces content in path with new_content, takes a snapshot using
131
+ # snapshot_message and tag_prefix and 3-way merges new and old content
132
+ # with a previous snapshotted file same path tag_prefix, if it exists.
133
+ # returns the number of conflicts
134
+ def merge_new_content(new_content, path, snapshot_message, tag_prefix)
135
+ from_directory do
136
+ log.debug "merging new content to #{path} with prefix #{tag_prefix}"
137
+ already_existing = File.exist? path
138
+ previous_tag = latest_tag(tag_prefix)
139
+
140
+ if already_existing
141
+ log.debug "moving #{path} to #{path}.tetra_user_edited"
142
+ File.rename path, "#{path}.tetra_user_edited"
143
+ end
144
+
145
+ File.open(path, "w") { |io| io.write(new_content) }
146
+ log.debug "taking snapshot with new content: #{snapshot_message}"
147
+ take_snapshot(snapshot_message, tag_prefix)
148
+
149
+ if already_existing
150
+ if previous_tag == ""
151
+ previous_tag = latest_tag(tag_prefix)
152
+ log.debug "there was no tag with prefix #{tag_prefix} before snapshot"
153
+ log.debug "defaulting to #{previous_tag} after snapshot"
154
+ end
155
+
156
+ # 3-way merge
157
+ conflict_count = @git.merge_with_tag("#{path}", "#{path}.tetra_user_edited", previous_tag)
158
+ File.delete "#{path}.tetra_user_edited"
159
+ return conflict_count
160
+ end
161
+ return 0
162
+ end
163
+ end
164
+
165
+ # returns the tag with maximum count for a given tag prefix
166
+ def latest_tag(prefix)
167
+ "#{prefix}_#{latest_tag_count(prefix)}"
168
+ end
169
+
170
+ # returns the maximum tag count for a given tag prefix
171
+ def latest_tag_count(prefix)
172
+ @git.get_tag_maximum_suffix(prefix)
173
+ end
174
+
175
+ # runs a block from the project directory or a subdirectory
176
+ def from_directory(subdirectory = "")
177
+ Dir.chdir(File.join(@full_path, subdirectory)) do
178
+ yield
179
+ end
180
+ end
181
+
182
+ # returns the latest dry run start directory
183
+ def latest_dry_run_directory
184
+ @git.get_message(latest_tag(:dry_run_started))
185
+ end
186
+
187
+ # returns a list of files produced during dry-runs in a certain package
188
+ def get_produced_files(package)
189
+ dry_run_count = latest_tag_count(:dry_run_changed)
190
+ log.debug "Getting produced files from #{dry_run_count} dry runs"
191
+ if dry_run_count >= 1
192
+ package_dir = File.join("src", package)
193
+ (1..dry_run_count).map do |i|
194
+ @git.changed_files_between("dry_run_started_#{i}", "dry_run_changed_#{i}", package_dir)
195
+ end
196
+ .flatten
197
+ .uniq
198
+ .sort
199
+ .map { |file| Pathname.new(file).relative_path_from(Pathname.new(package_dir)).to_s }
200
+ else
201
+ []
202
+ end
203
+ end
204
+
205
+ # moves any .jar from src/ to kit/ and links it back
206
+ def purge_jars
207
+ from_directory do
208
+ result = []
209
+ Find.find("src") do |file|
210
+ next unless file =~ /.jar$/ && !File.symlink?(file)
211
+
212
+ new_location = File.join("kit", "jars", Pathname.new(file).split[1])
213
+ FileUtils.mv(file, new_location)
214
+
215
+ link_target = Pathname.new(new_location)
216
+ .relative_path_from(Pathname.new(file).split.first)
217
+ .to_s
218
+
219
+ File.symlink(link_target, file)
220
+ result << [file, new_location]
221
+ end
222
+
223
+ result
224
+ end
225
+ end
226
+ end
227
+
228
+ # current directory is not a tetra project
229
+ class NoProjectDirectoryError < StandardError
230
+ attr_reader :directory
231
+
232
+ def initialize(directory)
233
+ @directory = directory
234
+ end
235
+ end
236
+
237
+ # current directory is not a tetra package directory
238
+ class NoPackageDirectoryError < StandardError
239
+ attr_reader :directory
240
+
241
+ def initialize(directory)
242
+ @directory = directory
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: UTF-8
2
+
3
+ module Tetra
4
+ # generates build scripts from bash_history
5
+ class ScriptGenerator
6
+ include Logging
7
+
8
+ def initialize(project, history_path)
9
+ @project = project
10
+ @ant_runner = Tetra::AntRunner.new(project)
11
+ @maven_runner = Tetra::MavenRunner.new(project)
12
+ @history_path = history_path
13
+ end
14
+
15
+ def generate_build_script(name)
16
+ @project.from_directory do
17
+ history_lines = File.readlines(@history_path).map { |e| e.strip }
18
+ relevant_lines =
19
+ history_lines
20
+ .reverse
21
+ .take_while { |e| e.match(/tetra +dry-run/).nil? }
22
+ .reverse
23
+ .take_while { |e| e.match(/tetra +finish/).nil? }
24
+ .select { |e| e.match(/^#/).nil? }
25
+
26
+ script_lines = [
27
+ "#!/bin/bash",
28
+ "PROJECT_PREFIX=`readlink -e .`",
29
+ "cd #{@project.latest_dry_run_directory}"
30
+ ] +
31
+ relevant_lines.map do |line|
32
+ if line =~ /tetra +mvn/
33
+ line.gsub(/tetra +mvn/, "#{@maven_runner.get_maven_commandline("$PROJECT_PREFIX", ["-o"])}")
34
+ elsif line =~ /tetra +ant/
35
+ line.gsub(/tetra +ant/, "#{@ant_runner.get_ant_commandline("$PROJECT_PREFIX")}")
36
+ else
37
+ line
38
+ end
39
+ end
40
+
41
+ new_content = script_lines.join("\n") + "\n"
42
+
43
+ script_name = "build.sh"
44
+ result_path = File.join("src", name, script_name)
45
+ conflict_count = @project.merge_new_content(new_content, result_path, "Build script generated",
46
+ "generate_#{name}_build_script")
47
+
48
+ destination_dir = File.join("output", name)
49
+ FileUtils.mkdir_p(destination_dir)
50
+ destination_script_path = File.join(destination_dir, script_name)
51
+ FileUtils.symlink(File.expand_path(result_path), destination_script_path, force: true)
52
+
53
+ [result_path, conflict_count]
54
+ end
55
+ end
56
+ end
57
+ end