p4_web_api 2014.2.0.pre2 → 2014.2.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/p4_web_api +8 -2
- data/lib/p4_web_api.rb +27 -670
- data/lib/p4_web_api/app/changes.rb +73 -0
- data/lib/p4_web_api/app/commands.rb +58 -0
- data/lib/p4_web_api/app/files.rb +166 -0
- data/lib/p4_web_api/app/protections.rb +33 -0
- data/lib/p4_web_api/app/sessions.rb +45 -0
- data/lib/p4_web_api/app/specs.rb +114 -0
- data/lib/p4_web_api/app/streams.rb +76 -0
- data/lib/p4_web_api/app/triggers.rb +31 -0
- data/lib/p4_web_api/app/users.rb +78 -0
- data/lib/p4_web_api/auth.rb +3 -3
- data/lib/p4_web_api/change_helper.rb +149 -0
- data/lib/p4_web_api/helpers.rb +84 -0
- data/lib/p4_web_api/p4_util.rb +90 -1
- data/lib/p4_web_api/version.rb +1 -1
- metadata +13 -2
@@ -0,0 +1,76 @@
|
|
1
|
+
module P4WebAPI
|
2
|
+
# Stream manipulation
|
3
|
+
#
|
4
|
+
# Streams are a little different from other spec types, in that the singluar
|
5
|
+
# 'stream' is identified by a depot-style path, as opposed to kind of a
|
6
|
+
# name. (And since our paths start with two slashes, we ignore those).
|
7
|
+
class App < Sinatra::Base
|
8
|
+
get '/v1/streams' do
|
9
|
+
streams = nil
|
10
|
+
|
11
|
+
open_p4 do |p4|
|
12
|
+
streams = p4.run_streams
|
13
|
+
end
|
14
|
+
|
15
|
+
normalize_streams(streams) if settings.normalize_output
|
16
|
+
|
17
|
+
streams.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
post '/v1/streams' do
|
21
|
+
open_p4 do |p4|
|
22
|
+
p4.save_stream(filter_params(params))
|
23
|
+
end
|
24
|
+
|
25
|
+
stream = params['Stream']
|
26
|
+
|
27
|
+
sub_path = stream[2..-1]
|
28
|
+
|
29
|
+
redirect "/v1/streams/#{sub_path}"
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/v1/streams/*' do
|
33
|
+
sub_path = params[:splat].join('')
|
34
|
+
|
35
|
+
stream = "//#{sub_path}"
|
36
|
+
|
37
|
+
results = nil
|
38
|
+
|
39
|
+
open_p4 do |p4|
|
40
|
+
results = p4.run_stream('-o', stream)
|
41
|
+
end
|
42
|
+
|
43
|
+
normalize_streams(results) if settings.normalize_output
|
44
|
+
|
45
|
+
results[0].to_json
|
46
|
+
end
|
47
|
+
|
48
|
+
patch '/v1/streams/*' do
|
49
|
+
sub_path = params[:splat].join('')
|
50
|
+
|
51
|
+
stream = "//#{sub_path}"
|
52
|
+
|
53
|
+
open_p4 do |p4|
|
54
|
+
spec = p4.run_stream('-o', stream)[0]
|
55
|
+
|
56
|
+
spec = spec.merge(filter_params(params))
|
57
|
+
|
58
|
+
p4.save_stream(spec, '-f')
|
59
|
+
end
|
60
|
+
|
61
|
+
''
|
62
|
+
end
|
63
|
+
|
64
|
+
delete '/v1/streams/*' do
|
65
|
+
sub_path = params[:splat].join('')
|
66
|
+
|
67
|
+
stream = "//#{sub_path}"
|
68
|
+
|
69
|
+
open_p4 do |p4|
|
70
|
+
p4.run_stream('-d', stream)
|
71
|
+
end
|
72
|
+
|
73
|
+
''
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module P4WebAPI
|
2
|
+
# Add methods for triggers resources.
|
3
|
+
#
|
4
|
+
# Like protections, there is no singular 'trigger' resource in the Perforce
|
5
|
+
# system, just the list of all of them.
|
6
|
+
class App < Sinatra::Base
|
7
|
+
get '/v1/triggers' do
|
8
|
+
triggers = nil
|
9
|
+
open_p4 do |p4|
|
10
|
+
triggers = p4.run_triggers('-o').first
|
11
|
+
end
|
12
|
+
|
13
|
+
normalize_triggers(results) if settings.normalize_output
|
14
|
+
|
15
|
+
triggers.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
put '/v1/triggers' do
|
19
|
+
triggers = {
|
20
|
+
'Triggers' => params['Triggers']
|
21
|
+
}
|
22
|
+
|
23
|
+
open_p4 do |p4|
|
24
|
+
p4.input = triggers
|
25
|
+
p4.run_triggers('-i')
|
26
|
+
end
|
27
|
+
|
28
|
+
''
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module P4WebAPI
|
2
|
+
# Methods for user resources.
|
3
|
+
#
|
4
|
+
# Users are a little different from standard specs, since deleting users
|
5
|
+
# can require removing session data, etc.
|
6
|
+
class App < Sinatra::Base
|
7
|
+
get '/v1/users' do
|
8
|
+
results = nil
|
9
|
+
|
10
|
+
open_p4 do |p4|
|
11
|
+
results = p4.run_users
|
12
|
+
end
|
13
|
+
|
14
|
+
normalize_users(results) if settings.normalize_output
|
15
|
+
|
16
|
+
results.to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
post '/v1/users' do
|
20
|
+
open_p4 do |p4|
|
21
|
+
p4.save_user(params, '-f')
|
22
|
+
end
|
23
|
+
|
24
|
+
user = params['User']
|
25
|
+
|
26
|
+
redirect "/v1/users/#{user}"
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/v1/users/:user' do |user|
|
30
|
+
results = nil
|
31
|
+
|
32
|
+
open_p4 do |p4|
|
33
|
+
results = p4.run_user('-o', user)
|
34
|
+
end
|
35
|
+
|
36
|
+
normalize_users(results) if settings.normalize_output
|
37
|
+
|
38
|
+
if results.empty?
|
39
|
+
halt 404
|
40
|
+
else
|
41
|
+
results[0].to_json
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
patch '/v1/users/:user' do |user|
|
46
|
+
open_p4 do |p4|
|
47
|
+
results = p4.run_user('-o', user)
|
48
|
+
|
49
|
+
if results.empty?
|
50
|
+
halt 404
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
spec = results[0]
|
55
|
+
spec = spec.merge(filter_params(params))
|
56
|
+
|
57
|
+
spec['User'] = user unless spec.key?('User')
|
58
|
+
|
59
|
+
p4.save_user(spec, '-f')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Avoid 'user jdoe saved' message from going out
|
63
|
+
''
|
64
|
+
end
|
65
|
+
|
66
|
+
delete '/v1/users/:user' do |user|
|
67
|
+
open_p4 do |p4|
|
68
|
+
p4.run_user('-f', '-d', user)
|
69
|
+
end
|
70
|
+
|
71
|
+
# If you're deleting yourself (ragequitting via API) remove thy session
|
72
|
+
if user == env['AUTH_CREDENTIALS'].first
|
73
|
+
token = env['AUTH_CREDENTIALS'].last
|
74
|
+
P4WebAPI::Auth.delete_session(token, settings)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/p4_web_api/auth.rb
CHANGED
@@ -134,7 +134,7 @@ module P4WebAPI
|
|
134
134
|
# Takes an array with [login, password], and returns true if the password
|
135
135
|
# is a legal token for that login, or looks like a p4 ticket.
|
136
136
|
def self.credentials_valid?(credentials, settings)
|
137
|
-
|
137
|
+
user = credentials.first
|
138
138
|
password = credentials.last
|
139
139
|
# Can't really determine much at this point, just return true. If we're
|
140
140
|
# a token, it's a UUID and we'll learn more about that later.
|
@@ -142,7 +142,7 @@ module P4WebAPI
|
|
142
142
|
|
143
143
|
user_info = read_token(password, settings)
|
144
144
|
if user_info
|
145
|
-
return user_info['
|
145
|
+
return user_info['user'] == user
|
146
146
|
else
|
147
147
|
false
|
148
148
|
end
|
@@ -258,7 +258,7 @@ module P4WebAPI
|
|
258
258
|
|
259
259
|
def self.user_info(p4, results, ticket)
|
260
260
|
{
|
261
|
-
|
261
|
+
user: p4.user,
|
262
262
|
email: results[0]['Email'],
|
263
263
|
full_name: results[0]['FullName'],
|
264
264
|
ticket: ticket
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module P4WebAPI
|
4
|
+
# This class assists in creating changelists based on an array of file
|
5
|
+
# changes, some of which may be file uploads.
|
6
|
+
#
|
7
|
+
# This is the implementation for file uploads and change creation.
|
8
|
+
class ChangeHelper
|
9
|
+
# The P4 connection we're using, that should already be logged in for a
|
10
|
+
# particular user.
|
11
|
+
attr_reader :p4
|
12
|
+
|
13
|
+
# The name of the temporary client
|
14
|
+
attr_reader :client_name
|
15
|
+
|
16
|
+
# The root directory of the temporary client
|
17
|
+
attr_reader :client_root
|
18
|
+
|
19
|
+
# Array of ChangeHelper::File instances.
|
20
|
+
attr_reader :files
|
21
|
+
|
22
|
+
# The change description
|
23
|
+
attr_reader :description
|
24
|
+
|
25
|
+
def initialize(p4: nil, client_name: nil, client_root: nil, files: [],
|
26
|
+
description: 'Updating files')
|
27
|
+
@p4 = p4
|
28
|
+
@client_name = client_name
|
29
|
+
@client_root = client_root
|
30
|
+
@files = files.map { |f| ChangeHelper::File.new(f) }
|
31
|
+
@description = description
|
32
|
+
end
|
33
|
+
|
34
|
+
def call
|
35
|
+
change_id = P4Util.init_changelist(p4, description)
|
36
|
+
|
37
|
+
begin
|
38
|
+
collect_file_results
|
39
|
+
|
40
|
+
files.each { |f| f.call(p4, change_id, client_root) }
|
41
|
+
|
42
|
+
p4.run_submit('-c', change_id)
|
43
|
+
|
44
|
+
rescue StandardError => ex
|
45
|
+
# Delete the changelist
|
46
|
+
p4.at_exception_level(P4::RAISE_NONE) do
|
47
|
+
p4.run_change('-d', '-f', change_id)
|
48
|
+
if P4Util.error?(p4)
|
49
|
+
puts "possible issues deleting change #{change_id}: #{p4.messages}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
raise ex
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Handles tracking 'file modification' operations on a single file for a
|
57
|
+
# change.
|
58
|
+
#
|
59
|
+
# One important aspect of initializing these files is assigning a 'files'
|
60
|
+
# result. The ChangeHelper pretty much grabs the output of 'p4 files' for
|
61
|
+
# all file changes, which lets us know if a file exists or not in the
|
62
|
+
# system, which changes downstream behavior during an upload process.
|
63
|
+
#
|
64
|
+
# Note that this will use our 'normalized' data style, where cases
|
65
|
+
class File
|
66
|
+
attr_accessor :depot_file
|
67
|
+
|
68
|
+
attr_accessor :action
|
69
|
+
|
70
|
+
# This should be a base64-encoded string
|
71
|
+
attr_accessor :content_base64
|
72
|
+
|
73
|
+
# For *some* actions, like a move, we need this indicated by the user.
|
74
|
+
attr_accessor :from_depot_file
|
75
|
+
|
76
|
+
# The output of p4 files on this depot_file path, if it exists.
|
77
|
+
attr_accessor :file_result
|
78
|
+
|
79
|
+
# Notice how the file *must* be specified using our 'external normalized'
|
80
|
+
# data style.
|
81
|
+
def initialize(obj = {})
|
82
|
+
@depot_file = obj['DepotFile'] if obj.key?('DepotFile')
|
83
|
+
@action = obj['Action'] if obj.key?('Action')
|
84
|
+
@from_depot_file = obj['FromDepotFile'] if obj.key?('FromDepotFile')
|
85
|
+
@content_base64 = obj['Content'] if obj.key?('Content')
|
86
|
+
end
|
87
|
+
|
88
|
+
def exists?
|
89
|
+
!file_result.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# This really should only be called by ChangeHelper::call to do work
|
94
|
+
# once we have associated any file_result and from_file properties.
|
95
|
+
def call(p4, change_id, client_root)
|
96
|
+
case action
|
97
|
+
when 'upload'
|
98
|
+
upload_file(p4, change_id, client_root)
|
99
|
+
when 'branch'
|
100
|
+
integrate_file(p4, change_id)
|
101
|
+
when 'move'
|
102
|
+
move_file(p4, change_id)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def upload_file(p4, change_id, client_root)
|
107
|
+
if exists?
|
108
|
+
P4Util.mark_change('edit', p4, change_id, client_root, depot_file)
|
109
|
+
P4Util.save_content(client_root, depot_file, content)
|
110
|
+
else
|
111
|
+
P4Util.save_content(client_root, depot_file, content)
|
112
|
+
P4Util.mark_change('add', p4, change_id, client_root, depot_file)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def integrate_file(p4, change_id)
|
117
|
+
p4.run_integrate('-c', change_id, from_depot_file, depot_file)
|
118
|
+
end
|
119
|
+
|
120
|
+
def move_file(p4, change_id)
|
121
|
+
p4.run_move('-c', change_id, from_depot_file, depot_file)
|
122
|
+
end
|
123
|
+
|
124
|
+
def content
|
125
|
+
Base64.decode64(content_base64)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Runs p4 files on all our depot paths, then updates each file model
|
132
|
+
# that exists.
|
133
|
+
def collect_file_results
|
134
|
+
file_results = p4.run_files(depot_files)
|
135
|
+
file_results.each do |file_result|
|
136
|
+
file = file_for_depot_file(file_result['depotFile'])
|
137
|
+
file.file_result = file_result if file
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def depot_files
|
142
|
+
files.map(&:depot_file)
|
143
|
+
end
|
144
|
+
|
145
|
+
def file_for_depot_file(depot_file)
|
146
|
+
files.find { |f| f.depot_file == depot_file }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'p4_web_api/p4_util'
|
2
|
+
|
3
|
+
module P4WebAPI
|
4
|
+
# Defines Sinatra helper methods in the main application.
|
5
|
+
module Helpers
|
6
|
+
# A block helper that uses the authentication credentials to create the p4
|
7
|
+
# connection
|
8
|
+
def open_p4(&block)
|
9
|
+
options = {
|
10
|
+
user: env['AUTH_CREDENTIALS'].first,
|
11
|
+
password: P4Util.resolve_password(env, settings),
|
12
|
+
host: P4Util.resolve_host(env, settings),
|
13
|
+
port: P4Util.resolve_port(env, settings)
|
14
|
+
}
|
15
|
+
|
16
|
+
charset = P4Util.resolve_charset(env, settings)
|
17
|
+
options[:charset] = charset if charset
|
18
|
+
|
19
|
+
P4Util.open(options, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Block helper to open a p4 handle with a temporary client workspace.
|
23
|
+
# The client workspace will map the series of depot path expressions
|
24
|
+
# directly into the client workspace.
|
25
|
+
#
|
26
|
+
# The depot_paths are generally expected to be complete file path
|
27
|
+
# expressions, e.g., '//depot/dir1/dir2/file'. They'll be mapped directly
|
28
|
+
# to the temporary working area: '//client/depot/dir/1/dir2/file'
|
29
|
+
#
|
30
|
+
# The block handler here will be called with the arguments (p4, root),
|
31
|
+
# where root is the temporary client's root directory.
|
32
|
+
def open_p4_temp_client(depot_paths, &block)
|
33
|
+
open_p4 do |p4|
|
34
|
+
name = (0...8).map { (65 + rand(26)).chr }.join
|
35
|
+
dir = init_temp_workspace_dir(name)
|
36
|
+
init_temp_client(p4, name, dir, depot_paths)
|
37
|
+
|
38
|
+
ex = nil
|
39
|
+
begin
|
40
|
+
block.call(p4, dir)
|
41
|
+
rescue StandardError => e
|
42
|
+
ex = e
|
43
|
+
end
|
44
|
+
|
45
|
+
delete_temp_client(p4, name, dir)
|
46
|
+
|
47
|
+
fail ex unless ex.nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def init_temp_workspace_dir(name)
|
54
|
+
dir = File.join(settings.workspace_folder, name)
|
55
|
+
unless Dir.exist?(dir)
|
56
|
+
FileUtils.mkpath(dir)
|
57
|
+
FileUtils.chmod(0700, dir)
|
58
|
+
end
|
59
|
+
dir
|
60
|
+
end
|
61
|
+
|
62
|
+
def init_temp_client(p4, name, dir, depot_paths)
|
63
|
+
spec = p4.fetch_client
|
64
|
+
spec._root = dir
|
65
|
+
spec._client = name
|
66
|
+
spec._description = 'p4_web_api temp client'
|
67
|
+
|
68
|
+
spec._view = depot_paths.map do |path|
|
69
|
+
stripped = path.gsub(/^[\/]*/, '')
|
70
|
+
"\"//#{stripped}\" \"//#{name}/#{stripped}\""
|
71
|
+
end
|
72
|
+
|
73
|
+
p4.save_client(spec)
|
74
|
+
p4.client = name
|
75
|
+
|
76
|
+
p4.run_sync('//...')
|
77
|
+
end
|
78
|
+
|
79
|
+
def delete_temp_client(p4, name, dir)
|
80
|
+
p4.run_client('-d', '-f', name)
|
81
|
+
FileUtils.rmtree(dir)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|