You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by rh...@apache.org on 2015/12/01 10:06:39 UTC

svn commit: r1717391 - /subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c

Author: rhuijben
Date: Tue Dec  1 09:06:39 2015
New Revision: 1717391

URL: http://svn.apache.org/viewvc?rev=1717391&view=rev
Log:
On the ra-git branch: Extend the git-revroot to basically support all
operations to perform a svn checkout/update/switch of/within 'trunk'.

As a side-effect this also adds support for 'svn info', 'svn ls', etc.

* subversion/libsvn_fs_git/git-revroot.c
  (includes): Add svn_dirent_uri.h.
  (svn_fs_git_root_t): Add branch map.
  (svn_fs_git_fs_id_t): New define.
  (cleanup_git_object,
   get_entry_object): New function.
  (fs_git_id_unparse,
   fs_git_id_compare): New function.
  (id_vtable): New var.
  (make_id): Implement.
  (relpath_reverse_split): New function.
  (find_branch,
   find_tree_entry): New function.
  (fs_git_paths_changed): Hardcode basic support for r0 and r1.
  (fs_git_check_path): Implement.
  (fs_git_node_id,
   fs_git_node_relation): Implement.
  (fs_git_copied_from,
   fs_git_closest_copy): Return not copied instead of not implemented.
  (fs_git_node_proplist,
   fs_git_props_changed): We don't have properties yet.
  (fs_git_dir_entries): Implement.
  (fs_git_dir_optimal_order): Implement.
  (fs_git_file_length,
   fs_git_file_checksum,
   fs_git_file_contents): Implement.
  (fs_git_try_process_file_contents): Return not succeeded.
  (fs_git_contents_changed): Compare files by matching their oid.
  (fs_git_get_file_delta_stream): Provide deltas.
  (svn_fs_git__revision_root): Initialize map.

Modified:
    subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c

Modified: subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c
URL: http://svn.apache.org/viewvc/subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c?rev=1717391&r1=1717390&r2=1717391&view=diff
==============================================================================
--- subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c (original)
+++ subversion/branches/ra-git/subversion/libsvn_fs_git/git-revroot.c Tue Dec  1 09:06:39 2015
@@ -28,6 +28,7 @@
 
 #include "svn_fs.h"
 #include "svn_hash.h"
+#include "svn_dirent_uri.h"
 #include "svn_version.h"
 #include "svn_pools.h"
 
@@ -43,8 +44,17 @@ typedef struct svn_fs_git_root_t
   git_commit *commit;
   const char *rev_path;
   svn_boolean_t exact;
+  apr_hash_t *branch_map;
 } svn_fs_git_root_t;
 
+typedef struct svn_fs_git_fs_id_t
+{
+  git_oid commit;
+  const char *path;
+  const char *branch;
+  svn_fs_root_t *root;
+} svn_fs_git_fs_id_t;
+
 static apr_status_t
 git_root_cleanup(void *baton)
 {
@@ -59,12 +69,195 @@ git_root_cleanup(void *baton)
   return APR_SUCCESS;
 }
 
