diff --git a/conda_smithy/cli.py b/conda_smithy/cli.py index ffce6220a..8822cfe5e 100755 --- a/conda_smithy/cli.py +++ b/conda_smithy/cli.py @@ -192,16 +192,52 @@ def __init__(self, parser): scp = self.subcommand_parser scp.add_argument("--feedstock_directory", default=os.getcwd(), help="The directory of the feedstock git repository.") + scp.add_argument("-c", "--commit", nargs='?', choices=["edit", "auto"], const="edit", + help="Whether to setup a commit or not.") def __call__(self, args): try: configure_feedstock.main(args.feedstock_directory) - print("\nCI support files regenerated. These need to be pushed to github!") - print("\nYou can commit the changes with:\n\n" - " git add -A\n" - " git commit -m 'MNT: Re-rendered with conda-smithy %s'" % __version__) + print("\nRe-rendered with conda-smithy %s'." % __version__) + + is_git_repo = os.path.exists(os.path.join(args.feedstock_directory, ".git")) + if is_git_repo: + has_staged_changes = subprocess.call( + [ + "git", "diff", "--cached", "--quiet", "--exit-code" + ], + cwd=args.feedstock_directory + ) + if has_staged_changes: + if args.commit: + git_args = [ + 'git', + 'commit', + '-m', + 'MNT: Re-rendered with conda-smithy %s' % __version__ + ] + if args.commit == "edit": + git_args += [ + '--edit', + '--status', + '--verbose' + ] + subprocess.check_call( + git_args, + cwd=args.feedstock_directory + ) + else: + print( + "\nYou can commit the changes with:\n\n" + " git commit -m 'MNT: Re-rendered with conda-smithy %s'\n" % __version__ + ) + print("\nThese changes need to be pushed to github!\n") + else: + print("\nNo changes made. This feedstock is up-to-date.\n") except RuntimeError as e: print(e) + except subprocess.CalledProcessError as e: + print(e) class RecipeLint(Subcommand): diff --git a/conda_smithy/configure_feedstock.py b/conda_smithy/configure_feedstock.py index 9633279a5..5009558c1 100755 --- a/conda_smithy/configure_feedstock.py +++ b/conda_smithy/configure_feedstock.py @@ -21,6 +21,14 @@ from conda_build_all.resolved_distribution import ResolvedDistribution from jinja2 import Environment, FileSystemLoader +from conda_smithy.feedstock_io import ( + get_mode_file, + set_mode_file, + write_file, + remove_file, + copy_file, +) + conda_forge_content = os.path.abspath(os.path.dirname(__file__)) @@ -55,8 +63,7 @@ def render_run_docker_build(jinja_env, forge_config, forge_dir): os.path.join(forge_dir, 'ci_support', 'checkout_merge_commit.sh'), ] for each_target_fname in target_fnames: - if os.path.exists(each_target_fname): - os.remove(each_target_fname) + remove_file(each_target_fname) else: forge_config["circle"]["enabled"] = True matrix = prepare_matrix_for_env_vars(matrix) @@ -91,7 +98,7 @@ def render_run_docker_build(jinja_env, forge_config, forge_dir): template = jinja_env.get_template(template_name) target_fname = os.path.join(forge_dir, 'ci_support', 'run_docker_build.sh') - with open(target_fname, 'w') as fh: + with write_file(target_fname) as fh: fh.write(template.render(**forge_config)) # Fix permissions. @@ -100,12 +107,11 @@ def render_run_docker_build(jinja_env, forge_config, forge_dir): os.path.join(forge_dir, 'ci_support', 'checkout_merge_commit.sh'), ] for each_target_fname in target_fnames: - if os.path.exists(each_target_fname): - st = os.stat(each_target_fname) - os.chmod( - each_target_fname, - st.st_mode | stat.S_IXOTH | stat.S_IXGRP | stat.S_IXUSR - ) + mode = get_mode_file(each_target_fname) + set_mode_file( + each_target_fname, + mode | stat.S_IXOTH | stat.S_IXGRP | stat.S_IXUSR + ) def render_circle(jinja_env, forge_config, forge_dir): @@ -126,7 +132,7 @@ def render_circle(jinja_env, forge_config, forge_dir): matrix = prepare_matrix_for_env_vars(matrix) forge_config = update_matrix(forge_config, matrix) template = jinja_env.get_template('circle.yml.tmpl') - with open(target_fname, 'w') as fh: + with write_file(target_fname) as fh: fh.write(template.render(**forge_config)) @@ -181,21 +187,20 @@ def render_travis(jinja_env, forge_config, forge_dir): # There are no cases to build (not even a case without any special # dependencies), so remove the .travis.yml if it exists. forge_config["travis"]["enabled"] = False - if os.path.exists(target_fname): - os.remove(target_fname) + remove_file(target_fname) else: forge_config["travis"]["enabled"] = True matrix = prepare_matrix_for_env_vars(matrix) forge_config = update_matrix(forge_config, matrix) template = jinja_env.get_template('travis.yml.tmpl') - with open(target_fname, 'w') as fh: + with write_file(target_fname) as fh: fh.write(template.render(**forge_config)) def render_README(jinja_env, forge_config, forge_dir): template = jinja_env.get_template('README.md.tmpl') target_fname = os.path.join(forge_dir, 'README.md') - with open(target_fname, 'w') as fh: + with write_file(target_fname) as fh: fh.write(template.render(**forge_config)) @@ -284,14 +289,13 @@ def render_appveyor(jinja_env, forge_config, forge_dir): # There are no cases to build (not even a case without any special # dependencies), so remove the appveyor.yml if it exists. forge_config["appveyor"]["enabled"] = False - if os.path.exists(target_fname): - os.remove(target_fname) + remove_file(target_fname) else: forge_config["appveyor"]["enabled"] = True matrix = prepare_matrix_for_env_vars(matrix) forge_config = update_matrix(forge_config, matrix) template = jinja_env.get_template('appveyor.yml.tmpl') - with open(target_fname, 'w') as fh: + with write_file(target_fname) as fh: fh.write(template.render(**forge_config)) @@ -345,7 +349,7 @@ def copytree(src, dst, ignore=(), root_dst=None): os.makedirs(d) copytree(s, d, ignore, root_dst=root_dst) else: - shutil.copy2(s, d) + copy_file(s, d) def copy_feedstock_content(forge_dir): @@ -411,9 +415,7 @@ def main(forge_file_directory): os.path.join('ci_support', 'upload_or_check_non_existence.py'), ] for old_file in old_files: - fpath = os.path.join(forge_dir, old_file) - if os.path.exists(fpath): - os.remove(fpath) + remove_file(os.path.join(forge_dir, old_file)) forge_yml = os.path.join(forge_dir, "conda-forge.yml") if not os.path.exists(forge_yml): diff --git a/conda_smithy/feedstock_io.py b/conda_smithy/feedstock_io.py new file mode 100644 index 000000000..bda427db5 --- /dev/null +++ b/conda_smithy/feedstock_io.py @@ -0,0 +1,74 @@ +from contextlib import contextmanager +import os +import shutil + + +def get_repo(path, search_parent_directories=True): + repo = None + try: + import git + repo = git.Repo( + path, + search_parent_directories=search_parent_directories + ) + except ImportError: + pass + except git.InvalidGitRepositoryError: + pass + + return repo + + +def get_file_blob(repo, filename): + idx = repo.index + rel_filepath = os.path.relpath(filename, repo.working_dir) + blob = idx.iter_blobs(lambda _: _[1].path == rel_filepath).next()[1] + return blob + + +def get_mode_file(filename): + repo = get_repo(filename) + if repo: + blob = get_file_blob(repo, filename) + mode = blob.mode + else: + mode = os.stat(filename).st_mode + + return mode + + +def set_mode_file(filename, mode): + repo = get_repo(filename) + if repo: + blob = get_file_blob(repo, filename) + blob.mode |= mode + repo.index.add([blob]) + + os.chmod(filename, mode) + + +@contextmanager +def write_file(filename): + with open(filename, "w") as fh: + yield fh + + repo = get_repo(filename) + if repo: + repo.index.add([filename]) + + +def remove_file(filename): + if os.path.exists(filename): + repo = get_repo(filename) + if repo: + repo.index.remove([filename]) + + os.remove(filename) + + +def copy_file(src, dst): + shutil.copy2(src, dst) + + repo = get_repo(dst) + if repo: + repo.index.add([dst])