def configify_file_name(path): ''' Given a path, name it and its config file appropriately for our config naming scheme. If no config file is necessary, None is used instead. Returns a tuple of the new name and the config file name. ''' base, name = os.path.split(path) # determine whether our path name is clean or not is_clean = not (name.startswith(constants.DOT_CHARACTER) or constants.MACHINE_SEPARATOR_CHARACTER in name) # always un-hide the name name = util.toggle_hidden(name, False) config_file_name = None if is_clean: # add our dot character if the file was originally hidden if util.is_hidden(path): name = constants.DOT_CHARACTER + name else: # otherwise, set the config file to the hidden version of the name config_file_name = util.toggle_hidden(name, True) configified_path = os.path.join(base, name) config_file_path = None if config_file_name is not None: config_file_path = os.path.join(base, config_file_name) return (configified_path, config_file_path)
def parse_file_config(path, dest): ''' Parse the given file name and return a normalized config. Naming Rules ---- * First leading '_' is replaced with a '.'. * When split on '@', every part excluding the first is treated as a machine id on which to link the given file. * Any file named the same as the local file preceded by a '.' is treated as a special config file for that local file. * Any of these rules may be combined. * A config file overrides all name modifiers, no matter what. * Any file that contains the '@' character or a leading '_' will need to have a compatible name and a corresponding config file to work. The rationale is that this is a very uncommon situation, so we don't bother accomodating it. ''' # get the file name for the given path raw_name = os.path.basename(path) # determine if we need to add a leading dot to the destination, then remove it # from the name if present. is_hidden = raw_name[0] == constants.DOT_CHARACTER if is_hidden: raw_name = raw_name[1:] # attempt to find machine ids parts = raw_name.split(constants.MACHINE_SEPARATOR_CHARACTER) # build the final base name name = util.toggle_hidden(parts[0], is_hidden) # get the remaining machine ids machine_ids = parts[1:] config = { 'paths': [name], 'machines': machine_ids, } return normalize_file_config(config, dest)
def get_config_path(path): '''Return the config file name for a given path.''' base, name = os.path.split(path) hidden_name = util.toggle_hidden(name, True) return os.path.join(base, hidden_name)
def manage(conf, args): ''' Move a file to the base directory and leave a link pointing to its new location in its place. ''' # bail if the file is already a link if os.path.islink(args.path): raise ValueError('Unable to manage ' + color.cyan(args.path) + " since it's already a link!") # make sure the path is a descendant of the destination directory if not util.is_descendant(args.path, conf['destination']): raise ValueError("Unable to manage files that aren't descendants of " + 'the destination directory (' + color.cyan(conf['destination']) + ')') # mark files that aren't direct descendants of the root as such unrooted = os.path.dirname(args.path) != conf['destination'] # get the path of the file if it will be copied into the repo directory dest_path = os.path.join(constants.REPO_DIR, os.path.basename(args.path)) # rename the file as appropriate to to its original name dest_path, config_file_path = config.configify_file_name(dest_path) # give unrooted files a config file path so they'll go to the correct place if unrooted and config_file_path is None: config_file_path = util.toggle_hidden(dest_path, True) # bail if the file is already managed and we're not overwriting dest_exists = os.path.exists(dest_path) config_exists = (config_file_path is not None and os.path.exists(config_file_path)) if (dest_exists or config_exists) and not args.force: raise ValueError("Can't manage " + color.cyan(args.path) + " since it already appears to be managed (use --force to override)") # create a file config if necessary # replace any existing dest file with a copy of the new one util.rm(dest_path, force=True) util.cp(args.path, dest_path, recursive=True) # replace any existing config file with our new one if config_file_path is not None: util.rm(config_file_path, force=True) # build a config for this file file_config = config.normalize_file_config({ 'paths': [args.path], }, conf['destination']) # create a config file from our config dict with open(config_file_path, 'w') as f: json.dump(file_config, f, indent=2) # create a link to the new location, overwriting the old file util.symlink(args.path, dest_path, overwrite=True) print(color.cyan(args.path), 'copied and linked') # add and commit the file to the repo if --save is specified if args.save: files = [color.cyan(os.path.basename(dest_path))] if config_file_path: files.append(color.cyan(os.path.basename(config_file_path))) files = files.join(' and ') print('Adding', files, 'to the repository...') # move us to the current repo directory so all git commands start there os.chdir(constants.REPO_DIR) # alert the user if we have uncommitted changes (git exits non-0 in this case) if git.diff(exit_code=True, quiet=True, _ok_code=(0, 1)).exit_code != 0: raise ValueError('The repository has uncommitted changes - the ' 'newly-managed file will have to be added to the repo manually.') # add the new files to the staging area git.add(dest_path) if config_file_path is not None: git.add(config_file_path) print('Successfully added', files, 'to the repository') print('Committing changes...') # commit the file to the repository commit_message = 'Manage %s' % os.path.basename(args.path) git.commit(m=commit_message, quiet=True) print('Commit successful!') print('Pushing committed changes...') # pull any changes down from upstream, then push our new addition git.pull(rebase=True, quiet=True) git.push(quiet=True) print('Push successful!')