Index: subversion/include/svn_path.h =================================================================== --- subversion/include/svn_path.h (revision 21125) +++ subversion/include/svn_path.h (working copy) @@ -161,6 +161,7 @@ * -
"/foo/bar/baz"  ==>  "/foo/bar" and "baz"
* -
"/bar"          ==>  "/"  and "bar"
* -
"/"             ==>  "/"  and "/"
+ * -
"X:/"           ==>  "X:/" and "X:/"
* -
"bar"           ==>  ""   and "bar"
* -
""              ==>  ""   and ""
*/ @@ -176,6 +177,9 @@ */ int svn_path_is_empty(const char *path); +/** Return TRUE if @a path is a root path ("/" or "X:/") + */ +svn_boolean_t svn_path_is_root(const char *path, apr_size_t len); /** Return a new path (or URL) like @a path, but transformed such that * some types of path specification redundancies are removed. Index: subversion/libsvn_client/commit.c =================================================================== --- subversion/libsvn_client/commit.c (revision 21125) +++ subversion/libsvn_client/commit.c (working copy) @@ -1309,8 +1309,9 @@ while (strcmp(target, base_dir) != 0) { - if ((target[0] == '/' && target[1] == '\0') || - (target[0] == '\0')) + if ((target[0] == '\0') || + svn_path_is_root(target, strlen(target), subpool) + ) abort(); APR_ARRAY_PUSH(dirs_to_lock, Index: subversion/libsvn_subr/path.c =================================================================== --- subversion/libsvn_subr/path.c (revision 21125) +++ subversion/libsvn_subr/path.c (working copy) @@ -24,6 +24,7 @@ #include #include +#include "svn_pools.h" #include "svn_string.h" #include "svn_path.h" #include "svn_private_config.h" /* for SVN_PATH_LOCAL_SEPARATOR */ @@ -98,7 +99,8 @@ apr_size_t len) { return (! SVN_PATH_IS_PLATFORM_EMPTY(path, len) - && (len <= 1 || path[len-1] != '/')); + && (svn_path_is_root(path, len) || + (len <= 1 || path[len-1] != '/'))); } #endif @@ -110,6 +112,7 @@ apr_size_t blen = strlen(base); apr_size_t clen = strlen(component); char *path; + int add_separator; assert(is_canonical(base, blen)); assert(is_canonical(component, clen)); @@ -124,14 +127,16 @@ if (SVN_PATH_IS_EMPTY(component)) return apr_pmemdup(pool, base, blen + 1); - if (blen == 1 && base[0] == '/') - blen = 0; /* Ignore base, just return separator + component */ + add_separator = 1; + if (svn_path_is_root(base, blen)) + add_separator = 0; /* Ignore base, just return separator + component */ /* Construct the new, combined path. */ - path = apr_palloc(pool, blen + 1 + clen + 1); + path = apr_palloc(pool, blen + add_separator + clen + 1); memcpy(path, base, blen); - path[blen] = '/'; - memcpy(path + blen + 1, component, clen + 1); + if (add_separator) + path[blen] = '/'; + memcpy(path + blen + add_separator, component, clen + 1); return path; } @@ -154,9 +159,9 @@ assert(is_canonical(base, total_len)); - if (total_len == 1 && *base == '/') - base_is_root = TRUE; - else if (SVN_PATH_IS_EMPTY(base)) + base_is_root = svn_path_is_root(base, total_len); + if (!base_is_root && + (SVN_PATH_IS_EMPTY(base))) { total_len = sizeof(SVN_EMPTY_PATH) - 1; base_is_empty = TRUE; @@ -309,8 +314,9 @@ while (len > 0 && path[--len] != '/') ; - if (len == 0 && path[0] == '/') - return 1; + /* check if the remaining segment including trailing '/' is a root path */ + if (svn_path_is_root(path, len + 1)) + return len + 1; else return len; } @@ -376,7 +382,7 @@ assert(is_canonical(path, len)); - if (len == 1 && path[0] == '/') + if (svn_path_is_root(path, len)) start = 0; else { @@ -418,6 +424,35 @@ } +svn_boolean_t +svn_path_is_root(const char *path, apr_size_t len) +{ + char *root_path = NULL; + apr_status_t status; + apr_pool_t *strpool = svn_pool_create(NULL); + char *rel_path = apr_pstrmemdup(strpool, path, len); + + /* svn_path_cstring_from_utf8 will create a copy of path. + + It should be safe to convert this error to a false return value. An error + in this case would indicate that the path isn't encoded in UTF-8, which + will cause problems elsewhere, anyway. */ + if (svn_path_cstring_from_utf8(&rel_path, rel_path, strpool) != APR_SUCCESS) + return FALSE; + + status = apr_filepath_root(&root_path, &rel_path, 0, strpool); + + apr_pool_destroy(strpool); + + if ((status == APR_SUCCESS || + status == APR_EINCOMPLETE) && + rel_path[0] == '\0') + return TRUE; + + return FALSE; +} + + int svn_path_compare_paths(const char *path1, const char *path2) @@ -498,7 +533,8 @@ that non-matching byte. */ if (((i == path1_len) && (path2[i] == '/')) || ((i == path2_len) && (path1[i] == '/')) - || ((i == path1_len) && (i == path2_len))) + || ((i == path1_len) && (i == path2_len)) + || svn_path_is_root(path1, i)) return i; else return last_dirsep; @@ -1106,6 +1142,7 @@ apr_size_t seglen; apr_size_t canon_segments = 0; svn_boolean_t uri; + svn_boolean_t strip_slash; dst = canon = apr_pcalloc(pool, strlen(path) + 1); @@ -1168,18 +1205,26 @@ src++; } - /* Remove the trailing slash. */ - if ((canon_segments > 0 || uri) && *(dst - 1) == '/') + strip_slash = TRUE; +#if defined(WIN32) + /* Do not strip the trailing slash in a path like this: X:/ */ + if (canon_segments == 1 && canon[1] == ':' && canon[2] == '/') + strip_slash = FALSE; +#endif /* WIN32 */ + + /* Remove the trailing slash if needed. */ + if (strip_slash && + (canon_segments > 0 || uri) && *(dst - 1) == '/') dst--; *dst = '\0'; -#if defined(WIN32) || defined(__CYGWIN__) +#if defined(WIN32) /* Skip leading double slashes when there are less than 2 * canon segments. UNC paths *MUST* have two segments. */ if (canon_segments < 2 && canon[0] == '/' && canon[1] == '/') return canon + 1; -#endif /* WIN32 or Cygwin */ +#endif /* WIN32 */ return canon; } Index: subversion/tests/cmdline/update_tests.py =================================================================== --- subversion/tests/cmdline/update_tests.py (revision 21125) +++ subversion/tests/cmdline/update_tests.py (working copy) @@ -2157,6 +2157,68 @@ None, None, None, None, 0, C_Path, '--force') +#---------------------------------------------------------------------- +# Test for issue #2556. The tests maps a virtual drive to a working copy +# and tries some basic update, commit and status actions on the virtual +# drive. +def update_wc_on_windows_drive(sbox): + "update wc on the root of a Windows (virtual) drive" + + def find_the_next_available_drive_letter(): + "find the first available drive" + + # get the list of used drive letters, use some Windows specific function. + import win32api + + drives=win32api.GetLogicalDriveStrings() + drives=string.splitfields(drives,'\000') + + for d in range(ord('G'), ord('Z')+1): + drive = chr(d) + if not drive + ':\\' in drives: + return drive + + return '' + + # skip this test on non-Windows platforms. + if not os.name == 'nt': + raise svntest.Skip + + sbox.build() + + # create a virtual drive to the working copy folder + drive = find_the_next_available_drive_letter() + if drive == '': + raise svntest.Skip + + os.popen3('subst ' + drive +': ' + sbox.wc_dir, 't') + wc_dir = drive + ':\\\\' + + was_cwd = os.getcwd() + os.chdir(wc_dir) + + try: + expected_disk = svntest.main.greek_state.copy() + + # Make a local mod to a file which will be committed + mu_path = os.path.join(wc_dir, 'A', 'mu') + svntest.main.file_append (mu_path, '\nAppended text for mu') + + # Commit. + expected_output = svntest.wc.State(wc_dir, { + 'A/mu' : Item(verb='Sending'), + }) + expected_status = svntest.actions.get_virginal_state(wc_dir, 2) + expected_status.tweak(wc_rev=1) + expected_status.tweak('A/mu', wc_rev=2) + svntest.actions.run_and_verify_commit(wc_dir, expected_output, + expected_status, None, + None, None, None, None, wc_dir) + finally: + os.chdir(was_cwd) + # cleanup the virtual drive + os.popen3('subst /D ' + drive +': ', 't') + ######################################################################## # Run the tests @@ -2194,7 +2256,8 @@ update_eolstyle_handling, XFail(update_copy_of_old_rev), forced_update, - forced_update_failures + forced_update_failures, + update_wc_on_windows_drive, ] if __name__ == '__main__': Index: subversion/tests/libsvn_subr/path-test.c =================================================================== --- subversion/tests/libsvn_subr/path-test.c (revision 21125) +++ subversion/tests/libsvn_subr/path-test.c (working copy) @@ -112,6 +112,11 @@ { "../foo", "..", "foo" }, { SVN_EMPTY_PATH, SVN_EMPTY_PATH, SVN_EMPTY_PATH }, { "/flu\\b/\\blarg", "/flu\\b", "\\blarg" }, + { "/", "/", "/" }, +#if defined(WIN32) + { "X:/", "X:/", "X:/" }, + { "X:/foo", "X:/", "foo" }, +#endif /* WIN32 */ }; *msg = "test svn_path_split"; @@ -453,6 +458,10 @@ { "abc", SVN_EMPTY_PATH, "abc" }, { SVN_EMPTY_PATH, "/abc", "/abc" }, { SVN_EMPTY_PATH, SVN_EMPTY_PATH, SVN_EMPTY_PATH }, +#if defined(WIN32) + { "X:/",SVN_EMPTY_PATH, "X:/" }, + { "X:/","abc", "X:/abc" }, +#endif /* WIN32 */ }; *msg = "test svn_path_join(_many)"; @@ -549,7 +558,11 @@ { "/b/a", "a" }, { "/b/a", "a" }, { "/", "/" }, - { SVN_EMPTY_PATH, SVN_EMPTY_PATH } + { SVN_EMPTY_PATH, SVN_EMPTY_PATH }, +#if defined(WIN32) + { "X:/", "X:/" }, + { "X:/abc", "abc" }, +#endif /* WIN32 */ }; *msg = "test svn_path_basename"; @@ -715,6 +728,10 @@ { "foo/bar", "foo" }, { "/foo/bar", "/foo" }, { "/foo", "/" }, +#if defined(WIN32) + { "X:/foo/bar", "X:/foo" }, + { "X:/foo", "X:/" }, +#endif /* WIN32 */ { NULL, NULL } }; int i; @@ -744,6 +761,58 @@ return SVN_NO_ERROR; } +static svn_error_t * +test_is_root(const char **msg, + svn_boolean_t msg_only, + svn_test_opts_t *opts, + apr_pool_t *pool) +{ + apr_size_t i; + + /* Paths to test. */ + static const char * const paths[] = { + "/foo/bar", + "/foo", + "/", +#if defined(WIN32) + "X:/foo", + "X:/", +#endif /* WIN32 */ + "", + }; + + /* Expected results of the tests. */ + static const svn_boolean_t retvals[] = { + FALSE, + FALSE, + TRUE, + FALSE, +#if defined(WIN32) + TRUE, + FALSE, +#endif /* WIN32 */ + }; + + *msg = "test svn_path_is_root"; + + if (msg_only) + return SVN_NO_ERROR; + + for (i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) + { + svn_boolean_t retval; + + retval = svn_path_is_root(paths[i], strlen(paths[i])); + if (retvals[i] != retval) + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "svn_path_is_root (%s) returned %s instead of %s", + paths[i], retval ? "TRUE" : "FALSE", retvals[i] ? "TRUE" : "FALSE"); + } + + return SVN_NO_ERROR; +} + /* The test table. */ @@ -763,5 +832,6 @@ SVN_TEST_PASS(test_decompose), SVN_TEST_PASS(test_canonicalize), SVN_TEST_PASS(test_remove_component), + SVN_TEST_PASS(test_is_root), SVN_TEST_NULL };