From c4a7ad92c42570a3bdb06b4e08ac5cfc3084170e Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Thu, 9 Jul 2020 18:54:45 +0200
Subject: [PATCH 1/5] feat(jobs): make suggestions for suboptimal commit
 messages

---
 alpine-qa-bot/jobs.vala | 98 +++++++++++++++++++++++++++++++++++++++++
 data/suggestions.json   | 10 +++++
 meson.build             |  1 +
 tests/jobs_test.vala    | 19 +++++++-
 tests/server_test.vala  |  5 +++
 5 files changed, 132 insertions(+), 1 deletion(-)
 create mode 100644 data/suggestions.json

diff --git a/alpine-qa-bot/jobs.vala b/alpine-qa-bot/jobs.vala
index d508225..912e367 100644
--- a/alpine-qa-bot/jobs.vala
+++ b/alpine-qa-bot/jobs.vala
@@ -90,9 +90,14 @@ namespace AlpineQaBot {
                     error ("Unknown merge request action %s", root_obj.get_string_member ("action"));
                 }
             }
+
+            if (root_obj.has_member ("last_commit") && !root_obj.get_null_member ("last_commit")) {
+                this.commit = Commit.from_json_object ((!)root_obj.get_object_member ("last_commit"));
+            }
         }
 
         public MergeRequestAction? action { get; private set; }
+        public Commit? commit { get; private set; }
         public int64 id { get; private set; }
         public int64 iid { get; private set; }
         public MergeRequestState state { get; private set; }
@@ -100,6 +105,55 @@ namespace AlpineQaBot {
         public int64 target_project_id { get; private set; }
     }
 
