fixman 0.1.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.
@@ -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