From f2a22d7461b628390e7d418db43b7ca090e47c7f Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.dos.anjos@gmail.com>
Date: Thu, 14 Feb 2019 14:01:15 +0100
Subject: [PATCH] [scripts][commitfile] New functionality to create
 merge-requests and commit changes to packages

---
 bob/devtools/release.py            | 62 ++++++++++++++++++++--
 bob/devtools/scripts/commitfile.py | 84 ++++++++++++++++++++++++++++++
 conda/meta.yaml                    |  1 +
 setup.py                           |  1 +
 4 files changed, 144 insertions(+), 4 deletions(-)
 create mode 100644 bob/devtools/scripts/commitfile.py

diff --git a/bob/devtools/release.py b/bob/devtools/release.py
index a26b04a6..a1cc6cdd 100644
--- a/bob/devtools/release.py
+++ b/bob/devtools/release.py
@@ -228,8 +228,62 @@ def update_tag_comments(gitpkg, tag_name, tag_comments_list, dry_run=False):
     return tag
 
 
-def commit_files(gitpkg, files_dict, message='Updated files', dry_run=False):
-    """Commit files of a given GitLab package.
+def update_files_with_mr(gitpkg, files_dict, message, branch, automerge,
+    dry_run):
+    """Update (via a commit) files of a given gitlab package, through an MR
+
+    This function can update a file in a gitlab package, but will do this
+    through a formal merge request.
+
+    Args:
+
+        gitpkg: gitlab package object
+        files_dict: Dictionary of file names and their contents (as text)
+        message: Commit message
+        branch: The branch name to use for the merge request
+        automerge: If we should set the "merge if build suceeds" flag on the
+          created MR
+        dry_run: If True, nothing will be pushed to gitlab
+
+    """
+
+    data = {
+        'branch': branch,
+        'start_branch': 'master',
+        'commit_message': message,
+        'actions': []
+    }
+
+    # add files to update
+    for filename in files_dict.keys():
+        update_action = dict(action='update', file_path=filename)
+        update_action['content'] = files_dict[filename]
+        data['actions'].append(update_action)
+
+    logger.debug("Committing changes in files (%s) to new branch '%s'",
+        ', '.join(files_dict.keys()), branch)
+    if not dry_run:
+        commit = gitpkg.commits.create(data)
+
+    logger.debug("Creating merge request %s -> master", branch)
+    logger.debug("Set merge-when-pipeline-succeeds = %s", automerge)
+    if not dry_run:
+        mr = project.mergerequests.create({
+          'source_branch': branch,
+          'target_branch': 'master',
+          'title': message,
+          })
+        accept = {
+            'merge_when_pipeline_succeeds': 'true' if automerge else 'false',
+            'should_remove_source_branch': 'true',
+            }
+        mr.merge(accept)
+
+
+
+def update_files_at_master(gitpkg, files_dict, message, dry_run):
+    """Update (via a commit) files of a given gitlab package, directly on the
+    master branch.
 
     Args:
 
@@ -398,7 +452,7 @@ def release_package(gitpkg, tag_name, tag_comments_list, dry_run=False):
     readme_content = readme_file.decode().decode()
     readme_content = _update_readme(readme_content, version_number)
     # commit and push changes
