tetra 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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