Module: Sinatra::AmpRepoMethods

Defined in:
lib/amp/server/extension/amp_extension.rb

Overview

These methods are helpers that the server will run to implement the Mercurial HTTP(S) protocol. These should not be overridden if Mercurial compatibility is required. All methods - unless otherwise specified - return the exact data string that the server will serve as the HTTP data.

Defined Under Namespace

Classes: DelayedGzipper

Instance Method Summary collapse

Instance Method Details

#amp_get_between(amp_repo) ⇒ String

Command: between

Takes a list of node pairs. Each pair has a “start” and an “end” node ID, which specify a range of revisions. The between command returns the nodes between the start and the end, exclusive, for each provided pair.

HTTP param: pairs. Each pair is presented as 2 node IDs, as hex, separated by a a hyphen. then, each pair is delimited by other pairs with a space. Example:

pair1startnodeid-pair1endnodeid pair2startnodeid-pair2endnodeid pair3startnodeid-pair3endnodeid

Parameters:

  • amp_repo (Repository)

    the repository upon which to perform node lookups

Returns:

  • (String)

    a response to deliver to the client, with the nodes between each pair provided. Each pair provided by the client will result in a list of node IDs - this list is returned as each node ID in the list, with spaces between the nodes. Each pair has its results on a new line. Example output for 3 provided pairs:

    abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde 1234567890123456789012345678901234567890
    1234567890123456789012345678901234567890
    abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde
    


171
172
173
174
175
176
177
178
179
180
181
# File 'lib/amp/server/extension/amp_extension.rb', line 171

def amp_get_between(amp_repo)
  pairs = []
  
  if params["pairs"]
    pairs = params["pairs"].split(" ").map {|p| p.split("-").map {|i| i.unhexlify } }
  end
  
  amp_repo.between(pairs).map do |nodes|
    nodes.map {|i| i.hexlify }.join " "
  end.join "\n"
end

#amp_get_branches(amp_repo) ⇒ Object



122
123
124
125
126
127
128
129
130
# File 'lib/amp/server/extension/amp_extension.rb', line 122

def amp_get_branches(amp_repo)
  nodes = []
  if params["nodes"]
    nodes = params["nodes"].split(" ").map {|x| x.unhexlify}
  end
  amp_repo.branches(nodes).map do |branches|
    branches.map {|branch| branch.hexlify}.join(" ")
  end.join "\n"
end

#amp_get_capabilities(amp_repo) ⇒ String

Command: capabilities

Returns what special commands the server is capable of performing. This is where new additions to the protocol are added, so new clients can check to make sure new features are supported.

Parameters:

  • amp_repo (Repository)

    the repository whose capabilities are returned

Returns:

  • (String)

    a response to deliver to the client, with each capability listed, separated by spaces. If the capability has multiple values (such as ‘unbundle’), it is returned in the format “capability=value1,value2,value3” instead of just “capability”. No spaces are allowed in the capability= fragment.



144
145
146
147
148
149
# File 'lib/amp/server/extension/amp_extension.rb', line 144

def amp_get_capabilities(amp_repo)
  caps = ["lookup", "changegroupsubset"]
  # uncompressed for streaming?
  caps << "unbundle=#{Amp::RevlogSupport::ChangeGroup::FORMAT_PRIORITIES.join(',')}"
  caps.join ' '
end

#amp_get_changegroup(amp_repo) ⇒ String

Command: changegroup

Gets a given changegroup from the repository. Starts at the requested roots, and then goes to the heads of the repository from those roots.

HTTP Param: roots. The roots of the trees we are requesting, in the form of a list of node IDs. the IDs are in hex, and separated by spaces.

Parameters:

  • amp_repo (Repository)

    the repository from which we are requesting the changegroup.

Returns:

  • (String)

    the changegroup to be returned to the client. Well, more specifically, we halt processing, return an object that will gzip our data on the fly without using ridiculous amounts of memory, and with the correct headers. It ends up being the changegroup, or a large bundled up set of changesets, for the client to add to its repo (or just examine).



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/amp/server/extension/amp_extension.rb', line 285

def amp_get_changegroup(amp_repo)
  headers = gzipped_response
  
  nodes = []
  if params["roots"]
    nodes = params["roots"].split(" ").map {|i| i.unhexlify }
  end
  
  result = DelayedGzipper.new do
    amp_repo.changegroup(nodes, :serve)
  end
  
  throw :halt, [200, headers, result]

end

#amp_get_changegroupsubset(amp_repo) ⇒ String

Command: changegroupsubset Requires an explicit capability: changegroupsubset

Gets a given changegroup subset from the repository. Starts at the requested roots, and then goes to the heads given as parameters. This is how one might “slice” a repository, just as one slices an array with arr. The “root” is 3, the “head” is 7. However, one can provide a number of roots or heads, as a mercurial repository is a DAG, and not a simple list of numbers such as 3..7.

HTTP Param: roots. The roots of the trees we are requesting, in the form of a list of node IDs. the IDs are in hex, and separated by spaces. HTTP Param: heads. The heads of the slice of the trees we are requesting. The changegroup will stop being processed at the heads. In the form of a list of node IDs, each in hex, and separated by spaces.

Parameters:

  • amp_repo (Repository)

    the repository from which we are requesting the changegroup subset.

Returns:

  • (String)

    the changegroup subset to be returned to the client. Well, more specifically, we halt processing, return an object that will gzip our data on the fly without using ridiculous amounts of memory, and with the correct headers. It ends up being the changegroup subset, or a large bundled up set of changesets, for the client to add to its repo (or just examine).



