|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import logging |
| 4 | +from collections.abc import Sequence |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from sentry.integrations.source_code_management.commit_context import ( |
| 8 | + CommitContextClient, |
| 9 | + FileBlameInfo, |
| 10 | + SourceLineInfo, |
| 11 | +) |
| 12 | +from sentry.integrations.source_code_management.repository import RepositoryClient |
| 13 | +from sentry.models.pullrequest import PullRequest, PullRequestComment |
| 14 | +from sentry.models.repository import Repository |
| 15 | + |
| 16 | +logger = logging.getLogger(__name__) |
| 17 | + |
| 18 | + |
| 19 | +class PerforceClient(RepositoryClient, CommitContextClient): |
| 20 | + """ |
| 21 | + Client for interacting with Perforce server. |
| 22 | + Uses P4Python library to execute P4 commands. |
| 23 | + """ |
| 24 | + |
| 25 | + def __init__( |
| 26 | + self, |
| 27 | + host: str | None = None, |
| 28 | + port: int | str | None = None, |
| 29 | + user: str | None = None, |
| 30 | + password: str | None = None, |
| 31 | + client: str | None = None, |
| 32 | + ticket: str | None = None, |
| 33 | + ): |
| 34 | + self.ticket = ticket |
| 35 | + self.host = host or "localhost" |
| 36 | + self.port = str(port) if port else "1666" |
| 37 | + self.user = user or "" |
| 38 | + self.password = password |
| 39 | + self.client_name = client |
| 40 | + self.base_url = f"p4://{self.host}:{self.port}" |
| 41 | + |
| 42 | + def _connect(self): |
| 43 | + """Create and connect a P4 instance.""" |
| 44 | + pass |
| 45 | + |
| 46 | + def _disconnect(self, p4): |
| 47 | + """Disconnect P4 instance.""" |
| 48 | + pass |
| 49 | + |
| 50 | + def check_file(self, repo: Repository, path: str, version: str | None) -> object | None: |
| 51 | + """ |
| 52 | + Check if a file exists in the depot. |
| 53 | +
|
| 54 | + Args: |
| 55 | + repo: Repository object containing depot path (includes stream if specified) |
| 56 | + path: File path relative to depot |
| 57 | + version: Not used (streams are part of depot_path) |
| 58 | +
|
| 59 | + Returns: |
| 60 | + File info dict if exists, None otherwise |
| 61 | + """ |
| 62 | + return None |
| 63 | + |
| 64 | + def get_file( |
| 65 | + self, repo: Repository, path: str, ref: str | None, codeowners: bool = False |
| 66 | + ) -> str: |
| 67 | + """ |
| 68 | + Get file contents from depot. |
| 69 | +
|
| 70 | + Args: |
| 71 | + repo: Repository object (depot_path includes stream if specified) |
| 72 | + path: File path |
| 73 | + ref: Not used (streams are part of depot_path) |
| 74 | + codeowners: Whether this is a CODEOWNERS file |
| 75 | +
|
| 76 | + Returns: |
| 77 | + File contents as string |
| 78 | + """ |
| 79 | + return "" |
| 80 | + |
| 81 | + def _build_depot_path(self, repo: Repository, path: str, ref: str | None = None) -> str: |
| 82 | + """ |
| 83 | + Build full depot path from repo config and file path. |
| 84 | +
|
| 85 | + Args: |
| 86 | + repo: Repository object |
| 87 | + path: File path (may include @revision syntax like "file.cpp@42") |
| 88 | + ref: Optional ref/revision (for compatibility, but Perforce uses @revision in path) |
| 89 | +
|
| 90 | + Returns: |
| 91 | + Full depot path with @revision preserved if present |
| 92 | + """ |
| 93 | + return "" |
| 94 | + |
| 95 | + def get_blame( |
| 96 | + self, repo: Repository, path: str, ref: str | None = None, lineno: int | None = None |
| 97 | + ) -> list[dict[str, Any]]: |
| 98 | + """ |
| 99 | + Get blame/annotate information for a file (like git blame). |
| 100 | +
|
| 101 | + Uses 'p4 filelog' + 'p4 describe' which is much faster than 'p4 annotate'. |
| 102 | + Returns the most recent changelist that modified the file and its author. |
| 103 | + This is used for CODEOWNERS-style ownership detection. |
| 104 | +
|
| 105 | + Args: |
| 106 | + repo: Repository object (depot_path includes stream if specified) |
| 107 | + path: File path relative to depot (may include @revision like "file.cpp@42") |
| 108 | + ref: Optional revision/changelist number (appended as @ref if not in path) |
| 109 | + lineno: Specific line number to blame (optional, currently ignored) |
| 110 | +
|
| 111 | + Returns: |
| 112 | + List with a single entry containing: |
| 113 | + - changelist: changelist number |
| 114 | + - user: username who made the change |
| 115 | + - date: date of change |
| 116 | + - description: changelist description |
| 117 | + """ |
| 118 | + return [] |
| 119 | + |
| 120 | + def get_depot_info(self) -> dict[str, Any]: |
| 121 | + """ |
| 122 | + Get server info for testing connection. |
| 123 | +
|
| 124 | + Returns: |
| 125 | + Server info dictionary |
| 126 | + """ |
| 127 | + return {} |
| 128 | + |
| 129 | + def get_depots(self) -> list[dict[str, Any]]: |
| 130 | + """ |
| 131 | + List all depots accessible to the user. |
| 132 | +
|
| 133 | + Returns: |
| 134 | + List of depot info dictionaries |
| 135 | + """ |
| 136 | + return [] |
| 137 | + |
| 138 | + def get_changes( |
| 139 | + self, depot_path: str, max_changes: int = 20, start_cl: str | None = None |
| 140 | + ) -> list[dict[str, Any]]: |
| 141 | + """ |
| 142 | + Get changelists for a depot path. |
| 143 | +
|
| 144 | + Args: |
| 145 | + depot_path: Depot path (e.g., //depot/main/...) |
| 146 | + max_changes: Maximum number of changes to return |
| 147 | + start_cl: Starting changelist number |
| 148 | +
|
| 149 | + Returns: |
| 150 | + List of changelist dictionaries |
| 151 | + """ |
| 152 | + return [] |
| 153 | + |
| 154 | + def get_blame_for_files( |
| 155 | + self, files: Sequence[SourceLineInfo], extra: dict[str, Any] |
| 156 | + ) -> list[FileBlameInfo]: |
| 157 | + """ |
| 158 | + Get blame information for multiple files using p4 filelog. |
| 159 | +
|
| 160 | + Uses 'p4 filelog' + 'p4 describe' which is much faster than 'p4 annotate'. |
| 161 | + Returns the most recent changelist that modified each file. |
| 162 | +
|
| 163 | + Note: This does not provide line-specific blame. It returns the most recent |
| 164 | + changelist for the entire file, which is sufficient for suspect commit detection. |
| 165 | +
|
| 166 | + Returns a list of FileBlameInfo objects containing commit details for each file. |
| 167 | + """ |
| 168 | + return [] |
| 169 | + |
| 170 | + def create_comment(self, repo: str, issue_id: str, data: dict[str, Any]) -> Any: |
| 171 | + """Create comment. Not applicable for Perforce.""" |
| 172 | + raise NotImplementedError("Perforce does not support issue comments") |
| 173 | + |
| 174 | + def update_comment( |
| 175 | + self, repo: str, issue_id: str, comment_id: str, data: dict[str, Any] |
| 176 | + ) -> Any: |
| 177 | + """Update comment. Not applicable for Perforce.""" |
| 178 | + raise NotImplementedError("Perforce does not support issue comments") |
| 179 | + |
| 180 | + def create_pr_comment(self, repo: Repository, pr: PullRequest, data: dict[str, Any]) -> Any: |
| 181 | + """Create PR comment. Not applicable for Perforce.""" |
| 182 | + raise NotImplementedError("Perforce does not have native pull requests") |
| 183 | + |
| 184 | + def update_pr_comment( |
| 185 | + self, |
| 186 | + repo: Repository, |
| 187 | + pr: PullRequest, |
| 188 | + pr_comment: PullRequestComment, |
| 189 | + data: dict[str, Any], |
| 190 | + ) -> Any: |
| 191 | + """Update PR comment. Not applicable for Perforce.""" |
| 192 | + raise NotImplementedError("Perforce does not have native pull requests") |
| 193 | + |
| 194 | + def get_merge_commit_sha_from_commit(self, repo: Repository, sha: str) -> str | None: |
| 195 | + """Get merge commit. Not applicable for Perforce.""" |
| 196 | + return None |
0 commit comments