Wiki

Clone wiki

amp / Architecture

Amp Repository API

This document outlines an architecture for Repository subclasses that will allow others to implement new Repository formats within the Amp system.

This architecture is new - the original Amp implementation simply ported much of Mercurial's codebase over. We are in the process of moving the current Mercurial implementation into fitting this architecture.

Design Approach

In designing this API, we have broken down a generic VCS repository into simple responsibilities. Any VCS must be able to:

  • Mark files for addition, removal, copies in the next changeset.
  • Mark a modified file for inclusion in the next changeset.
  • Create local history with metadata
  • Retrieve old history's metadata
  • Retrieve old versions of files
  • Retrieve changesets from a remote repository
  • Send changesets to a remote repository
  • Manage merge conflicts

Given these responsibilities, we have attempted to provide a *minimal API* to implement to provide these features. Our goal over the coming weeks is to switch the existing Mercurial codebase to rely upon this minimal API, and to ensure commands only invoke this minimal API. Thus, anyone who implements these methods will be guaranteed compatibility with existing commands.

  • Note: We have not yet included the concepts of branches and tags into this API, despite agreeing they are in fact basic features any VCS should implement. This will be provided in future versions of this API.*
Notes about Implementation

In Ruby, we have no interfaces no formal contracts, and so this "abstract class" is in fact the API you must implement. The default implementation of these methods is as follows:

raise NotImplementedError.new("[name of method] is not yet implemented.")

If you are getting NotImplementedErrors, then you have not yet implemented one of the core API methods.

LocalRepository

First, there is the LocalRepository. This is the entry point for commands: any command that works on a repository is passed in an object that is a subclass of this class. Given the below API, you should be able to access all of the repository's history, as well as the Staging Area (discussed below).

commit is defined as creating a *local changeset*. For a system such as SVN, which does not by default have local commits, you will have to provide for this functionality. This is crucial to Amp's philosophy of separating commands from systems.

class AbstractLocalRepository
	##
        # Returns the staging area for the repository, which provides the ability to add/remove
        # files in the next commit.
        # Returns a subclass of AbstractStagingArea
	def staging_area()
	
	##
        # Creates a local changeset.
	# Returns boolean for success/failure
	def commit(options = {})
	
	##
        # Pushes changesets to a remote repository.
        # Returns boolean for success/failure
	def push(options = {})
	
	##
        # Pulls changesets from a remote repository 
        # Does *not* apply them to the working directory.
        # Returns boolean for success/failure
	def pull(options = {})
	
	##
        # Returns a changeset for the given revision.
        # Must support at least integer indexing as well as a string "node ID", if the repository
        # system has such IDs. Also "tip" should return the tip of the revision tree.
	# Returns an AbstractChangeset
	def [](revision)
	
	##
        # Returns the number of changesets in the repository.
	# Returns Integer
	def size
	
	##
        # Gets a given file at the given revision, in the form of an AbstractVersionedFile object.
	# Returns AbstractVersionedFile
	def get_file(file, revision)
	
	##
        # In whatever conflict-resolution system your repository format defines, mark a given file
        # as in conflict. If your format does not manage conflict resolution, re-define this method as
        # a no-op.
	# Returns boolean
	def mark_conflicted(*filenames)
	
	##
        # In whatever conflict-resolution system your repository format defines, mark a given file
        # as no longer in conflict (resolved). If your format does not manage conflict resolution,
        # re-define this method as a no-op.
	# Returns boolean
	def mark_resolved(*filenames)
end

Staging Area

While git is well-known for its concept of the staging area, all VCS systems have the idea of an intermediate space which holds a list of changes to make upon creation of the next changeset. In both Mercurial and Subversion, when you add/remove a file from the repository, these actions do not take place until the next commit. In git, you must add any modified file to the staging area for it to be included in the next commit.

We abstract this idea in a subclass of AbstractStagingArea. How your particular VCS implements the staging area is unimportant.

class AbstractStagingArea

    ##
    # Marks a file to be added to the repository upon the next commit.
    # return value is success/failure
    def add(*filenames)

    ##
    # Marks a file to be removed from the repository upon the next commit.
    # return value is success/failure
    def remove(*filenames)

    ##
    # Marks a file to be copied from the +from+ location to the +to+ location
    # in the next commit, while retaining history.
    # return value is success/failure
    def copy(from, to)

    ##
    # Marks a file to be moved from the +from+ location to the +to+ location
    # in the next commit, while retaining history.
    # return value is success/failure
    def move(from, to)

    ##
    # Marks a modified file to be included in the next commit.
    # If your VCS does this implicitly, this should be defined as a no-op.
    # return value is success/failure
    def include(*filenames)
    alias_method :stage, :include

    ##
    # Mark a modified file to not be included in the next commit.
    # If your VCS does not include this idea because staging a file is implicit, this should
    # be defined as a no-op.
    # return value is success/failure
    def exclude(*filenames)
    alias_method :unstage, :exclude

    ##
    # Returns a Symbol.
    # Possible results:
    # :added (subset of :included)
    # :removed
    # :unknown
    # :included
    # :normal
    #
    def status(filename)
end

Changeset

A Changeset represents a single revision with the version control system. While such revisions can contain all sorts of data, they all share some things in common:

  • The revision most likely (but not necessarily) changed the contents of some files
  • The revision was committed by a user with a username
  • The revision took place at a given time
  • The revision had a message attached to it
  • The revision had a revision before it (parent).

These basic bits of information are provided in the current version of the API. In addition, a user will frequently wish to check out a file's contents at the given revision. For that purpose, we provide the #get_file method (aliased to #[]). This will return a VersionedFile, which is detailed below.

class AbstractChangeset
  
  ##
  # Returns Array of AbstractChangesets ( [AbstractChangeset] )
  def parents

  ##
  # Returns AbstractVersionedFile
  def get_file(filename)
  alias_method :[], :get_file  

  ##
  # Returns Date object
  def date
  
  ##
  # Returns String
  def user
  
  ##
  # Returns String
  def description
  
  ##
  # Returns Array of String ( [String] )
  def changed_files
end

Versioned Files

A VersionedFile represents a file at a given point in time. The most common use of such a file is to get its data, so for this initial public API, we only require that you implement a #data method, and a #changeset method that refers to the changeset (i.e.: point in time) to which the VersionedFile belongs.

class AbstractVersionedFile

  # changeset this VersionedFile is a part of.
  def changeset

  # data at the given revision
  # Returned as String
  def data
end

Updated