324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/amp/server/extension/amp_extension.rb', line 324

def amp_get_changegroupsubset(amp_repo)
  headers = gzipped_response
  
  bases, heads = [], []
  
  bases = params["bases"].split(" ").map {|i| i.unhexlify } if params["bases"]
  heads = params["heads"].split(" ").map {|i| i.unhexlify } if params["heads"]
  
  result = DelayedGzipper.new do
    amp_repo.changegroup_subset bases, heads, :serve
  end
  
  throw :halt, [200, headers, result]
end

#amp_get_fake_writing(amp_repo) ⇒ Object



339
340
341
# File 'lib/amp/server/extension/amp_extension.rb', line 339

def amp_get_fake_writing(amp_repo)
  "You're logged in!"
end

#amp_get_heads(amp_repo) ⇒ String

Command: heads

Looks up the heads for the given repository. No parameters are taken - just the heads are returned.

Parameters:

  • amp_repo (Repository)

    the repository whose heads are examined

Returns:

  • (String)

    a response to deliver to the client, with each head returned as a full node-id, in hex form (so 40 bytes total), each separated by a single space.



117
118
119
120
# File 'lib/amp/server/extension/amp_extension.rb', line 117

def amp_get_heads(amp_repo)
  repo = amp_repo
  repo.heads.map {|x| x.hexlify}.join(" ")
end

#amp_get_lookup(amp_repo) ⇒ String

Command: lookup

Looks up a node-id for a key - the key could be an integer (for a revision index), a partial node_id (such as 12dead34beef), or even “tip” to get the current tip. Only concerns revisions in the changelog (the “global” revisions)

HTTP parameter: “key” => the key being looked up in the changelogs

Parameters:

  • amp_repo (Repository)

    the repository being inspected

Returns:

  • (String)

    a response to deliver to the client, in the format “#success #node_id”, where success is 1 for a successful lookup and node_id is 0 for a failed lookup.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/amp/server/extension/amp_extension.rb', line 96

def amp_get_lookup(amp_repo)
  begin
    rev = amp_repo.lookup(params["key"]).hexlify
    success = 1
  rescue StandardError => e
    rev = e.to_s
    success = 0
  end
  
  "#{success} #{rev}\n"
end

#amp_get_unbundle(amp_repo) ⇒ String

TODO:

locking

TODO:

finish this method!

Command: unbundle

This command is used when a client wishes to push over HTTP. A bundle is posted as the request’s data body.

HTTP Method: post HTTP parameters: heads. The client repo’s heads. Could be “force”.hexlify, if

the client is going to push anyway.

HTTP post body: the bundled up set of changegroups.

Parameters:

  • amp_repo (Repository)

    the repository to be pushed to

Returns:

  • (String)

    the results of the push, which are streamed to the client.



359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/amp/server/extension/amp_extension.rb', line 359

def amp_get_unbundle(amp_repo)
  their_heads = params["heads"].split(" ")
  
  check_heads = proc do
    heads = amp_repo.heads.map {|i| i.hexlify}
    return their_heads == ["force".hexlify] || their_heads == heads
  end
  
  unless check_heads.call
    throw :halt, [200, "unsynced changes"]
  end
  
  Tempfile.open("amp-unbundle-") do |fp|
    length = request.content_length
    fp.write request.body
    
    unless check_heads.call
      # in case our heads have changed in the last few milliseconds
      throw :halt, [200, "unsynced changes"]
    end
    fp.seek(0, IO::SEEK_SET)
    header = fp.read(6)
    if header.start_with?("HG") && !header.start_with?("HG10")
      raise ArgumentError.new("unknown bundle version")
    elsif !Amp::RevlogSupport::ChangeGroup::BUNDLE_HEADERS.include?(header)
      raise ArgumentError.new("unknown bundle compression type")
    end
    
    stream = Amp::RevlogSupport::ChangeGroup.unbundle(header, fp)
    
  end
  
end

#command_reads?(cmd) ⇒ Boolean

Checks if the given command performs a read operation

Parameters:

  • cmd (String)

    the command to check

Returns:

  • (Boolean)

    does the command perform any reads on the repo?



75
# File 'lib/amp/server/extension/amp_extension.rb', line 75

def command_reads?(cmd);   AmpExtension::READABLE_COMMANDS.include?(cmd); end

#command_writes?(cmd) ⇒ Boolean

Checks if the given command performs a write operation

Parameters:

  • cmd (String)

    the command to check

Returns:

  • (Boolean)

    does the command perform any writes on the repo?



82
# File 'lib/amp/server/extension/amp_extension.rb', line 82

def command_writes?(cmd); !command_reads?(cmd); end

#gzipped_responseRack::Utils::HeaderHash

Helper method for setting up the headers for lazily gzipped results in a sinatra app.

Returns:

  • (Rack::Utils::HeaderHash)

    the headers that tell a client to expect gzipped data, and that we don’t know how big the data is going to be, because we’re gzipping it on the fly!



256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/amp/server/extension/amp_extension.rb', line 256

def gzipped_response
  headers = Rack::Utils::HeaderHash.new(response.headers)
  vary = headers["Vary"].to_s.split(",").map { |v| v.strip }
  
  unless vary.include?("*") || vary.include?("Accept-Encoding")
    headers["Vary"] = vary.push("Accept-Encoding").join ","
  end
  
  headers.delete 'Content-Length'
  headers["Content-Encoding"] = "gzip"
  headers
end