Fossil-to-Git Replayer
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.idea
src/main
.gitignore
.travis.yml
README.md
io7m-ftgr.iml
pom.xml

README.md

io7m-ftgr

Build Status

What is this?

ftgr is a tool made for a very specific purpose: Take a Fossil repository that has only had PGP-signed commits from one person (possibly with multiple keys), and produce a semantically equivalent Git repository. The resulting Git repository's commits will have the same dates and signatures as the original Fossil commits (and the dates inside those signatures will also match).

Requirements

Building

$ mvn clean package

Running

Compilation produces a jar file containing all of the dependencies.

$ java -jar target/io7m-ftgr-*-main.jar
usage: ftgr.conf [logback.xml]

The program accepts a configuration file and an optional Logback configuration file to control logging. The default is to log everything.

The ftgr configuration file is in Java Properties format:

# Absolute path to fossil executable
com.io7m.ftgr.fossil_executable = /usr/bin/fossil

# Absolute path to git executable
com.io7m.ftgr.git_executable = /usr/bin/git

# Absolute path to GPG executable
com.io7m.ftgr.gpg_executable = /usr/bin/gpg

# Absolute path to faketime executable
com.io7m.ftgr.faketime_executable = /usr/bin/faketime

# True if no changes should be written to disk
com.io7m.ftgr.dry_run = false

# True if repository verification should be performed (See "Verification" below)
com.io7m.ftgr.verify = true

# Absolute path to the Git repository that will be created
com.io7m.ftgr.git_repository = /tmp/output

# Absolute path to the input Fossil repository
com.io7m.ftgr.fossil_repository = /tmp/input.fossil

# See "Commit mappings" below.
com.io7m.ftgr.commit_map = /tmp/output-commits.txt

# See "Name mappings" below.
com.io7m.ftgr.name_map.someone      = Some One|someone@example.org
com.io7m.ftgr.name_map.someone_else = Some One|someone@example.org

Name mappings

Fossil uses short usernames to mark each commit. Git, however, tends towards full names and email addresses in each message. In order to produce the latter from the former, the configuration file requires the definition of name mappings. For a given username u, the key com.io7m.ftgr.name_map.u defines a name and email address separated by the pipe symbol | (U+007C). It is an error to fail to provide a mapping for a Fossil username: The program will fail loudly before attempting to write any data if one or more are missing.

Commit mappings

In order to allow for verification of commits at a later date, ftgr saves a log of the Git commits made for each Fossil commit. The resulting log consists of one mapping m per line, where m has the form:

git:gc|fossil:fc

Where gc is a the SHA-1 hash of a Git commit, and fc is the SHA-1 hash of a Fossil commit.

Verification

Verification of commits proceeds by checking out each Git commit mentioned in the recorded commit map, and then checking out the corresponding Fossil commit and checking that the same files with the same contents are present in both commits.

How?

Fossil and Git use a similar internal model: A directed acyclic graph of immutable objects representing files, directories, and commits. Therefore, it's a fairly simple case of transforming one to the other by literally performing each commit in a Git repository as it was performed in the original Fossil repository, with minor adjustments to account for differences in how merges and branches are represented.

  1. Read all commits from the Fossil repository into a directed acyclic graph structure, keeping track of the branch, time, user, and comment of the original commits. The vertices of the graph are the commits and the edges of the graph are the parent → child links. Fossil implicitly marks the first commit of each branch; the details of how that happens aren't important here, just the fact that it's possible to know unambiguously whether a commit was responsible for "creating" the branch or not.

  2. Fetch the manifest for each commit, and extract the key ID of the PGP key used to sign the manifest. Maintain a list of all keys used.

  3. Check that the running user has the private key for each of the listed keys above. Give up if any are missing.

  4. Check that a mapping exists from all Fossil user names to Git authors. This is so that the author/committer user names in the original commits can be transformed into the conventional An A Author <someone@example.com> format. Give up if any are missing.

  5. Create an empty Git repository. Create an initial unsigned root commit consisting of a single .gitignore file that hides any relevant Fossil metadata files.

  6. Open the Fossil repository into the Git repository directory. This allows Fossil operations to be performed by executing the fossil command line with that directory as the current working directory.

  7. Order the commits by time. Oldest commits come first.

  8. For each commit c:

  • If c was the first commit of a branch, create a new Git branch.
  • If c has two parents, merge the current branch with whichever of the two parents are on the other branch.
  • Otherwise, checkout commit c from Fossil, replacing all files in the current directory (and removing all files that are not part of the commit). Switch Git to the relevant branch. Add all files to the Git index and commit using the original message, time, and signing the result with the original PGP key.

Internally, faketime is used to execute both git and gpg. This allows for new commits to have the exact times as specified in the original commits. Amusingly, it also allows for the use of expired keys in GnuPG.