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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +24 -0
- data/README.md +217 -0
- data/Rakefile +4 -0
- data/bin/git-contest +43 -0
- data/bin/git-contest-finish +163 -0
- data/bin/git-contest-init +77 -0
- data/bin/git-contest-rebase +76 -0
- data/bin/git-contest-start +68 -0
- data/bin/git-contest-submit +188 -0
- data/git-contest.gemspec +34 -0
- data/lib/contest/driver.rb +15 -0
- data/lib/contest/driver/aizu_online_judge.rb +159 -0
- data/lib/contest/driver/base.rb +103 -0
- data/lib/contest/driver/codeforces.rb +196 -0
- data/lib/contest/driver/common.rb +72 -0
- data/lib/contest/driver/driver_event.rb +30 -0
- data/lib/contest/driver/uva_online_judge.rb +131 -0
- data/lib/git/contest.rb +14 -0
- data/lib/git/contest/common.rb +48 -0
- data/lib/git/contest/git.rb +189 -0
- data/lib/git/contest/test.rb +10 -0
- data/lib/git/contest/version.rb +12 -0
- data/spec/bin/t004_git_contest_submit_spec.rb +143 -0
- data/spec/bin/t005_git_contest_branching_spec.rb +83 -0
- data/spec/bin/t007_git_contest_start_spec.rb +121 -0
- data/spec/bin/t008_git_contest_finish_spec.rb +229 -0
- data/spec/bin/t009_git_contest_init_spec.rb +82 -0
- data/spec/lib/contest/driver/t001_aizu_online_judge_spec.rb +177 -0
- data/spec/lib/contest/driver/t002_codeforces_spec.rb +34 -0
- data/spec/lib/contest/driver/t003_uva_online_judge_spec.rb +33 -0
- data/spec/mock/default_config/config.yml +0 -0
- data/spec/mock/default_config/plugins/driver_dummy.rb +113 -0
- data/spec/mock/t001/002.status_log.xml +55 -0
- data/spec/mock/t001/config.yml +5 -0
- data/spec/mock/t001/description.html +48 -0
- data/spec/mock/t001/status.html +49 -0
- data/spec/mock/t001/status_log.xml +29 -0
- data/spec/mock/t002/my_submissions.html +58 -0
- data/spec/mock/t003/after_submit.html +28 -0
- data/spec/mock/t003/my_submissions.compile_error.html +160 -0
- data/spec/mock/t003/my_submissions.sent_to_judge.html +358 -0
- data/spec/mock/t004/config.yml +17 -0
- data/spec/mock/t005/001/config.yml +5 -0
- data/spec/mock/t006/001/001/001/config.yml +8 -0
- data/spec/mock/t006/001/001/002/config.yml +8 -0
- data/spec/mock/t006/001/001/003/config.yml +8 -0
- data/spec/mock/t006/001/002/001/config.yml +8 -0
- data/spec/mock/t006/001/002/002/config.yml +8 -0
- data/spec/mock/t006/001/002/003/config.yml +8 -0
- data/spec/mock/t006/001/003/001/config.yml +9 -0
- data/spec/mock/t006/001/003/002/config.yml +9 -0
- data/spec/mock/t006/001/003/003/config.yml +9 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/spec_list.txt +1 -0
- data/spec/t006_config_spec.rb +168 -0
- 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
|
data/lib/git/contest.rb
ADDED
@@ -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
|
+
|