hglib 0.1.pre20180129173049 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +3 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +94 -11
- data/History.md +1 -1
- data/Manifest.txt +12 -0
- data/Rakefile +1 -1
- data/examples/clone.rb +13 -0
- data/integration/commands/clone_spec.rb +52 -0
- data/integration/spec_helper.rb +29 -0
- data/lib/hglib.rb +7 -7
- data/lib/hglib/repo.rb +129 -0
- data/lib/hglib/repo/id.rb +81 -0
- data/lib/hglib/repo/log_entry.rb +128 -0
- data/lib/hglib/server.rb +320 -0
- data/spec/.status +39 -0
- data/spec/hglib/repo/id_spec.rb +182 -0
- data/spec/hglib/repo/log_entry_spec.rb +69 -0
- data/spec/hglib/repo_spec.rb +106 -0
- data/spec/hglib/server_spec.rb +166 -0
- data/spec/spec_helper.rb +6 -1
- metadata +44 -34
- metadata.gz.sig +0 -0
@@ -0,0 +1,128 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
require 'hglib/repo' unless defined?( Hglib::Repo )
|
7
|
+
|
8
|
+
|
9
|
+
# An entry in a repository's revision log.
|
10
|
+
class Hglib::Repo::LogEntry
|
11
|
+
extend Loggability
|
12
|
+
|
13
|
+
|
14
|
+
# Loggability API -- output to the hglib logger
|
15
|
+
log_to :hglib
|
16
|
+
|
17
|
+
|
18
|
+
# {
|
19
|
+
# "bookmarks" => [],
|
20
|
+
# "branch" => "default",
|
21
|
+
# "date" => [1527021225, 25200],
|
22
|
+
# "desc" => "Add Assemblage commit script",
|
23
|
+
# "node" => "4a1cbb9f8d56abd4e72aa2860eecef718dad48dd",
|
24
|
+
# "parents" => ["ac2b07cce0fc307e787a91b9e74b4514f7b71f09"],
|
25
|
+
# "phase" => "draft",
|
26
|
+
# "rev" => 7,
|
27
|
+
# "tags" => [],
|
28
|
+
# "user" => "Michael Granger <ged@FaerieMUD.org>"
|
29
|
+
# }
|
30
|
+
|
31
|
+
### Create a new log entry from the raw +entryhash+.
|
32
|
+
def initialize( entryhash )
|
33
|
+
@bookmarks = entryhash[ "bookmarks" ]
|
34
|
+
@branch = entryhash[ "branch" ]
|
35
|
+
@date = entryhash[ "date" ]
|
36
|
+
@desc = entryhash[ "desc" ]
|
37
|
+
@node = entryhash[ "node" ]
|
38
|
+
@parents = entryhash[ "parents" ]
|
39
|
+
@phase = entryhash[ "phase" ]
|
40
|
+
@rev = entryhash[ "rev" ]
|
41
|
+
@tags = entryhash[ "tags" ]
|
42
|
+
@user = entryhash[ "user" ]
|
43
|
+
@date = entryhash[ "date" ]
|
44
|
+
@files = entryhash[ "files" ] || []
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
######
|
49
|
+
public
|
50
|
+
######
|
51
|
+
|
52
|
+
##
|
53
|
+
# Return the Array of bookmarks corresponding to the entry (if any)
|
54
|
+
attr_reader :bookmarks
|
55
|
+
|
56
|
+
##
|
57
|
+
# Return the name of the branch the commit is on
|
58
|
+
attr_reader :branch
|
59
|
+
|
60
|
+
##
|
61
|
+
# Return the description from the entry
|
62
|
+
attr_reader :desc
|
63
|
+
alias_method :summary, :desc
|
64
|
+
|
65
|
+
##
|
66
|
+
# Return the node (changeset ID) from the entry
|
67
|
+
attr_reader :node
|
68
|
+
|
69
|
+
##
|
70
|
+
# Return the changeset IDs of the parents of the entry
|
71
|
+
attr_reader :parents
|
72
|
+
|
73
|
+
##
|
74
|
+
# Return the phase from the entry
|
75
|
+
attr_reader :phase
|
76
|
+
|
77
|
+
##
|
78
|
+
# Return the revision number from the entry
|
79
|
+
attr_reader :rev
|
80
|
+
|
81
|
+
##
|
82
|
+
# Return the Array of the entry's tags
|
83
|
+
attr_reader :tags
|
84
|
+
|
85
|
+
##
|
86
|
+
# Return the name and email of the committing user
|
87
|
+
attr_reader :user
|
88
|
+
|
89
|
+
##
|
90
|
+
# The diff of the commit, if --patch was specified.
|
91
|
+
attr_reader :diff
|
92
|
+
|
93
|
+
##
|
94
|
+
# The files affected by the commit, if run with `verbose: true`.
|
95
|
+
attr_reader :files
|
96
|
+
|
97
|
+
|
98
|
+
### The Time the revision associated with the entry was committed
|
99
|
+
def date
|
100
|
+
return Time.at( @date[0] )
|
101
|
+
end
|
102
|
+
alias_method :time, :date
|
103
|
+
|
104
|
+
|
105
|
+
### Return the shortened changeset ID (in the form {rev}:{shortnode})
|
106
|
+
def changeset
|
107
|
+
return "%d:%s" % [ self.rev, self.node[0,12] ]
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
### Return a human-readable representation of the LogEntry as a String.
|
112
|
+
def inspect
|
113
|
+
parts = []
|
114
|
+
parts += self.tags.map {|t| "##{t}" }
|
115
|
+
parts += self.bookmarks.map {|b| "@#{b}" }
|
116
|
+
|
117
|
+
return "#<%p:#%x %s {%s} %p%s>" % [
|
118
|
+
self.class,
|
119
|
+
self.object_id * 2,
|
120
|
+
self.changeset,
|
121
|
+
self.date,
|
122
|
+
self.summary,
|
123
|
+
parts.empty? ? '' : " " + parts.join(' ')
|
124
|
+
]
|
125
|
+
end
|
126
|
+
|
127
|
+
end # class Hglib::Repo::LogEntry
|
128
|
+
|
data/lib/hglib/server.rb
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'shellwords'
|
5
|
+
require 'loggability'
|
6
|
+
require 'hglib' unless defined?( Hglib )
|
7
|
+
|
8
|
+
|
9
|
+
# A mercurial server object. This uses the Mercurial Command Server protocol to
|
10
|
+
# execute Mercurial commands.
|
11
|
+
#
|
12
|
+
# Refs:
|
13
|
+
# - https://www.mercurial-scm.org/wiki/CommandServer
|
14
|
+
#
|
15
|
+
class Hglib::Server
|
16
|
+
extend Loggability
|
17
|
+
|
18
|
+
|
19
|
+
# String#unpack template for message headers from the command server
|
20
|
+
HEADER_TEMPLATE = 'aI>'
|
21
|
+
|
22
|
+
# Array#pack template for commands sent to the command server
|
23
|
+
COMMAND_TEMPLATE = 'A*I>A*'
|
24
|
+
|
25
|
+
# Array#pack template for plain messages sent to the command server
|
26
|
+
MESSAGE_TEMPLATE = 'I>A*'
|
27
|
+
|
28
|
+
|
29
|
+
# Loggability API -- send logs to the logger in the top-level module
|
30
|
+
log_to :hglib
|
31
|
+
|
32
|
+
|
33
|
+
### Turn the specified +opthash+ into an Array of command line options.
|
34
|
+
def self::mangle_options( **options )
|
35
|
+
return options.flat_map do |name, val|
|
36
|
+
prefix = name.length > 1 ? '--' : '-'
|
37
|
+
optname = "%s%s" % [ prefix, name.to_s.gsub(/_/, '-') ]
|
38
|
+
|
39
|
+
case val
|
40
|
+
when TrueClass
|
41
|
+
[ optname ]
|
42
|
+
when FalseClass, NilClass
|
43
|
+
[ optname.sub(/\A--/, '--no-') ] if optname.start_with?( '--' )
|
44
|
+
when String
|
45
|
+
if optname.start_with?( '--' )
|
46
|
+
[ "#{optname}=#{val}" ]
|
47
|
+
else
|
48
|
+
[ optname, val ]
|
49
|
+
end
|
50
|
+
else
|
51
|
+
raise ArgumentError, "can't handle command option: %p" % [{ name => val }]
|
52
|
+
end
|
53
|
+
end.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
### Create a new Hglib::Server that will be invoked for the specified +repo+.
|
58
|
+
### Any additional +args+ given will be passed to the `hg serve` command
|
59
|
+
### on startup.
|
60
|
+
def initialize( repo=nil, **args )
|
61
|
+
@repo = Pathname( repo ) if repo
|
62
|
+
|
63
|
+
@reader = nil
|
64
|
+
@writer = nil
|
65
|
+
|
66
|
+
@pid = nil
|
67
|
+
|
68
|
+
@byte_input_callback = nil
|
69
|
+
@line_input_callback = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
######
|
74
|
+
public
|
75
|
+
######
|
76
|
+
|
77
|
+
##
|
78
|
+
# The Pathname to the repository the server should target
|
79
|
+
attr_reader :repo
|
80
|
+
|
81
|
+
##
|
82
|
+
# The additional arguments to send to the command server on startup
|
83
|
+
attr_reader :args
|
84
|
+
|
85
|
+
##
|
86
|
+
# The reader end of the pipe used to communicate with the command server.
|
87
|
+
attr_accessor :reader
|
88
|
+
|
89
|
+
##
|
90
|
+
# The writer end of the pipe used to communicate with the command server.
|
91
|
+
attr_accessor :writer
|
92
|
+
|
93
|
+
##
|
94
|
+
# The PID of the running command server if there is one
|
95
|
+
attr_accessor :pid
|
96
|
+
|
97
|
+
##
|
98
|
+
# The callable used to fetch byte-oriented input
|
99
|
+
attr_accessor :byte_input_callback
|
100
|
+
protected :byte_input_callback=
|
101
|
+
|
102
|
+
##
|
103
|
+
# The callable used to fetch line-oriented input
|
104
|
+
attr_accessor :line_input_callback
|
105
|
+
protected :line_input_callback=
|
106
|
+
|
107
|
+
|
108
|
+
### Register a +callback+ that will be called when the command server asks for
|
109
|
+
### byte-oriented input. The callback will be called with the (maximum) number
|
110
|
+
### of bytes to return.
|
111
|
+
def on_byte_input( &callback )
|
112
|
+
raise LocalJumpError, "no block given" unless callback
|
113
|
+
self.byte_input_callback = callback
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
### Register a +callback+ that will be called when the command server asks for
|
118
|
+
### line-oriented input. The callback will be called with the (maximum) number
|
119
|
+
### of bytes to return.
|
120
|
+
def on_line_input( &callback )
|
121
|
+
raise LocalJumpError, "no block given" unless callback
|
122
|
+
self.line_input_callback = callback
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
### Register the callbacks necessary to read both line and byte input from the
|
127
|
+
### specified +io+, which is expected to respond to #gets and #read.
|
128
|
+
def register_input_callbacks( io=$stdin )
|
129
|
+
self.on_byte_input( &io.method(:read) )
|
130
|
+
self.on_line_input( &io.method(:gets) )
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
### Run the specified +command+ with the given +args+ via the server and return
|
135
|
+
### the result. If the command requires +input+, the callbacks registered with
|
136
|
+
### #on_byte_input and #on_line_input will be used to read it. If one of these
|
137
|
+
### callbacks is not registered, an IOError will be raised.
|
138
|
+
def run( command, *args, **options )
|
139
|
+
self.log.debug "Running command: %p" % [ Shellwords.join([command.to_s] + args) ]
|
140
|
+
self.start unless self.started?
|
141
|
+
|
142
|
+
done = false
|
143
|
+
output = []
|
144
|
+
|
145
|
+
args.compact!
|
146
|
+
args += self.class.mangle_options( options )
|
147
|
+
|
148
|
+
self.write_command( 'runcommand', command, *args )
|
149
|
+
|
150
|
+
until done
|
151
|
+
channel, data = self.read_message
|
152
|
+
|
153
|
+
case channel
|
154
|
+
when 'o'
|
155
|
+
# self.log.debug "Got command output: %p" % [ data ]
|
156
|
+
output << data
|
157
|
+
when 'r'
|
158
|
+
done = true
|
159
|
+
when 'e'
|
160
|
+
self.log.error "Got command error: %p" % [ data ]
|
161
|
+
raise Hglib::CommandError, data
|
162
|
+
when 'L'
|
163
|
+
self.log.debug "Server requested line input (%d bytes)" % [ data ]
|
164
|
+
input = self.get_line_input( data.to_i )
|
165
|
+
self.write_message( input.chomp + "\n" )
|
166
|
+
when 'I'
|
167
|
+
self.log.debug "Server requested byte input (%d bytes)" % [ data ]
|
168
|
+
input = self.get_byte_input( data.to_i )
|
169
|
+
self.write_message( input )
|
170
|
+
else
|
171
|
+
msg = "Unexpected channel %p" % [ channel ]
|
172
|
+
self.log.error( msg )
|
173
|
+
raise( msg ) if channel =~ /\p{Upper}/ # Mandatory
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
return output
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
### Returns +true+ if the underlying command server has been started.
|
182
|
+
def is_started?
|
183
|
+
return self.pid ? true : false
|
184
|
+
end
|
185
|
+
alias_method :started?, :is_started?
|
186
|
+
|
187
|
+
|
188
|
+
### Open a pipe and start the command server.
|
189
|
+
def start
|
190
|
+
self.log.debug "Starting."
|
191
|
+
self.spawn_server
|
192
|
+
self.read_hello
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
### Stop the command server and clean up the pipes.
|
197
|
+
def stop
|
198
|
+
return unless self.started?
|
199
|
+
|
200
|
+
self.log.debug "Stopping."
|
201
|
+
self.writer.close if self.writer
|
202
|
+
self.writer = nil
|
203
|
+
self.reader.close if self.reader
|
204
|
+
self.reader = nil
|
205
|
+
self.stop_server
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
#########
|
210
|
+
protected
|
211
|
+
#########
|
212
|
+
|
213
|
+
### Call the #on_line_input callback to read at most +max_bytes+. Raises an
|
214
|
+
### IOError if no callback is registered.
|
215
|
+
def get_line_input( max_bytes )
|
216
|
+
callback = self.line_input_callback or
|
217
|
+
raise IOError, "cannot read input: no line input callback registered"
|
218
|
+
|
219
|
+
return callback.call( max_bytes )
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
### Call the #on_byte_input callback to read at most +max_bytes+. Raises an
|
224
|
+
### IOError if no callback is registered.
|
225
|
+
def get_byte_input( max_bytes )
|
226
|
+
callback = self.byte_input_callback or
|
227
|
+
raise IOError, "cannot read input: no byte input callback registered"
|
228
|
+
|
229
|
+
return callback.call( max_bytes )
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
### Fork a child and run Mercurial in command-server mode.
|
234
|
+
def spawn_server
|
235
|
+
self.reader, child_writer = IO.pipe
|
236
|
+
child_reader, self.writer = IO.pipe
|
237
|
+
|
238
|
+
cmd = self.server_start_command
|
239
|
+
self.pid = Process.spawn( *cmd, out: child_writer, in: child_reader, close_others: true )
|
240
|
+
self.log.debug "Spawned command server at PID %d" % [ self.pid ]
|
241
|
+
|
242
|
+
child_writer.close
|
243
|
+
child_reader.close
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
### Kill the command server if it's running
|
248
|
+
def stop_server
|
249
|
+
if self.pid
|
250
|
+
self.log.debug "Stopping command server at PID %d" % [ self.pid ]
|
251
|
+
Process.kill( :TERM, self.pid )
|
252
|
+
Process.wait( self.pid, Process::WNOHANG )
|
253
|
+
self.pid = nil
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
### Write the specified message to the command server. Raises an exception if
|
259
|
+
### the server is not yet started.
|
260
|
+
def write_command( command, *args )
|
261
|
+
data = args.map( &:to_s ).join( "\0" )
|
262
|
+
message = [ command + "\n", data.bytesize, data ].pack( COMMAND_TEMPLATE )
|
263
|
+
self.log.debug "Writing command %p to command server." % [ message ]
|
264
|
+
self.writer.write( message )
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
### Write the specified +message+ to the command server.
|
269
|
+
def write_message( data )
|
270
|
+
message = [ data.bytesize, data ].pack( MESSAGE_TEMPLATE )
|
271
|
+
self.log.debug "Writing message %p to command server." % [ message ]
|
272
|
+
self.writer.write( message )
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
### Read the cmdserver's banner.
|
277
|
+
def read_hello
|
278
|
+
_, message = self.read_message
|
279
|
+
self.log.debug "Hello message:\n%s" % [ message ]
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
### Read a single channel identifier and message from the command server. Raises
|
284
|
+
### an exception if the server is not yet started.
|
285
|
+
def read_message
|
286
|
+
raise "Server is not yet started" unless self.started?
|
287
|
+
header = self.reader.read( 5 ) or raise "Server aborted."
|
288
|
+
channel, bytes = header.unpack( HEADER_TEMPLATE )
|
289
|
+
self.log.debug "Read channel %p message (%d bytes)" % [ channel, bytes ]
|
290
|
+
|
291
|
+
# Input requested; return the requested length as the message
|
292
|
+
if channel == 'I' || channel == 'L'
|
293
|
+
return channel, bytes
|
294
|
+
end
|
295
|
+
|
296
|
+
self.log.debug "Reading %d more bytes of the message" % [ bytes ]
|
297
|
+
message = self.reader.read( bytes ) unless bytes.zero?
|
298
|
+
self.log.debug " read message: %p" % [ message ]
|
299
|
+
return channel, message
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
### Return the command-line command for starting the command server.
|
304
|
+
def server_start_command
|
305
|
+
cmd = [
|
306
|
+
Hglib.hg_path.to_s,
|
307
|
+
'--config',
|
308
|
+
'ui.interactive=True',
|
309
|
+
'serve',
|
310
|
+
'--cmdserver',
|
311
|
+
'pipe',
|
312
|
+
]
|
313
|
+
|
314
|
+
cmd << '--repository' << self.repo.to_s if self.repo
|
315
|
+
|
316
|
+
return cmd
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
end # class Hglib::Server
|
data/spec/.status
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
---------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/hglib/repo/id_spec.rb[1:1] | passed | 0.00013 seconds |
|
4
|
+
./spec/hglib/repo/id_spec.rb[1:2] | passed | 0.00068 seconds |
|
5
|
+
./spec/hglib/repo/id_spec.rb[1:3] | passed | 0.00012 seconds |
|
6
|
+
./spec/hglib/repo/id_spec.rb[1:4] | passed | 0.00011 seconds |
|
7
|
+
./spec/hglib/repo/id_spec.rb[1:5] | passed | 0.00007 seconds |
|
8
|
+
./spec/hglib/repo/id_spec.rb[1:6] | passed | 0.00008 seconds |
|
9
|
+
./spec/hglib/repo/id_spec.rb[1:7:1] | passed | 0.00004 seconds |
|
10
|
+
./spec/hglib/repo/id_spec.rb[1:7:2] | passed | 0.00004 seconds |
|
11
|
+
./spec/hglib/repo/id_spec.rb[1:8:1] | passed | 0.00009 seconds |
|
12
|
+
./spec/hglib/repo/id_spec.rb[1:8:2] | passed | 0.00008 seconds |
|
13
|
+
./spec/hglib/repo/id_spec.rb[1:8:3] | passed | 0.00109 seconds |
|
14
|
+
./spec/hglib/repo/id_spec.rb[1:8:4] | passed | 0.00006 seconds |
|
15
|
+
./spec/hglib/repo/id_spec.rb[1:8:5] | passed | 0.00005 seconds |
|
16
|
+
./spec/hglib/repo/id_spec.rb[1:8:6] | passed | 0.00005 seconds |
|
17
|
+
./spec/hglib/repo/id_spec.rb[1:9:1] | passed | 0.00005 seconds |
|
18
|
+
./spec/hglib/repo/id_spec.rb[1:9:2] | passed | 0.00004 seconds |
|
19
|
+
./spec/hglib/repo/id_spec.rb[1:9:3] | passed | 0.00004 seconds |
|
20
|
+
./spec/hglib/repo/id_spec.rb[1:9:4] | passed | 0.00004 seconds |
|
21
|
+
./spec/hglib/repo/log_entry_spec.rb[1:1] | passed | 0.0025 seconds |
|
22
|
+
./spec/hglib/repo/log_entry_spec.rb[1:2] | passed | 0.00022 seconds |
|
23
|
+
./spec/hglib/repo_spec.rb[1:1] | passed | 0.00117 seconds |
|
24
|
+
./spec/hglib/repo_spec.rb[1:2] | passed | 0.00052 seconds |
|
25
|
+
./spec/hglib/repo_spec.rb[1:3] | passed | 0.00748 seconds |
|
26
|
+
./spec/hglib/server_spec.rb[1:1:1] | passed | 0.00038 seconds |
|
27
|
+
./spec/hglib/server_spec.rb[1:1:2] | passed | 0.00164 seconds |
|
28
|
+
./spec/hglib/server_spec.rb[1:1:3] | passed | 0.00039 seconds |
|
29
|
+
./spec/hglib/server_spec.rb[1:1:4] | passed | 0.00039 seconds |
|
30
|
+
./spec/hglib/server_spec.rb[1:1:5] | passed | 0.00076 seconds |
|
31
|
+
./spec/hglib/server_spec.rb[1:1:6] | passed | 0.00052 seconds |
|
32
|
+
./spec/hglib/server_spec.rb[1:2] | passed | 0.00148 seconds |
|
33
|
+
./spec/hglib/server_spec.rb[1:3] | passed | 0.00057 seconds |
|
34
|
+
./spec/hglib/server_spec.rb[1:4] | passed | 0.00139 seconds |
|
35
|
+
./spec/hglib/server_spec.rb[1:5] | passed | 0.00099 seconds |
|
36
|
+
./spec/hglib/server_spec.rb[1:6] | passed | 0.00121 seconds |
|
37
|
+
./spec/hglib_spec.rb[1:1:1] | passed | 0.00104 seconds |
|
38
|
+
./spec/hglib_spec.rb[1:1:2] | passed | 0.0013 seconds |
|
39
|
+
./spec/hglib_spec.rb[1:2:1] | passed | 0.00097 seconds |
|