def token(config, token): """Store and fetch a GitHub access token""" if not token: info_out("To generate a personal API token, go to:\n\n\t" "https://github.com/settings/tokens\n\n" "To read more about it, go to:\n\n\t" "https://help.github.com/articles/creating-an-access" "-token-for-command-line-use/\n\n" 'Remember to enable "repo" in the scopes.') token = getpass.getpass("GitHub API Token: ").strip() url = urllib.parse.urljoin(config.github_url, "/user") assert url.startswith("https://"), url response = requests.get(url, headers={"Authorization": f"token {token}"}) if response.status_code == 200: update( config.configfile, { "GITHUB": { "github_url": config.github_url, "token": token, "login": response.json()["login"], } }, ) name = response.json()["name"] or response.json()["login"] success_out(f"Hi! {name}") else: error_out(f"Failed - {response.status_code} ({response.content})")
def test(config, bugnumber): """Test your saved Bugzilla API Key.""" state = read(config.configfile) credentials = state.get("BUGZILLA") if not credentials: error_out("No API Key saved. Run: gg bugzilla login") if config.verbose: info_out(f"Using: {credentials['bugzilla_url']}") if bugnumber: summary, _ = get_summary(config, bugnumber) if summary: info_out("It worked!") success_out(summary) else: error_out("Unable to fetch") else: url = urllib.parse.urljoin(credentials["bugzilla_url"], "/rest/whoami") assert url.startswith("https://"), url response = requests.get(url, params={"api_key": credentials["api_key"]}) if response.status_code == 200: if response.json().get("error"): error_out(f"Failed! - {response.json()}") else: success_out(json.dumps(response.json(), indent=2)) else: error_out( f"Failed to query - {response.status_code} ({response.json()})" )
def test(config, issue_url): """Test your saved GitHub API Token.""" state = read(config.configfile) credentials = state.get("GITHUB") if not credentials: error_out("No credentials saved. Run: gg github token") if config.verbose: info_out(f"Using: {credentials['github_url']}") if issue_url: github_url_regex = re.compile( r"https://github.com/([^/]+)/([^/]+)/issues/(\d+)") org, repo, number = github_url_regex.search(issue_url).groups() title, _ = get_title(config, org, repo, number) if title: info_out("It worked!") success_out(title) else: error_out("Unable to fetch") else: url = urllib.parse.urljoin(credentials["github_url"], "/user") assert url.startswith("https://"), url response = requests.get( url, headers={"Authorization": f"token {credentials['token']}"}) if response.status_code == 200: success_out(json.dumps(response.json(), indent=2)) else: error_out( f"Failed to query - {response.status_code} ({response.json()})" )
def login(config, api_key=""): """Store your Bugzilla API Key""" if not api_key: info_out("If you don't have an API Key, go to:\n" "https://bugzilla.mozilla.org/userprefs.cgi?tab=apikey\n") api_key = getpass.getpass("API Key: ") # Before we store it, let's test it. url = urllib.parse.urljoin(config.bugzilla_url, "/rest/whoami") assert url.startswith("https://"), url response = requests.get(url, params={"api_key": api_key}) if response.status_code == 200: if response.json().get("error"): error_out(f"Failed - {response.json()}") else: update( config.configfile, { "BUGZILLA": { "bugzilla_url": config.bugzilla_url, "api_key": api_key, # "login": login, } }, ) success_out("Yay! It worked!") else: error_out(f"Failed - {response.status_code} ({response.json()})")
def rebase(config): """Rebase the current branch against $origin/$branch""" repo = config.repo state = read(config.configfile) default_branch = state.get("DEFAULT_BRANCH", "master") active_branch = repo.active_branch if active_branch.name == default_branch: error_out(f"You're already on the {default_branch} branch.") active_branch_name = active_branch.name if repo.is_dirty(): error_out('Repo is "dirty". ({})'.format(", ".join( [repr(x.b_path) for x in repo.index.diff(None)]))) origin_name = state.get("ORIGIN_NAME", "origin") upstream_remote = None for remote in repo.remotes: if remote.name == origin_name: upstream_remote = remote break if not upstream_remote: error_out("No remote called {!r} found".format(origin_name)) repo.heads[default_branch].checkout() repo.remotes[origin_name].pull(default_branch) repo.heads[active_branch_name].checkout() print(repo.git.rebase(default_branch)) success_out(f"Rebased against {origin_name}/{default_branch}") info_out(f"If you want to start interactive rebase " f"run:\n\n\tgit rebase -i {default_branch}\n")
def mastermerge(config): """Merge the origin_name/default_branch into the the current branch""" repo = config.repo state = read(config.configfile) origin_name = state.get("ORIGIN_NAME", "origin") default_branch = get_default_branch(repo, origin_name) active_branch = repo.active_branch if active_branch.name == default_branch: error_out(f"You're already on the {default_branch} branch.") active_branch_name = active_branch.name if repo.is_dirty(): error_out( 'Repo is "dirty". ({})'.format( ", ".join([repr(x.b_path) for x in repo.index.diff(None)]) ) ) upstream_remote = None for remote in repo.remotes: if remote.name == origin_name: upstream_remote = remote break if not upstream_remote: error_out(f"No remote called {origin_name!r} found") repo.heads[default_branch].checkout() repo.remotes[origin_name].pull(default_branch) repo.heads[active_branch_name].checkout() repo.git.merge(default_branch) success_out(f"Merged against {origin_name}/{default_branch}")
def burn(config): """Remove and forget your GitHub credentials""" state = read(config.configfile) if state.get("GITHUB"): remove(config.configfile, "GITHUB") success_out("Forgotten") else: error_out("No stored GitHub credentials")
def logout(config): """Remove and forget your Bugzilla credentials""" state = read(config.configfile) if state.get("BUGZILLA"): remove(config.configfile, "BUGZILLA") success_out("Forgotten") else: error_out("No stored Bugzilla credentials")
def print_list(heads, merged_names, cutoff=10): def wrap(head): commit = head.commit return { "head": head, "info": { "date": commit.committed_datetime, "message": commit.message }, } def format_age(dt): # This `dt` is timezone aware. So cheat, so we don't need to figure out # our timezone is. delta = datetime.datetime.utcnow().timestamp() - dt.timestamp() return str(datetime.timedelta(seconds=delta)) def format_msg(message): message = message.strip().replace("\n", "\\n") if len(message) > 80: return message[:76] + "…" return message wrapped = sorted( [wrap(head) for head in heads], key=lambda x: x["info"].get("date"), reverse=True, ) for each in wrapped[:cutoff]: info_out("".center(80, "-")) success_out(each["head"].name + ( each["head"].name in merged_names and " (MERGED ALREADY)" or "")) if each.get("error"): info_out(f"\tError getting ref log ({each['error']!r})") info_out("\t" + each["info"]["date"].isoformat()) info_out("\t" + format_age(each["info"]["date"])) info_out("\t" + format_msg(each["info"].get("message", "*no commit yet*"))) info_out("") if len(heads) > cutoff: warning_out( f"Note! Found total of {len(heads)} but only showing {cutoff} most recent." )
def merge(config): """Merge the current branch into $default_branch.""" repo = config.repo state = read(config.configfile) default_branch = state.get("DEFAULT_BRANCH", "master") active_branch = repo.active_branch if active_branch.name == default_branch: error_out(f"You're already on the {default_branch} branch.") if repo.is_dirty(): error_out('Repo is "dirty". ({})'.format(", ".join( [repr(x.b_path) for x in repo.index.diff(None)]))) branch_name = active_branch.name origin_name = state.get("ORIGIN_NAME", "origin") upstream_remote = None for remote in repo.remotes: if remote.name == origin_name: upstream_remote = remote break if not upstream_remote: error_out(f"No remote called {origin_name!r} found") repo.heads[default_branch].checkout() upstream_remote.pull(repo.heads[default_branch]) repo.git.merge(branch_name) repo.git.branch("-d", branch_name) success_out("Branch {!r} deleted.".format(branch_name)) info_out("NOW, you might want to run:\n") info_out(f"git push {origin_name} {default_branch}\n\n") push_for_you = input("Run that push? [Y/n] ").lower().strip() != "n" if push_for_you: upstream_remote.push(default_branch) success_out( f"Current {default_branch} pushed to {upstream_remote.name}")
def config(config, fork_name="", origin_name="", default_branch=""): """Setting various configuration options""" state = read(config.configfile) if fork_name: update(config.configfile, {"FORK_NAME": fork_name}) success_out(f"fork-name set to: {fork_name}") else: info_out(f"fork-name: {state['FORK_NAME']}") if origin_name: update(config.configfile, {"ORIGIN_NAME": origin_name}) success_out(f"origin-name set to: {origin_name}") else: info_out(f"origin-name: {state.get('ORIGIN_NAME', '*not set*')}") if default_branch: update(config.configfile, {"DEFAULT_BRANCH": default_branch}) success_out(f"default-branch set to: {default_branch}") else: info_out(f"default-branch: {state.get('DEFAULT_BRANCH', '*not set*')}")
def push(config, force=False): """Create push the current branch.""" repo = config.repo state = read(config.configfile) default_branch = state.get("DEFAULT_BRANCH", "master") active_branch = repo.active_branch if active_branch.name == default_branch: error_out(f"Can't commit when on the {default_branch} branch. " "You really ought to do work in branches.") if not state.get("FORK_NAME"): info_out( "Can't help you push the commit. Please run: gg config --help") return 0 try: push_to_origin = load_config(config.configfile, "push_to_origin") except KeyError: push_to_origin = False try: repo.remotes[state["FORK_NAME"]] except IndexError: error_out("There is no remote called '{}'".format( state["FORK_NAME"])) origin_name = state.get("ORIGIN_NAME", "origin") destination = repo.remotes[ origin_name if push_to_origin else state["FORK_NAME"]] if force: (pushed, ) = destination.push(force=True) info_out(pushed.summary) else: (pushed, ) = destination.push() # print("PUSHED...") # for enum_name in [ # "DELETED", # "ERROR", # "FAST_FORWARD", # "NEW_HEAD", # "NEW_TAG", # "NO_MATCH", # "REMOTE_FAILURE", # ]: # print( # f"{enum_name}?:", pushed.flags & getattr(git.remote.PushInfo, enum_name) # ) if pushed.flags & git.remote.PushInfo.FORCED_UPDATE: success_out(f"Successfully force pushed to {destination}") elif (pushed.flags & git.remote.PushInfo.REJECTED or pushed.flags & git.remote.PushInfo.REMOTE_REJECTED): error_out('The push was rejected ("{}")'.format(pushed.summary), False) try_force_push = input("Try to force push? [Y/n] ").lower().strip() if try_force_push not in ("no", "n"): (pushed, ) = destination.push(force=True) info_out(pushed.summary) else: return 0 elif pushed.flags & git.remote.PushInfo.UP_TO_DATE: info_out(f"{destination} already up-to-date") else: success_out(f"Successfully pushed to {destination}") return 0
def commit(config, no_verify, yes): """Commit the current branch with all files.""" repo = config.repo state = read(config.configfile) origin_name = state.get("ORIGIN_NAME", "origin") default_branch = get_default_branch(repo, origin_name) active_branch = repo.active_branch if active_branch.name == default_branch: error_out(f"Can't commit when on the {default_branch} branch. " f"You really ought to do work in branches.") now = time.time() def count_files_in_directory(directory): count = 0 for root, _, files in os.walk(directory): # We COULD crosscheck these files against the .gitignore # if we ever felt overachievious. count += len(files) return count # First group all untracked files by root folder all_untracked_files = {} for path in repo.untracked_files: root = path.split(os.path.sep)[0] if root not in all_untracked_files: all_untracked_files[root] = { "files": [], "total_count": count_files_in_directory(root), } all_untracked_files[root]["files"].append(path) # Now filter this based on it being single files or a bunch untracked_files = {} for root, info in all_untracked_files.items(): for path in info["files"]: age = now - os.stat(path).st_mtime # If there's fewer untracked files in its directory, suggest # the directory instead. if info["total_count"] == 1: path = root if path in untracked_files: if age < untracked_files[path]: # youngest file in that directory untracked_files[path] = age else: untracked_files[path] = age if untracked_files: ordered = sorted(untracked_files.items(), key=lambda x: x[1], reverse=True) info_out("NOTE! There are untracked files:") for path, age in ordered: if os.path.isdir(path): path = path + "/" print("\t", path.ljust(60), humanize_seconds(age), "old") # But only put up this input question if one the files is # younger than 12 hours. young_ones = [x for x in untracked_files.values() if x < 60 * 60 * 12] if young_ones: ignore = input("Ignore untracked files? [Y/n] ").lower().strip() if ignore.lower().strip() == "n": error_out("\n\tLeaving it up to you to figure out what to do " "with those untracked files.") return 1 print("") state = read(config.configfile) try: push_to_origin = load_config(config.configfile, "push_to_origin") except KeyError: push_to_origin = False try: fixes_message = load_config(config.configfile, "fixes_message") except KeyError: fixes_message = True try: data = load(config.configfile, active_branch.name) except KeyError: error_out("You're in a branch that was not created with gg.\n" "No branch information available.") print("Commit message: (type a new one if you want to override)") msg = data["description"] if data.get("bugnumber"): if is_bugzilla(data): msg = "bug {} - {}".format(data["bugnumber"], data["description"]) msg = input('"{}" '.format(msg)).strip() or msg elif is_github(data): msg = input('"{}" '.format(msg)).strip() or msg if fixes_message: msg += "\n\nPart of #{}".format(data["bugnumber"]) if data["bugnumber"] and fixes_message: question = 'Add the "fixes" mention? [N/y] ' fixes = input(question).lower().strip() if fixes in ("y", "yes") or yes: if is_bugzilla(data): msg = "fixes " + msg elif is_github(data): msg = msg.replace("Part of ", "Fixes ") else: raise NotImplementedError # Now we're going to do the equivalent of `git commit -a -m "..."` index = repo.index files_added = [] files_removed = [] for x in repo.index.diff(None): if x.deleted_file: files_removed.append(x.b_path) else: files_added.append(x.b_path) files_new = [] for x in repo.index.diff(repo.head.commit): files_new.append(x.b_path) proceed = True if not (files_added or files_removed or files_new): info_out("No files to add or remove.") proceed = False if input("Proceed anyway? [Y/n] ").lower().strip() == "n": proceed = True if proceed: if not repo.is_dirty(): error_out("Branch is not dirty. There is nothing to commit.") if files_added: index.add(files_added) if files_removed: index.remove(files_removed) try: # Do it like this (instead of `repo.git.commit(msg)`) # so that git signing works. commit = repo.git.commit(["-m", msg]) except git.exc.HookExecutionError as exception: if not no_verify: info_out("Commit hook failed ({}, exit code {})".format( exception.command, exception.status)) if exception.stdout: error_out(exception.stdout) elif exception.stderr: error_out(exception.stderr) else: error_out("Commit hook failed.") else: commit = index.commit(msg, skip_hooks=True) success_out("Commit created {}".format(commit)) if not state.get("FORK_NAME"): info_out( "Can't help you push the commit. Please run: gg config --help") return 0 if push_to_origin: try: repo.remotes[origin_name] except IndexError: error_out(f"There is no remote called {origin_name!r}") else: try: repo.remotes[state["FORK_NAME"]] except IndexError: error_out(f"There is no remote called {state['FORK_NAME']!r}") remote_name = origin_name if push_to_origin else state["FORK_NAME"] if yes: push_for_you = "yes" else: push_for_you = input( f"Push branch to {remote_name!r}? [Y/n] ").lower().strip() if push_for_you not in ("n", "no"): push_output = repo.git.push("--set-upstream", remote_name, active_branch.name) print(push_output) else: # If you don't want to push, then don't bother with GitHub # Pull Request stuff. return 0 if not state.get("GITHUB"): if config.verbose: info_out("Can't help create a GitHub Pull Request.\n" "Consider running: gg github --help") return 0 origin = repo.remotes[state.get("ORIGIN_NAME", "origin")] rest = re.split(r"github\.com[:/]", origin.url)[1] org, repo = rest.split(".git")[0].split("/", 1) # Search for an existing open pull request, and remind us of the link # to it. search = { "head": f"{remote_name}:{active_branch.name}", "state": "open", } for pull_request in github.find_pull_requests(config, org, repo, **search): print("Pull Request already created:") print("") print("\t", pull_request["html_url"]) print("") break else: # If no known Pull Request exists, make a link to create a new one. if remote_name == origin.name: github_url = "https://github.com/{}/{}/compare/{}...{}?expand=1".format( org, repo, default_branch, active_branch.name) else: github_url = ( "https://github.com/{}/{}/compare/{}:{}...{}:{}?expand=1". format( org, repo, org, default_branch, remote_name, active_branch.name, )) print("Now, to make a Pull Request, go to:") print("") success_out(github_url) print("(⌘-click to open URLs)") return 0