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 _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 __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 __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 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 __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 __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 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 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 __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 __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 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 __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 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 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 check(self) -> None: """ Run some sanity checks and warn the user if necessary """ if self._local_repo.active_branch.name == "master": Utils.log( LogType.WARNING, "Creating merge requests from master is a bad idea.", "Please check out a new a branch before creating a merge request.", "To cancel, please press Ctrl + C.", ) if (not self._local_repo.active_branch.name.startswith("work/") and not self.__fork and "invent.kde.org" in self._connection.url): Utils.log( LogType.WARNING, "Pushing to the upstream repository, but the branch name doesn't start with work/.", ) print( "This is not recommended on KDE infrastructure,", "as it doesn't allow to rebase or force-push the branch.", "To cancel, please press Ctrl + C.", )
def checkout(self, start: str, name: str) -> None: """ Checkouts a branch if it exists or creates a new one """ try: if name in self.__repo.refs: self.__git.checkout(name) Utils.log(LogType.INFO, "Switched to branch '{}'".format(name)) else: self.__git.checkout(start, b=name) # create a new branch Utils.log(LogType.INFO, "Switched to a new branch '{}'".format(name)) except GitCommandError as git_error: Utils.log(LogType.ERROR, git_error.stderr.strip())
def __init__(self) -> None: self._local_repo = Utils.get_cwd_repo() self.__config = Config() try: origin = self._local_repo.remote(name="origin") except ValueError: Utils.log(LogType.ERROR, "No origin remote exists") sys.exit(1) repository: str = next(origin.urls) if repository.startswith("http"): Utils.log( LogType.INFO, "Found http remote, if you want to switch this " + "repository to ssh, run `git lab rewrite-remote " + "origin`", ) print() gitlab_url = Utils.gitlab_instance_url(repository) gitlab_hostname: Optional[str] = urlparse(gitlab_url).hostname if not gitlab_hostname: Utils.log(LogType.ERROR, "Failed to detect GitLab hostname") sys.exit(1) auth_token: Optional[str] = self.__config.token(gitlab_hostname) if not auth_token: Utils.log(LogType.ERROR, "No authentication token found. ") print( "Please create a token with the api and write_repository scopes on {}/-/{}." .format(gitlab_url, "profile/personal_access_tokens")) print('Afterwards use "git lab login --host {} --token t0k3n"'. format(gitlab_hostname)) sys.exit(1) self.__login(gitlab_url, auth_token) if not self._connection: Utils.log(LogType.ERROR, "Failed to connect to GitLab") sys.exit(1) try: self._remote_project = self._connection.projects.get( Utils.str_id_for_url(repository)) except (GitlabHttpError, GitlabGetError): Utils.log( LogType.ERROR, "The repository could not be found on the GitLab instance.", ) print( "If the repository was recently moved, please update the origin remote using git." ) sys.exit(1)