diff --git a/src/python/pants/goal/BUILD b/src/python/pants/goal/BUILD index 65d41317db1..6c7c5d0aec6 100644 --- a/src/python/pants/goal/BUILD +++ b/src/python/pants/goal/BUILD @@ -32,7 +32,6 @@ python_library( sources = ['context.py'], dependencies = [ ':products', - ':workspace', 'src/python/pants/base:build_environment', 'src/python/pants/build_graph', 'src/python/pants/base:deprecated', @@ -98,12 +97,3 @@ python_library( 'src/python/pants/util:memo', ], ) - -python_library( - name = 'workspace', - sources = ['workspace.py'], - dependencies = [ - 'src/python/pants/base:build_environment', - 'src/python/pants/scm', - ], -) diff --git a/src/python/pants/goal/context.py b/src/python/pants/goal/context.py index a565df6c091..ed8af2001d9 100644 --- a/src/python/pants/goal/context.py +++ b/src/python/pants/goal/context.py @@ -18,7 +18,6 @@ ) from pants.goal.products import Products from pants.goal.run_tracker import RunTracker -from pants.goal.workspace import ScmWorkspace from pants.option.options import Options from pants.process.lock import OwnerPrintingInterProcessFileLock from pants.source.source_root import SourceRootConfig @@ -77,7 +76,7 @@ def __init__( self.requested_goals = requested_goals or [] self._console_outstream = console_outstream or sys.stdout.buffer self._scm = scm or get_scm() - self._workspace = workspace or (ScmWorkspace(self._scm) if self._scm else None) + self._workspace = workspace self._replace_targets(target_roots) self._invalidation_report = invalidation_report self._scheduler = scheduler diff --git a/src/python/pants/goal/workspace.py b/src/python/pants/goal/workspace.py deleted file mode 100644 index 5db37934e64..00000000000 --- a/src/python/pants/goal/workspace.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -from abc import ABC, abstractmethod - -from pants.base.build_environment import get_buildroot -from pants.scm.scm import Scm - - -class Workspace(ABC): - """Tracks the state of the current workspace.""" - - class WorkspaceError(Exception): - """Indicates a problem reading the local workspace.""" - - @abstractmethod - def touched_files(self, parent): - """Returns the paths modified between the parent state and the current workspace state.""" - - @abstractmethod - def changes_in(self, rev_or_range): - """Returns the paths modified by some revision, revision range or other identifier.""" - - -class ScmWorkspace(Workspace): - """A workspace that uses an Scm to determine the touched files. - - :API: public - """ - - def __init__(self, scm): - """ - :API: public - """ - super().__init__() - - if scm is None: - raise self.WorkspaceError( - "Cannot figure out what changed without a configured " "source-control system." - ) - self._scm = scm - - def touched_files(self, parent): - """ - :API: public - """ - try: - return self._scm.changed_files( - from_commit=parent, include_untracked=True, relative_to=get_buildroot() - ) - except Scm.ScmException as e: - raise self.WorkspaceError("Problem detecting changed files.", e) - - def changes_in(self, rev_or_range): - """ - :API: public - """ - try: - return self._scm.changes_in(rev_or_range, relative_to=get_buildroot()) - except Scm.ScmException as e: - raise self.WorkspaceError("Problem detecting changes in {}.".format(rev_or_range), e) diff --git a/src/python/pants/scm/BUILD b/src/python/pants/scm/BUILD index 4204bd7f809..5833d3df2a5 100644 --- a/src/python/pants/scm/BUILD +++ b/src/python/pants/scm/BUILD @@ -4,8 +4,6 @@ python_library( dependencies = [ 'src/python/pants/util:contextutil', - 'src/python/pants/util:memo', - 'src/python/pants/util:strutil', ], ) diff --git a/src/python/pants/scm/git.py b/src/python/pants/scm/git.py index b533eb66b1f..a7d3490b4c5 100644 --- a/src/python/pants/scm/git.py +++ b/src/python/pants/scm/git.py @@ -1,18 +1,12 @@ # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -import binascii -import io import logging import os import subprocess -import traceback -from contextlib import contextmanager from pants.scm.scm import Scm from pants.util.contextutil import pushd -from pants.util.memo import memoized_method -from pants.util.strutil import ensure_text # 40 is Linux's hard-coded limit for total symlinks followed when resolving a path. MAX_SYMLINKS_IN_REALPATH = 40 @@ -53,19 +47,6 @@ def detect_worktree(cls, binary="git", subdir=None): return None return cls._cleanse(out) - @classmethod - def clone(cls, repo_url, dest, binary="git"): - """Clone the repo at repo_url into dest. - - :param string binary: The path to the git binary to use, 'git' by default. - :returns: an instance of this class representing the cloned repo. - :rtype: Git - """ - cmd = [binary, "clone", repo_url, dest] - process, out = cls._invoke(cmd) - cls._check_result(cmd, process.returncode) - return cls(binary=binary, worktree=dest) - @classmethod def _invoke(cls, cmd, stderr=None): """Invoke the given command, and return a tuple of process and raw binary output. @@ -109,7 +90,6 @@ def __init__(self, binary="git", gitdir=None, worktree=None, remote=None, branch branch: The default remote branch to use. """ super().__init__() - self._gitcmd = binary self._worktree = os.path.realpath(worktree or os.getcwd()) self._gitdir = os.path.realpath(gitdir) if gitdir else os.path.join(self._worktree, ".git") @@ -128,31 +108,6 @@ def worktree(self): def commit_id(self): return self._check_output(["rev-parse", "HEAD"], raise_type=Scm.LocalException) - @property - def server_url(self): - git_output = self._check_output(["remote", "--verbose"], raise_type=Scm.LocalException) - - def origin_urls(): - for line in git_output.splitlines(): - name, url, action = line.split() - if name == "origin" and action == "(push)": - yield url - - origins = list(origin_urls()) - if len(origins) != 1: - raise Scm.LocalException( - "Unable to find remote named 'origin' that accepts pushes " - f"amongst:\n{git_output}" - ) - return origins[0] - - @property - def tag_name(self): - # Calls to git describe can have bad performance on large repos. Be aware - # of the performance hit if you use this property. - tag = self._check_output(["describe", "--tags", "--always"], raise_type=Scm.LocalException) - return None if "cannot" in tag else tag - @property def branch_name(self): branch = self._check_output( @@ -195,65 +150,6 @@ def changes_in(self, diffspec, relative_to=None): files = self._check_output(cmd, raise_type=Scm.LocalException).split() return {self.fix_git_relative_path(f.strip(), relative_to) for f in files} - def changelog(self, from_commit=None, files=None): - # We force the log output encoding to be UTF-8 here since the user may have a git config that - # overrides the git UTF-8 default log output encoding. - args = [ - "log", - "--encoding=UTF-8", - "--no-merges", - "--stat", - "--find-renames", - "--find-copies", - ] - if from_commit: - args.append(from_commit + "..HEAD") - if files: - args.append("--") - args.extend(files) - - # There are various circumstances that can lead to git logs that are not transcodeable to utf-8, - # for example: http://comments.gmane.org/gmane.comp.version-control.git/262685 - # Git will not error in these cases and we do not wish to either. Here we direct byte sequences - # that can not be utf-8 decoded to be replaced with the utf-8 replacement character. - return self._check_output(args, raise_type=Scm.LocalException, errors="replace") - - def merge_base(self, left="master", right="HEAD"): - """Returns the merge-base of master and HEAD in bash: `git merge-base left right`""" - return self._check_output(["merge-base", left, right], raise_type=Scm.LocalException) - - def refresh(self, leave_clean=False): - """Attempt to pull-with-rebase from upstream. - - This is implemented as fetch-plus-rebase so that we can distinguish between errors in the - fetch stage (likely network errors) and errors in the rebase stage (conflicts). If - leave_clean is true, then in the event of a rebase failure, the branch will be rolled back. - Otherwise, it will be left in the conflicted state. - """ - remote, merge = self._get_upstream() - self._check_call(["fetch", "--tags", remote, merge], raise_type=Scm.RemoteException) - try: - self._check_call(["rebase", "FETCH_HEAD"], raise_type=Scm.LocalException) - except Scm.LocalException as e: - if leave_clean: - logger.debug("Cleaning up after failed rebase") - try: - self._check_call(["rebase", "--abort"], raise_type=Scm.LocalException) - except Scm.LocalException as abort_exc: - logger.debug("Failed to up after failed rebase") - logger.debug(traceback.format_exc(abort_exc)) - # But let the original exception propagate, since that's the more interesting one - raise e - - def tag(self, name, message=None): - # We use -a here instead of --annotate to maintain maximum git compatibility. - # --annotate was only introduced in 1.7.8 via: - # https://github.com/git/git/commit/c97eff5a95d57a9561b7c7429e7fcc5d0e3a7f5d - self._check_call( - ["tag", "-a", "--message=" + (message or ""), name], raise_type=Scm.LocalException - ) - self.push("refs/tags/" + name) - def commit(self, message, verify=True): cmd = ["commit", "--all", "--message=" + message] if not verify: @@ -263,35 +159,6 @@ def commit(self, message, verify=True): def add(self, *paths): self._check_call(["add"] + list(paths), raise_type=Scm.LocalException) - def commit_date(self, commit_reference): - return self._check_output( - ["log", "-1", "--pretty=tformat:%ci", commit_reference], raise_type=Scm.LocalException - ) - - def push(self, *refs): - remote, merge = self._get_upstream() - self._check_call(["push", remote, merge] + list(refs), raise_type=Scm.RemoteException) - - def set_state(self, rev): - self._check_call(["checkout", rev]) - - def _get_upstream(self): - """Return the remote and remote merge branch for the current branch.""" - if not self._remote or not self._branch: - branch = self.branch_name - if not branch: - raise Scm.LocalException("Failed to determine local branch") - - def get_local_config(key): - value = self._check_output( - ["config", "--local", "--get", key], raise_type=Scm.LocalException - ) - return value.strip() - - self._remote = self._remote or get_local_config(f"branch.{branch}.remote") - self._branch = self._branch or get_local_config(f"branch.{branch}.merge") - return self._remote, self._branch - def _check_call(self, args, failure_msg=None, raise_type=None): cmd = self._create_git_cmdline(args) self._log_call(cmd) @@ -312,340 +179,3 @@ def _create_git_cmdline(self, args): def _log_call(self, cmd): logger.debug("Executing: " + " ".join(cmd)) - - def repo_reader(self, rev): - return GitRepositoryReader(self, rev) - - -class GitRepositoryReader: - """Allows reading from files and directory information from an arbitrary git commit. - - This is useful for pants-aware git sparse checkouts. - """ - - def __init__(self, scm, rev): - self.scm = scm - self.rev = rev - self._cat_file_process = None - # Trees is a dict from path to [list of Dir, Symlink or File objects] - self._trees = {} - self._realpath_cache = {".": "./", "": "./"} - - def _maybe_start_cat_file_process(self): - if not self._cat_file_process: - cmdline = self.scm._create_git_cmdline(["cat-file", "--batch"]) - self._cat_file_process = subprocess.Popen( - cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - - class MissingFileException(Exception): - def __init__(self, rev, relpath): - self.relpath = relpath - self.rev = rev - - def __str__(self): - return f"MissingFileException({self.relpath}, {self.rev})" - - class IsDirException(Exception): - def __init__(self, rev, relpath): - self.relpath = relpath - self.rev = rev - - def __str__(self): - return f"IsDirException({self.relpath}, {self.rev})" - - class NotADirException(Exception): - def __init__(self, rev, relpath): - self.relpath = relpath - self.rev = rev - - def __str__(self): - return f"NotADirException({self.relpath}, {self.rev})" - - class SymlinkLoopException(Exception): - def __init__(self, rev, relpath): - self.relpath = relpath - self.rev = rev - - def __str__(self): - return f"SymlinkLoop({self.relpath}, {self.rev})" - - class ExternalSymlinkException(Exception): - def __init__(self, rev, relpath): - self.relpath = relpath - self.rev = rev - - def __str__(self): - return f"ExternalSymlink({self.relpath}, {self.rev})" - - class GitDiedException(Exception): - pass - - class UnexpectedGitObjectTypeException(Exception): - # Programmer error - pass - - def _safe_realpath(self, relpath): - try: - return self._realpath(relpath) - except self.MissingFileException: - return None - except self.NotADirException: - return None - - def _safe_read_object(self, relpath, max_symlinks): - try: - return self._read_object(relpath, max_symlinks) - except self.MissingFileException: - return None, relpath - except self.NotADirException: - return None, relpath - - def exists(self, relpath): - path = self._safe_realpath(relpath) - return bool(path) - - def isfile(self, relpath): - path = self._safe_realpath(relpath) - if path: - return not path.endswith("/") - return False - - def isdir(self, relpath): - path = self._safe_realpath(relpath) - if path: - return path.endswith("/") - return False - - def lstat(self, relpath): - obj, _ = self._safe_read_object(relpath, max_symlinks=0) - return obj - - def readlink(self, relpath): - # TODO: Relatively inefficient, but easier than changing read_object, unfortunately. - if type(self.lstat(relpath)) != self.Symlink: - return None - obj, path_so_far = self._safe_read_object(relpath, max_symlinks=1) - if obj is None: - return None - return path_so_far - - class Symlink: - def __init__(self, name, sha): - self.name = name - self.sha = sha - - class Dir: - def __init__(self, name, sha): - self.name = name - self.sha = sha - - class File: - def __init__(self, name, sha): - self.name = name - self.sha = sha - - def listdir(self, relpath): - """Like os.listdir, but reads from the git repository. - - :returns: a list of relative filenames - """ - - path = self._realpath(relpath) - if not path.endswith("/"): - raise self.NotADirException(self.rev, relpath) - - if path[0] == "/" or path.startswith("../"): - return os.listdir(path) - - tree = self._read_tree(path[:-1]) - return list(tree.keys()) - - @contextmanager - def open(self, relpath): - """Read a file out of the repository at a certain revision. - - This is complicated because, unlike vanilla git cat-file, this follows symlinks in the repo. - If a symlink points outside repo, the file is read from the filesystem; that's because - presumably whoever put that symlink there knew what they were doing. - """ - - path = self._realpath(relpath) - if path.endswith("/"): - raise self.IsDirException(self.rev, relpath) - - if path.startswith("../") or path[0] == "/": - yield open(path, "rb") - return - - object_type, data = self._read_object_from_repo(rev=self.rev, relpath=path) - if object_type == b"tree": - raise self.IsDirException(self.rev, relpath) - assert object_type == b"blob" - yield io.BytesIO(data) - - @memoized_method - def _realpath(self, relpath): - """Follow symlinks to find the real path to a file or directory in the repo. - - :returns: if the expanded path points to a file, the relative path - to that file; if a directory, the relative path + '/'; if - a symlink outside the repo, a path starting with / or ../. - """ - obj, path_so_far = self._read_object(relpath, MAX_SYMLINKS_IN_REALPATH) - if isinstance(obj, self.Symlink): - raise self.SymlinkLoopException(self.rev, relpath) - return path_so_far - - def _read_object(self, relpath, max_symlinks): - path_so_far = "" - components = list(relpath.split(os.path.sep)) - symlinks = 0 - - # Consume components to build path_so_far - while components: - component = components.pop(0) - if component == "" or component == ".": - continue - - parent_tree = self._read_tree(path_so_far) - parent_path = path_so_far - - if path_so_far != "": - path_so_far += "/" - path_so_far += component - - try: - obj = parent_tree[component.encode()] - except KeyError: - raise self.MissingFileException(self.rev, relpath) - - if isinstance(obj, self.File): - if components: - # We've encountered a file while searching for a directory - raise self.NotADirException(self.rev, relpath) - else: - return obj, path_so_far - elif isinstance(obj, self.Dir): - if not components: - return obj, path_so_far + "/" - # A dir is OK; we just descend from here - elif isinstance(obj, self.Symlink): - symlinks += 1 - if symlinks > max_symlinks: - return obj, path_so_far - # A git symlink is stored as a blob containing the name of the target. - # Read that blob. - object_type, path_data = self._read_object_from_repo(sha=obj.sha) - assert object_type == b"blob" - - if path_data[0] == b"/": - # Is absolute, thus likely points outside the repo. - raise self.ExternalSymlinkException(self.rev, relpath) - - link_to = os.path.normpath(os.path.join(parent_path, path_data.decode())) - if link_to.startswith("../") or link_to[0] == "/": - # Points outside the repo. - raise self.ExternalSymlinkException(self.rev, relpath) - - # Restart our search at the top with the new path. - # Git stores symlinks in terms of Unix paths, so split on '/' instead of os.path.sep - components = link_to.split("/") + components - path_so_far = "" - else: - # Programmer error - raise self.UnexpectedGitObjectTypeException() - return self.Dir("./", None), "./" - - def _fixup_dot_relative(self, path): - """Git doesn't understand dot-relative paths.""" - if path.startswith("./"): - return path[2:] - elif path == ".": - return "" - return path - - def _read_tree(self, path): - """Given a revision and path, parse the tree data out of git cat-file output. - - :returns: a dict from filename -> [list of Symlink, Dir, and File objects] - """ - - path = self._fixup_dot_relative(path) - - tree = self._trees.get(path) - if tree: - return tree - tree = {} - object_type, tree_data = self._read_object_from_repo(rev=self.rev, relpath=path) - assert object_type == b"tree" - # The tree data here is (mode ' ' filename \0 20-byte-sha)* - # It's transformed to a list of byte chars to allow iteration. - # See http://python-future.org/compatible_idioms.html#byte-string-literals. - tree_data = [bytes([b]) for b in tree_data] - i = 0 - while i < len(tree_data): - start = i - while tree_data[i] != b" ": - i += 1 - mode = b"".join(tree_data[start:i]) - i += 1 # skip space - start = i - while tree_data[i] != NUL: - i += 1 - name = b"".join(tree_data[start:i]) - sha = b"".join(tree_data[i + 1 : i + 1 + GIT_HASH_LENGTH]) - sha_hex = binascii.hexlify(sha) - i += 1 + GIT_HASH_LENGTH - if mode == b"120000": - tree[name] = self.Symlink(name, sha_hex) - elif mode == b"40000": - tree[name] = self.Dir(name, sha_hex) - else: - tree[name] = self.File(name, sha_hex) - self._trees[path] = tree - return tree - - def _read_object_from_repo(self, rev=None, relpath=None, sha=None): - """Read an object from the git repo. - - This is implemented via a pipe to git cat-file --batch - """ - if sha: - spec = sha + b"\n" - else: - assert rev is not None - assert relpath is not None - rev = ensure_text(rev) - relpath = ensure_text(relpath) - relpath = self._fixup_dot_relative(relpath) - spec = f"{rev}:{relpath}\n".encode() - - self._maybe_start_cat_file_process() - self._cat_file_process.stdin.write(spec) - self._cat_file_process.stdin.flush() - header = None - while not header: - header = self._cat_file_process.stdout.readline() - if self._cat_file_process.poll() is not None: - raise self.GitDiedException(f"Git cat-file died while trying to read '{spec}'.") - - header = header.rstrip() - parts = header.rsplit(SPACE, 2) - if len(parts) == 2: - assert parts[1] == b"missing" - raise self.MissingFileException(rev, relpath) - - _, object_type, object_len = parts - - # Read the object data - blob = bytes(self._cat_file_process.stdout.read(int(object_len))) - - # Read the trailing newline - assert self._cat_file_process.stdout.read(1) == b"\n" - assert len(blob) == int(object_len) - return object_type, blob - - def __del__(self): - if self._cat_file_process: - self._cat_file_process.communicate() diff --git a/src/python/pants/scm/git_test.py b/src/python/pants/scm/git_test.py index 9a467ac4b47..fb6ca3b2a24 100644 --- a/src/python/pants/scm/git_test.py +++ b/src/python/pants/scm/git_test.py @@ -5,11 +5,9 @@ import subprocess import unittest from contextlib import contextmanager -from textwrap import dedent from unittest.case import skipIf from pants.scm.git import Git -from pants.scm.scm import Scm from pants.testutil.git_util import MIN_REQUIRED_GIT_VERSION, git_version from pants.util.contextutil import environment_as, pushd, temporary_dir from pants.util.dirutil import chmod_plus_x, safe_mkdir, safe_mkdtemp, safe_open, safe_rmtree, touch @@ -85,166 +83,18 @@ def setUp(self): self.git = Git(gitdir=self.gitdir, worktree=self.worktree) - @contextmanager - def mkremote(self, remote_name): - with temporary_dir() as remote_uri: - subprocess.check_call(["git", "remote", "add", remote_name, remote_uri]) - try: - yield remote_uri - finally: - subprocess.check_call(["git", "remote", "remove", remote_name]) - def tearDown(self): safe_rmtree(self.origin) safe_rmtree(self.gitdir) safe_rmtree(self.worktree) safe_rmtree(self.clone2) - def test_listdir(self): - reader = self.git.repo_reader(self.initial_rev) - - for dirname in ".", "./.": - results = reader.listdir(dirname) - self.assertEqual( - [b"README", b"dir", b"link-to-dir", b"loop1", b"loop2", b"not-a-dir"], - sorted(results), - ) - - for dirname in "dir", "./dir": - results = reader.listdir(dirname) - self.assertEqual( - [ - b"f", - "not-absolute\u2764".encode(), - b"relative-dotdot", - b"relative-nonexistent", - b"relative-symlink", - ], - sorted(results), - ) - - results = reader.listdir("link-to-dir") - self.assertEqual( - [ - b"f", - "not-absolute\u2764".encode(), - b"relative-dotdot", - b"relative-nonexistent", - b"relative-symlink", - ], - sorted(results), - ) - - with self.assertRaises(reader.MissingFileException): - with reader.listdir("bogus"): - pass - - def test_lstat(self): - reader = self.git.repo_reader(self.initial_rev) - - def lstat(*components): - return type(reader.lstat(os.path.join(*components))) - - self.assertEqual(reader.Symlink, lstat("dir", "relative-symlink")) - self.assertEqual(reader.Symlink, lstat("not-a-dir")) - self.assertEqual(reader.File, lstat("README")) - self.assertEqual(reader.Dir, lstat("dir")) - self.assertEqual(type(None), lstat("nope-not-here")) - - def test_readlink(self): - reader = self.git.repo_reader(self.initial_rev) - - def readlink(*components): - return reader.readlink(os.path.join(*components)) - - self.assertEqual("dir/f", readlink("dir", "relative-symlink")) - self.assertEqual(None, readlink("not-a-dir")) - self.assertEqual(None, readlink("README")) - self.assertEqual(None, readlink("dir")) - self.assertEqual(None, readlink("nope-not-here")) - - def test_open(self): - reader = self.git.repo_reader(self.initial_rev) - - with reader.open("README") as f: - self.assertEqual(b"", f.read()) - - with reader.open("dir/f") as f: - self.assertEqual(b"file in subdir", f.read()) - - with self.assertRaises(reader.MissingFileException): - with reader.open("no-such-file") as f: - self.assertEqual(b"", f.read()) - - with self.assertRaises(reader.MissingFileException): - with reader.open("dir/no-such-file") as f: - pass - - with self.assertRaises(reader.IsDirException): - with reader.open("dir") as f: - self.assertEqual(b"", f.read()) - - current_reader = self.git.repo_reader(self.current_rev) - - with current_reader.open("README") as f: - self.assertEqual("Hello World.\u2764".encode(), f.read()) - - with current_reader.open("link-to-dir/f") as f: - self.assertEqual(b"file in subdir", f.read()) - - with current_reader.open("dir/relative-symlink") as f: - self.assertEqual(b"file in subdir", f.read()) - - with self.assertRaises(current_reader.SymlinkLoopException): - with current_reader.open("loop1") as f: - pass - - with self.assertRaises(current_reader.MissingFileException): - with current_reader.open("dir/relative-nonexistent") as f: - pass - - with self.assertRaises(current_reader.NotADirException): - with current_reader.open("not-a-dir") as f: - pass - - with self.assertRaises(current_reader.MissingFileException): - with current_reader.open("dir/not-absolute\u2764") as f: - pass - - with self.assertRaises(current_reader.MissingFileException): - with current_reader.open("dir/relative-nonexistent") as f: - pass - - with current_reader.open("dir/relative-dotdot") as f: - self.assertEqual("Hello World.\u2764".encode(), f.read()) - def test_integration(self): self.assertEqual(set(), self.git.changed_files()) self.assertEqual({"README"}, self.git.changed_files(from_commit="HEAD^")) tip_sha = self.git.commit_id self.assertTrue(tip_sha) - - self.assertTrue(tip_sha in self.git.changelog()) - - merge_base = self.git.merge_base() - self.assertTrue(merge_base) - - self.assertTrue(merge_base in self.git.changelog()) - - with self.assertRaises(Scm.LocalException): - self.git.server_url - - with environment_as(GIT_DIR=self.gitdir, GIT_WORK_TREE=self.worktree): - with self.mkremote("origin") as origin_uri: - # We shouldn't be fooled by remotes with origin in their name. - with self.mkremote("temp_origin"): - origin_url = self.git.server_url - self.assertEqual(origin_url, origin_uri) - - self.assertTrue( - self.git.tag_name.startswith("first-"), msg="un-annotated tags should be found" - ) self.assertEqual("master", self.git.branch_name) def edit_readme(): @@ -260,43 +110,6 @@ def edit_readme(): # Confirm that files outside of a given relative_to path are ignored self.assertEqual(set(), self.git.changed_files(relative_to="non-existent")) - self.git.commit("API Changes.") - try: - # These changes should be rejected because our branch point from origin is 1 commit behind - # the changes pushed there in clone 2. - self.git.push() - except Scm.RemoteException: - with environment_as(GIT_DIR=self.gitdir, GIT_WORK_TREE=self.worktree): - subprocess.check_call(["git", "reset", "--hard", "depot/master"]) - self.git.refresh() - edit_readme() - - self.git.commit("""API '"' " Changes.""") - self.git.push() - # HEAD is merged into master - self.assertEqual(self.git.commit_date(self.git.merge_base()), self.git.commit_date("HEAD")) - self.assertEqual(self.git.commit_date("HEAD"), self.git.commit_date("HEAD")) - self.git.tag("second", message="""Tagged ' " Changes""") - - with temporary_dir() as clone: - with pushd(clone): - self.init_repo("origin", self.origin) - subprocess.check_call(["git", "pull", "--tags", "origin", "master:master"]) - - with open(os.path.realpath("README"), "r") as readme: - self.assertEqual("--More data.", readme.read()) - - git = Git() - - # Check that we can pick up committed and uncommitted changes. - with safe_open(os.path.realpath("CHANGES"), "w") as changes: - changes.write("none") - subprocess.check_call(["git", "add", "CHANGES"]) - self.assertEqual({"README", "CHANGES"}, git.changed_files(from_commit="first")) - - self.assertEqual("master", git.branch_name) - self.assertEqual("second", git.tag_name, msg="annotated tags should be found") - def test_detect_worktree(self): with temporary_dir() as _clone: with pushd(_clone): @@ -402,112 +215,6 @@ def commit_contents_to_files(content, *files): self.assertEqual({"foo", "bar", "baz"}, self.git.changes_in(f"{c1}..HEAD")) self.assertEqual({"foo", "bar", "baz"}, self.git.changes_in(f"{c1}..{c4}")) - def test_changelog_utf8(self): - with environment_as(GIT_DIR=self.gitdir, GIT_WORK_TREE=self.worktree): - - def commit_contents_to_files(message, encoding, content, *files): - for path in files: - with safe_open(os.path.join(self.worktree, path), "w") as fp: - fp.write(content) - subprocess.check_call(["git", "add", "."]) - - subprocess.check_call( - ["git", "config", "--local", "--add", "i18n.commitencoding", encoding] - ) - subprocess.check_call(["git", "config", "--local", "commit.gpgSign", "false"]) - try: - subprocess.check_call(["git", "commit", "-m", message.encode(encoding)]) - finally: - subprocess.check_call( - ["git", "config", "--local", "--unset-all", "i18n.commitencoding"] - ) - - return subprocess.check_output(["git", "rev-parse", "HEAD"]).strip() - - # Mix in a non-UTF-8 author to all commits to exercise the corner described here does not - # adversely impact the ability to render the changelog (even if rendering for certain - # characters is incorrect): http://comments.gmane.org/gmane.comp.version-control.git/262685 - # NB: This method of override requires we include `user.name` and `user.email` even though we - # only use `user.name` to exercise non-UTF-8. Without `user.email`, it will be unset and - # commits can then fail on machines without a proper hostname setup for git to fall back to - # when concocting a last-ditch `user.email`. - non_utf8_config = dedent( - """ - [user] - name = Noralf Trønnes - email = noralf@example.com - """ - ).encode("iso-8859-1") - - with open(os.path.join(self.gitdir, "config"), "wb") as fp: - fp.write(non_utf8_config) - - # Note the copyright symbol is used as the non-ascii character in the next 3 commits - commit_contents_to_files("START1 © END", "iso-8859-1", "1", "foo") - commit_contents_to_files("START2 © END", "latin1", "1", "bar") - commit_contents_to_files("START3 © END", "utf-8", "1", "baz") - - commit_contents_to_files("START4 ~ END", "us-ascii", "1", "bip") - - # Prove our non-utf-8 encodings were stored in the commit metadata. - log = subprocess.check_output(["git", "log", "--format=%e"]) - self.assertEqual( - [b"us-ascii", b"latin1", b"iso-8859-1"], - [_f for _f in log.strip().splitlines() if _f], - ) - - # And show that the git log successfully transcodes all the commits none-the-less to utf-8 - changelog = self.git.changelog() - - # The ascii commit should combine with the iso-8859-1 author an fail to transcode the - # o-with-stroke character, and so it should be replaced with the utf-8 replacement character - # \uFFF or �. - self.assertIn("Noralf Tr�nnes", changelog) - self.assertIn("Noralf Tr\uFFFDnnes", changelog) - - # For the other 3 commits, each of iso-8859-1, latin1 and utf-8 have an encoding for the - # o-with-stroke character - \u00F8 or ø - so we should find it; - self.assertIn("Noralf Trønnes", changelog) - self.assertIn("Noralf Tr\u00F8nnes", changelog) - - self.assertIn("START1 © END", changelog) - self.assertIn("START2 © END", changelog) - self.assertIn("START3 © END", changelog) - self.assertIn("START4 ~ END", changelog) - - def test_refresh_with_conflict(self): - with environment_as(GIT_DIR=self.gitdir, GIT_WORK_TREE=self.worktree): - self.assertEqual(set(), self.git.changed_files()) - self.assertEqual({"README"}, self.git.changed_files(from_commit="HEAD^")) - self.assertEqual({"README"}, self.git.changes_in("HEAD")) - - # Create a change on this branch that is incompatible with the change to master - with open(self.readme_file, "w") as readme: - readme.write("Conflict") - - subprocess.check_call(["git", "commit", "-am", "Conflict"]) - - self.assertEqual( - set(), self.git.changed_files(include_untracked=True, from_commit="HEAD") - ) - with self.assertRaises(Scm.LocalException): - self.git.refresh(leave_clean=False) - # The repo is dirty - self.assertEqual( - {"README"}, self.git.changed_files(include_untracked=True, from_commit="HEAD") - ) - - with environment_as(GIT_DIR=self.gitdir, GIT_WORK_TREE=self.worktree): - subprocess.check_call(["git", "reset", "--hard", "HEAD"]) - - # Now try with leave_clean - with self.assertRaises(Scm.LocalException): - self.git.refresh(leave_clean=True) - # The repo is clean - self.assertEqual( - set(), self.git.changed_files(include_untracked=True, from_commit="HEAD") - ) - def test_commit_with_new_untracked_file_adds_file(self): new_file = os.path.join(self.worktree, "untracked_file") diff --git a/src/python/pants/scm/scm.py b/src/python/pants/scm/scm.py index 154319ee697..2968c08c1ca 100644 --- a/src/python/pants/scm/scm.py +++ b/src/python/pants/scm/scm.py @@ -16,12 +16,6 @@ class ScmException(Exception): :API: public """ - class RemoteException(ScmException): - """Indicates a problem performing a remote scm operation. - - :API: public - """ - class LocalException(ScmException): """Indicates a problem performing a local scm operation. @@ -44,19 +38,6 @@ def commit_id(self) -> str: :API: public """ - @property - @abstractmethod - def server_url(self) -> str: - """Returns the url of the (default) remote server.""" - - @property - @abstractmethod - def tag_name(self) -> str: - """Returns the name of the current tag if any. - - :API: public - """ - @property @abstractmethod def branch_name(self) -> str: @@ -65,13 +46,6 @@ def branch_name(self) -> str: :API: public """ - @abstractmethod - def commit_date(self, commit_reference): - """Returns the commit date of the referenced commit. - - :API: public - """ - @property @abstractmethod def worktree(self): @@ -103,35 +77,6 @@ def changes_in(self, diffspec, relative_to=None): :param str relative_to: a path to which results should be relative (instead of SCM root) """ - @abstractmethod - def changelog(self, from_commit=None, files=None): - """Produces a changelog from the given commit or the 1st commit if none is specified until - the present workspace commit for the changes affecting the given files. - - If no files are given then the full change log should be produced. - - :API: public - """ - - @abstractmethod - def refresh(self): - """Refreshes the local workspace with any changes on the server. - - Subclasses should raise some form of ScmException to indicate a refresh error whether it be - a conflict or a communication channel error. - - :API: public - """ - - @abstractmethod - def tag(self, name, message=None): - """Tags the state in the local workspace and ensures this tag is on the server. - - Subclasses should raise RemoteException if there is a problem getting the tag to the server. - - :API: public - """ - @abstractmethod def commit(self, message, verify=True): """Commits all the changes for tracked files in the local workspace. @@ -149,21 +94,3 @@ def add(self, *paths): :API: public """ - - @abstractmethod - def push(self): - """Push the current branch of the local repository to the corresponding local branch on the - server. - - Subclasses should raise RemoteException if there is a problem getting the commit to the - server. - - :API: public - """ - - @abstractmethod - def set_state(self, rev): - """Set the repo state to the specified rev. - - :API: public - """ diff --git a/src/python/pants/scm/subsystems/BUILD b/src/python/pants/scm/subsystems/BUILD index 42e5f759ffd..5e08b945cd7 100644 --- a/src/python/pants/scm/subsystems/BUILD +++ b/src/python/pants/scm/subsystems/BUILD @@ -10,7 +10,6 @@ python_library( 'src/python/pants/base:build_environment', 'src/python/pants/base:exceptions', 'src/python/pants/engine:addresses', - 'src/python/pants/goal:workspace', 'src/python/pants/subsystem', ], ) diff --git a/src/python/pants/scm/subsystems/changed.py b/src/python/pants/scm/subsystems/changed.py index 251fb9bc5a2..1397f726499 100644 --- a/src/python/pants/scm/subsystems/changed.py +++ b/src/python/pants/scm/subsystems/changed.py @@ -11,13 +11,13 @@ find_dependees, map_addresses_to_dependees, ) +from pants.base.build_environment import get_buildroot from pants.base.deprecated import resolve_conflicting_options from pants.engine.addresses import Address from pants.engine.collection import Collection from pants.engine.internals.graph import Owners, OwnersRequest from pants.engine.rules import RootRule, rule from pants.engine.selectors import Get -from pants.goal.workspace import ScmWorkspace from pants.option.option_value_container import OptionValueContainer from pants.scm.scm import Scm from pants.subsystem.subsystem import Subsystem @@ -92,12 +92,16 @@ def is_actionable(self) -> bool: def changed_files(self, *, scm: Scm) -> List[str]: """Determines the files changed according to SCM/workspace and options.""" - workspace = ScmWorkspace(scm) if self.diffspec: - return cast(List[str], workspace.changes_in(self.diffspec)) + return cast(List[str], scm.changes_in(self.diffspec, relative_to=get_buildroot())) changes_since = self.since or scm.current_rev_identifier - return cast(List[str], workspace.touched_files(changes_since)) + return cast( + List[str], + scm.changed_files( + from_commit=changes_since, include_untracked=True, relative_to=get_buildroot() + ), + ) class Changed(Subsystem): diff --git a/src/python/pants/task/BUILD b/src/python/pants/task/BUILD index 314c83ebb46..73dcb9c8291 100644 --- a/src/python/pants/task/BUILD +++ b/src/python/pants/task/BUILD @@ -14,7 +14,6 @@ python_library( 'src/python/pants/build_graph', 'src/python/pants/cache', 'src/python/pants/console:stty_utils', - 'src/python/pants/goal:workspace', 'src/python/pants/invalidation', 'src/python/pants/option', 'src/python/pants/reporting',