diff options
| -rw-r--r-- | manifest_xml.py | 4 | ||||
| -rwxr-xr-x | project.py | 146 | ||||
| -rw-r--r-- | tests/test_project.py | 204 |
3 files changed, 314 insertions, 40 deletions
diff --git a/manifest_xml.py b/manifest_xml.py index 69105c9e..4f7bd498 100644 --- a/manifest_xml.py +++ b/manifest_xml.py | |||
| @@ -1026,7 +1026,7 @@ class XmlManifest(object): | |||
| 1026 | # dest is relative to the top of the tree. | 1026 | # dest is relative to the top of the tree. |
| 1027 | # We only validate paths if we actually plan to process them. | 1027 | # We only validate paths if we actually plan to process them. |
| 1028 | self._ValidateFilePaths('copyfile', src, dest) | 1028 | self._ValidateFilePaths('copyfile', src, dest) |
| 1029 | project.AddCopyFile(src, dest, os.path.join(self.topdir, dest)) | 1029 | project.AddCopyFile(src, dest, self.topdir) |
| 1030 | 1030 | ||
| 1031 | def _ParseLinkFile(self, project, node): | 1031 | def _ParseLinkFile(self, project, node): |
| 1032 | src = self._reqatt(node, 'src') | 1032 | src = self._reqatt(node, 'src') |
| @@ -1036,7 +1036,7 @@ class XmlManifest(object): | |||
| 1036 | # dest is relative to the top of the tree. | 1036 | # dest is relative to the top of the tree. |
| 1037 | # We only validate paths if we actually plan to process them. | 1037 | # We only validate paths if we actually plan to process them. |
| 1038 | self._ValidateFilePaths('linkfile', src, dest) | 1038 | self._ValidateFilePaths('linkfile', src, dest) |
| 1039 | project.AddLinkFile(src, dest, os.path.join(self.topdir, dest)) | 1039 | project.AddLinkFile(src, dest, self.topdir) |
| 1040 | 1040 | ||
| 1041 | def _ParseAnnotation(self, project, node): | 1041 | def _ParseAnnotation(self, project, node): |
| 1042 | name = self._reqatt(node, 'name') | 1042 | name = self._reqatt(node, 'name') |
| @@ -36,7 +36,7 @@ from git_command import GitCommand, git_require | |||
| 36 | from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ | 36 | from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ |
| 37 | ID_RE | 37 | ID_RE |
| 38 | from error import GitError, HookError, UploadError, DownloadError | 38 | from error import GitError, HookError, UploadError, DownloadError |
| 39 | from error import ManifestInvalidRevisionError | 39 | from error import ManifestInvalidRevisionError, ManifestInvalidPathError |
| 40 | from error import NoManifestException | 40 | from error import NoManifestException |
| 41 | import platform_utils | 41 | import platform_utils |
| 42 | import progress | 42 | import progress |
| @@ -261,17 +261,70 @@ class _Annotation(object): | |||
| 261 | self.keep = keep | 261 | self.keep = keep |
| 262 | 262 | ||
| 263 | 263 | ||
| 264 | def _SafeExpandPath(base, subpath, skipfinal=False): | ||
| 265 | """Make sure |subpath| is completely safe under |base|. | ||
| 266 | |||
| 267 | We make sure no intermediate symlinks are traversed, and that the final path | ||
| 268 | is not a special file (e.g. not a socket or fifo). | ||
| 269 | |||
| 270 | NB: We rely on a number of paths already being filtered out while parsing the | ||
| 271 | manifest. See the validation logic in manifest_xml.py for more details. | ||
| 272 | """ | ||
| 273 | components = subpath.split(os.path.sep) | ||
| 274 | if skipfinal: | ||
| 275 | # Whether the caller handles the final component itself. | ||
| 276 | finalpart = components.pop() | ||
| 277 | |||
| 278 | path = base | ||
| 279 | for part in components: | ||
| 280 | if part in {'.', '..'}: | ||
| 281 | raise ManifestInvalidPathError( | ||
| 282 | '%s: "%s" not allowed in paths' % (subpath, part)) | ||
| 283 | |||
| 284 | path = os.path.join(path, part) | ||
| 285 | if platform_utils.islink(path): | ||
| 286 | raise ManifestInvalidPathError( | ||
| 287 | '%s: traversing symlinks not allow' % (path,)) | ||
| 288 | |||
| 289 | if os.path.exists(path): | ||
| 290 | if not os.path.isfile(path) and not platform_utils.isdir(path): | ||
| 291 | raise ManifestInvalidPathError( | ||
| 292 | '%s: only regular files & directories allowed' % (path,)) | ||
| 293 | |||
| 294 | if skipfinal: | ||
| 295 | path = os.path.join(path, finalpart) | ||
| 296 | |||
| 297 | return path | ||
| 298 | |||
| 299 | |||
| 264 | class _CopyFile(object): | 300 | class _CopyFile(object): |
| 301 | """Container for <copyfile> manifest element.""" | ||
| 302 | |||
| 303 | def __init__(self, git_worktree, src, topdir, dest): | ||
| 304 | """Register a <copyfile> request. | ||
| 265 | 305 | ||
| 266 | def __init__(self, src, dest, abssrc, absdest): | 306 | Args: |
| 307 | git_worktree: Absolute path to the git project checkout. | ||
| 308 | src: Relative path under |git_worktree| of file to read. | ||
| 309 | topdir: Absolute path to the top of the repo client checkout. | ||
| 310 | dest: Relative path under |topdir| of file to write. | ||
| 311 | """ | ||
| 312 | self.git_worktree = git_worktree | ||
| 313 | self.topdir = topdir | ||
| 267 | self.src = src | 314 | self.src = src |
| 268 | self.dest = dest | 315 | self.dest = dest |
| 269 | self.abs_src = abssrc | ||
| 270 | self.abs_dest = absdest | ||
| 271 | 316 | ||
| 272 | def _Copy(self): | 317 | def _Copy(self): |
| 273 | src = self.abs_src | 318 | src = _SafeExpandPath(self.git_worktree, self.src) |
| 274 | dest = self.abs_dest | 319 | dest = _SafeExpandPath(self.topdir, self.dest) |
| 320 | |||
| 321 | if platform_utils.isdir(src): | ||
| 322 | raise ManifestInvalidPathError( | ||
| 323 | '%s: copying from directory not supported' % (self.src,)) | ||
| 324 | if platform_utils.isdir(dest): | ||
| 325 | raise ManifestInvalidPathError( | ||
| 326 | '%s: copying to directory not allowed' % (self.dest,)) | ||
| 327 | |||
| 275 | # copy file if it does not exist or is out of date | 328 | # copy file if it does not exist or is out of date |
| 276 | if not os.path.exists(dest) or not filecmp.cmp(src, dest): | 329 | if not os.path.exists(dest) or not filecmp.cmp(src, dest): |
| 277 | try: | 330 | try: |
| @@ -292,13 +345,21 @@ class _CopyFile(object): | |||
| 292 | 345 | ||
| 293 | 346 | ||
| 294 | class _LinkFile(object): | 347 | class _LinkFile(object): |
| 348 | """Container for <linkfile> manifest element.""" | ||
| 295 | 349 | ||
| 296 | def __init__(self, git_worktree, src, dest, relsrc, absdest): | 350 | def __init__(self, git_worktree, src, topdir, dest): |
| 351 | """Register a <linkfile> request. | ||
| 352 | |||
| 353 | Args: | ||
| 354 | git_worktree: Absolute path to the git project checkout. | ||
| 355 | src: Target of symlink relative to path under |git_worktree|. | ||
| 356 | topdir: Absolute path to the top of the repo client checkout. | ||
| 357 | dest: Relative path under |topdir| of symlink to create. | ||
| 358 | """ | ||
| 297 | self.git_worktree = git_worktree | 359 | self.git_worktree = git_worktree |
| 360 | self.topdir = topdir | ||
| 298 | self.src = src | 361 | self.src = src |
| 299 | self.dest = dest | 362 | self.dest = dest |
| 300 | self.src_rel_to_dest = relsrc | ||
| 301 | self.abs_dest = absdest | ||
| 302 | 363 | ||
| 303 | def __linkIt(self, relSrc, absDest): | 364 | def __linkIt(self, relSrc, absDest): |
| 304 | # link file if it does not exist or is out of date | 365 | # link file if it does not exist or is out of date |
| @@ -316,35 +377,37 @@ class _LinkFile(object): | |||
| 316 | _error('Cannot link file %s to %s', relSrc, absDest) | 377 | _error('Cannot link file %s to %s', relSrc, absDest) |
| 317 | 378 | ||
| 318 | def _Link(self): | 379 | def _Link(self): |
| 319 | """Link the self.rel_src_to_dest and self.abs_dest. Handles wild cards | 380 | """Link the self.src & self.dest paths. |
| 320 | on the src linking all of the files in the source in to the destination | 381 | |
| 321 | directory. | 382 | Handles wild cards on the src linking all of the files in the source in to |
| 383 | the destination directory. | ||
| 322 | """ | 384 | """ |
| 323 | # We use the absSrc to handle the situation where the current directory | 385 | src = _SafeExpandPath(self.git_worktree, self.src) |
| 324 | # is not the root of the repo | 386 | |
| 325 | absSrc = os.path.join(self.git_worktree, self.src) | 387 | if os.path.exists(src): |
| 326 | if os.path.exists(absSrc): | 388 | # Entity exists so just a simple one to one link operation. |
| 327 | # Entity exists so just a simple one to one link operation | 389 | dest = _SafeExpandPath(self.topdir, self.dest, skipfinal=True) |
| 328 | self.__linkIt(self.src_rel_to_dest, self.abs_dest) | 390 | # dest & src are absolute paths at this point. Make sure the target of |
| 391 | # the symlink is relative in the context of the repo client checkout. | ||
| 392 | relpath = os.path.relpath(src, os.path.dirname(dest)) | ||
| 393 | self.__linkIt(relpath, dest) | ||
| 329 | else: | 394 | else: |
| 395 | dest = _SafeExpandPath(self.topdir, self.dest) | ||
| 330 | # Entity doesn't exist assume there is a wild card | 396 | # Entity doesn't exist assume there is a wild card |
| 331 | absDestDir = self.abs_dest | 397 | if os.path.exists(dest) and not platform_utils.isdir(dest): |
| 332 | if os.path.exists(absDestDir) and not platform_utils.isdir(absDestDir): | 398 | _error('Link error: src with wildcard, %s must be a directory', dest) |
| 333 | _error('Link error: src with wildcard, %s must be a directory', | ||
| 334 | absDestDir) | ||
| 335 | else: | 399 | else: |
| 336 | absSrcFiles = glob.glob(absSrc) | 400 | for absSrcFile in glob.glob(src): |
| 337 | for absSrcFile in absSrcFiles: | ||
| 338 | # Create a releative path from source dir to destination dir | 401 | # Create a releative path from source dir to destination dir |
| 339 | absSrcDir = os.path.dirname(absSrcFile) | 402 | absSrcDir = os.path.dirname(absSrcFile) |
| 340 | relSrcDir = os.path.relpath(absSrcDir, absDestDir) | 403 | relSrcDir = os.path.relpath(absSrcDir, dest) |
| 341 | 404 | ||
| 342 | # Get the source file name | 405 | # Get the source file name |
| 343 | srcFile = os.path.basename(absSrcFile) | 406 | srcFile = os.path.basename(absSrcFile) |
| 344 | 407 | ||
| 345 | # Now form the final full paths to srcFile. They will be | 408 | # Now form the final full paths to srcFile. They will be |
| 346 | # absolute for the desintaiton and relative for the srouce. | 409 | # absolute for the desintaiton and relative for the srouce. |
| 347 | absDest = os.path.join(absDestDir, srcFile) | 410 | absDest = os.path.join(dest, srcFile) |
| 348 | relSrc = os.path.join(relSrcDir, srcFile) | 411 | relSrc = os.path.join(relSrcDir, srcFile) |
| 349 | self.__linkIt(relSrc, absDest) | 412 | self.__linkIt(relSrc, absDest) |
| 350 | 413 | ||
| @@ -1712,18 +1775,25 @@ class Project(object): | |||
| 1712 | if submodules: | 1775 | if submodules: |
| 1713 | syncbuf.later1(self, _dosubmodules) | 1776 | syncbuf.later1(self, _dosubmodules) |
| 1714 | 1777 | ||
| 1715 | def AddCopyFile(self, src, dest, absdest): | 1778 | def AddCopyFile(self, src, dest, topdir): |
| 1716 | # dest should already be an absolute path, but src is project relative | 1779 | """Mark |src| for copying to |dest| (relative to |topdir|). |
| 1717 | # make src an absolute path | 1780 | |
| 1718 | abssrc = os.path.join(self.worktree, src) | 1781 | No filesystem changes occur here. Actual copying happens later on. |
| 1719 | self.copyfiles.append(_CopyFile(src, dest, abssrc, absdest)) | 1782 | |
| 1720 | 1783 | Paths should have basic validation run on them before being queued. | |
| 1721 | def AddLinkFile(self, src, dest, absdest): | 1784 | Further checking will be handled when the actual copy happens. |
| 1722 | # dest should already be an absolute path, but src is project relative | 1785 | """ |
| 1723 | # make src relative path to dest | 1786 | self.copyfiles.append(_CopyFile(self.worktree, src, topdir, dest)) |
| 1724 | absdestdir = os.path.dirname(absdest) | 1787 | |
| 1725 | relsrc = os.path.relpath(os.path.join(self.worktree, src), absdestdir) | 1788 | def AddLinkFile(self, src, dest, topdir): |
| 1726 | self.linkfiles.append(_LinkFile(self.worktree, src, dest, relsrc, absdest)) | 1789 | """Mark |dest| to create a symlink (relative to |topdir|) pointing to |src|. |
| 1790 | |||
| 1791 | No filesystem changes occur here. Actual linking happens later on. | ||
| 1792 | |||
| 1793 | Paths should have basic validation run on them before being queued. | ||
| 1794 | Further checking will be handled when the actual link happens. | ||
| 1795 | """ | ||
| 1796 | self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest)) | ||
| 1727 | 1797 | ||
| 1728 | def AddAnnotation(self, name, value, keep): | 1798 | def AddAnnotation(self, name, value, keep): |
| 1729 | self.annotations.append(_Annotation(name, value, keep)) | 1799 | self.annotations.append(_Annotation(name, value, keep)) |
diff --git a/tests/test_project.py b/tests/test_project.py index 77126dff..6d82da11 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -25,6 +25,7 @@ import subprocess | |||
| 25 | import tempfile | 25 | import tempfile |
| 26 | import unittest | 26 | import unittest |
| 27 | 27 | ||
| 28 | import error | ||
| 28 | import git_config | 29 | import git_config |
| 29 | import project | 30 | import project |
| 30 | 31 | ||
| @@ -134,3 +135,206 @@ class ReviewableBranchTests(unittest.TestCase): | |||
| 134 | self.assertFalse(rb.base_exists) | 135 | self.assertFalse(rb.base_exists) |
| 135 | # Hard to assert anything useful about this. | 136 | # Hard to assert anything useful about this. |
| 136 | self.assertTrue(rb.date) | 137 | self.assertTrue(rb.date) |
| 138 | |||
| 139 | |||
| 140 | class CopyLinkTestCase(unittest.TestCase): | ||
| 141 | """TestCase for stub repo client checkouts. | ||
| 142 | |||
| 143 | It'll have a layout like: | ||
| 144 | tempdir/ # self.tempdir | ||
| 145 | checkout/ # self.topdir | ||
| 146 | git-project/ # self.worktree | ||
| 147 | |||
| 148 | Attributes: | ||
| 149 | tempdir: A dedicated temporary directory. | ||
| 150 | worktree: The top of the repo client checkout. | ||
| 151 | topdir: The top of a project checkout. | ||
| 152 | """ | ||
| 153 | |||
| 154 | def setUp(self): | ||
| 155 | self.tempdir = tempfile.mkdtemp(prefix='repo_tests') | ||
| 156 | self.topdir = os.path.join(self.tempdir, 'checkout') | ||
| 157 | self.worktree = os.path.join(self.topdir, 'git-project') | ||
| 158 | os.makedirs(self.topdir) | ||
| 159 | os.makedirs(self.worktree) | ||
| 160 | |||
| 161 | def tearDown(self): | ||
| 162 | shutil.rmtree(self.tempdir, ignore_errors=True) | ||
| 163 | |||
| 164 | @staticmethod | ||
| 165 | def touch(path): | ||
| 166 | with open(path, 'w') as f: | ||
| 167 | pass | ||
| 168 | |||
| 169 | def assertExists(self, path, msg=None): | ||
| 170 | """Make sure |path| exists.""" | ||
| 171 | if os.path.exists(path): | ||
| 172 | return | ||
| 173 | |||
| 174 | if msg is None: | ||
| 175 | msg = ['path is missing: %s' % path] | ||
| 176 | while path != '/': | ||
| 177 | path = os.path.dirname(path) | ||
| 178 | if not path: | ||
| 179 | # If we're given something like "foo", abort once we get to "". | ||
| 180 | break | ||
| 181 | result = os.path.exists(path) | ||
| 182 | msg.append('\tos.path.exists(%s): %s' % (path, result)) | ||
| 183 | if result: | ||
| 184 | msg.append('\tcontents: %r' % os.listdir(path)) | ||
| 185 | break | ||
| 186 | msg = '\n'.join(msg) | ||
| 187 | |||
| 188 | raise self.failureException(msg) | ||
| 189 | |||
| 190 | |||
| 191 | class CopyFile(CopyLinkTestCase): | ||
| 192 | """Check _CopyFile handling.""" | ||
| 193 | |||
| 194 | def CopyFile(self, src, dest): | ||
| 195 | return project._CopyFile(self.worktree, src, self.topdir, dest) | ||
| 196 | |||
| 197 | def test_basic(self): | ||
| 198 | """Basic test of copying a file from a project to the toplevel.""" | ||
| 199 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 200 | self.touch(src) | ||
| 201 | cf = self.CopyFile('foo.txt', 'foo') | ||
| 202 | cf._Copy() | ||
| 203 | self.assertExists(os.path.join(self.topdir, 'foo')) | ||
| 204 | |||
| 205 | def test_src_subdir(self): | ||
| 206 | """Copy a file from a subdir of a project.""" | ||
| 207 | src = os.path.join(self.worktree, 'bar', 'foo.txt') | ||
| 208 | os.makedirs(os.path.dirname(src)) | ||
| 209 | self.touch(src) | ||
| 210 | cf = self.CopyFile('bar/foo.txt', 'new.txt') | ||
| 211 | cf._Copy() | ||
| 212 | self.assertExists(os.path.join(self.topdir, 'new.txt')) | ||
| 213 | |||
| 214 | def test_dest_subdir(self): | ||
| 215 | """Copy a file to a subdir of a checkout.""" | ||
| 216 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 217 | self.touch(src) | ||
| 218 | cf = self.CopyFile('foo.txt', 'sub/dir/new.txt') | ||
| 219 | self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub'))) | ||
| 220 | cf._Copy() | ||
| 221 | self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt')) | ||
| 222 | |||
| 223 | def test_update(self): | ||
| 224 | """Make sure changed files get copied again.""" | ||
| 225 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 226 | dest = os.path.join(self.topdir, 'bar') | ||
| 227 | with open(src, 'w') as f: | ||
| 228 | f.write('1st') | ||
| 229 | cf = self.CopyFile('foo.txt', 'bar') | ||
| 230 | cf._Copy() | ||
| 231 | self.assertExists(dest) | ||
| 232 | with open(dest) as f: | ||
| 233 | self.assertEqual(f.read(), '1st') | ||
| 234 | |||
| 235 | with open(src, 'w') as f: | ||
| 236 | f.write('2nd!') | ||
| 237 | cf._Copy() | ||
| 238 | with open(dest) as f: | ||
| 239 | self.assertEqual(f.read(), '2nd!') | ||
| 240 | |||
| 241 | def test_src_block_symlink(self): | ||
| 242 | """Do not allow reading from a symlinked path.""" | ||
| 243 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 244 | sym = os.path.join(self.worktree, 'sym') | ||
| 245 | self.touch(src) | ||
| 246 | os.symlink('foo.txt', sym) | ||
| 247 | self.assertExists(sym) | ||
| 248 | cf = self.CopyFile('sym', 'foo') | ||
| 249 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 250 | |||
| 251 | def test_src_block_symlink_traversal(self): | ||
| 252 | """Do not allow reading through a symlink dir.""" | ||
| 253 | src = os.path.join(self.worktree, 'bar', 'passwd') | ||
| 254 | os.symlink('/etc', os.path.join(self.worktree, 'bar')) | ||
| 255 | self.assertExists(src) | ||
| 256 | cf = self.CopyFile('bar/foo.txt', 'foo') | ||
| 257 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 258 | |||
| 259 | def test_src_block_dir(self): | ||
| 260 | """Do not allow copying from a directory.""" | ||
| 261 | src = os.path.join(self.worktree, 'dir') | ||
| 262 | os.makedirs(src) | ||
| 263 | cf = self.CopyFile('dir', 'foo') | ||
| 264 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 265 | |||
| 266 | def test_dest_block_symlink(self): | ||
| 267 | """Do not allow writing to a symlink.""" | ||
| 268 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 269 | self.touch(src) | ||
| 270 | os.symlink('dest', os.path.join(self.topdir, 'sym')) | ||
| 271 | cf = self.CopyFile('foo.txt', 'sym') | ||
| 272 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 273 | |||
| 274 | def test_dest_block_symlink_traversal(self): | ||
| 275 | """Do not allow writing through a symlink dir.""" | ||
| 276 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 277 | self.touch(src) | ||
| 278 | os.symlink('/tmp', os.path.join(self.topdir, 'sym')) | ||
| 279 | cf = self.CopyFile('foo.txt', 'sym/foo.txt') | ||
| 280 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 281 | |||
| 282 | def test_src_block_dir(self): | ||
| 283 | """Do not allow copying to a directory.""" | ||
| 284 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 285 | self.touch(src) | ||
| 286 | os.makedirs(os.path.join(self.topdir, 'dir')) | ||
| 287 | cf = self.CopyFile('foo.txt', 'dir') | ||
| 288 | self.assertRaises(error.ManifestInvalidPathError, cf._Copy) | ||
| 289 | |||
| 290 | |||
| 291 | class LinkFile(CopyLinkTestCase): | ||
| 292 | """Check _LinkFile handling.""" | ||
| 293 | |||
| 294 | def LinkFile(self, src, dest): | ||
| 295 | return project._LinkFile(self.worktree, src, self.topdir, dest) | ||
| 296 | |||
| 297 | def test_basic(self): | ||
| 298 | """Basic test of linking a file from a project into the toplevel.""" | ||
| 299 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 300 | self.touch(src) | ||
| 301 | lf = self.LinkFile('foo.txt', 'foo') | ||
| 302 | lf._Link() | ||
| 303 | dest = os.path.join(self.topdir, 'foo') | ||
| 304 | self.assertExists(dest) | ||
| 305 | self.assertTrue(os.path.islink(dest)) | ||
| 306 | self.assertEqual('git-project/foo.txt', os.readlink(dest)) | ||
| 307 | |||
| 308 | def test_src_subdir(self): | ||
| 309 | """Link to a file in a subdir of a project.""" | ||
| 310 | src = os.path.join(self.worktree, 'bar', 'foo.txt') | ||
| 311 | os.makedirs(os.path.dirname(src)) | ||
| 312 | self.touch(src) | ||
| 313 | lf = self.LinkFile('bar/foo.txt', 'foo') | ||
| 314 | lf._Link() | ||
| 315 | self.assertExists(os.path.join(self.topdir, 'foo')) | ||
| 316 | |||
| 317 | def test_dest_subdir(self): | ||
| 318 | """Link a file to a subdir of a checkout.""" | ||
| 319 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 320 | self.touch(src) | ||
| 321 | lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar') | ||
| 322 | self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub'))) | ||
| 323 | lf._Link() | ||
| 324 | self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar')) | ||
| 325 | |||
| 326 | def test_update(self): | ||
| 327 | """Make sure changed targets get updated.""" | ||
| 328 | dest = os.path.join(self.topdir, 'sym') | ||
| 329 | |||
| 330 | src = os.path.join(self.worktree, 'foo.txt') | ||
| 331 | self.touch(src) | ||
| 332 | lf = self.LinkFile('foo.txt', 'sym') | ||
| 333 | lf._Link() | ||
| 334 | self.assertEqual('git-project/foo.txt', os.readlink(dest)) | ||
| 335 | |||
| 336 | # Point the symlink somewhere else. | ||
| 337 | os.unlink(dest) | ||
| 338 | os.symlink('/', dest) | ||
| 339 | lf._Link() | ||
| 340 | self.assertEqual('git-project/foo.txt', os.readlink(dest)) | ||