+/* Helper for get_entry_object */
+static apr_status_t
+cleanup_git_object(void *baton)
+{
+  git_object_free(baton);
+  return APR_SUCCESS;
+}
+
+/* Gets the raw git object behind an entry. Takes care of the 'will free'
+   promise via the pool */
+static svn_error_t *
+get_entry_object(git_object **obj,
+                 const git_tree *tree,
+                 const git_tree_entry *entry,
+                 apr_pool_t *result_pool)
+{
+  git_object *obj_out;
+
+  GIT2_ERR(git_tree_entry_to_object(&obj_out, git_tree_owner(tree), entry));
+
+  apr_pool_cleanup_register(result_pool, obj_out, cleanup_git_object,
+                            apr_pool_cleanup_null);
+
+  *obj = obj_out;
+  return SVN_NO_ERROR;
+}
+
+/* We don't have real ids (yet) */
+static svn_string_t *fs_git_id_unparse(const svn_fs_id_t *id,
+                                       apr_pool_t *pool)
+{
+  return svn_string_create("", pool);
+}
+
+/* Fake an id via the node relation check to make the repos layer
+   happy */
+static svn_fs_node_relation_t fs_git_id_compare(const svn_fs_id_t *a,
+                                                const svn_fs_id_t *b)
+{
+  const svn_fs_git_fs_id_t *id_a, *id_b;
+  if (a->vtable != b->vtable)
+    return svn_fs_node_unrelated;
+
+  id_a = a->fsap_data;
+  id_b = b->fsap_data;
+
+  if (id_a->root && id_b->root
+      && id_a->root->fs == id_b->root->fs)
+    {
+      svn_fs_node_relation_t rel;
+      svn_error_t *err;
+
+      err = id_a->root->vtable->node_relation(&rel,
+                                              id_a->root, id_a->path,
+                                              id_b->root, id_b->path,
+                                              id_a->root->pool);
+
+      if (!err)
+        return rel;
+
+      svn_error_clear(err);
+    }
+
+  return svn_fs_node_unrelated;
+}
+
+static id_vtable_t id_vtable =
+{
+  fs_git_id_unparse,
+  fs_git_id_compare
+};
+
 static svn_fs_id_t *
 make_id(svn_fs_root_t *root,
         const char *path,
         apr_pool_t *result_pool)
 {
-  return NULL;
+  svn_fs_git_fs_id_t *id = apr_pcalloc(result_pool, sizeof(*id));
+  svn_fs_id_t *fsid = apr_pcalloc(result_pool, sizeof(*fsid));
+
+  fsid->fsap_data = id;
+  fsid->vtable = &id_vtable;
+
+  id->path = apr_pstrdup(result_pool, path);
+  id->root = root;
+  memset(&id->commit, 0, sizeof(id->commit));
+
+  return fsid;
+}
+
+/* svn_relpath_split, but then for the first component instead of the last */
+static svn_error_t *
+relpath_reverse_split(const char **root, const char **remaining,
+                      const char *relpath,
+                      apr_pool_t *result_pool)
+{
+  const char *ch = strchr(relpath, '/');
+  SVN_ERR_ASSERT(svn_relpath_is_canonical(relpath));
+
+  if (!ch)
+    {
+      if (root)
+        *root = apr_pstrdup(result_pool, relpath);
+      if (remaining)
+        *remaining = "";
+    }
+  else
+    {
+      if (root)
+        *root = apr_pstrmemdup(result_pool, relpath, ch - relpath);
+      if (remaining)
+        *remaining = apr_pstrdup(result_pool, ch + 1);
+    }
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+find_branch(const git_commit **commit, const char **relpath,
+            svn_fs_root_t *root, const char *path, apr_pool_t *pool)
+{
+  svn_fs_git_root_t *fgr = root->fsap_data;
+
+  if (*path == '/')
+    path++;
+
+  if (fgr->rev_path)
+    {
+      apr_size_t len;
+      len = strlen(fgr->rev_path);
+
+      if (!strncmp(path, fgr->rev_path, len)
+          && (!path[len] || path[len] == '/'))
+        {
+          *commit = fgr->commit;
+          *relpath = path[len] ? &path[len + 1] : "";
+          return SVN_NO_ERROR;
+        }
+    }
+
+  *commit = NULL;
+  *relpath = NULL;
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+find_tree_entry(const git_tree_entry **entry, git_tree *tree,
+                const char *relpath,
+                apr_pool_t *result_pool, apr_pool_t *scratch_pool)
+{
+  const char *basename, *tail;
+  const git_tree_entry *e;
+
+  SVN_ERR(relpath_reverse_split(&basename, &tail, relpath, scratch_pool));
+
+  e = git_tree_entry_byname(tree, basename);
+  if (e && !*tail)
+    {
+      *entry = e;
+      return SVN_NO_ERROR;
+    }
+  else if (!e)
+    {
+      *entry = NULL;
+      return SVN_NO_ERROR;
+    }
+
+  switch (git_tree_entry_type(e))
+    {
+      case GIT_OBJ_TREE:
+        {
+          git_object *obj;
+          git_tree *sub_tree;
+
+          SVN_ERR(get_entry_object(&obj, tree, e, result_pool));
+
+          sub_tree = (git_tree*)obj;
+
+          SVN_ERR(find_tree_entry(entry, sub_tree, tail,
+                                  result_pool, scratch_pool));
+          break;
+        }
+      case GIT_OBJ_BLOB:
+        *entry = NULL;
+        break;
+      default:
+        SVN_ERR_MALFUNCTION();
+    }
+
+  return SVN_NO_ERROR;
 }
 
  /* Determining what has changed in a root */
@@ -73,7 +266,29 @@ fs_git_paths_changed(apr_hash_t **change
                      svn_fs_root_t *root,
                      apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  apr_hash_t *changed_paths = apr_hash_make(pool);
+  *changed_paths_p = changed_paths;
+  if (root->rev == 0)
+    return SVN_NO_ERROR;
+  else if (root->rev == 1)
+    {
+      svn_fs_path_change2_t *ch;
+
+      ch = svn_fs__path_change_create_internal(make_id(root, "/trunk", pool),
+                                               svn_fs_path_change_add,
+                                               pool);
+      ch->node_kind = svn_node_dir;
+      svn_hash_sets(changed_paths, "/trunk", ch);
+
+      ch = svn_fs__path_change_create_internal(make_id(root, "/branches",
+                                                       pool),
+                                               svn_fs_path_change_add,
+                                               pool);
+      ch->node_kind = svn_node_dir;
+      svn_hash_sets(changed_paths, "/branches", ch);
+    }
+
+  return SVN_NO_ERROR;
 }
 
 /* Generic node operations */
@@ -81,6 +296,11 @@ static svn_error_t *
 fs_git_check_path(svn_node_kind_t *kind_p, svn_fs_root_t *root,
                   const char *path, apr_pool_t *pool)
 {
+  const git_commit *commit;
+  const char *relpath;
+  git_tree *tree;
+  git_tree_entry *entry;
+
   if (*path == '/')
     path++;
   if (!*path)
@@ -89,7 +309,47 @@ fs_git_check_path(svn_node_kind_t *kind_
       return SVN_NO_ERROR;
     }
 
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  SVN_ERR(find_branch(&commit, &relpath, root, path, pool));
+  if (!commit)
+    {
+      if (!strcmp(path, "branches") || !strcmp(path, "tags"))
+        {
+          *kind_p = svn_node_dir;
+          return SVN_NO_ERROR;
+        }
+
+      *kind_p = svn_node_none;
+      return SVN_NO_ERROR;
+    }
+
+  if (!relpath[0])
+    {
+      *kind_p = svn_node_dir;
+      return SVN_NO_ERROR;
+    }
+
+  GIT2_ERR(git_commit_tree(&tree, commit));
+  SVN_ERR(find_tree_entry(&entry, tree, relpath, pool, pool));
+
+  if (!entry)
+    *kind_p = svn_node_none;
+  else
+    {
+      switch (git_tree_entry_type(entry))
+        {
+          case GIT_OBJ_TREE:
+            *kind_p = svn_node_dir;
+            break;
+          case GIT_OBJ_BLOB:
+            *kind_p = svn_node_file;
+            break;
+          default:
+            *kind_p = svn_node_none;
+            break;
+        }
+    }
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -105,7 +365,8 @@ static svn_error_t *
 fs_git_node_id(const svn_fs_id_t **id_p, svn_fs_root_t *root,
                const char *path, apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *id_p = make_id(root, path, pool);
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -114,7 +375,79 @@ fs_git_node_relation(svn_fs_node_relatio
                      svn_fs_root_t *root_b, const char *path_b,
                      apr_pool_t *scratch_pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  const git_commit *commit_a, *commit_b;
+  const char *relpath_a, *relpath_b;
+  git_tree *tree_a, *tree_b;
+  const git_tree_entry *entry_a, *entry_b;
+
+  if (*path_a == '/')
+    path_a++;
+  if (*path_b == '/')
+    path_b++;
+
+  if (!*path_a || !*path_b)
+    {
+      if (!*path_a && !*path_b)
+        {
+          if (root_a->rev == root_b->rev)
+            *relation = svn_fs_node_unchanged;
+          else
+            *relation = svn_fs_node_common_ancestor;
+        }
+      else
+        *relation = svn_fs_node_unrelated;
+      return SVN_NO_ERROR;
+    }
+
+  SVN_ERR(find_branch(&commit_a, &relpath_a, root_a, path_a, scratch_pool));
+  SVN_ERR(find_branch(&commit_b, &relpath_b, root_b, path_b, scratch_pool));
+
+  if (!(commit_a && commit_b))
+    {
+      *relation = svn_fs_node_unrelated;
+      return SVN_NO_ERROR;
+    }
+  else if ((*relpath_a == '\0') || (*relpath_b == '\0'))
+    {
+      if ((*relpath_a == '\0') && (*relpath_b == '\0'))
+        *relation = svn_fs_node_common_ancestor;
+      else
+        *relation = svn_fs_node_unrelated;
+
+      return SVN_NO_ERROR;
+    }
+
+  if (strcmp(relpath_a, relpath_b))
+    {
+      *relation = svn_fs_node_unrelated;
+      return SVN_NO_ERROR;
+    }
+
+  GIT2_ERR(git_commit_tree(&tree_a, commit_a));
+  GIT2_ERR(git_commit_tree(&tree_b, commit_b));
+
+  SVN_ERR(find_tree_entry(&entry_a, tree_a, relpath_a,
+                          scratch_pool, scratch_pool));
+  SVN_ERR(find_tree_entry(&entry_b, tree_b, relpath_b,
+                          scratch_pool, scratch_pool));
+
+  if (!entry_a || !entry_b)
+    {
+      *relation = svn_fs_node_unrelated;
+      return SVN_NO_ERROR;
+    }
+
+  if (git_tree_entry_type(entry_a) != git_tree_entry_type(entry_b))
+    *relation = svn_fs_node_unrelated;
+  else if (!git_oid_cmp(git_tree_entry_id(entry_a),
+                        git_tree_entry_id(entry_b)))
+    {
+      *relation = svn_fs_node_unchanged;
+    }
+  else 
+    *relation = svn_fs_node_common_ancestor;
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -179,7 +512,10 @@ fs_git_copied_from(svn_revnum_t *rev_p,
                    svn_fs_root_t *root, const char *path,
                    apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *rev_p = SVN_INVALID_REVNUM;
+  *path_p = NULL;
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -187,7 +523,10 @@ fs_git_closest_copy(svn_fs_root_t **root
                     svn_fs_root_t *root, const char *path,
                     apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *root_p = NULL;
+  *path_p = NULL;
+
+  return SVN_NO_ERROR;
 }
 
 /* Property operations */
@@ -202,7 +541,9 @@ static svn_error_t *
 fs_git_node_proplist(apr_hash_t **table_p, svn_fs_root_t *root,
                      const char *path, apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *table_p = apr_hash_make(pool);
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -228,7 +569,8 @@ fs_git_props_changed(int *changed_p, svn
                      const char *path2, svn_boolean_t strict,
                      apr_pool_t *scratch_pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *changed_p = FALSE;
+  return SVN_NO_ERROR;
 }
 
 /* Directories */
@@ -237,11 +579,18 @@ fs_git_dir_entries(apr_hash_t **entries_
                    const char *path, apr_pool_t *pool)
 {
   svn_fs_dirent_t *de;
+  const git_commit *commit;
+  git_tree *tree;
+  const char *relpath;
+  apr_size_t idx;
+
+  if (*path == '/')
+    path++;
 
   *entries_p = apr_hash_make(pool);
   if (!root->rev)
     return SVN_NO_ERROR;
-  else if (*path == '/' && path[1] == '\0')
+  else if (*path == '\0')
     {
       de = apr_pcalloc(pool, sizeof(*de));
       de->kind = svn_node_dir;
@@ -263,7 +612,48 @@ fs_git_dir_entries(apr_hash_t **entries_
 
       return SVN_NO_ERROR;
     }
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+
+  SVN_ERR(find_branch(&commit, &relpath, root, path, pool));
+  if (!commit)
+    {
+      /* TODO: List 'tags' and 'branches' */
+      return SVN_NO_ERROR;
+    }
+
+  GIT2_ERR(git_commit_tree(&tree, commit));
+
+  if (*relpath)
+    {
+      const git_tree_entry *entry;
+      git_object *obj;
+
+      SVN_ERR(find_tree_entry(&entry, tree, relpath, pool, pool));
+
+      if (!entry || git_tree_entry_type(entry) != GIT_OBJ_TREE)
+        return SVN_FS__ERR_NOT_DIRECTORY(root->fs, path);
+
+      SVN_ERR(get_entry_object(&obj, tree, entry, pool));
+
+      tree = (git_tree*)obj;
+    }
+
+  for (idx = 0; idx < git_tree_entrycount(tree); idx++)
+    {
+      const git_tree_entry *e = git_tree_entry_byindex(tree, idx);
+
+      de = apr_pcalloc(pool, sizeof(*de));
+      de->id = make_id(root, path, pool);
+      de->name = git_tree_entry_name(e);
+
+      if (git_tree_entry_type(e) == GIT_OBJ_TREE)
+        de->kind = svn_node_dir;
+      else
+        de->kind = svn_node_file;
+
+      svn_hash_sets(*entries_p, de->name, de);
+    }
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -273,7 +663,16 @@ fs_git_dir_optimal_order(apr_array_heade
                          apr_pool_t *result_pool,
                          apr_pool_t *scratch_pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  /* 1:1 copy of entries with no difference in ordering */
+  apr_hash_index_t *hi;
+  apr_array_header_t *result
+    = apr_array_make(result_pool, apr_hash_count(entries),
+                     sizeof(svn_fs_dirent_t *));
+  for (hi = apr_hash_first(scratch_pool, entries); hi; hi = apr_hash_next(hi))
+    APR_ARRAY_PUSH(result, svn_fs_dirent_t *) = apr_hash_this_val(hi);
+
+  *ordered_p = result;
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -288,7 +687,31 @@ static svn_error_t *
 fs_git_file_length(svn_filesize_t *length_p, svn_fs_root_t *root,
                    const char *path, apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  const git_commit *commit;
+  git_tree *tree;
+  git_tree_entry *entry;
+  git_object *obj;
+  git_blob *blob;
+  const char *relpath;
+
+  SVN_ERR(find_branch(&commit, &relpath, root, path, pool));
+
+  if (!commit)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  GIT2_ERR(git_commit_tree(&tree, commit));
+
+  SVN_ERR(find_tree_entry(&entry, tree, relpath, pool, pool));
+
+  if (!entry || git_tree_entry_type(entry) != GIT_OBJ_BLOB)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  SVN_ERR(get_entry_object(&obj, tree, entry, pool));
+
+  blob = (git_blob*)obj;
+
+  *length_p = git_blob_rawsize(blob);
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -296,7 +719,31 @@ fs_git_file_checksum(svn_checksum_t **ch
                      svn_checksum_kind_t kind, svn_fs_root_t *root,
                      const char *path, apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  const git_commit *commit;
+  git_tree *tree;
+  git_tree_entry *entry;
+  git_object *obj;
+  git_blob *blob;
+  const char *relpath;
+
+  SVN_ERR(find_branch(&commit, &relpath, root, path, pool));
+
+  if (!commit)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  GIT2_ERR(git_commit_tree(&tree, commit));
+
+  SVN_ERR(find_tree_entry(&entry, tree, relpath, pool, pool));
+
+  if (!entry || git_tree_entry_type(entry) != GIT_OBJ_BLOB)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  SVN_ERR(get_entry_object(&obj, tree, entry, pool));
+
+  blob = (git_blob*)obj;
+
+  *checksum = NULL; /* ### TODO: Get via DB cache */
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -304,7 +751,44 @@ fs_git_file_contents(svn_stream_t **cont
                      svn_fs_root_t *root, const char *path,
                      apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  const git_commit *commit;
+  git_tree *tree;
+  git_tree_entry *entry;
+  git_object *obj;
+  git_blob *blob;
+  const char *relpath;
+  svn_filesize_t sz;
+
+  SVN_ERR(find_branch(&commit, &relpath, root, path, pool));
+
+  if (!commit)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  GIT2_ERR(git_commit_tree(&tree, commit));
+
+  SVN_ERR(find_tree_entry(&entry, tree, relpath, pool, pool));
+
+  if (!entry || git_tree_entry_type(entry) != GIT_OBJ_BLOB)
+    return SVN_FS__ERR_NOT_FILE(root->fs, path);
+
+  SVN_ERR(get_entry_object(&obj, tree, entry, pool));
+
+  blob = (git_blob*)obj;
+
+  sz = git_blob_rawsize(blob);
+
+  /* For now use the github 10 MB limit */
+  if (sz < (10 * 1024 * 1024))
+    {
+      svn_string_t *s = svn_string_ncreate(
+        git_blob_rawcontent(blob), (apr_size_t)sz, pool);
+
+      *contents = svn_stream_from_string(s, pool);
+    }
+  else
+    *contents = svn_stream_empty(pool);
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -315,7 +799,9 @@ fs_git_try_process_file_contents(svn_boo
                                  void* baton,
                                  apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  *success = FALSE;
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -345,12 +831,47 @@ fs_git_apply_text(svn_stream_t **content
 }
 
 static svn_error_t *
-fs_git_contents_changed(int *changed_p, svn_fs_root_t *root1,
-                        const char *path1, svn_fs_root_t *root2,
-                        const char *path2, svn_boolean_t strict,
+fs_git_contents_changed(int *changed_p,
+                        svn_fs_root_t *root_a, const char *path_a,
+                        svn_fs_root_t *root_b, const char *path_b,
+                        svn_boolean_t strict,
                         apr_pool_t *scratch_pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  const git_commit *commit_a, *commit_b;
+  const char *relpath_a, *relpath_b;
+  git_tree *tree_a, *tree_b;
+  const git_tree_entry *entry_a, *entry_b;
+
+  SVN_ERR(find_branch(&commit_a, &relpath_a, root_a, path_a, scratch_pool));
+  SVN_ERR(find_branch(&commit_b, &relpath_b, root_b, path_b, scratch_pool));
+
+  if (!commit_a)
+    return SVN_FS__ERR_NOT_FILE(root_a->fs, path_a);
+  else if (!commit_b)
+    return SVN_FS__ERR_NOT_FILE(root_b->fs, path_b);
+
+  GIT2_ERR(git_commit_tree(&tree_a, commit_a));
+  GIT2_ERR(git_commit_tree(&tree_b, commit_b));
+
+  SVN_ERR(find_tree_entry(&entry_a, tree_a, relpath_a,
+                          scratch_pool, scratch_pool));
+  SVN_ERR(find_tree_entry(&entry_b, tree_b, relpath_b,
+                          scratch_pool, scratch_pool));
+
+  if (!entry_a)
+    return SVN_FS__ERR_NOT_FILE(root_a->fs, path_a);
+  else if (!entry_b)
+    return SVN_FS__ERR_NOT_FILE(root_b->fs, path_b);
+
+  if (!git_oid_cmp(git_tree_entry_id(entry_a),
+                   git_tree_entry_id(entry_b)))
+    {
+      *changed_p = FALSE;
+    }
+  else
+    *changed_p = TRUE;
+
+  return SVN_NO_ERROR;
 }
 
 static svn_error_t *
@@ -361,7 +882,23 @@ fs_git_get_file_delta_stream(svn_txdelta
                              const char *target_path,
                              apr_pool_t *pool)
 {
-  return svn_error_create(APR_ENOTIMPL, NULL, NULL);
+  svn_stream_t *source, *target;
+  svn_txdelta_stream_t *delta_stream;
+
+  /* Get read functions for the source file contents.  */
+  if (source_root && source_path)
+    SVN_ERR(fs_git_file_contents(&source, source_root, source_path, pool));
+  else
+    source = svn_stream_empty(pool);
+
+  /* Get read functions for the target file contents.  */
+  SVN_ERR(fs_git_file_contents(&target, target_root, target_path, pool));
+
+  /* Create a delta stream that turns the ancestor into the target.  */
+  svn_txdelta2(&delta_stream, source, target, TRUE, pool);
+
+  *stream_p = delta_stream;
+  return SVN_NO_ERROR;
 }
 
 /* Merging. */
@@ -450,6 +987,8 @@ svn_fs_git__revision_root(svn_fs_root_t
   root->vtable = &root_vtable;
   root->fsap_data = fgr;
 
+  fgr->branch_map = apr_hash_make(pool);
+
   if (rev > 0)
     {
       git_oid *oid;