├── .gitignore ├── LICENSE ├── README.md ├── git-passport.py └── passport ├── __init__.py ├── arg.py ├── case.py ├── configuration.py ├── dialog.py ├── git.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Same as Git. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is git-passport? 2 | git-passport is a Git command and hook written in Python to manage multiple Git 3 | users / user identities. 4 | 5 | 6 | ## Get it! 7 | ``` 8 | mkdir -p ~/.git/hooks/bin && cd $_ 9 | git clone git://github.com/frace/git-passport.git 10 | chmod +x ./git-passport/git-passport.py 11 | ``` 12 | 13 | 14 | ## Installation 15 | There are many ways to handle your hooks. What I do in order to work with 16 | multiple hooks is the following solution: 17 | ``` 18 | mkdir -p ~/.git/hooks/pre-commit.d && cd $_ 19 | ln -sf ~/.git/hooks/bin/git-passport/git-passport.py ./00-git-passport 20 | mkdir -p ~/.git/templates/hooks && cd $_ 21 | touch pre-commit && chmod +x $_ 22 | ``` 23 | 24 | Add the Git template directory path into your `~/.gitconfig` and create an 25 | alias in order to be able to execute the script manually as a «native» git 26 | command by invoking `git passport`: 27 | ``` 28 | [alias] 29 | passport = !${HOME}/.git/hooks/bin/git-passport/git-passport.py 30 | 31 | [init] 32 | templatedir = ~/.git/templates 33 | ``` 34 | 35 | In `~/.git/templates/hooks/pre-commit` I put a little bash script which 36 | loads one hook after another: 37 | ``` 38 | #!/usr/bin/env bash 39 | 40 | hooks_pre_commit="${HOME}/.git/hooks/pre-commit.d/" 41 | 42 | for hook in ${hooks_pre_commit}*; do 43 | "$hook" 44 | done 45 | ``` 46 | 47 | Afterwards each `git init` or `git clone` command will distribute 48 | the hook into a new repository. 49 | If you want to apply the hook to already exisiting repos then just run 50 | `git init` inside the repository in order to reinitialize it. 51 | 52 | 53 | ## Configuration 54 | On the first run `git-passport.py` generates a sample configuration file inside 55 | your home directory: 56 | ``` 57 | cd ~/.git/hooks/bin/git-passport 58 | ./git-passport.py 59 | No configuration file found in ~/. 60 | Generating a sample configuration file. 61 | ``` 62 | 63 | The configuration file `~/.gitpassport` is rather self-explanatory: 64 | ``` 65 | [general] 66 | enable_hook = True 67 | sleep_duration = 0.5 68 | 69 | [passport 0] 70 | email = email_0@example.com 71 | name = name_0 72 | service = github.com 73 | 74 | [passport 1] 75 | email = email_1@example.com 76 | name = name_1 77 | service = gitlab.com 78 | ``` 79 | 80 | Adjust the existing sections and add as many passports as you like by following 81 | the section scheme. 82 | 83 | 84 | ## Usage 85 | If you setup the script as a hook only it will be invoked automatically 86 | during each `git commit` command. 87 | You can pass the following options if you use `git-passport.py` as a Git 88 | command, too: 89 | ``` 90 | git passport -h 91 | usage: git passport (--select | --delete | --active | --passports) 92 | 93 | manage multiple Git identities 94 | 95 | optional arguments: 96 | -h show this help message and exit 97 | -s, --select select a passport 98 | -d, --delete delete the active passport in .git/config 99 | -a, --active print the active passport in .git/config 100 | -p, --passports print all passports in ~/.gitpassport 101 | ``` 102 | 103 | 104 | ## Bugs 105 | You are welcome to report bugs at the [project bugtracker][project-bugtracker] 106 | at github.com. 107 | 108 | [project-bugtracker]: https://github.com/frace/git-passport/issues 109 | 110 | 111 | * * * 112 | ## Credits: 113 | + Inspired by [ORR SELLA][credits-1] 114 | + Grew at [stackoverflow.com][credits-2] 115 | 116 | [credits-1]: https://orrsella.com/2013/08/10/git-using-different-user-emails-for-different-repositories/ 117 | [credits-2]: http://stackoverflow.com/questions/4220416/can-i-specify-multiple-users-for-myself-in-gitconfig/23107012#23107012 118 | [credits-3]: http://codereview.stackexchange.com/questions/76935/python-based-git-pre-commit-hook-to-manage-multiple-users-git-identities 119 | -------------------------------------------------------------------------------- /git-passport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | """ git-passport is a Git command and hook written in Python to manage multiple 6 | Git users / user identities. 7 | """ 8 | 9 | if __name__ == "__main__": 10 | import os.path 11 | import sys 12 | 13 | from passport import ( 14 | arg, 15 | case, 16 | configuration, 17 | dialog, 18 | git 19 | ) 20 | 21 | args = arg.release() 22 | config_file = os.path.expanduser("~/.gitpassport") 23 | 24 | if ( 25 | not configuration.preset(config_file) or 26 | not configuration.validate_scheme(config_file) or 27 | not configuration.validate_values(config_file) or 28 | not git.infected() 29 | ): 30 | sys.exit(1) 31 | else: 32 | config = configuration.release(config_file) 33 | 34 | if config["enable_hook"]: 35 | local_email = git.config_get(config, "local", "email") 36 | local_name = git.config_get(config, "local", "name") 37 | local_url = git.config_get(config, "local", "url") 38 | 39 | if args.select: 40 | local_name = None 41 | local_email = None 42 | git.config_remove(verbose=False) 43 | 44 | if args.delete: 45 | git.config_remove() 46 | sys.exit(0) 47 | 48 | if args.active: 49 | case.active_identity( 50 | config, 51 | local_email, 52 | local_name, 53 | local_url, 54 | style="compact" 55 | ) 56 | sys.exit(0) 57 | 58 | if args.passports: 59 | dialog.print_choice(config["git_passports"]) 60 | exit(0) 61 | 62 | if local_email and local_name: 63 | case.active_identity( 64 | config, 65 | local_email, 66 | local_name, 67 | local_url 68 | ) 69 | sys.exit(0) 70 | 71 | if local_url: 72 | candidates = case.url_exists(config, local_url) 73 | else: 74 | candidates = case.no_url_exists(config) 75 | 76 | selected_id = dialog.get_input(candidates.keys()) 77 | if selected_id is not None: 78 | git.config_set(config, candidates[selected_id]["email"], "email") 79 | git.config_set(config, candidates[selected_id]["name"], "name") 80 | sys.exit(0) 81 | else: 82 | print("git-passport is currently disabled.") 83 | sys.exit(0) 84 | -------------------------------------------------------------------------------- /passport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frace/git-passport/82884f782cdf040b94ea8f917bb3efa1eb72c870/passport/__init__.py -------------------------------------------------------------------------------- /passport/arg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ..................................................................... Imports 4 | import argparse 5 | 6 | 7 | # .......................................................... Argparse functions 8 | def release(): 9 | """ Define available arguments for the command line usage. 10 | 11 | Returns: 12 | args (obj): An object containing predefined args 13 | """ 14 | arg_parser = argparse.ArgumentParser(add_help=False) 15 | arg_parser.description = "manage multiple Git identities" 16 | arg_parser.usage = ( 17 | "git passport (--select | --delete | --active | --passports)" 18 | ) 19 | 20 | arg_group = arg_parser.add_mutually_exclusive_group() 21 | arg_group.add_argument( 22 | "-h", 23 | action="help", 24 | help="show this help message and exit" 25 | ) 26 | 27 | arg_group.add_argument( 28 | "-s", 29 | "--select", 30 | action="store_true", 31 | help="select a passport" 32 | ) 33 | 34 | arg_group.add_argument( 35 | "-d", 36 | "--delete", 37 | action="store_true", 38 | help="delete the active passport in .git/config" 39 | ) 40 | 41 | arg_group.add_argument( 42 | "-a", 43 | "--active", 44 | action="store_true", 45 | help="print the active passport in .git/config" 46 | ) 47 | 48 | arg_group.add_argument( 49 | "-p", 50 | "--passports", 51 | action="store_true", 52 | help="print all passports in ~/.gitpassport" 53 | ) 54 | 55 | args = arg_parser.parse_args() 56 | return args 57 | -------------------------------------------------------------------------------- /passport/case.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # ..................................................................... Imports 5 | import time 6 | import urllib.parse 7 | 8 | from . import ( 9 | configuration, 10 | dialog, 11 | util 12 | ) 13 | 14 | 15 | # .............................................................. Case functions 16 | def active_identity(config, email, name, url, style=None): 17 | """ Prints an existing ID of a local gitconfig. 18 | 19 | Args: 20 | config (dict): Contains validated configuration options 21 | email (str): An email address 22 | name (str): A name 23 | url (str): A remote.origin.url 24 | 25 | Returns: 26 | True (bool): If an active passport could be found 27 | False (bool): If an active passport could not be found 28 | """ 29 | duration = config["sleep_duration"] 30 | strip = "strip" if style == "compact" else "lstrip" 31 | 32 | if not url: 33 | url = "Not set" 34 | 35 | if email and name: 36 | msg = """ 37 | ~Active Passport: 38 | . User: {} 39 | . E-Mail: {} 40 | . Remote: {} 41 | """.format( 42 | name, 43 | email, 44 | url 45 | ) 46 | 47 | print(util.dedented(msg, strip)) 48 | else: 49 | msg = "No passport set." 50 | 51 | print(msg) 52 | return False 53 | 54 | time.sleep(duration) 55 | return True 56 | 57 | 58 | def url_exists(config, url): 59 | """ If a local gitconfig contains a remote.origin.url add all user defined 60 | Git IDs matching remote.origin.url as a candidate. However if there is 61 | not a single match then add all available user defined Git IDs and the 62 | global Git ID as candidates. 63 | 64 | Args: 65 | config (dict): Contains validated configuration options 66 | url (str): A remote.origin.url 67 | 68 | Returns: 69 | candidates (dict): Contains preselected Git ID candidates 70 | """ 71 | # A generator to filter matching sections by options: 72 | # Let's see if user defined IDs match remote.origin.url 73 | def gen_candidates(ids, url): 74 | for key, value in ids.items(): 75 | if value.get("service") == url: 76 | yield (key, value) 77 | 78 | local_passports = config["git_passports"] 79 | netloc = urllib.parse.urlparse(url)[1] 80 | 81 | candidates = dict(gen_candidates(local_passports, netloc)) 82 | 83 | if len(candidates) >= 1: 84 | msg = """ 85 | One or more passports match your current Git provider. 86 | remote.origin.url: {} 87 | """.format(url) 88 | 89 | print(util.dedented(msg, "lstrip")) 90 | else: 91 | candidates = local_passports 92 | msg = """ 93 | Zero suitable passports found - listing all passports. 94 | remote.origin.url: {} 95 | """.format(url) 96 | 97 | print(util.dedented(msg, "lstrip")) 98 | configuration.add_global_id(config, candidates) 99 | 100 | dialog.print_choice(candidates) 101 | return candidates 102 | 103 | 104 | def no_url_exists(config): 105 | """ If a local gitconfig does not contain a remote.origin.url add 106 | all available user defined Git IDs and the global Git ID as 107 | candidates. 108 | 109 | Args: 110 | config (dict): Contains validated configuration options 111 | 112 | Returns: 113 | candidates (dict): Contains preselected Git ID candidates 114 | """ 115 | candidates = config["git_passports"] 116 | msg = "«remote.origin.url» is not set, listing all passports:\n" 117 | 118 | print(msg) 119 | configuration.add_global_id(config, candidates) 120 | dialog.print_choice(candidates) 121 | 122 | return candidates 123 | -------------------------------------------------------------------------------- /passport/configuration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # ..................................................................... Imports 5 | import configparser 6 | import os.path 7 | import re 8 | 9 | from . import ( 10 | git, 11 | util 12 | ) 13 | 14 | 15 | # ............................................................ Config functions 16 | def preset(filename): 17 | """ Create a configuration file containing sample data inside the home 18 | directory if none exists yet. 19 | 20 | Args: 21 | filename (str): The complete `filepath` of the configuration file 22 | 23 | Returns: 24 | True (bool): If the configfile exists already 25 | False (bool): If a new configfile was successfully created 26 | """ 27 | if os.path.exists(filename): 28 | return True 29 | 30 | preset = configparser.ConfigParser() 31 | 32 | preset["general"] = {} 33 | preset["general"]["enable_hook"] = "True" 34 | preset["general"]["sleep_duration"] = "0.75" 35 | 36 | preset["passport 0"] = {} 37 | preset["passport 0"]["email"] = "email_0@example.com" 38 | preset["passport 0"]["name"] = "name_0" 39 | preset["passport 0"]["service"] = "github.com" 40 | 41 | preset["passport 1"] = {} 42 | preset["passport 1"]["email"] = "email_1@example.com" 43 | preset["passport 1"]["name"] = "name_1" 44 | preset["passport 1"]["service"] = "gitlab.com" 45 | 46 | try: 47 | msg = """ 48 | No configuration file found ~/. 49 | Generating a sample configuration file. 50 | """ 51 | 52 | print(util.dedented(msg, "strip")) 53 | 54 | with open(filename, "w") as configfile: 55 | preset.write(configfile) 56 | return False 57 | 58 | except Exception: 59 | raise 60 | 61 | finally: 62 | configfile.close() 63 | 64 | 65 | def validate_scheme(filename): 66 | """ Validate section and option names of a provided configuration file. 67 | Quit the script and tell the user if we find false names. 68 | 69 | Args: 70 | filename (str): The complete `filepath` of the configuration file 71 | 72 | Returns: 73 | True (bool): If the configfile contains valid sections and options 74 | False (bool): If the configfile contains false sections or options 75 | """ 76 | raw_config = configparser.ConfigParser() 77 | raw_config.read(filename) 78 | 79 | pattern_section = r"^(passport)\s[0-9]+$" 80 | whitelist_sections = frozenset([ 81 | "general", 82 | "passport" 83 | ]) 84 | 85 | whitelist_options = frozenset([ 86 | "email", 87 | "enable_hook", 88 | "name", 89 | "service", 90 | "sleep_duration" 91 | ]) 92 | 93 | # Create sets containing non-whitelisted section and option names 94 | false_sections = set([ 95 | section 96 | for section in raw_config.sections() 97 | if section not in whitelist_sections 98 | if not re.match(pattern_section, section) 99 | ]) 100 | 101 | false_options = set([ 102 | option 103 | for section in raw_config.sections() 104 | for option in raw_config.options(section) 105 | if option not in whitelist_options 106 | ]) 107 | 108 | # Quit if we have wrong section names 109 | if len(false_sections): 110 | msg = """ 111 | E > Configuration > Invalid sections: 112 | >>> {} 113 | 114 | Allowed sections (Passport sections scheme: "passport 0"): 115 | >>> {} 116 | """.format( 117 | ", ".join(false_sections), 118 | ", ".join(whitelist_sections) 119 | ) 120 | 121 | print(util.dedented(msg, "strip")) 122 | return False 123 | 124 | # Quit if we have wrong option names 125 | if len(false_options): 126 | msg = """ 127 | E > Configuration > Invalid options: 128 | >>> {} 129 | 130 | Allowed options: 131 | >>> {} 132 | """.format( 133 | ", ".join(false_options), 134 | ", ".join(whitelist_options) 135 | ) 136 | 137 | print(util.dedented(msg, "strip")) 138 | return False 139 | 140 | return True 141 | 142 | 143 | def validate_values(filename): 144 | """ Validate certain values of a provided configuration file. 145 | Quit the script and tell the user if we find false values. 146 | 147 | Values to be validated: 148 | email: E-Mail scheme 149 | sleep_duration: Float 150 | enable_hook: Boolean 151 | 152 | Args: 153 | filename (str): The complete `filepath` of the configuration file 154 | 155 | Returns: 156 | True (bool): If the configfile contains valid values 157 | False (bool): If the configfile contains invalid values 158 | """ 159 | def filter_email(config): 160 | pattern_section = r"^(passport)\s[0-9]+$" 161 | pattern_email = r"[^@]+@[^@]+\.[^@]+" 162 | for section in config.sections(): 163 | if re.match(pattern_section, section): 164 | email = config.get(section, "email") 165 | if not re.match(pattern_email, email): 166 | yield email 167 | 168 | raw_config = configparser.ConfigParser() 169 | raw_config.read(filename) 170 | 171 | false_email = set(filter_email(raw_config)) 172 | 173 | # Quit if we have wrong email addresses 174 | if len(false_email): 175 | msg = """ 176 | E > Configuration > Invalid email address: 177 | >>> {} 178 | """.format(", ".join(false_email)) 179 | 180 | print(util.dedented(msg, "strip")) 181 | return False 182 | 183 | # Quit if we have wrong boolean values 184 | try: 185 | raw_config.getboolean("general", "enable_hook") 186 | except ValueError: 187 | msg = "E > Configuration > enable_hook: Expecting True or False." 188 | 189 | print(msg) 190 | return False 191 | 192 | # Quit if we have wrong float values 193 | try: 194 | raw_config.getfloat("general", "sleep_duration") 195 | except ValueError: 196 | msg = "E > Configuration > sleep_duration: Expecting float or number." 197 | 198 | print(msg) 199 | return False 200 | 201 | return True 202 | 203 | 204 | def release(filename): 205 | """ Read a provided configuration file and «import» sections and their 206 | validated keys/values into a dictionary. 207 | 208 | Args: 209 | filename (str): The complete `filepath` of the configuration file 210 | 211 | Returns: 212 | config (dict): Contains all allowed configuration sections 213 | """ 214 | def passport(config): 215 | pattern_section = r"^(passport)\s[0-9]+$" 216 | for passport in config.items(): 217 | if re.match(pattern_section, passport[0]): 218 | yield dict(passport[1]) 219 | 220 | raw_config = configparser.ConfigParser() 221 | raw_config.read(filename) 222 | 223 | config = {} 224 | config["enable_hook"] = raw_config.getboolean("general", "enable_hook") 225 | config["sleep_duration"] = raw_config.getfloat("general", "sleep_duration") 226 | config["git_passports"] = dict(enumerate(passport(raw_config))) 227 | 228 | return config 229 | 230 | 231 | def add_global_id(config, target): 232 | """ If available add the global Git ID as a fallback ID to a 233 | dictionary containing potential preselected candidates. 234 | 235 | Args: 236 | config (dict): Contains validated configuration options 237 | target (dict): Contains preselected local Git IDs 238 | 239 | Returns: 240 | True (bool): If a global Git ID could be found 241 | False (bool): If a global Git ID could not be found 242 | """ 243 | global_email = git.config_get(config, "global", "email") 244 | global_name = git.config_get(config, "global", "name") 245 | local_passports = config["git_passports"] 246 | 247 | if global_email and global_name: 248 | position = len(local_passports) 249 | target[position] = {} 250 | target[position]["email"] = global_email 251 | target[position]["name"] = global_name 252 | target[position]["flag"] = "global" 253 | else: 254 | msg = """ 255 | ~Note 256 | Tried to add your global Git ID as a passport candidate but 257 | couldn't find one. 258 | Consider to setup a global Git ID in order to get it listed 259 | as a fallback passport. 260 | """ 261 | 262 | print(util.dedented(msg, "lstrip")) 263 | return False 264 | 265 | return True 266 | -------------------------------------------------------------------------------- /passport/dialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # ..................................................................... Imports 5 | import sys 6 | 7 | from . import util 8 | 9 | 10 | # ............................................................ Dialog functions 11 | def get_input(pool): 12 | """ Prompt a user to select a number from a list of numbers representing 13 | available Git IDs. Optionally the user can choose `q` to quit the 14 | selection process. 15 | 16 | Args: 17 | pool (list): A list of numbers representing available Git IDs 18 | 19 | Returns: 20 | None (NoneType): If the user quits the selection dialog 21 | selection (int): A number representing a Git ID chosen by a user 22 | """ 23 | while True: 24 | # Redirect sys.stdin to an open filehandle from which input() 25 | # is able to read 26 | sys.stdin = open("/dev/tty") 27 | selection = input("» Select an [ID] or enter «(q)uit» to exit: ") 28 | 29 | try: 30 | selection = int(selection) 31 | 32 | if selection in pool: 33 | return selection 34 | 35 | except ValueError: 36 | if selection == "q" or selection == "quit": 37 | return None 38 | continue 39 | 40 | # Reset sys.stdin to its default value, even if we return early 41 | # when an exception occurs 42 | finally: 43 | sys.stdin = sys.__stdin__ 44 | 45 | 46 | def print_choice(choice): 47 | """ Before showing the actual prompt by calling `get_user_input()` print a 48 | list of available Git IDs containing properties ID, «scope», name, 49 | email and service. 50 | 51 | Args: 52 | choice (dict): Contains a list of preselected Git ID candidates 53 | 54 | Returns: 55 | True (bool): On success 56 | """ 57 | for key, value in choice.items(): 58 | if value.get("flag") == "global": 59 | msg = """ 60 | ~:Global ID: {} 61 | . User: {} 62 | . E-Mail: {} 63 | """.format( 64 | key, 65 | value["name"], 66 | value["email"] 67 | ) 68 | 69 | print(util.dedented(msg, "lstrip")) 70 | elif value.get("service"): 71 | msg = """ 72 | ~Passport ID: {} 73 | . User: {} 74 | . E-Mail: {} 75 | . Service: {} 76 | """.format( 77 | key, 78 | value["name"], 79 | value["email"], 80 | value["service"] 81 | ) 82 | 83 | print(util.dedented(msg, "lstrip")) 84 | else: 85 | msg = """ 86 | ~:Passport ID: {} 87 | . User: {} 88 | . E-Mail: {} 89 | """.format( 90 | key, 91 | value["name"], 92 | value["email"] 93 | ) 94 | 95 | print(util.dedented(msg, "lstrip")) 96 | 97 | return True 98 | -------------------------------------------------------------------------------- /passport/git.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # ..................................................................... Imports 5 | import subprocess 6 | 7 | 8 | # ............................................................... Git functions 9 | def infected(): 10 | """ Checks if the current directory is under Git version control. 11 | 12 | Returns: 13 | True (bool): If the current directory is a Git repository 14 | False (bool): If the current directory is not a Git repository 15 | 16 | Raises: 17 | Exception: If subprocess.Popen() fails 18 | """ 19 | try: 20 | git_process = subprocess.Popen([ 21 | "git", 22 | "rev-parse", 23 | "--is-inside-work-tree" 24 | ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 25 | 26 | # Captures the git return code 27 | exit_status = git_process.wait() 28 | 29 | if exit_status == 0: 30 | return True 31 | elif exit_status == 128: 32 | msg = "The current directory does not seem to be a Git repository." 33 | 34 | print(msg) 35 | return False 36 | 37 | except Exception: 38 | raise 39 | 40 | 41 | def config_get(config, scope, property): 42 | """ Get the email address, username of the global or local Git ID. 43 | Also gets the local remote.origin.url of a Git repository. 44 | 45 | Args: 46 | config (dict): Contains validated configuration options 47 | scope (str): Search inside a `global` or `local` scope 48 | property (str): Type of `email` or `name` or `url` 49 | 50 | Returns: 51 | value (str): A name, email address or url 52 | 53 | Raises: 54 | Exception: If subprocess.Popen() fails 55 | """ 56 | git_args = "remote.origin.url" if property == "url" else "user." + property 57 | 58 | try: 59 | git_process = subprocess.Popen([ 60 | "git", 61 | "config", 62 | "--get", 63 | "--" + scope, 64 | git_args 65 | ], stdout=subprocess.PIPE) 66 | 67 | value = git_process.communicate()[0].decode("utf-8") 68 | return value.replace("\n", "") 69 | 70 | except Exception: 71 | raise 72 | 73 | 74 | def config_set(config, value, property): 75 | """ Set the email address or username as a local Git ID for a repository. 76 | 77 | Args: 78 | config (dict): Contains validated configuration options 79 | value (str): A name or email address 80 | property (str): Type of `email` or `name` 81 | 82 | Returns: 83 | True (bool): On success 84 | 85 | Raises: 86 | Exception: If subprocess.Popen() fails 87 | """ 88 | try: 89 | subprocess.Popen([ 90 | "git", 91 | "config", 92 | "--local", 93 | "user." + property, 94 | value 95 | ], stdout=subprocess.PIPE) 96 | 97 | except Exception: 98 | raise 99 | 100 | return True 101 | 102 | 103 | def config_remove(verbose=True): 104 | """ Remove an existing Git identity. 105 | 106 | Returns: 107 | True (bool): On success 108 | 109 | Raises: 110 | Exception: If subprocess.Popen() fails 111 | """ 112 | try: 113 | git_process = subprocess.Popen([ 114 | "git", 115 | "config", 116 | "--local", 117 | "--remove-section", 118 | "user" 119 | ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 120 | 121 | # Captures the git return code 122 | exit_status = git_process.wait() 123 | 124 | if verbose: 125 | if exit_status == 0: 126 | msg = "Passport removed." 127 | elif exit_status == 128: 128 | msg = "No passport set." 129 | 130 | print(msg) 131 | 132 | except Exception: 133 | raise 134 | 135 | return True 136 | -------------------------------------------------------------------------------- /passport/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | # ..................................................................... Imports 5 | import textwrap 6 | 7 | 8 | # ........................................................... Utility functions 9 | def dedented(message, strip_type): 10 | """ Dedents a multiline string and strips leading (lstrip) 11 | or leading and trailing characters (strip). 12 | 13 | Args: 14 | message (str): An arbitrary string 15 | strip_type (str): Defines the type of strip() 16 | 17 | Returns: 18 | string (str): A stripped and dedented string 19 | """ 20 | if strip_type == "strip": 21 | string = textwrap.dedent(message).strip() 22 | elif strip_type == "lstrip": 23 | string = textwrap.dedent(message).lstrip() 24 | 25 | return string 26 | --------------------------------------------------------------------------------