git-contest 0.0.1

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +15 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +24 -0
  7. data/README.md +217 -0
  8. data/Rakefile +4 -0
  9. data/bin/git-contest +43 -0
  10. data/bin/git-contest-finish +163 -0
  11. data/bin/git-contest-init +77 -0
  12. data/bin/git-contest-rebase +76 -0
  13. data/bin/git-contest-start +68 -0
  14. data/bin/git-contest-submit +188 -0
  15. data/git-contest.gemspec +34 -0
  16. data/lib/contest/driver.rb +15 -0
  17. data/lib/contest/driver/aizu_online_judge.rb +159 -0
  18. data/lib/contest/driver/base.rb +103 -0
  19. data/lib/contest/driver/codeforces.rb +196 -0
  20. data/lib/contest/driver/common.rb +72 -0
  21. data/lib/contest/driver/driver_event.rb +30 -0
  22. data/lib/contest/driver/uva_online_judge.rb +131 -0
  23. data/lib/git/contest.rb +14 -0
  24. data/lib/git/contest/common.rb +48 -0
  25. data/lib/git/contest/git.rb +189 -0
  26. data/lib/git/contest/test.rb +10 -0
  27. data/lib/git/contest/version.rb +12 -0
  28. data/spec/bin/t004_git_contest_submit_spec.rb +143 -0
  29. data/spec/bin/t005_git_contest_branching_spec.rb +83 -0
  30. data/spec/bin/t007_git_contest_start_spec.rb +121 -0
  31. data/spec/bin/t008_git_contest_finish_spec.rb +229 -0
  32. data/spec/bin/t009_git_contest_init_spec.rb +82 -0
  33. data/spec/lib/contest/driver/t001_aizu_online_judge_spec.rb +177 -0
  34. data/spec/lib/contest/driver/t002_codeforces_spec.rb +34 -0
  35. data/spec/lib/contest/driver/t003_uva_online_judge_spec.rb +33 -0
  36. data/spec/mock/default_config/config.yml +0 -0
  37. data/spec/mock/default_config/plugins/driver_dummy.rb +113 -0
  38. data/spec/mock/t001/002.status_log.xml +55 -0
  39. data/spec/mock/t001/config.yml +5 -0
  40. data/spec/mock/t001/description.html +48 -0
  41. data/spec/mock/t001/status.html +49 -0
  42. data/spec/mock/t001/status_log.xml +29 -0
  43. data/spec/mock/t002/my_submissions.html +58 -0
  44. data/spec/mock/t003/after_submit.html +28 -0
  45. data/spec/mock/t003/my_submissions.compile_error.html +160 -0
  46. data/spec/mock/t003/my_submissions.sent_to_judge.html +358 -0
  47. data/spec/mock/t004/config.yml +17 -0
  48. data/spec/mock/t005/001/config.yml +5 -0
  49. data/spec/mock/t006/001/001/001/config.yml +8 -0
  50. data/spec/mock/t006/001/001/002/config.yml +8 -0
  51. data/spec/mock/t006/001/001/003/config.yml +8 -0
  52. data/spec/mock/t006/001/002/001/config.yml +8 -0
  53. data/spec/mock/t006/001/002/002/config.yml +8 -0
  54. data/spec/mock/t006/001/002/003/config.yml +8 -0
  55. data/spec/mock/t006/001/003/001/config.yml +9 -0
  56. data/spec/mock/t006/001/003/002/config.yml +9 -0
  57. data/spec/mock/t006/001/003/003/config.yml +9 -0
  58. data/spec/spec_helper.rb +45 -0
  59. data/spec/spec_list.txt +1 -0
  60. data/spec/t006_config_spec.rb +168 -0
  61. metadata +269 -0