+    public struct Commit {
+        public Commit (string id, string message) {
+            this.id = id;
+            this.message = message;
+        }
+
+        public Commit.from_json_object (Json.Object root_obj) {
+            this.id = root_obj.get_string_member ("id");
+            this.message = root_obj.get_string_member ("message");
+        }
+
+        public string id { get; private set; }
+        public string message { get; private set; }
+    }
+
+    public struct CommitSuggestion {
+        public CommitSuggestion (Gee.ArrayList<string> offenders, string suggestion) {
+            this.offenders = offenders;
+            this.suggestion = suggestion;
+        }
+
+        public CommitSuggestion.from_json_object (Json.Object root_obj) {
+            this.offenders = new Gee.ArrayList<string>();
+            var offenders_arr = root_obj.get_array_member ("offenders").get_elements ();
+            foreach (var offenders_element in offenders_arr) {
+                this.offenders.add (offenders_element.get_string ());
+            }
+            this.suggestion = root_obj.get_string_member ("suggestion");
+        }
+
+        public string? match (string commit_message) {
+            foreach (var offender in this.offenders) {
+                try {
+                    var regex = new GLib.Regex (offender);
+                    if (regex.match (commit_message)) {
+                        return this.suggestion;
+                    }
+                } catch (GLib.RegexError e) {
+                    warning ("Failed to compile Regex due to error %s", e.message);
+                }
+            }
+
+            return null;
+        }
+
+        public Gee.ArrayList<string> offenders;
+        public string suggestion { get; private set; }
+    }
+
     public abstract class Job : GLib.Object {
         protected Job (Project? project, string? gitlab_instance_url, string? api_authentication_token) {
             this.project = project;
@@ -235,6 +289,35 @@ namespace AlpineQaBot {
         }
 
         public override bool process (Soup.Session? default_soup_session = null) {
+            string? commit_message_suggestion = null;
+            try {
+                commit_message_suggestion = this.get_commit_message_suggestion ();
+            } catch (GLib.Error e) {
+                warning ("Failed to get a suggestion for an alternative commit message due to error %s", e.message);
+            }
+
+            if (commit_message_suggestion != null) {
+                var soup_session = default_soup_session ?? new Soup.Session ();
+                var query_url = "%s/api/v4/projects/%lld/merge_requests/%lld/notes".printf (this.gitlab_instance_url, this.project.id, this.merge_request.iid);
+                info ("Querying URL %s", query_url);
+                var soup_msg = new Soup.Message ("POST", query_url);
+                assert_nonnull (soup_msg);
+                soup_msg.request_headers.append ("Private-Token", this.api_authentication_token);
+                soup_msg.set_request ("application/json", Soup.MemoryUse.COPY, @"{\"body\": \"$commit_message_suggestion\"}".data);
+
+                try {
+                    soup_session.send (soup_msg);
+                } catch (GLib.Error e) {
+                    warning ("Failed to run REST API call: %s", e.message);
+                    return false;
+                }
+
+                if (soup_msg.status_code != 200) {
+                    warning ("Got HTTP status code %u back from gitlab. Response: %s", soup_msg.status_code, (string) soup_msg.response_body.data);
+                    return false;
+                }
+            }
+
             if (this.merge_request.state == MergeRequestState.Opened && this.merge_request.action == MergeRequestAction.Open) {
                 debug ("Querying Gitlab to allow commit from maintainers for MR %lld", this.merge_request.iid);
 
@@ -265,6 +348,21 @@ namespace AlpineQaBot {
             return true;
         }
 
+        public string? get_commit_message_suggestion (string? suggestion_file_path = null) throws GLib.Error {
+            var parser = new Json.Parser ();
+            parser.load_from_file (suggestion_file_path ?? "%s/suggestions.json".printf (Config.SYSCONFIG_DIR));
+
+            foreach (var commit_suggestion_obj in parser.get_root ().get_object ().get_array_member ("commit").get_elements ()) {
+                var commit_suggestion = CommitSuggestion.from_json_object ((!)commit_suggestion_obj.get_object ());
+                var match = commit_suggestion.match (this.merge_request.commit.message);
+                if (match != null) {
+                    return match;
+                }
+            }
+
+            return null;
+        }
+
         public MergeRequest merge_request { get; private set; }
     }
 }
diff --git a/data/suggestions.json b/data/suggestions.json
new file mode 100644
index 0000000..b69c8a3
--- /dev/null
+++ b/data/suggestions.json
@@ -0,0 +1,10 @@
+{
+    "commit": [
+        {
+            "offenders": [
+                ".*\/.*: update to.*"
+            ],
+            "suggestion": "$repository/$pkgname: upgrade to $pkgver"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/meson.build b/meson.build
index a83ba65..4e80cb6 100644
--- a/meson.build
+++ b/meson.build
@@ -11,6 +11,7 @@ deps = [
     dependency('libsoup-2.4', version: '>=2.46'),
     dependency('threads'),
     dependency('json-glib-1.0'),
+    dependency('gee-0.8'),
 ]
 
 test_deps = [
diff --git a/tests/jobs_test.vala b/tests/jobs_test.vala
index a220d80..68c0742 100644
--- a/tests/jobs_test.vala
+++ b/tests/jobs_test.vala
@@ -7,7 +7,7 @@
 
 Uhm.Server mock_server = null;
 TestLib.TestMode mock_serve_test_mode = TestLib.TestMode.Testing;
-const string MERGE_REQUEST_TEST_JSON = """{"object_kind":"merge_request","user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon"},"project":{"id":19765543,"name":"Gitlab Test","description":"Aut reprehenderit ut est.","web_url":"http://example.com/gitlabhq/gitlab-test","avatar_url":null,"git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git","git_http_url":"http://example.com/gitlabhq/gitlab-test.git","namespace":"GitlabHQ","visibility_level":20,"path_with_namespace":"gitlabhq/gitlab-test","default_branch":"master","homepage":"http://example.com/gitlabhq/gitlab-test","url":"http://example.com/gitlabhq/gitlab-test.git","ssh_url":"git@example.com:gitlabhq/gitlab-test.git","http_url":"http://example.com/gitlabhq/gitlab-test.git"},"repository":{"name":"Gitlab Test","url":"http://example.com/gitlabhq/gitlab-test.git","description":"Aut reprehenderit ut est.","homepage":"http://example.com/gitlabhq/gitlab-test"},"object_attributes":{"id":99,"target_branch":"master","source_branch":"ms-viewport","source_project_id":14,"author_id":51,"assignee_id":6,"title":"MS-Viewport","created_at":"2013-12-03T17:23:34Z","updated_at":"2013-12-03T17:23:34Z","milestone_id":null,"state":"opened","merge_status":"unchecked","target_project_id":14,"iid":9,"description":"","source":{"name":"Awesome Project","description":"Aut reprehenderit ut est.","web_url":"http://example.com/awesome_space/awesome_project","avatar_url":null,"git_ssh_url":"git@example.com:awesome_space/awesome_project.git","git_http_url":"http://example.com/awesome_space/awesome_project.git","namespace":"Awesome Space","visibility_level":20,"path_with_namespace":"awesome_space/awesome_project","default_branch":"master","homepage":"http://example.com/awesome_space/awesome_project","url":"http://example.com/awesome_space/awesome_project.git","ssh_url":"git@example.com:awesome_space/awesome_project.git","http_url":"http://example.com/awesome_space/awesome_project.git"},"target":{"name":"Awesome Project","description":"Aut reprehenderit ut est.","web_url":"http://example.com/awesome_space/awesome_project","avatar_url":null,"git_ssh_url":"git@example.com:awesome_space/awesome_project.git","git_http_url":"http://example.com/awesome_space/awesome_project.git","namespace":"Awesome Space","visibility_level":20,"path_with_namespace":"awesome_space/awesome_project","default_branch":"master","homepage":"http://example.com/awesome_space/awesome_project","url":"http://example.com/awesome_space/awesome_project.git","ssh_url":"git@example.com:awesome_space/awesome_project.git","http_url":"http://example.com/awesome_space/awesome_project.git"},"last_commit":{"id":"da1560886d4f094c3e6c9ef40349f7d38b5d27d7","message":"fixed readme","timestamp":"2012-01-03T23:36:29+02:00","url":"http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7","author":{"name":"GitLab dev user","email":"gitlabdev@dv6700.(none)"}},"work_in_progress":false,"url":"http://example.com/diaspora/merge_requests/1","action":"open","assignee":{"name":"User1","username":"user1","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon"}},"labels":[{"id":206,"title":"API","color":"#ffffff","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"API related issues","type":"ProjectLabel","group_id":41}],"changes":{"updated_by_id":{"previous":null,"current":1},"updated_at":{"previous":"2017-09-15 16:50:55 UTC","current":"2017-09-15 16:52:00 UTC"},"labels":{"previous":[{"id":206,"title":"API","color":"#ffffff","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"API related issues","type":"ProjectLabel","group_id":41}],"current":[{"id":205,"title":"Platform","color":"#123123","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"Platform related issues","type":"ProjectLabel","group_id":41}]}}}""";
+const string MERGE_REQUEST_TEST_JSON = """{"object_kind":"merge_request","user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon"},"project":{"id":19765543,"name":"Gitlab Test","description":"Aut reprehenderit ut est.","web_url":"http://example.com/gitlabhq/gitlab-test","avatar_url":null,"git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git","git_http_url":"http://example.com/gitlabhq/gitlab-test.git","namespace":"GitlabHQ","visibility_level":20,"path_with_namespace":"gitlabhq/gitlab-test","default_branch":"master","homepage":"http://example.com/gitlabhq/gitlab-test","url":"http://example.com/gitlabhq/gitlab-test.git","ssh_url":"git@example.com:gitlabhq/gitlab-test.git","http_url":"http://example.com/gitlabhq/gitlab-test.git"},"repository":{"name":"Gitlab Test","url":"http://example.com/gitlabhq/gitlab-test.git","description":"Aut reprehenderit ut est.","homepage":"http://example.com/gitlabhq/gitlab-test"},"object_attributes":{"id":99,"target_branch":"master","source_branch":"ms-viewport","source_project_id":14,"author_id":51,"assignee_id":6,"title":"MS-Viewport","created_at":"2013-12-03T17:23:34Z","updated_at":"2013-12-03T17:23:34Z","milestone_id":null,"state":"opened","merge_status":"unchecked","target_project_id":14,"iid":9,"description":"","source":{"name":"Awesome Project","description":"Aut reprehenderit ut est.","web_url":"http://example.com/awesome_space/awesome_project","avatar_url":null,"git_ssh_url":"git@example.com:awesome_space/awesome_project.git","git_http_url":"http://example.com/awesome_space/awesome_project.git","namespace":"Awesome Space","visibility_level":20,"path_with_namespace":"awesome_space/awesome_project","default_branch":"master","homepage":"http://example.com/awesome_space/awesome_project","url":"http://example.com/awesome_space/awesome_project.git","ssh_url":"git@example.com:awesome_space/awesome_project.git","http_url":"http://example.com/awesome_space/awesome_project.git"},"target":{"name":"Awesome Project","description":"Aut reprehenderit ut est.","web_url":"http://example.com/awesome_space/awesome_project","avatar_url":null,"git_ssh_url":"git@example.com:awesome_space/awesome_project.git","git_http_url":"http://example.com/awesome_space/awesome_project.git","namespace":"Awesome Space","visibility_level":20,"path_with_namespace":"awesome_space/awesome_project","default_branch":"master","homepage":"http://example.com/awesome_space/awesome_project","url":"http://example.com/awesome_space/awesome_project.git","ssh_url":"git@example.com:awesome_space/awesome_project.git","http_url":"http://example.com/awesome_space/awesome_project.git"},"last_commit":{"id":"da1560886d4f094c3e6c9ef40349f7d38b5d27d7","message":"testing/alpine-qa-bot: update to 0.2","timestamp":"2012-01-03T23:36:29+02:00","url":"http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7","author":{"name":"GitLab dev user","email":"gitlabdev@dv6700.(none)"}},"work_in_progress":false,"url":"http://example.com/diaspora/merge_requests/1","action":"open","assignee":{"name":"User1","username":"user1","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon"}},"labels":[{"id":206,"title":"API","color":"#ffffff","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"API related issues","type":"ProjectLabel","group_id":41}],"changes":{"updated_by_id":{"previous":null,"current":1},"updated_at":{"previous":"2017-09-15 16:50:55 UTC","current":"2017-09-15 16:52:00 UTC"},"labels":{"previous":[{"id":206,"title":"API","color":"#ffffff","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"API related issues","type":"ProjectLabel","group_id":41}],"current":[{"id":205,"title":"Platform","color":"#123123","project_id":14,"created_at":"2013-12-03T17:15:43Z","updated_at":"2013-12-03T17:15:43Z","template":false,"description":"Platform related issues","type":"ProjectLabel","group_id":41}]}}}""";
 const string PIPELINE_TEST_JSON = """{"object_kind":"pipeline","object_attributes":{"id":31,"ref":"master","tag":false,"sha":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","before_sha":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","source":"merge_request_event","status":"success","stages":["build","test","deploy"],"created_at":"2016-08-12 15:23:28 UTC","finished_at":"2016-08-12 15:26:29 UTC","duration":63,"variables":[{"key":"NESTOR_PROD_ENVIRONMENT","value":"us-west-1"}]},"merge_request":{"id":1,"iid":1,"title":"Test","source_branch":"test","source_project_id":1,"target_branch":"master","target_project_id":1,"state":"opened","merge_status":"can_be_merged","url":"http://192.168.64.1:3005/gitlab-org/gitlab-test/merge_requests/1"},"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon","email":"user_email@gitlab.com"},"project":{"id":1,"name":"Gitlab Test","description":"Atque in sunt eos similique dolores voluptatem.","web_url":"http://192.168.64.1:3005/gitlab-org/gitlab-test","avatar_url":null,"git_ssh_url":"git@192.168.64.1:gitlab-org/gitlab-test.git","git_http_url":"http://192.168.64.1:3005/gitlab-org/gitlab-test.git","namespace":"Gitlab Org","visibility_level":20,"path_with_namespace":"gitlab-org/gitlab-test","default_branch":"master"},"commit":{"id":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","message":"test\n","timestamp":"2016-08-12T17:23:21+02:00","url":"http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2","author":{"name":"User","email":"user@gitlab.com"}},"builds":[{"id":380,"stage":"deploy","name":"production","status":"skipped","created_at":"2016-08-12 15:23:28 UTC","started_at":null,"finished_at":null,"when":"manual","manual":true,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":null,"artifacts_file":{"filename":null,"size":null}},{"id":377,"stage":"test","name":"test-image","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:26:12 UTC","finished_at":null,"when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":378,"stage":"test","name":"test-build","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:26:12 UTC","finished_at":"2016-08-12 15:26:29 UTC","when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":376,"stage":"build","name":"build-image","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:24:56 UTC","finished_at":"2016-08-12 15:25:26 UTC","when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":379,"stage":"deploy","name":"staging","status":"created","created_at":"2016-08-12 15:23:28 UTC","started_at":null,"finished_at":null,"when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":null,"artifacts_file":{"filename":null,"size":null}}]}""";
 const string PIPELINE_TEST_JSON_MERGE_REQUEST_NULL = """{"object_kind":"pipeline","object_attributes":{"id":31,"ref":"master","tag":false,"sha":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","before_sha":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","source":"merge_request_event","status":"success","stages":["build","test","deploy"],"created_at":"2016-08-12 15:23:28 UTC","finished_at":"2016-08-12 15:26:29 UTC","duration":63,"variables":[{"key":"NESTOR_PROD_ENVIRONMENT","value":"us-west-1"}]},"merge_request": null,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon","email":"user_email@gitlab.com"},"project":{"id":1,"name":"Gitlab Test","description":"Atque in sunt eos similique dolores voluptatem.","web_url":"http://192.168.64.1:3005/gitlab-org/gitlab-test","avatar_url":null,"git_ssh_url":"git@192.168.64.1:gitlab-org/gitlab-test.git","git_http_url":"http://192.168.64.1:3005/gitlab-org/gitlab-test.git","namespace":"Gitlab Org","visibility_level":20,"path_with_namespace":"gitlab-org/gitlab-test","default_branch":"master"},"commit":{"id":"bcbb5ec396a2c0f828686f14fac9b80b780504f2","message":"test\n","timestamp":"2016-08-12T17:23:21+02:00","url":"http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2","author":{"name":"User","email":"user@gitlab.com"}},"builds":[{"id":380,"stage":"deploy","name":"production","status":"skipped","created_at":"2016-08-12 15:23:28 UTC","started_at":null,"finished_at":null,"when":"manual","manual":true,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":null,"artifacts_file":{"filename":null,"size":null}},{"id":377,"stage":"test","name":"test-image","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:26:12 UTC","finished_at":null,"when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":378,"stage":"test","name":"test-build","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:26:12 UTC","finished_at":"2016-08-12 15:26:29 UTC","when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":376,"stage":"build","name":"build-image","status":"success","created_at":"2016-08-12 15:23:28 UTC","started_at":"2016-08-12 15:24:56 UTC","finished_at":"2016-08-12 15:25:26 UTC","when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":{"id":380987,"description":"shared-runners-manager-6.gitlab.com","active":true,"is_shared":true},"artifacts_file":{"filename":null,"size":null}},{"id":379,"stage":"deploy","name":"staging","status":"created","created_at":"2016-08-12 15:23:28 UTC","started_at":null,"finished_at":null,"when":"on_success","manual":false,"allow_failure":false,"user":{"name":"Administrator","username":"root","avatar_url":"http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80&d=identicon"},"runner":null,"artifacts_file":{"filename":null,"size":null}}]}""";
 const string ONLINE_TEST_GITLAB_INSTANCE = "https://gitlab.com";
@@ -76,11 +76,27 @@ void test_merge_request_process () {
     }
 
     var soup_session = TestLib.get_test_soup_session (mock_server);
+
+    Test.expect_message (null, GLib.LogLevelFlags.LEVEL_WARNING, "*Failed to get a suggestion for an alternative commit message due to error Failed to open file*");
+
     merge_request_job.process (soup_session);
 
+    Test.assert_expected_messages ();
+
     mock_server.end_trace ();
 }
 
+void test_merge_request_commit_message_suggestion () {
+    try {
+        var job = new AlpineQaBot.MergeRequestJob.from_json (MERGE_REQUEST_TEST_JSON, "https://gitlab.com", "token");
+        assert (job != null);
+        info ("%s", job.get_commit_message_suggestion (Test.build_filename (Test.FileType.DIST, "../data/suggestions.json")));
+        assert (job.get_commit_message_suggestion (Test.build_filename (Test.FileType.DIST, "../data/suggestions.json")) == "$repository/$pkgname: upgrade to $pkgver");
+    } catch (Error e) {
+        assert (false);
+    }
+}
+
 public void main (string[] args) {
     Test.init (ref args);
 
@@ -91,5 +107,6 @@ public void main (string[] args) {
     Test.add_func ("/test/jobs/pipeline_job/from_json_merge_request_null", test_pipeline_job_from_json_merge_request_null);
     Test.add_func ("/test/jobs/merge_request_job/from_json", test_merge_request_job_from_json);
     Test.add_func ("/test/jobs/merge_request_job/process", test_merge_request_process);
+    Test.add_func ("/test/jobs/merge_request_job/commit_message_suggestion", test_merge_request_commit_message_suggestion);
     Test.run ();
 }
diff --git a/tests/server_test.vala b/tests/server_test.vala
index b35f171..a3aea2f 100644
--- a/tests/server_test.vala
+++ b/tests/server_test.vala
@@ -99,8 +99,13 @@ void test_server_merge_request_job_process () {
         }
         job.process (TestLib.get_test_soup_session (mock_server));
     });
+
+    Test.expect_message (null, GLib.LogLevelFlags.LEVEL_WARNING, "*Failed to get a suggestion for an alternative commit message due to error Failed to open file*");
+
     loop.run ();
 
+    Test.assert_expected_messages ();
+
     mock_server.end_trace ();
 }
 
-- 
GitLab


From 4d9d9793f7d10e40179c379544d8915c5adc2e10 Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Thu, 9 Jul 2020 19:11:31 +0200
Subject: [PATCH 2/5] refactor(jobs): add RequestSender for making gitlab API
 requests

---
 alpine-qa-bot/jobs.vala | 92 ++++++++++++++++++++---------------------
 1 file changed, 45 insertions(+), 47 deletions(-)

diff --git a/alpine-qa-bot/jobs.vala b/alpine-qa-bot/jobs.vala
index 912e367..1e88dd9 100644
--- a/alpine-qa-bot/jobs.vala
+++ b/alpine-qa-bot/jobs.vala
@@ -154,6 +154,39 @@ namespace AlpineQaBot {
         public string suggestion { get; private set; }
     }
 
+    public class RequestSender : GLib.Object {
+        public RequestSender (string query_url, string http_method, string api_authentication_token, uint8[]? data, Soup.Session? default_soup_session) {
+            this.soup_session = default_soup_session ?? new Soup.Session ();
+            this.soup_message = new Soup.Message (http_method, query_url);
+
+            info ("Querying URL %s", query_url);
+            assert_nonnull (soup_message);
+            soup_message.request_headers.append ("Private-Token", api_authentication_token);
+            if (data != null) {
+                soup_message.set_request ("application/json", Soup.MemoryUse.COPY, data);
+            }
+        }
+
+        public bool send () {
+            try {
+                soup_session.send (this.soup_message);
+            } catch (GLib.Error e) {
+                warning ("Failed to run REST API call: %s", e.message);
+                return false;
+            }
+
+            if (this.soup_message.status_code != 200) {
+                warning ("Got HTTP status code %u back from gitlab. Response: %s", this.soup_message.status_code, (string) this.soup_message.response_body.data);
+                return false;
+            }
+
+            return true;
+        }
+
+        private Soup.Session soup_session;
+        private Soup.Message soup_message;
+    }
+
     public abstract class Job : GLib.Object {
         protected Job (Project? project, string? gitlab_instance_url, string? api_authentication_token) {
             this.project = project;
@@ -239,27 +272,18 @@ namespace AlpineQaBot {
             if ((this.status == PipelineStatus.Failed || this.status == PipelineStatus.Success) && this.merge_request != null) {
                 debug ("Querying Gitlab to add/remove status:mr-build-broken label to successfull/failing MR");
 
-                var soup_session = default_soup_session ?? new Soup.Session ();
                 var query_url = "%s/api/v4/projects/%lld/merge_requests/%lld".printf (this.gitlab_instance_url, this.project.id, this.merge_request.iid);
-                info ("Querying URL %s", query_url);
-                var soup_msg = new Soup.Message ("PUT", query_url);
-                assert_nonnull (soup_msg);
-                soup_msg.request_headers.append ("Private-Token", this.api_authentication_token);
+
+                string message = null;
                 if (this.status == PipelineStatus.Failed) {
-                    soup_msg.set_request ("application/json", Soup.MemoryUse.COPY, "{\"add_labels\": [\"status:mr-build-broken\"]}".data);
+                    message = "{\"add_labels\": [\"status:mr-build-broken\"]}";
                 } else {
-                    soup_msg.set_request ("application/json", Soup.MemoryUse.COPY, "{\"remove_labels\": [\"status:mr-build-broken\"]}".data);
+                    message = "{\"remove_labels\": [\"status:mr-build-broken\"]}";
                 }
 
-                try {
-                    soup_session.send (soup_msg);
-                } catch (GLib.Error e) {
-                    warning ("Failed to run REST API call: %s", e.message);
-                    return false;
-                }
+                var request_sender = new RequestSender (query_url, "PUT", this.api_authentication_token, message.data, default_soup_session);
 
-                if (soup_msg.status_code != 200) {
-                    warning ("Got HTTP status code %u back from gitlab. Response: %s", soup_msg.status_code, (string) soup_msg.response_body.data);
+                if (!request_sender.send ()) {
                     return false;
                 }
             }
@@ -297,23 +321,11 @@ namespace AlpineQaBot {
             }
 
             if (commit_message_suggestion != null) {
-                var soup_session = default_soup_session ?? new Soup.Session ();
-                var query_url = "%s/api/v4/projects/%lld/merge_requests/%lld/notes".printf (this.gitlab_instance_url, this.project.id, this.merge_request.iid);
-                info ("Querying URL %s", query_url);
-                var soup_msg = new Soup.Message ("POST", query_url);
-                assert_nonnull (soup_msg);
-                soup_msg.request_headers.append ("Private-Token", this.api_authentication_token);
-                soup_msg.set_request ("application/json", Soup.MemoryUse.COPY, @"{\"body\": \"$commit_message_suggestion\"}".data);
-
-                try {
-                    soup_session.send (soup_msg);
-                } catch (GLib.Error e) {
-                    warning ("Failed to run REST API call: %s", e.message);
-                    return false;
-                }
+                debug ("Querying Gitlab to suggest better commit message for MR %lld", this.merge_request.iid);
 
-                if (soup_msg.status_code != 200) {
-                    warning ("Got HTTP status code %u back from gitlab. Response: %s", soup_msg.status_code, (string) soup_msg.response_body.data);
+                var query_url = "%s/api/v4/projects/%lld/merge_requests/%lld/notes".printf (this.gitlab_instance_url, this.project.id, this.merge_request.iid);
+                var request_sender = new RequestSender (query_url, "POST", this.api_authentication_token, @"{\"body\": \"$commit_message_suggestion\"}".data, default_soup_session);
+                if (!request_sender.send ()) {
                     return false;
                 }
             }
@@ -321,25 +333,11 @@ namespace AlpineQaBot {
             if (this.merge_request.state == MergeRequestState.Opened && this.merge_request.action == MergeRequestAction.Open) {
                 debug ("Querying Gitlab to allow commit from maintainers for MR %lld", this.merge_request.iid);
 
-                var soup_session = default_soup_session ?? new Soup.Session ();
                 var query_url = "%s/api/v4/projects/%lld/merge_requests/%lld".printf (this.gitlab_instance_url, this.project.id, this.merge_request.iid);
-                info ("Querying URL %s", query_url);
-                var soup_msg = new Soup.Message ("PUT", query_url);
-                assert_nonnull (soup_msg);
-                soup_msg.request_headers.append ("Private-Token", this.api_authentication_token);
                 // FIXME: Gitlab API doesn't know allow_collaboration is a valid parameter and wants us to specify at least one valid param,
                 // so we just specify an empty add_labels here.
-                soup_msg.set_request ("application/json", Soup.MemoryUse.COPY, "{\"add_labels\": null,\"allow_collaboration\": true}".data);
-
-                try {
-                    soup_session.send (soup_msg);
-                } catch (GLib.Error e) {
-                    warning ("Failed to run REST API call: %s", e.message);
-                    return false;
-                }
-
-                if (soup_msg.status_code != 200) {
-                    warning ("Got HTTP status code %u back from gitlab. Response: %s", soup_msg.status_code, (string) soup_msg.response_body.data);
+                var request_sender = new RequestSender (query_url, "PUT", this.api_authentication_token, "{\"add_labels\": null,\"allow_collaboration\": true}".data, default_soup_session);
+                if (!request_sender.send ()) {
                     return false;
                 }
             }
-- 
GitLab


From bc8b2f107edf7f4969e6ab4d59d081763beb6326 Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Fri, 10 Jul 2020 17:20:56 +0200
Subject: [PATCH 3/5] test(job): add tests for commit message suggestions

---
 tests/jobs_test.vala | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/tests/jobs_test.vala b/tests/jobs_test.vala
index 68c0742..5d365fd 100644
--- a/tests/jobs_test.vala
+++ b/tests/jobs_test.vala
@@ -97,6 +97,36 @@ void test_merge_request_commit_message_suggestion () {
     }
 }
 
+void commit_suggestion_test_all () {
+    var parser = new Json.Parser ();
+    try {
+        parser.load_from_file (Test.build_filename (Test.FileType.DIST, "../data/suggestions.json"));
+    } catch (GLib.Error e) {
+        error ("Failed to open suggestions file due to error %s", e.message);
+    }
+    var commit_suggestions = new Gee.ArrayList<AlpineQaBot.CommitSuggestion? >();
+
+    foreach (var commit_suggestion_obj in parser.get_root ().get_object ().get_array_member ("commit").get_elements ()) {
+        commit_suggestions.add (AlpineQaBot.CommitSuggestion.from_json_object ((!)commit_suggestion_obj.get_object ()));
+    }
+
+    var value_map = new Gee.HashMap<string, string>();
+    value_map.set ("testing/alpine-qa-bot: update to 0.2", "$repository/$pkgname: upgrade to $pkgver");
+
+    foreach (var bad_msg in value_map.keys) {
+        string suggestion = null;
+        foreach (var commit_suggestion in commit_suggestions) {
+            suggestion = commit_suggestion.match (bad_msg);
+            if (suggestion != null) {
+                break;
+            }
+        }
+
+        assert (value_map.get (bad_msg) == suggestion);
+    }
+
+}
+
 public void main (string[] args) {
     Test.init (ref args);
 
@@ -108,5 +138,6 @@ public void main (string[] args) {
     Test.add_func ("/test/jobs/merge_request_job/from_json", test_merge_request_job_from_json);
     Test.add_func ("/test/jobs/merge_request_job/process", test_merge_request_process);
     Test.add_func ("/test/jobs/merge_request_job/commit_message_suggestion", test_merge_request_commit_message_suggestion);
+    Test.add_func ("/test/jobs/commit_suggestion/test_all", commit_suggestion_test_all);
     Test.run ();
 }
-- 
GitLab


From fea804392effea0c4d6e373d4ce50de60af2e990 Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Fri, 10 Jul 2020 18:38:10 +0200
Subject: [PATCH 4/5] chore(README): add

---
 README.md            | 44 ++++++++++++++++++++++++++++++++++++++++++++
 tests/jobs_test.vala |  1 +
 2 files changed, 45 insertions(+)
 create mode 100644 README.md

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3f50b4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# alpine-qa-bot
+[![Gitlab CI status](https://gitlab.alpinelinux.org/Cogitri/alpine-qa-bot/badges/master/pipeline.svg)](https://gitlab.alpineliux.org/Cogitri/alpine-qa-bot/commits/master)
+
+alpine-qa-bot is a gitlab bot, written in Vala. It receives events via Gitlab's webhooks and reacts to them, e.g. by allowing maintainers to push to making newly created merge requests. It also periodically polls Gitlab for certain actions which aren't exposed via Gitlab's webhooks.
+
+## Setting Up
+First, you have to build&install alpine-qa-bot:
+
+```sh
+sudo apk add pc:glib-2.0 pc:gobject-2.0 pc:libsoup-2.4 pc:json-glib-1.0 pc:gee-0.8 pc:libuhttpmock-0.0 openssl vala meson
+meson build && meson compile -C build && meson test -v -C build && meson install -C build
+```
+
+This will install alpine-qa-bot into /usr/local/bin/alpine-qa-bot-server.
+Now you have to edit alpine-qa-bot's config to your liking. You have to set at least `GitlabToken` and `AuthenticationToken`. The former is the token you set as secret in your webhook settings, the latter is the API token of the bot account, which is used for making comments etc.
+
+## Contributing
+
+You can install and test alpine-qa-bot as described in `Setting Up`.
+
+### Code formatting
+
+alpine-qa-bot Vala code is formatted via uncrustify. Please install uncrustify and run it via `uncrustify -c uncrustify.cfg -l VALA src/*` from the top of the repo to make sure your code is formatted correctly.
+
+### Adding commit suggestions
+
+You can add additional commit suggestions to `data/suggestions.json` by adding new objects to the `"commit"` array. You can put strings containing PCRE regex into the `offenders` array of strings. If one of the offenders is matched against the commit message, the `sugggestion` will be posted on the MR, like this:
+
+```
+Beep Boop, I'm a Bot.
+
+It seems one of your commit's message doesn't follow the Alpine Linux guidelines for commit messages. Please follow the format `$SUGGESTION`.
+If you believe this was a mistake, please feel free to open an issue at https://gitlab.alpinelinux.org/Cogitri/alpine-qa-bot or ping @Cogitri.
+
+Thanks!
+```
+
+Keep in mind that the suggestion objects are matched in order. If multiple `offender`s match a commit message the first one (as in the one that's closest to the top of the `suggestions.json` file will win and the `suggestion` it's attached to will be posted.
+
+Please add a unittest for newly added `offenders`. Do do that, edit `tests/jobs_test.vala` and add your new `suggestion`/`offender` to the test list like so under `// Add new suggestions here`:
+
+```
+value_map.set("COMMIT_MESSAGE_THAT_TRIGGERS_SUGGESTION", "EXPECTED_SUGGESTION");
+```
diff --git a/tests/jobs_test.vala b/tests/jobs_test.vala
index 5d365fd..955259f 100644
--- a/tests/jobs_test.vala
+++ b/tests/jobs_test.vala
@@ -110,6 +110,7 @@ void commit_suggestion_test_all () {
         commit_suggestions.add (AlpineQaBot.CommitSuggestion.from_json_object ((!)commit_suggestion_obj.get_object ()));
     }
 
+    // Add new suggestions here
     var value_map = new Gee.HashMap<string, string>();
     value_map.set ("testing/alpine-qa-bot: update to 0.2", "$repository/$pkgname: upgrade to $pkgver");
 
-- 
GitLab


From 8e74ffe97979f3260adee23b2ae6bdb1f8b11163 Mon Sep 17 00:00:00 2001
From: Rasmus Thomsen <oss@cogitri.dev>
Date: Fri, 10 Jul 2020 19:04:53 +0200
Subject: [PATCH 5/5] chore(README): add libgee-0.8 dep

Also switch to pc: deps on Alpine and use meson commands for building
and testing instead of ninja.
---
 .gitlab-ci.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1eba444..eb12fe7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,9 +6,9 @@ stages:
 test:
   stage: test
   before_script:
-    - apk add meson vala glib-dev libsoup-dev json-glib-dev uhttpmock-dev alpine-sdk openssl
+    - apk add meson vala pc:glib-2.0 pc:gobject-2.0 pc:libsoup-2.4 pc:json-glib-1.0 pc:gee-0.8 pc:libuhttpmock-0.0 openssl alpine-sdk
   script:
-    - meson build && ninja -C build test
+    - meson build && meson compile -C build && meson test -v -C build
 
 coverage:
   image: debian:bullseye-slim
@@ -18,7 +18,7 @@ coverage:
       - coverage
   before_script:
     - apt update
-    - env DEBIAN_FRONTEND=noninteractive apt install -fy valac libsoup2.4-dev libglib2.0-dev libjson-glib-dev openssl meson build-essential gcovr curl libgirepository1.0-dev lcov
+    - env DEBIAN_FRONTEND=noninteractive apt install -fy valac libsoup2.4-dev libglib2.0-dev libjson-glib-dev openssl meson build-essential gcovr curl libgirepository1.0-dev lcov libgee-0.8-dev
     - curl -L -O https://tecnocode.co.uk/downloads/uhttpmock/uhttpmock-0.5.2.tar.xz && tar xf uhttpmock-0.5.2.tar.xz && cd uhttpmock-0.5.2
     - curl -L https://gitlab.alpinelinux.org/alpine/aports/-/raw//13129b99661d439847296d2609e361ccda81e0b4/community/uhttpmock/only-listen-on-ipv4.patch | patch -p1
     - ./configure --enable-vala --enable-introspection && make && make install
-- 
GitLab