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.
@@ -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
+
@@ -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
@@ -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 |