def _missing_(cls, value: object) -> None: """ Called if a given value does not exist. Supported in all version of Python since 3.6. """ Utils.log(LogType.WARNING, f"Invalid status '{str(value)}'") sys.exit(1)
def main() -> None: """ Entry point """ parser: Parser = Parser() try: parser.parse() except GitCommandError as git_error: Utils.log(LogType.ERROR, str(git_error)) except SystemExit: pass except KeyboardInterrupt: pass except: # noqa: E722 print() Utils.log(LogType.ERROR, "git-lab crashed. This should not happen.") print( "Please help us to fix it by opening an issue on", "https://invent.kde.org/sdk/git-lab/-/issues.", "Make sure to include the information below:", "\n```\n", traceback.format_exc(), "```", )
def __init__(self, issue_id: int): RepositoryConnection.__init__(self) try: self.issue: ProjectIssue = self._remote_project.issues.get(issue_id, lazy=False) except GitlabGetError: Utils.log(LogType.WARNING, f"No issue with ID {issue_id}") sys.exit(1)
def __upload_assets(self, text: str) -> str: """ Scans the text for local file paths, uploads the files and returns the text modified to load the files from the uploaded urls """ find_expr = re.compile(r"!\[[^\[\(]*\]\([^\[\(]*\)") extract_expr = re.compile(r"(?<=\().+?(?=\))") matches: List[Any] = find_expr.findall(text) output_text: str = text for match in matches: image = extract_expr.findall(match)[0] if not image.startswith("http"): Utils.log(LogType.INFO, "Uploading", image) filename: str = os.path.basename(image) try: uploaded_file = self._remote_project.upload(filename, filepath=image) output_text = output_text.replace(image, uploaded_file["url"]) except FileNotFoundError: Utils.log(LogType.WARNING, "Failed to upload image", image) print("The file does not exist.") return output_text
def __init__(self, pipeline_id: int) -> None: RepositoryConnection.__init__(self) try: self.pipeline: ProjectPipeline = self._remote_project.pipelines.get( pipeline_id, lazy=False) except GitlabGetError: Utils.log(LogType.WARNING, f"No pipeline with ID {pipeline_id}") sys.exit(1)
def run(args: argparse.Namespace) -> None: """ :param args: parsed arguments """ repo: Repo = Utils.get_cwd_repo() remote_url = repo.git.remote("get-url", args.remote) ssh_url = Utils.ssh_url_from_http(Utils.normalize_url(remote_url)) repo.git.remote("set-url", args.remote, ssh_url)
def __login(self, hostname: str, token: str) -> None: try: connection: Gitlab = Gitlab(hostname, private_token=token) connection.auth() self._connections.append(connection) except GitlabAuthenticationError: Utils.log(LogType.ERROR, "Could not log into GitLab") sys.exit(1)
def __login(self, hostname: str, token: str) -> None: try: self._connection: Gitlab = Gitlab(hostname, private_token=token) self._connection.auth() except (GitlabAuthenticationError, GitlabGetError): Utils.log(LogType.ERROR, "Could not log into GitLab: {}".format(hostname)) sys.exit(1)
def open_web(self) -> None: """ Open issue with xdg-open """ if self._remote_project.issues_enabled: Utils.xdg_open(f"{self._remote_project.web_url}/-/issues") else: Utils.log(LogType.ERROR, "Issue are disabled for this project")
def test_normalize_url(self): scp_url: str = "[email protected]:KDE/kaidan.git" url = Utils.normalize_url(scp_url) self.assertEqual(url, "ssh://[email protected]/KDE/kaidan.git") http_url: str = "https://invent.kde.org/KDE/kaidan.git" url = Utils.normalize_url(http_url) self.assertEqual(url, "https://invent.kde.org/KDE/kaidan.git")
def __fulltext_valid(self) -> bool: lines = self.__fulltext.splitlines() if not lines or not lines[0]: Utils.log(LogType.ERROR, "The first line (title) can't be empty") return False return True
def update_estimated(self, time_str: str) -> None: """Updates the estimated time for the issue. Overrides the old value.""" if not is_valid_time_str(time_str): Utils.log(LogType.WARNING, f"{time_str} is an invalid time string.") sys.exit(1) self.issue.time_estimate(time_str) self.issue.save() print(TextFormatting.green(f"Set estimate to {time_str}"))
def update_spent(self, time_str: str) -> None: """Adds time spent to the already existing time spent.""" if not is_valid_time_str(time_str): Utils.log(LogType.WARNING, f"{time_str} is an invalid time string.") sys.exit(1) self.issue.add_spent_time(time_str) self.issue.save() print(TextFormatting.green(f"Added time entry of {time_str}"))
def __init__(self, issue_id: int): """ Creates a new issue connection. Requires a valid issue ID for the current project. """ RepositoryConnection.__init__(self) try: self.issue: ProjectIssue = self._remote_project.issues.get( issue_id, lazy=False) except GitlabGetError: Utils.log(LogType.WARNING, f"No issue with ID {issue_id}") sys.exit(1)
def test_str_id_for_url(self): url: str = "ssh://[email protected]/KDE/kaidan.git" str_id: str = Utils.str_id_for_url(url) self.assertEqual(str_id, "KDE/kaidan") # This is not a valid repository name for KDE invent, though url = "ssh://[email protected]/KDE/kaidan%.git" str_id = Utils.str_id_for_url(url) self.assertEqual(str_id, "KDE/kaidan%")
def __init__(self, status: Optional[PipelineStatus] = None, ref: Optional[str] = None) -> None: RepositoryConnection.__init__(self) self.status: Optional[PipelineStatus] = status self.ref: Optional[str] = ref if ref is not None and ref not in self._local_repo.refs: # Print a warning, if the ref is not found LOCALLY # The remote may contain refs, that do not exists inside the local copy, # therefore only a warning is printed. Utils.log(LogType.WARNING, f"Ref '{ref}' is not found locally.")
def __init__(self, extra_text: str = "", placeholder_title: str = "", placeholder_body: str = "") -> None: self.__input(extra_text, placeholder_title, placeholder_body) self.__fulltext_remove_comments() if not self.__fulltext_valid(): Utils.log(LogType.ERROR, "Text not valid, aborting") sys.exit(1) lines: List[str] = self.__fulltext.splitlines() self.title = lines[0] self.body = "\n".join(lines[1:])
def create_mr(self) -> None: """ Creates a merge request with the changes from the current branch """ mrs: List[ ProjectMergeRequest] = self._remote_project.mergerequests.list( source_branch=self._local_repo.active_branch.name, target_branch=self.__target_branch, target_project_id=self._remote_project.id, ) if len(mrs) > 0: merge_request = mrs[0] Utils.log( LogType.INFO, 'Updating existing merge request "{}" at: {}'.format( merge_request.title, merge_request.web_url), ) return e_input = EditorInput( placeholder_title=self._local_repo.head.commit.summary, placeholder_body=self._local_repo.head.commit.message.split( "\n", 1)[1].strip(), extra_text="The markdown syntax for embedding images " + "![description](/path/to/file) can be used to upload images.", ) project: Project = self.__remote_fork if self.__fork else self._remote_project merge_request = project.mergerequests.create({ "source_branch": self._local_repo.active_branch.name, "target_branch": self.__target_branch, "title": e_input.title, "description": self.__upload_assets(e_input.body), "target_project_id": self._remote_project.id, "allow_maintainer_to_push": True, "remove_source_branch": True, }) Utils.log(LogType.INFO, "Created merge request at", merge_request.web_url)
def __migrate_to_version_1(self) -> None: if "version" not in self.__config: Utils.log(LogType.INFO, "Migrating configuration file to version 1") new_config: Dict[str, Any] = {"version": 1, "instances": {}} for hostname in self.__config.keys(): new_config["instances"][hostname] = { "auth_type": "token", "token": self.__config[hostname], } self.__config = new_config self.save()
def __init__(self) -> None: repository_path: Optional[str] = Utils.find_dotgit(os.getcwd()) if repository_path: self.config_path = repository_path + os.path.sep + ".git" + os.path.sep + "gitlabconfig" else: Utils.log(LogType.ERROR, "Current directory is not a git repository") sys.exit(1) if not os.path.isfile(self.config_path): with open(self.config_path, "w+") as file: json.dump({}, file) file.close() self.__file = open(self.config_path, "r+") self.__config = json.load(self.__file)
def paste(self, file: TextIO, title: Optional[str]) -> None: """ paste the contents of a TextIO object """ snippet: Snippet = self._connection.snippets.create({ "title": title, "file_name": file.name, "content": file.read(), "visibility": "public" }) Utils.log(LogType.INFO, "Created snippet at", snippet.web_url) print("You can access it raw at", snippet.raw_url)
def print_formatted_list(self) -> None: """ Print the list of pipelines to terminal formatted as a table """ table = Table() # Compute args that are sent to GitLab args: Dict[str, str] = {} if self.status is not None: # Only yield pipelines that have the specific status # If empty all pipelines will be returned by GitLab args["status"] = str(self.status) if self.ref is not None: # Only yield pipeline that match a given reference args["ref"] = self.ref # Build the printable table pipelines: List[ProjectPipeline] = self._remote_project.pipelines.list( **args) for pipeline in pipelines: row: List[str] = [ TextFormatting.BOLD + "#" + str(pipeline.id) + TextFormatting.END, pipeline.ref, Utils.pretty_date(pipeline.created_at), PipelineStatus.format(pipeline.status), ] table.add_row(row) table.print()
def test_gitlab_instance_url(self): # https repos = ( "[email protected]:KDE/kaidan.git", "ssh://[email protected]/KDE/kaidan.git" "https://invent.kde.org/KDE/kaidan", "git://invent.kde.org/KDE/kaidan", ) for repo in repos: url = Utils.gitlab_instance_url(repo) self.assertEqual(url, "https://invent.kde.org") # http url = Utils.gitlab_instance_url("http://invent.kde.org/KDE/kaidan") self.assertEqual(url, "http://invent.kde.org")
def run(args: argparse.Namespace) -> None: """ run snippet creation commands :param args: parsed arguments """ snippets = Snippets() file: TextIO if args.filename: try: file = open(args.filename, "r") except FileNotFoundError: Utils.log(LogType.ERROR, "Failed to open file", args.filename) sys.exit(1) else: file = sys.stdin snippets.paste(file, title=args.title)
def test_timezone_awareness_of_pretty_date(self) -> None: ref_date = datetime(2020, 1, 1, tzinfo=timezone.utc) fmt = "%Y-%m-%dT%H:%M:%S.%fZ" iso_date_1 = ref_date + timedelta(seconds=9) self.assertEqual( Utils.pretty_date( ref_date.replace(tzinfo=None).strftime(fmt), iso_date_1), "just now")
def fork(self) -> None: """ Try to create a fork of the remote repository. If the fork already exists, no new fork will be created. """ if "fork" in self._local_repo.remotes: # Fork already exists fork_str_id: str = Utils.str_id_for_url( self._local_repo.remotes.fork.url) # Try to retrieve the remote project object, if it doesn't exist on the server, # go on with the logic to create a new fork. try: self.__remote_fork = self._connection.projects.get(fork_str_id) return except GitlabGetError: pass try: self.__remote_fork = self._remote_project.forks.create({}) # WORKAROUND: the return of create() is unreliable, # and sometimes doesn't allow to create merge requests, # so request a fresh project object. self.__remote_fork = self._connection.projects.get( self.__remote_fork.id) self._local_repo.create_remote( "fork", url=self.__remote_fork.ssh_url_to_repo) except GitlabCreateError: Utils.log( LogType.INFO, "Fork exists, but no fork remote exists locally, trying to guess the url", ) # Detect ssh url url = Utils.ssh_url_from_http(self._connection.user.web_url + "/" + self._remote_project.path) self._local_repo.create_remote("fork", url=url) str_id: str = Utils.str_id_for_url( self._local_repo.remotes.fork.url) self.__remote_fork = self._connection.projects.get(str_id)
def commit(self) -> None: """ Determine whether there are uncommitted changes, and ask the user what to do about them """ index: IndexFile = self._local_repo.index if len(index.diff("HEAD")) > 0: Utils.log(LogType.INFO, "You have staged but uncommited changes.") create_commit: bool = Utils.ask_bool( "do you want to create a new commit?") if create_commit: # We can't use self.local_repo().git.commit() here, as it would # start the editor in the background try: subprocess.check_call(["git", "commit"]) except subprocess.CalledProcessError: Utils.log(LogType.ERROR, "git exited with an error code") sys.exit(1)
def checkout(self, merge_request_id: int) -> None: """ Checks out the merge request with the specified id in the local worktree """ self.__mr = self._remote_project.mergerequests.get(merge_request_id, lazy=False) print('Checking out merge request "{}"...'.format(self.__mr.title)) print(" branch:", self.__mr.source_branch) fetch_info = self._local_repo.remotes.origin.fetch( "merge-requests/{}/head".format(merge_request_id) )[0] if self.__mr.source_branch in self._local_repo.refs: # Make sure not to overwrite local changes overwrite = Utils.ask_bool( 'Branch "{}" already exists locally, do you want to overwrite it?'.format( self.__mr.source_branch ) ) if not overwrite: print("Aborting") sys.exit(1) # If the branch that we want to overwrite is currently checked out, # that will of course not work, so try to switch to another branch in the meantime. if self.__mr.source_branch == self._local_repo.head.reference.name: if "main" in self._local_repo.refs: self._local_repo.refs.main.checkout() elif "master" in self._local_repo.refs: self._local_repo.refs.master.checkout() else: Utils.log( LogType.ERROR, "The branch that you want to overwrite is currently checked out \ and no other branch to temporarily switch to could be found. Please check out \ a different branch and try again.", ) sys.exit(1) self._local_repo.delete_head(self.__mr.source_branch, "-f") head = self._local_repo.create_head(self.__mr.source_branch, fetch_info.ref) head.checkout()
def test_pretty_time_delta(self) -> None: delta = timedelta(days=1, hours=12, minutes=10, seconds=20) tot_sec = int(delta.total_seconds()) self.assertEqual(Utils.pretty_time_delta(tot_sec), "1d 12h 10m 20s") delta = timedelta(hours=23, minutes=57, seconds=0) tot_sec = int(delta.total_seconds()) self.assertEqual(Utils.pretty_time_delta(tot_sec), "23h 57m 0s") delta = timedelta(minutes=2, seconds=59) tot_sec = int(delta.total_seconds()) self.assertEqual(Utils.pretty_time_delta(tot_sec), "2m 59s") delta = timedelta(seconds=3) tot_sec = int(delta.total_seconds()) self.assertEqual(Utils.pretty_time_delta(tot_sec), "3s") # Also test sign delta = timedelta(seconds=3) tot_sec = -int(delta.total_seconds()) self.assertEqual(Utils.pretty_time_delta(tot_sec), "3s")
def test_pretty_date(self) -> None: ref_date = datetime(2020, 1, 1) fmt = "%Y-%m-%dT%H:%M:%S.%fZ" iso_date_1 = ref_date + timedelta(seconds=9) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "just now") iso_date_1 = ref_date + timedelta(seconds=59) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "59 seconds ago") iso_date_1 = ref_date + timedelta(seconds=5 * 60) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "5 minutes ago") iso_date_1 = ref_date + timedelta(seconds=60 * 60) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "an hour ago") iso_date_1 = ref_date + timedelta(seconds=24 * 60 * 60) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "Yesterday") iso_date_1 = ref_date + timedelta(seconds=14 * 24 * 60 * 60) self.assertEqual(Utils.pretty_date(ref_date.strftime(fmt), iso_date_1), "2 weeks ago")