@@ -0,0 +1,72 @@
1
+ #
2
+ # common.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ require 'mechanize'
9
+ require 'nokogiri'
10
+ require 'trollop'
11
+ require 'contest/driver/base'
12
+
13
+ module Contest
14
+ module Driver
15
+ module Utils
16
+ def self.resolve_path path
17
+ path = `ls #{path} | cat | head -n 1`
18
+ path.strip
19
+ end
20
+
21
+ def self.resolve_language path
22
+ regexp = /\.([a-z0-9]+)$/
23
+ if path.match(regexp)
24
+ return path.match(regexp)[1]
25
+ else
26
+ return nil
27
+ end
28
+ end
29
+
30
+ def self.normalize_language label
31
+ case label
32
+ when "c", "C"
33
+ return "clang"
34
+ when "cpp", "C++", "c++"
35
+ return "cpp"
36
+ when "c++11", "C++11"
37
+ return "cpp11"
38
+ when "cs", "c#", "C#"
39
+ return "cs"
40
+ when "d", "D", "dlang"
41
+ return "dlang"
42
+ when "go", "golang"
43
+ return "golang"
44
+ when "hs", "haskell", "Haskell"
45
+ return "haskell"
46
+ when "java", "Java"
47
+ return "java"
48
+ when "ocaml", "ml", "OCaml"
49
+ return "ocaml"
50
+ when "Delphi", "delphi"
51
+ return "delphi"
52
+ when "pascal", "Pascal"
53
+ return "pascal"
54
+ when "perl", "Perl", "pl"
55
+ return "perl"
56
+ when "php", "PHP"
57
+ return "php"
58
+ when "python2"
59
+ return "python2"
60
+ when "python3", "python", "Python", "py"
61
+ return "python3"
62
+ when "ruby", "rb", "Ruby"
63
+ return "ruby"
64
+ when "scala", "Scala"
65
+ return "scala"
66
+ else
67
+ abort "unknown language @ normalize language"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ #
2
+ # driver_event.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ module Contest
9
+ module Driver
10
+ class DriverEvent
11
+ def initialize
12
+ @callbacks = {}
13
+ end
14
+ def on(type, proc)
15
+ @callbacks[type] = [] unless @callbacks.has_key?(type)
16
+ @callbacks[type].push proc
17
+ end
18
+ def off(type, proc)
19
+ @callbacks[type] = [] unless @callbacks.has_key?(type)
20
+ @callbacks[type].delete proc
21
+ end
22
+ def trigger(type, *params)
23
+ @callbacks[type] = [] unless @callbacks.has_key?(type)
24
+ @callbacks[type].each do |proc|
25
+ proc.call *params
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,131 @@
1
+ #
2
+ # uva_online_judge.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ require 'contest/driver/common'
9
+
10
+ module Contest
11
+ module Driver
12
+ class UvaOnlineJudge < DriverBase
13
+ def get_opts_ext
14
+ define_options do
15
+ opt(
16
+ :problem_id,
17
+ "Problem ID (Ex: 100, 200, etc...)",
18
+ :type => :string,
19
+ :required => true,
20
+ )
21
+ end
22
+ end
23
+
24
+ def get_site_name
25
+ "UVa"
26
+ end
27
+
28
+ def get_desc
29
+ "UVa Online Judge (URL: http://uva.onlinejudge.org/)"
30
+ end
31
+
32
+ def resolve_language(label)
33
+ case label
34
+ when "c"
35
+ return "1"
36
+ when "cpp"
37
+ return "3"
38
+ when "java"
39
+ return "2"
40
+ when "pascal"
41
+ return "4"
42
+ else
43
+ abort "unknown language"
44
+ end
45
+ end
46
+
47
+ def submit_ext(config, source_path, options)
48
+ trigger 'start'
49
+ problem_id = options[:problem_id]
50
+
51
+ @client = Mechanize.new {|agent|
52
+ agent.user_agent_alias = 'Windows IE 7'
53
+ }
54
+
55
+ # submit
56
+ trigger 'before_login'
57
+ login_page = @client.get 'http://uva.onlinejudge.org/'
58
+ login_page.form_with(:id => 'mod_loginform') do |form|
59
+ form.username = config["user"]
60
+ form.passwd = config["password"]
61
+ end.submit
62
+ trigger 'after_login'
63
+
64
+ trigger 'before_submit', options
65
+ submit_page = @client.get 'http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=25'
66
+ res_page = submit_page.form_with(:action => 'index.php?option=com_onlinejudge&Itemid=25&page=save_submission') do |form|
67
+ form.localid = problem_id
68
+ form.radiobutton_with(:name => 'language', :value => options[:language]).check
69
+ form.code = File.read(source_path)
70
+ end.submit
71
+ trigger 'after_submit'
72
+
73
+ # <div class="message">Submission received with ID 12499981</div>
74
+ trigger 'before_wait'
75
+ submission_id = get_submission_id(res_page.body)
76
+ status = get_status_wait(submission_id)
77
+ trigger(
78
+ 'after_wait',
79
+ {
80
+ :submission_id => submission_id,
81
+ :status => status,
82
+ :result => get_commit_message($config["submit_rules"]["message"], status, options),
83
+ }
84
+ )
85
+
86
+ trigger 'finish'
87
+ get_commit_message($config["submit_rules"]["message"], status, options)
88
+ end
89
+
90
+ def get_status_wait(submission_id)
91
+ submission_id = submission_id.to_s
92
+ # wait result
93
+ 12.times do
94
+ sleep 10
95
+ my_page = @client.get 'http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=9'
96
+ status = get_submission_status(submission_id, my_page.body)
97
+ return status unless status == 'Sent to judge' || status == ''
98
+ trigger 'retry'
99
+ end
100
+ trigger 'timeout'
101
+ return 'timeout'
102
+ end
103
+
104
+ def get_submission_id(body)
105
+ doc = Nokogiri::HTML(body)
106
+ text = doc.xpath('//div[@class="message"]')[0].text().strip
107
+ # Submission received with ID 12500010
108
+ text.match(/Submission received with ID ([0-9]+)/)[1]
109
+ end
110
+
111
+ def get_submission_status(submission_id, body)
112
+ doc = Nokogiri::HTML(body)
113
+ doc.xpath('//tr[@class="sectiontableheader"]/following-sibling::node()').search('tr').each do |elm|
114
+ td_list = elm.search('td')
115
+ item_submission_id = td_list[0].text.strip
116
+ if item_submission_id == submission_id
117
+ item_problem_id = td_list[1].text.strip
118
+ item_status = td_list[3].text.strip
119
+ return item_status
120
+ end
121
+ end
122
+ 'timeout'
123
+ end
124
+
125
+ if is_test_mode?
126
+ attr_writer :client
127
+ else
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,14 @@
1
+ #
2
+ # contest.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ require "git/contest/version"
9
+
10
+ module Git
11
+ module Contest
12
+ # Your code goes here...
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ #
2
+ # common.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ require 'git/contest/version'
9
+ require 'git/contest/test'
10
+ require 'git/contest/git'
11
+ require 'contest/driver'
12
+ require 'yaml'
13
+
14
+ def init
15
+ init_global
16
+ init_home
17
+ end
18
+
19
+ def init_global
20
+ $GIT_CONTEST_HOME = File.expand_path(ENV['GIT_CONTEST_HOME'] || "~/.git-contest")
21
+ $GIT_CONTEST_CONFIG = File.expand_path(ENV['GIT_CONTEST_CONFIG'] || "#{$GIT_CONTEST_HOME}/config.yml")
22
+ if git_do_no_echo 'branch'
23
+ $MASTER = git_do 'config --get git.contest.branch.master'
24
+ $PREFIX = git_do 'config --get git.contest.branch.prefix'
25
+ $ORIGIN = git_do 'config --get git.contest.origin'
26
+ if $ORIGIN == ''
27
+ $ORIGIN = 'origin'
28
+ end
29
+ $GIT_CONTEST_GIT_OK = true
30
+ else
31
+ $GIT_CONTEST_GIT_OK = false
32
+ end
33
+ end
34
+
35
+ def init_home
36
+ if ! FileTest.exists? $GIT_CONTEST_HOME
37
+ FileUtils.mkdir $GIT_CONTEST_HOME
38
+ end
39
+ if ! FileTest.exists? $GIT_CONTEST_CONFIG
40
+ FileUtils.touch $GIT_CONTEST_CONFIG
41
+ end
42
+ end
43
+
44
+ def get_config
45
+ config_path = File.expand_path($GIT_CONTEST_CONFIG)
46
+ YAML.load_file config_path
47
+ end
48
+
@@ -0,0 +1,189 @@
1
+ #
2
+ # git.rb
3
+ #
4
+ # Copyright (c) 2013 Hiroyuki Sano <sh19910711 at gmail.com>
5
+ # Licensed under the MIT-License.
6
+ #
7
+
8
+ def git_do(*args)
9
+ puts "git #{args.join(' ')}" if ENV['GIT_CONTEST_DEBUG'] == 'ON'
10
+ return `git #{args.join(' ')} 2>&1`.strip
11
+ end
12
+
13
+ # use return value
14
+ def git_do_no_echo(*args)
15
+ puts "git #{args.join(' ')}" if ENV['GIT_CONTEST_DEBUG'] == 'ON'
16
+ system "git #{args.join(' ')} >/dev/null 2>&1"
17
+ end
18
+
19
+ #
20
+ def git_contest_is_initialized
21
+ git_contest_has_master_configured &&
22
+ git_contest_has_prefix_configured &&
23
+ git_do('config --get git.contest.branch.master') != git_do('config --get git.contest.branch.develop')
24
+ end
25
+
26
+ def git_contest_has_master_configured
27
+ master = (git_do 'config --get git.contest.branch.master').strip
28
+ master != '' && git_local_branches().include?(master)
29
+ end
30
+
31
+ def git_contest_has_develop_configured
32
+ develop = (git_do 'config --get git.contest.branch.develop').strip
33
+ develop != '' && git_local_branches().include?(develop)
34
+ end
35
+
36
+ def git_contest_has_prefix_configured
37
+ git_do_no_echo 'config --get git.contest.branch.prefix'
38
+ end
39
+
40
+ def git_contest_resolve_nameprefix name, prefix
41
+ if git_local_branch_exists "#{prefix}/#{name}"
42
+ return name
43
+ end
44
+ branches = git_local_branches().select {|branch| branch.start_with? "#{prefix}/#{name}" }
45
+ if branches.size == 0
46
+ abort "No branch matches prefix '#{name}'"
47
+ else
48
+ if branches.size == 1
49
+ return branches[0][prefix.length..-1]
50
+ else
51
+ abort "Multiple branches match prefix '#{name}'"
52
+ end
53
+ end
54
+ end
55
+
56
+ #
57
+ def git_remote_branch_exists(branch_name)
58
+ git_remote_branches().include?(branch_name)
59
+ end
60
+
61
+ def git_local_branch_exists(branch_name)
62
+ git_local_branches().include?(branch_name)
63
+ end
64
+
65
+ def git_branch_exists(branch_name)
66
+ git_all_branches().include?(branch_name)
67
+ end
68
+
69
+ def git_remote_branches
70
+ cmd_ret = git_do 'branch -r --no-color'
71
+ cmd_ret.lines.map {|line|
72
+ line.gsub(/^[*]?\s*/, '').gsub(/\s*$/, '').strip
73
+ }
74
+ end
75
+
76
+ def git_local_branches
77
+ cmd_ret = git_do 'branch --no-color'
78
+ cmd_ret.lines.map {|line|
79
+ line.gsub(/^[*]?\s*/, '').gsub(/\s*$/, '').strip
80
+ }
81
+ end
82
+
83
+ def git_all_branches
84
+ cmd_ret1 = git_do 'branch --no-color'
85
+ cmd_ret2 = git_do 'branch -r --no-color'
86
+ lines = ( cmd_ret1 + cmd_ret2 ).lines
87
+ lines.map {|line|
88
+ line.gsub(/^[*]?\s*/, '').gsub(/\s*$/, '').strip
89
+ }
90
+ end
91
+
92
+ def git_current_branch
93
+ ret = git_do('branch --no-color').lines
94
+ ret = ret.grep /^\*/
95
+ ret[0].gsub(/^[* ] /, '').strip
96
+ end
97
+
98
+ def git_is_clean_working_tree
99
+ if ! git_do_no_echo 'diff --no-ext-diff --ignore-submodules --quiet --exit-code'
100
+ return 1
101
+ elsif ! git_do_no_echo 'diff-index --cached --quiet --ignore-submodules HEAD --'
102
+ return 2
103
+ else
104
+ return 0
105
+ end
106
+ end
107
+
108
+ def git_repo_is_headless
109
+ ! git_do_no_echo 'rev-parse --quiet --verify HEAD'
110
+ end
111
+
112
+ #
113
+ # 0: same
114
+ # 1: first branch needs ff
115
+ # 2: second branch needs ff
116
+ # 3: branch needs merge
117
+ # 4: there is no merge
118
+ #
119
+ def git_compare_branches first, second
120
+ commit1 = git_do "rev-parse \"#{first}\""
121
+ commit2 = git_do "rev-parse \"#{second}\""
122
+ if commit1 != commit2
123
+ if git_do_no_echo("merge-base \"#{commit1}\" \"#{commit2}\"") > 0
124
+ return 4
125
+ else
126
+ base = git_do "merge-base \"#{commit1}\" \"#{commit2}\""
127
+ if commit1 == base
128
+ return 1
129
+ elsif commit2 == base
130
+ return 2
131
+ else
132
+ return 3
133
+ end
134
+ end
135
+ else
136
+ return 0
137
+ end
138
+ end
139
+
140
+ def require_branch(branch)
141
+ if ! git_all_branches().include?(branch)
142
+ abort "Branch #{branch} does not exist."
143
+ end
144
+ end
145
+
146
+ def require_branch_absent(branch)
147
+ if git_all_branches().include?(branch)
148
+ abort "Branch #{branch} already exists. Pick another name."
149
+ end
150
+ end
151
+
152
+ def require_clean_working_tree
153
+ ret = git_is_clean_working_tree
154
+ if ret == 1
155
+ abort "fatal: Working tree contains unstaged changes. Aborting."
156
+ end
157
+ if ret == 2
158
+ abort "fatal: Index contains uncommited changes. Aborting."
159
+ end
160
+ end
161
+
162
+ def require_local_branch branch
163
+ if ! git_local_branch_exists branch
164
+ abort "fatal: Local branch '#{branch}' does not exist and is required."
165
+ end
166
+ end
167
+
168
+ def require_remote_branch branch
169
+ if ! git_remote_branch_exists branch
170
+ abort "fatal: Remote branch '#{branch}' does not exist and is required."
171
+ end
172
+ end
173
+
174
+ def require_branches_equal local, remote
175
+ require_local_branch local
176
+ require_remote_branch remote
177
+ ret = git_compare_branches local, remote
178
+ if ret > 0
179
+ puts "Branches '#{local}' and '#{remote}' have diverged."
180
+ if ret == 1
181
+ abort "And branch #{local} may be fast-forwarded."
182
+ elsif ret == 2
183
+ puts "And local branch #{local} is ahead of #{remote}"
184
+ else
185
+ abort "Branches need merging first."
186
+ end
187
+ end
188
+ end
189
+