fixman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ require 'English'
2
+ require 'yaml'
3
+ require 'pathname'
4
+ require 'fixman/utilities'
5
+ require 'fixman/task_parse_error'
6
+ require 'classy_hash'
7
+
8
+ module Fixman
9
+ class Configuration
10
+ DEFAULT_LEDGER_FILE = '.fixman_ledger.yaml'
11
+ DEFAULT_CONF_FILE = '.fixman_conf.yaml'
12
+
13
+ CONDITION_OR_CLEANUP_SCHEMA = ->(h) {
14
+ return ':type is mising' unless h.has_key? :type
15
+ unless [:ruby, :shell].include? h[:type]
16
+ return ':type must be one of :ruby or :shell'
17
+ end
18
+ return ':action is mising' unless h.has_key? :action
19
+ return ':action should be a String' unless h[:action].is_a? String
20
+
21
+ if h[:type] == :ruby
22
+ begin
23
+ # Here a misformed Ruby source would also throw an ArgumentError
24
+ raise ArgumentError unless eval(h[:action]).is_a? Proc
25
+ rescue ArgumentError
26
+ return ':action should evaluate to a Proc object'
27
+ end
28
+ elsif h[:type] == :shell && h[:exit_status]
29
+ es = h[:exit_status]
30
+ unless es.is_a?(Integer) && (0..255).include?(es)
31
+ return ':exit_status should be an integer in 0..255 range'
32
+ end
33
+ end
34
+
35
+ true
36
+ }
37
+
38
+ REPO_INFO_SCHEMA = {
39
+ symbol: Symbol,
40
+ prompt: String,
41
+ label: String,
42
+ type: Symbol,
43
+ choices: [ :optional, [[String]] ]
44
+ }
45
+
46
+ TASK_SCHEMA = {
47
+ name: String,
48
+ target_placeholder: [ :optional, String ],
49
+ command: {
50
+ action: String,
51
+ exit_status: [ :optional, 0..255 ]
52
+ },
53
+ condition: [ :optional, CONDITION_OR_CLEANUP_SCHEMA ],
54
+ cleanup: [ :optional, CONDITION_OR_CLEANUP_SCHEMA ],
55
+ groups: [ :optional, [[ String ]] ]
56
+ }
57
+
58
+ CONF_SCHEMA = {
59
+ fixtures_base: String,
60
+ fixtures_ledger: [ :optional, String ],
61
+ tasks: ->(tasks) {
62
+ if tasks.is_a?(Array) && tasks.size > 0
63
+ begin
64
+ tasks.all? do |task|
65
+ begin
66
+ CH.validate task, TASK_SCHEMA
67
+ rescue => e
68
+ index = tasks.find_index task
69
+ raise Fixman::TaskParseError.new(e, index)
70
+ end
71
+ true
72
+ end
73
+ rescue Fixman::TaskParseError => e
74
+ e.message
75
+ end
76
+ else
77
+ "a non-empty array"
78
+ end
79
+ },
80
+ groups: [ :optional, [[ String ]] ],
81
+ extra_repo_info: [ :optional, [[ REPO_INFO_SCHEMA ]] ]
82
+ }
83
+
84
+ include Fixman::Utilities
85
+
86
+ attr_reader :fixtures_base, :fixture_ledger, :raw_tasks, :extra_repo_info, :groups
87
+
88
+ def initialize(fixtures_base,
89
+ fixture_ledger,
90
+ raw_tasks,
91
+ groups,
92
+ extra_repo_info)
93
+ @fixtures_base = Pathname.new(fixtures_base)
94
+ @fixture_ledger = Pathname.new(fixture_ledger)
95
+ @raw_tasks = raw_tasks
96
+ @extra_repo_info = extra_repo_info
97
+ @groups = groups
98
+ end
99
+
100
+ class << self
101
+ def read(path_to_conf)
102
+ conf_yaml = YAML.load IO.read(path_to_conf)
103
+
104
+ ClassyHash.validate conf_yaml, CONF_SCHEMA
105
+ initialize_defaults conf_yaml
106
+
107
+ raw_tasks = conf_yaml[:tasks].map do |task|
108
+ RawTask.new task
109
+ end
110
+
111
+ Configuration.new(conf_yaml[:fixtures_base],
112
+ conf_yaml[:fixture_ledger],
113
+ raw_tasks,
114
+ conf_yaml[:groups],
115
+ conf_yaml[:extra_repo_info])
116
+ end
117
+
118
+ def initialize_defaults(conf_hash)
119
+ conf_hash[:tasks].each do |task|
120
+ command = task[:command]
121
+ command[:exit_status] = 0 unless command[:exit_status]
122
+ unless task[:target_placeholder]
123
+ task[:target_placeholder] = 'TARGET'
124
+ end
125
+
126
+ condition = task[:condition]
127
+ if !condition
128
+ task[:condition] = {
129
+ type: :ruby,
130
+ action: 'proc { true }'
131
+ }
132
+ elsif condition[:type] == :shell && !condition[:exit_status]
133
+ condition[:exit_status] = 0
134
+ end
135
+
136
+ cleanup = task[:cleanup]
137
+ if !cleanup
138
+ task[:cleanup] = {
139
+ type: :ruby,
140
+ action: 'proc { true }'
141
+ }
142
+ end
143
+
144
+ task[:variables] = [] unless task[:variables]
145
+ end
146
+
147
+ unless conf_hash[:fixture_ledger]
148
+ conf_hash[:fixture_ledger] = DEFAULT_LEDGER_FILE
149
+ end
150
+
151
+ [:groups, :extra_repo_info].each do |key|
152
+ conf_hash[key] = [] unless conf_hash[key]
153
+ end
154
+
155
+ conf_hash[:extra_repo_info].each do |repo_info|
156
+ repo_info[:prompt] << ' '
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,121 @@
1
+ require 'fixman/repository'
2
+ require 'git'
3
+ require 'yaml'
4
+
5
+ module Fixman
6
+ class Controller
7
+ attr_reader :repos
8
+
9
+ def initialize(repos = [])
10
+ @repos = repos
11
+ end
12
+
13
+ # Add a new repository to the collection.
14
+ def add(input, fixtures_base, extra_repo_info)
15
+ repo = Repository.new input, fixtures_base, extra_repo_info
16
+
17
+ @repos << repo
18
+ end
19
+
20
+ # Remove repository from the fixtures list.
21
+ def delete(canonical_name)
22
+ repo = find canonical_name
23
+
24
+ if repo
25
+ @repos.delete repo
26
+ repo.destroy
27
+ else
28
+ fail ArgumentError
29
+ end
30
+ end
31
+
32
+ # Download repositories belonging to the at least one of the given groups.
33
+ def fetch(groups)
34
+ repos = find_by_groups(groups).reject { |repo| repo.fetched? }
35
+ repos.each do |repo|
36
+ begin
37
+ repo.fetch
38
+ rescue Git::GitExecuteError => error
39
+ STDERR.puts "Warning: Repository #{repo.canonical_name} could not be fetched."
40
+ STDERR.puts error.message
41
+ end
42
+ end
43
+ end
44
+
45
+ def update(canonical_name, sha = nil)
46
+ repo = find canonical_name
47
+ fail ArgumentError unless repo
48
+
49
+ repo.sha = sha || repo.retrieve_head_sha
50
+ end
51
+
52
+ def upgrade(groups)
53
+ repos = find_by_groups(groups)
54
+ repos.each do |repo|
55
+ begin
56
+ repo.upgrade
57
+ rescue Git::GitExecuteError => error
58
+ STDERR.puts "Warning: Repisotiry #{repo.canonical_name} could not be \"
59
+ upgraded."
60
+ STDERR.puts error.message
61
+ end
62
+ end
63
+ end
64
+
65
+ class << self
66
+ # Controller for the command line interface.
67
+ def open(conf)
68
+ original_ledger = ""
69
+
70
+ controller =
71
+ begin
72
+ original_ledger = YAML.load(IO.read conf.fixture_ledger)
73
+ original_ledger.map! { |entry| YAML.load entry }
74
+
75
+ repos = []
76
+ original_ledger.each do |repo_params|
77
+ repos << Repository.new(repo_params,
78
+ conf.fixtures_base,
79
+ conf.extra_repo_info)
80
+ end
81
+
82
+ Controller.new repos
83
+ rescue Errno::ENOENT
84
+ Controller.new
85
+ end
86
+
87
+ result = yield controller
88
+
89
+ write_ledger controller.repos, original_ledger, conf.fixture_ledger
90
+
91
+ result
92
+ rescue Git::GitExecuteError => error
93
+ puts error.message
94
+ exit 1
95
+ end
96
+
97
+ private
98
+
99
+ def write_ledger(repos, original_ledger, fixture_ledger)
100
+ new_ledger = YAML.dump repos.map(&:to_yaml)
101
+ if new_ledger != original_ledger
102
+ IO.write fixture_ledger, new_ledger
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def find_by_groups(groups)
110
+ if groups.empty?
111
+ @repos
112
+ else
113
+ @repos.select { |repo| !(repo.groups & groups).empty? }
114
+ end
115
+ end
116
+
117
+ def find(canonical_name)
118
+ @repos.find { |repo| canonical_name == repo.canonical_name }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,103 @@
1
+ require 'fileutils'
2
+ require 'fixman/utilities'
3
+
4
+ module Fixman
5
+ class RawTask
6
+ include Utilities
7
+
8
+ def initialize params
9
+ # Common to commands, cleanup, condition
10
+ @name = params[:name]
11
+ @target_placeholder = params[:target_placeholder]
12
+
13
+ # Command to execute and check exit status
14
+ @command = params[:command]
15
+ @variables = params[:variables] || []
16
+
17
+ # Condition to run the task
18
+ @condition = params[:condition]
19
+
20
+ # Clean up
21
+ @cleanup = params[:cleanup]
22
+ end
23
+
24
+ def refine
25
+ condition_proc = refine_condition
26
+ cleanup_proc = refine_cleanup
27
+ command_procs = refine_command
28
+
29
+ command_procs.map do |command_proc|
30
+ Task.new @name, condition_proc, command_proc, cleanup_proc
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Defines refine_cleanup and refine_condition methods.
37
+ # ivar is the associated instance variable i.e. @condition and @cleanup
38
+ %w(condition cleanup).each do |ivar_name|
39
+ define_method("refine_#{ivar_name}") do
40
+ ivar = instance_variable_get("@#{ivar_name}")
41
+
42
+ return proc { true } unless ivar
43
+
44
+ result =
45
+ case ivar[:type]
46
+ when :ruby
47
+ eval ivar[:action]
48
+ when :shell
49
+ shell_to_proc ivar[:action], ivar[:exit_status]
50
+ else
51
+ error "Unknown #{ivar_name} type"
52
+ end
53
+
54
+ error 'Invalid action' unless result.class == Proc
55
+
56
+ result
57
+ end
58
+ end
59
+
60
+ def refine_command
61
+ actions = substitute_variables
62
+
63
+ actions.map do |action|
64
+ shell_to_proc action, @command[:exit_status]
65
+ end
66
+ end
67
+
68
+ def substitute_variables
69
+ actions = [@command[:action]]
70
+
71
+ @variables.each do |variable|
72
+ key_regexp = Regexp.new variable[:key]
73
+ actions.map! do |action|
74
+ if key_regexp =~ action
75
+ variable[:values].map { |value| action.gsub(variable[:key], value) }
76
+ else
77
+ action
78
+ end
79
+ end.flatten!
80
+ end
81
+
82
+ actions
83
+ end
84
+
85
+ def shell_to_proc(shell_command, exit_status)
86
+ proc do |target|
87
+ final_command_str =
88
+ if target
89
+ shell_command.gsub(@target_placeholder, target.to_s)
90
+ else
91
+ shell_command
92
+ end
93
+
94
+ final_command_str << ' &> /dev/null'
95
+
96
+ system(final_command_str)
97
+ if exit_status
98
+ $CHILD_STATUS.exitstatus == exit_status
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,99 @@
1
+ require 'English'
2
+ require 'git'
3
+ require 'fileutils'
4
+ require 'yaml'
5
+
6
+ module Fixman
7
+ class Repository
8
+ FIELD_LABEL_WIDTH = 20
9
+
10
+ GIT_URL_REGEXP = %r{
11
+ (?:github\.com)\/
12
+ (?<owner>(?:\w|-)+)
13
+ \/
14
+ (?<name>(?:\w|-)+)
15
+ (?:\.git)?
16
+ $
17
+ }xi
18
+
19
+ attr_accessor :url, :name, :owner, :sha, :groups, :other_fields
20
+ attr_reader :path
21
+
22
+ def initialize(repo_params, fixtures_base, extra_repo_info)
23
+ @url = repo_params[:url]
24
+ @name = repo_params[:name]
25
+ @owner = repo_params[:owner]
26
+ @sha = repo_params[:sha]
27
+ @groups = repo_params[:groups]
28
+
29
+ [:url, :name, :owner, :sha, :groups].each { |key| repo_params.delete key }
30
+ @other_fields = repo_params
31
+
32
+ @extra_repo_info = extra_repo_info
33
+ @path = fixtures_base + canonical_name
34
+ end
35
+
36
+ def fetched?
37
+ File.exist? @path
38
+ end
39
+
40
+ def fetch
41
+ git = Git.clone @url, @path
42
+ git.reset_hard @sha
43
+ end
44
+
45
+ def upgrade
46
+ git = Git.open @path
47
+ git.pull
48
+ git.reset_hard @sha
49
+ end
50
+
51
+ def destroy
52
+ FileUtils.rm_rf @path if File.exist? @path
53
+ end
54
+
55
+ def canonical_name
56
+ "#{@owner}/#{@name}"
57
+ end
58
+
59
+ def summary
60
+ "#{canonical_name}\t#{@sha}"
61
+ end
62
+
63
+ def to_s
64
+ str = StringIO.new
65
+ str.puts 'Repository'.ljust(FIELD_LABEL_WIDTH) + canonical_name
66
+ str.puts 'URL'.ljust(FIELD_LABEL_WIDTH) + @url
67
+ str.puts 'Commit SHA'.ljust(FIELD_LABEL_WIDTH) + @sha
68
+ str.puts 'Groups'.ljust(FIELD_LABEL_WIDTH) + @groups.join(', ')
69
+ @extra_repo_info.each do |info|
70
+ str.puts info[:label].ljust(FIELD_LABEL_WIDTH) +
71
+ @other_fields[info[:symbol]]
72
+ end
73
+ str.string
74
+ end
75
+
76
+ def to_yaml
77
+ {
78
+ name: @name,
79
+ owner: @owner,
80
+ sha: @sha,
81
+ url: @url,
82
+ access_right: @access_right,
83
+ notes: @notes
84
+ }.merge(@other_fields).to_yaml
85
+ end
86
+
87
+ class << self
88
+ def extract_owner_and_name(url)
89
+ match_data = GIT_URL_REGEXP.match url
90
+ [match_data[:owner], match_data[:name]] if match_data
91
+ end
92
+
93
+ def retrieve_head_sha url
94
+ ref = Git.ls_remote url
95
+ ref['head'][:sha]
96
+ end
97
+ end
98
+ end
99
+ end