-    commit_files(gitpkg,
+    update_files_at_master(gitpkg,
         {
           'README.rst': readme_content,
           'version.txt': version_number
@@ -427,7 +481,7 @@ def release_package(gitpkg, tag_name, tag_comments_list, dry_run=False):
     major, minor, patch = version_number.split('.')
     version_number = '{}.{}.{}b0'.format(major, minor, int(patch)+1)
     # commit and push changes
-    commit_files(gitpkg, {
+    update_files_at_master(gitpkg, {
       'README.rst': readme_content,
       'version.txt': version_number,
       },
diff --git a/bob/devtools/scripts/commitfile.py b/bob/devtools/scripts/commitfile.py
new file mode 100644
index 00000000..c1e9cae3
--- /dev/null
+++ b/bob/devtools/scripts/commitfile.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+import os
+import logging
+logger = logging.getLogger(__name__)
+
+import click
+
+from . import bdt
+from ..log import verbosity_option
+from ..release import get_gitlab_instance, update_files_with_mr
+
+
+@click.command(epilog='''
+Examples:
+
+  1. Replaces the README.rst file on the package bob/bob.extension, through a merge-request, using the contents of the local file with the same name:
+
+     $ bdt commitfile -vv bob/bob.extension README.rst
+
+
+  2. Replaces the README.rst file on the package beat/beat.core, specifying a commit/merge-request message:
+
+\b
+     $ bdt commitfile -vv --message="[readme] Update [ci skip]" beat/beat.core README.rst
+
+
+  3. Replaces the file conda/meta.yaml on the package bob/bob.blitz through a merge request, specifying a commit/merge-request message, using the contents of the local file new.yaml, set merge-when-pipeline-succeeds, and the name of the branch to be creatd:
+
+\b
+     $ bdt commitfile -vv bob/bob.blitz --path=conda/meta.yaml --branch=conda-changes --auto-merge new.yaml
+
+''')
+@click.argument('package')
+@click.argument('file', type=click.Path(file_okay=True, dir_okay=False,
+  exists=True))
+@click.option('-m', '--message',
+    help='Message to set for this commit',)
+@click.option('-p', '--path',
+    help='Which path to replace on the remote package',)
+@click.option('-b', '--branch',
+    help='Name of the branch to create for this MR',)
+@click.option('-a', '--auto-merge/--no-auto-merge', default=False,
+    help='If set, then the created merge request will be merged when ' \
+        'a potentially associated pipeline succeeds')
+@click.option('-d', '--dry-run/--no-dry-run', default=False,
+    help='Only goes through the actions, but does not execute them ' \
+        '(combine with the verbosity flags - e.g. ``-vvv``) to enable ' \
+        'printing to help you understand what will be done')
+@verbosity_option()
+@bdt.raise_on_error
+def commitfile(package, message, file, path, branch, auto_merge, dry_run):
+    """Changes a file on a given package, directly on the master branch
+    """
+
+    if '/' not in package:
+        raise RuntimeError('PACKAGE should be specified as "group/name"')
+
+    gl = get_gitlab_instance()
+
+    # we lookup the gitlab package once
+    use_package = gl.projects.get(package)
+    logger.info('Found gitlab project %s (id=%d)',
+        use_package.attributes['path_with_namespace'], use_package.id)
+
+    # if we are in a dry-run mode, let's let it be known
+    if dry_run:
+        logger.warn('!!!! DRY RUN MODE !!!!')
+        logger.warn('Nothing is being committed to Gitlab')
+
+    path = path or file
+
+    # load file contents
+    with open(file, 'rt') as f:
+      contents = f.read()
+
+    components = os.path.splitext(path)[0].split(os.sep)
+    branch = 'update-%s' % components[-1].lower()
+    message = message or ("[%s] update" % \
+        ''.join(['[%s]' % k for k in components]))
+
+    # commit and push changes
+    update_files_with_mr(use_package, {path: contents}, message, branch,
+      auto_merge, dry_run)
diff --git a/conda/meta.yaml b/conda/meta.yaml
index b99a1a9b..09cd327e 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -59,6 +59,7 @@ test:
     - bdt --help
     - bdt lasttag --help
       #- bdt lasttag -vv bob/bob.devtools
+    - bdt commitfile --help
     - bdt changelog --help
       #- bdt changelog -vv bob/bob.devtools changelog.md
     - bdt release --help
diff --git a/setup.py b/setup.py
index 3b9dca20..d1e0f1ab 100644
--- a/setup.py
+++ b/setup.py
@@ -47,6 +47,7 @@ setup(
         'bdt.cli': [
           'release = bob.devtools.scripts.release:release',
           'new = bob.devtools.scripts.new:new',
+          'commitfile = bob.devtools.scripts.commitfile:commitfile',
           'changelog = bob.devtools.scripts.changelog:changelog',
           'lasttag = bob.devtools.scripts.lasttag:lasttag',
           'visibility = bob.devtools.scripts.visibility:visibility',
-- 
GitLab