p4_web_api 2014.2.0.pre2 → 2014.2.0.pre4
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 +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
|