=== subversion/svnsync/main.c ================================================================== --- subversion/svnsync/main.c (revision 46915) +++ subversion/svnsync/main.c (local) @@ -29,11 +29,27 @@ #include #include +/* The following revision properties are set on revision 0 of + * destination repositories by svnsync: */ #define PROP_PREFIX "svn:sync-" +/* Used to enforce mutually exclusive destination repository access. */ #define LOCK_PROP PROP_PREFIX "lock" +/* Identifies the repository's source. */ #define FROM_URL_PROP PROP_PREFIX "from-url" #define FROM_UUID_PROP PROP_PREFIX "from-uuid" +/* Identifies the last completely mirrored revision, and the revision + * (if any) that is in the process of being mirrored. (Mirroring is + * not an atomic action, because revision properties are copied + * separately from the revision's contents. + * + * At any time that currently-copying is not set, then last-merged-rev + * should be the HEAD revision of the destination repository. + * + * If currently-copying *is* set, it must be either last-merged-rev or + * last-merged-rev + 1, and the HEAD revision must be equal to either + * last-merged-rev or currently-copying. If this is not the case, + * somebody has meddled with the destination without using svnsync. */ #define LAST_MERGED_REV_PROP PROP_PREFIX "last-merged-rev" #define CURRENTLY_COPYING_PROP PROP_PREFIX "currently-copying" @@ -900,25 +916,43 @@ { svn_string_t *currently_copying; - svn_revnum_t to_latest, copying; + svn_revnum_t to_latest, copying, last_merged; SVN_ERR(svn_ra_rev_prop(to_session, 0, CURRENTLY_COPYING_PROP, ¤tly_copying, pool)); SVN_ERR(svn_ra_get_latest_revnum(to_session, &to_latest, pool)); + + last_merged = atol (last_merged_rev->data); if (currently_copying) { copying = atol(currently_copying->data); - if (copying == to_latest) + if ((copying < last_merged) + || (copying > (last_merged + 1)) + || ((to_latest != last_merged) && (to_latest != copying))) { - SVN_ERR(copy_revprops(from_session, to_session, to_latest, - pool)); + return svn_error_createf + (APR_EINVAL, NULL, + _("Revision being currently copied (%ld), last merged " + "revision (%ld), and destination HEAD (%ld) are " + "inconsistent; have you committed to the destination " + "without using svnsync?"), + copying, last_merged, to_latest); + } + else if (copying == to_latest) + { + if (copying > last_merged) + { + SVN_ERR(copy_revprops(from_session, to_session, to_latest, + pool)); - last_merged_rev = svn_string_create(apr_psprintf(pool, "%ld", - to_latest), - pool); + last_merged = copying; + last_merged_rev = svn_string_create(apr_psprintf(pool, "%ld", + last_merged), + pool); + } /* Now update last merged rev and drop currently changing. * Note that the order here is significant, if we do them @@ -934,13 +968,21 @@ CURRENTLY_COPYING_PROP, NULL, pool)); } - else if (copying < to_latest) - return svn_error_createf - (APR_EINVAL, NULL, - _("Currently copying rev %ld in source is less than " - "latest rev in destination (%ld)"), - copying, to_latest); + /* If copying > to_latest, then we just fall through to + * attempting to copy the revision again. */ } + else + { + if (to_latest != last_merged) + { + return svn_error_createf + (APR_EINVAL, NULL, + _("Destination HEAD (%ld) is not the last merged revision " + "(%ld); have you committed to the destination without " + "using svnsync?"), + to_latest, last_merged); + } + } } /* Now check to see if there are any revisions to copy. */ === subversion/tests/cmdline/svnsync_tests.py ================================================================== --- subversion/tests/cmdline/svnsync_tests.py (revision 46915) +++ subversion/tests/cmdline/svnsync_tests.py (local) @@ -46,15 +46,20 @@ svntest.main.set_repos_paths(sbox.repo_dir) -def run_sync(url): +def run_sync(url, expected_error=None): "Synchronize the mirror repository with the master" output, errput = svntest.main.run_svnsync( "synchronize", url, "--username", svntest.main.wc_author, "--password", svntest.main.wc_passwd) if errput: - raise svntest.actions.SVNUnexpectedStderr(errput) - if not output: + if expected_error is None: + raise svntest.actions.SVNUnexpectedStderr(errput) + else: + svntest.actions.match_or_fail(None, "STDERR", expected_error, errput) + elif expected_error is not None: + raise svntest.actions.SVNExpectedStderr() + if not output and not expected_error: # should be: ['Committed revision 1.\n', 'Committed revision 2.\n'] raise svntest.actions.SVNUnexpectedStdout("Missing stdout") @@ -361,7 +366,49 @@ 'baz', dest_sbox.repo_url + '/A/P') +def detect_meddling(sbox): + "verify detection of non-svnsync commits" + sbox.build("svnsync-meddling") + + dest_sbox = sbox.clone_dependent() + build_repos(dest_sbox) + + # Make our own destination checkout (have to do it ourself because it is not greek) + svntest.main.safe_rmtree(dest_sbox.wc_dir) + svntest.actions.run_and_verify_svn(None, + None, + [], + 'co', + dest_sbox.repo_url, + dest_sbox.wc_dir) + + svntest.actions.enable_revprop_changes(svntest.main.current_repo_dir) + + run_init(dest_sbox.repo_url, sbox.repo_url) + run_sync(dest_sbox.repo_url) + + svntest.actions.run_and_verify_svn(None, + None, + [], + 'up', + dest_sbox.wc_dir) + + # Commit some change to the destination, which should be detected by svnsync + was_cwd = os.getcwd() + os.chdir(dest_sbox.wc_dir) + svntest.main.file_append(os.path.join('A', 'B', 'lambda'), 'new lambda text') + svntest.actions.run_and_verify_svn(None, + None, + [], + 'ci', + '-m', 'msg') + os.chdir(was_cwd) + + run_sync(dest_sbox.repo_url, ".*Destination HEAD \\(2\\) is not the last merged revision \\(1\\).*") + + + ######################################################################## # Run the tests @@ -382,6 +429,7 @@ copy_parent_modify_prop, basic_authz, copy_from_unreadable_dir, + detect_meddling, ] if __name__ == '__main__':