summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFrederik Gladhorn <frederik.gladhorn@qt.io>2019-03-26 09:27:36 +0100
committerFrederik Gladhorn <frederik.gladhorn@qt.io>2019-03-29 11:46:56 +0000
commitba041bc942056d52804acd5011ae5fe7877be045 (patch)
treef1a4540219f5287f395b6d9d1e9eb6e1695048ba
parentdc4cbd9fb654c1c0e06f7ea3183602fa714c4cb6 (diff)
Initial version of the JIRA-Gerrit bot
This bot listens to Gerrit ssh events and closes JIRA issues when the commit message contains a Fixes: QTBUG-### line. Change-Id: I6d68b69e624143e2aae84ad0b172583d3855703d Reviewed-by: Jędrzej Nowacki <jedrzej.nowacki@qt.io>
-rw-r--r--scripts/jira/jira-bug-closer/.gitignore8
-rw-r--r--scripts/jira/jira-bug-closer/Pipfile23
-rw-r--r--scripts/jira/jira-bug-closer/Pipfile.lock571
-rw-r--r--scripts/jira/jira-bug-closer/README.md50
-rw-r--r--scripts/jira/jira-bug-closer/_ssh_config6
-rw-r--r--scripts/jira/jira-bug-closer/bot/__init__.py3
-rw-r--r--scripts/jira/jira-bug-closer/bot/args.py49
-rw-r--r--scripts/jira/jira-bug-closer/bot/bot.py90
-rw-r--r--scripts/jira/jira-bug-closer/config.ini18
-rw-r--r--scripts/jira/jira-bug-closer/config/__init__.py3
-rw-r--r--scripts/jira/jira-bug-closer/config/config.py70
-rwxr-xr-xscripts/jira/jira-bug-closer/dump_jira_versions.py50
-rw-r--r--scripts/jira/jira-bug-closer/gerrit/__init__.py10
-rw-r--r--scripts/jira/jira-bug-closer/gerrit/streamevents.py156
-rw-r--r--scripts/jira/jira-bug-closer/gerrit/streamparser.py64
-rw-r--r--scripts/jira/jira-bug-closer/git/__init__.py5
-rw-r--r--scripts/jira/jira-bug-closer/git/repository.py318
-rw-r--r--scripts/jira/jira-bug-closer/jiracloser/__init__.py5
-rw-r--r--scripts/jira/jira-bug-closer/jiracloser/closer.py235
-rw-r--r--scripts/jira/jira-bug-closer/logger/__init__.py5
-rw-r--r--scripts/jira/jira-bug-closer/logger/logger.py41
-rwxr-xr-xscripts/jira/jira-bug-closer/main.py42
-rwxr-xr-xscripts/jira/jira-bug-closer/oauth_dance.py80
-rw-r--r--scripts/jira/jira-bug-closer/systemd/jira_gerrit_bot.service40
-rw-r--r--scripts/jira/jira-bug-closer/tests/__init__.py1
-rw-r--r--scripts/jira/jira-bug-closer/tests/test_gitlog.py213
-rw-r--r--scripts/jira/jira-bug-closer/tests/test_jira_close_issue.py102
-rw-r--r--scripts/jira/jira-bug-closer/tests/test_jira_versions.py415
-rw-r--r--scripts/jira/jira-bug-closer/tests/test_listing_projects.py39
-rw-r--r--scripts/jira/jira-bug-closer/tests/test_streamparser.py111
-rw-r--r--scripts/jira/jira-bug-closer/tox.ini3
31 files changed, 2826 insertions, 0 deletions
diff --git a/scripts/jira/jira-bug-closer/.gitignore b/scripts/jira/jira-bug-closer/.gitignore
new file mode 100644
index 00000000..40bdce4f
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/.gitignore
@@ -0,0 +1,8 @@
+# our copy of relevant repos
+git_repos
+
+.coverage
+.mypy_cache
+.pytest_cache
+.vscode
+htmlcov
diff --git a/scripts/jira/jira-bug-closer/Pipfile b/scripts/jira/jira-bug-closer/Pipfile
new file mode 100644
index 00000000..f38cbfdc
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/Pipfile
@@ -0,0 +1,23 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+jira = "*"
+asyncssh = "*"
+coloredlogs = "*"
+
+[dev-packages]
+pylint = "*"
+ipython = "*"
+nose = "*"
+pytest = "*"
+pytest-cov = "*"
+rope = "*"
+mypy = "*"
+"flake8" = "*"
+pytest-asyncio = "*"
+
+[requires]
+python_version = "3.6"
diff --git a/scripts/jira/jira-bug-closer/Pipfile.lock b/scripts/jira/jira-bug-closer/Pipfile.lock
new file mode 100644
index 00000000..ea0d06ab
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/Pipfile.lock
@@ -0,0 +1,571 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "40312c1984873ea0c990ab3fd66d71091861ffb69b962dc2257ef3f4ce36d3e9"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.6"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "asn1crypto": {
+ "hashes": [
+ "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
+ "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
+ ],
+ "version": "==0.24.0"
+ },
+ "asyncssh": {
+ "hashes": [
+ "sha256:528a121f6f8c2cb8f8a1f8a57405f3d14a216d6f075535862025337318420262",
+ "sha256:eb5b190badc5cd2a506a1b6ced3e92f948166974eef7d1abab61acc67aa379e6"
+ ],
+ "index": "pypi",
+ "version": "==1.13.3"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
+ "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
+ ],
+ "version": "==2018.4.16"
+ },
+ "cffi": {
+ "hashes": [
+ "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
+ "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
+ "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
+ "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
+ "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
+ "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
+ "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
+ "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
+ "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
+ "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
+ "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
+ "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
+ "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
+ "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
+ "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
+ "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
+ "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
+ "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
+ "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
+ "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
+ "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
+ "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
+ "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
+ "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
+ "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
+ "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
+ "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
+ "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
+ "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
+ "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
+ "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
+ "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
+ ],
+ "version": "==1.11.5"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+ "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+ ],
+ "version": "==3.0.4"
+ },
+ "coloredlogs": {
+ "hashes": [
+ "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8",
+ "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36"
+ ],
+ "index": "pypi",
+ "version": "==10.0"
+ },
+ "cryptography": {
+ "hashes": [
+ "sha256:21af753934f2f6d1a10fe8f4c0a64315af209ef6adeaee63ca349797d747d687",
+ "sha256:27bb401a20a838d6d0ea380f08c6ead3ccd8c9d8a0232dc9adcc0e4994576a66",
+ "sha256:29720c4253263cff9aea64585adbbe85013ba647f6e98367efff9db2d7193ded",
+ "sha256:2a35b7570d8f247889784010aac8b384fd2e4a47b33e15c4a60b45a7c1944120",
+ "sha256:42c531a6a354407f42ee07fda5c2c0dc822cf6d52744949c182f2b295fbd4183",
+ "sha256:5eb86f03f9c4f0ac2336ac5431271072ddf7ecc76b338e26366732cfac58aa19",
+ "sha256:67f7f57eae8dede577f3f7775957f5bec93edd6bdb6ce597bb5b28e1bdf3d4fb",
+ "sha256:6ec84edcbc966ae460560a51a90046503ff0b5b66157a9efc61515c68059f6c8",
+ "sha256:7ba834564daef87557e7fcd35c3c3183a4147b0b3a57314e53317360b9b201b3",
+ "sha256:7d7f084cbe1fdb82be5a0545062b59b1ad3637bc5a48612ac2eb428ff31b31ea",
+ "sha256:82409f5150e529d699e5c33fa8fd85e965104db03bc564f5f4b6a9199e591f7c",
+ "sha256:87d092a7c2a44e5f7414ab02fb4145723ebba411425e1a99773531dd4c0e9b8d",
+ "sha256:8c56ef989342e42b9fcaba7c74b446f0cc9bed546dd00034fa7ad66fc00307ef",
+ "sha256:9449f5d4d7c516a6118fa9210c4a00f34384cb1d2028672100ee0c6cce49d7f6",
+ "sha256:bc2301170986ad82d9349a91eb8884e0e191209c45f5541b16aa7c0cfb135978",
+ "sha256:c132bab45d4bd0fff1d3fe294d92b0a6eb8404e93337b3127bdec9f21de117e6",
+ "sha256:c3d945b7b577f07a477700f618f46cbc287af3a9222cd73035c6ef527ef2c363",
+ "sha256:cee18beb4c807b5c0b178f4fa2fae03cef9d51821a358c6890f8b23465b7e5d2",
+ "sha256:d01dfc5c2b3495184f683574e03c70022674ca9a7be88589c5aba130d835ea90"
+ ],
+ "version": "==2.3"
+ },
+ "defusedxml": {
+ "hashes": [
+ "sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4",
+ "sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20"
+ ],
+ "version": "==0.5.0"
+ },
+ "humanfriendly": {
+ "hashes": [
+ "sha256:49196aaebc41a73106256bf2dce468452e8cf2f12b73eb54b50c25fbc1644363",
+ "sha256:ed1e98ae056b597f15b41bddcc32b9f21e6ab4f3445f9faad1668675de759f7b"
+ ],
+ "version": "==4.16.1"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
+ "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
+ ],
+ "version": "==2.7"
+ },
+ "jira": {
+ "hashes": [
+ "sha256:9adeead4d5f5a6aff74c630787f8bd2d4b0e154f3a3036641298064e91b2d25d",
+ "sha256:e2a94adff98e45b29ded030adc76103eab34fa7d4d57303f211f572bedba0e93"
+ ],
+ "index": "pypi",
+ "version": "==2.0.0"
+ },
+ "oauthlib": {
+ "hashes": [
+ "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162",
+ "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"
+ ],
+ "version": "==2.1.0"
+ },
+ "pbr": {
+ "hashes": [
+ "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
+ "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
+ ],
+ "version": "==4.2.0"
+ },
+ "pycparser": {
+ "hashes": [
+ "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226"
+ ],
+ "version": "==2.18"
+ },
+ "pyjwt": {
+ "hashes": [
+ "sha256:30b1380ff43b55441283cc2b2676b755cca45693ae3097325dea01f3d110628c",
+ "sha256:4ee413b357d53fd3fb44704577afac88e72e878716116270d722723d65b42176"
+ ],
+ "version": "==1.6.4"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
+ "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
+ ],
+ "version": "==2.19.1"
+ },
+ "requests-oauthlib": {
+ "hashes": [
+ "sha256:8886bfec5ad7afb391ed5443b1f697c6f4ae98d0e5620839d8b4499c032ada3f",
+ "sha256:e21232e2465808c0e892e0e4dbb8c2faafec16ac6dc067dd546e9b466f3deac8"
+ ],
+ "version": "==1.0.0"
+ },
+ "requests-toolbelt": {
+ "hashes": [
+ "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
+ "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
+ ],
+ "version": "==0.8.0"
+ },
+ "six": {
+ "hashes": [
+ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
+ "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
+ ],
+ "version": "==1.11.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
+ "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
+ ],
+ "markers": "python_version != '3.3.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.2.*'",
+ "version": "==1.23"
+ }
+ },
+ "develop": {
+ "astroid": {
+ "hashes": [
+ "sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19",
+ "sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc"
+ ],
+ "version": "==2.0.2"
+ },
+ "atomicwrites": {
+ "hashes": [
+ "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
+ "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
+ ],
+ "version": "==1.1.5"
+ },
+ "attrs": {
+ "hashes": [
+ "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
+ "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
+ ],
+ "version": "==18.1.0"
+ },
+ "backcall": {
+ "hashes": [
+ "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
+ "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
+ ],
+ "version": "==0.1.0"
+ },
+ "coverage": {
+ "hashes": [
+ "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
+ "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
+ "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
+ "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
+ "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
+ "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
+ "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
+ "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
+ "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
+ "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
+ "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
+ "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
+ "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
+ "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
+ "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
+ "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
+ "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
+ "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
+ "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
+ "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
+ "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
+ "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
+ "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
+ "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
+ "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
+ "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
+ "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
+ "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
+ "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
+ "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
+ "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80"
+ ],
+ "markers": "python_version < '4' and python_version != '3.0.*' and python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.2.*'",
+ "version": "==4.5.1"
+ },
+ "decorator": {
+ "hashes": [
+ "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82",
+ "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"
+ ],
+ "version": "==4.3.0"
+ },
+ "flake8": {
+ "hashes": [
+ "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
+ "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
+ ],
+ "index": "pypi",
+ "version": "==3.5.0"
+ },
+ "ipython": {
+ "hashes": [
+ "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62",
+ "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"
+ ],
+ "index": "pypi",
+ "version": "==6.5.0"
+ },
+ "ipython-genutils": {
+ "hashes": [
+ "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
+ "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
+ ],
+ "version": "==0.2.0"
+ },
+ "isort": {
+ "hashes": [
+ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
+ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
+ "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
+ ],
+ "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'",
+ "version": "==4.3.4"
+ },
+ "jedi": {
+ "hashes": [
+ "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
+ "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f"
+ ],
+ "version": "==0.12.1"
+ },
+ "lazy-object-proxy": {
+ "hashes": [
+ "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
+ "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
+ "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
+ "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
+ "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
+ "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
+ "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
+ "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
+ "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
+ "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
+ "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
+ "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
+ "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
+ "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
+ "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
+ "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
+ "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
+ "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
+ "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
+ "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
+ "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
+ "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
+ "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
+ "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
+ "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
+ "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
+ "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
+ "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
+ "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
+ ],
+ "version": "==1.3.1"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+ "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+ ],
+ "version": "==0.6.1"
+ },
+ "more-itertools": {
+ "hashes": [
+ "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
+ "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
+ "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
+ ],
+ "version": "==4.3.0"
+ },
+ "mypy": {
+ "hashes": [
+ "sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a",
+ "sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c"
+ ],
+ "index": "pypi",
+ "version": "==0.620"
+ },
+ "nose": {
+ "hashes": [
+ "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
+ "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
+ "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
+ ],
+ "index": "pypi",
+ "version": "==1.3.7"
+ },
+ "parso": {
+ "hashes": [
+ "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2",
+ "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"
+ ],
+ "version": "==0.3.1"
+ },
+ "pexpect": {
+ "hashes": [
+ "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba",
+ "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"
+ ],
+ "markers": "sys_platform != 'win32'",
+ "version": "==4.6.0"
+ },
+ "pickleshare": {
+ "hashes": [
+ "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b",
+ "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5"
+ ],
+ "version": "==0.7.4"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
+ "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
+ ],
+ "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'",
+ "version": "==0.7.1"
+ },
+ "prompt-toolkit": {
+ "hashes": [
+ "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381",
+ "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4",
+ "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917"
+ ],
+ "version": "==1.0.15"
+ },
+ "ptyprocess": {
+ "hashes": [
+ "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
+ "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
+ ],
+ "version": "==0.6.0"
+ },
+ "py": {
+ "hashes": [
+ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
+ "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
+ ],
+ "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7'",
+ "version": "==1.5.4"
+ },
+ "pycodestyle": {
+ "hashes": [
+ "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
+ "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
+ ],
+ "version": "==2.3.1"
+ },
+ "pyflakes": {
+ "hashes": [
+ "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
+ "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
+ ],
+ "version": "==1.6.0"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
+ "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
+ ],
+ "version": "==2.2.0"
+ },
+ "pylint": {
+ "hashes": [
+ "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
+ "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
+ ],
+ "index": "pypi",
+ "version": "==2.1.1"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:86a8dbf407e437351cef4dba46736e9c5a6e3c3ac71b2e942209748e76ff2086",
+ "sha256:e74466e97ac14582a8188ff4c53e6cc3810315f342f6096899332ae864c1d432"
+ ],
+ "index": "pypi",
+ "version": "==3.7.1"
+ },
+ "pytest-asyncio": {
+ "hashes": [
+ "sha256:a962e8e1b6ec28648c8fe214edab4e16bacdb37b52df26eb9d63050af309b2a9",
+ "sha256:fbd92c067c16111174a1286bfb253660f1e564e5146b39eeed1133315cf2c2cf"
+ ],
+ "index": "pypi",
+ "version": "==0.9.0"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
+ "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
+ ],
+ "index": "pypi",
+ "version": "==2.5.1"
+ },
+ "rope": {
+ "hashes": [
+ "sha256:a09edfd2034fd50099a67822f9bd851fbd0f4e98d3b87519f6267b60e50d80d1"
+ ],
+ "index": "pypi",
+ "version": "==0.10.7"
+ },
+ "simplegeneric": {
+ "hashes": [
+ "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"
+ ],
+ "version": "==0.8.1"
+ },
+ "six": {
+ "hashes": [
+ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
+ "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
+ ],
+ "version": "==1.11.0"
+ },
+ "traitlets": {
+ "hashes": [
+ "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
+ "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
+ ],
+ "version": "==4.3.2"
+ },
+ "typed-ast": {
+ "hashes": [
+ "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
+ "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
+ "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
+ "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
+ "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
+ "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
+ "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
+ "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
+ "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
+ "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
+ "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
+ "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
+ "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
+ "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
+ "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
+ "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
+ "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
+ "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
+ "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
+ "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
+ "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
+ "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
+ "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
+ ],
+ "version": "==1.1.0"
+ },
+ "typing": {
+ "hashes": [
+ "sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
+ "sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
+ "sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
+ ],
+ "version": "==3.6.4"
+ },
+ "wcwidth": {
+ "hashes": [
+ "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
+ "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
+ ],
+ "version": "==0.1.7"
+ },
+ "wrapt": {
+ "hashes": [
+ "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
+ ],
+ "version": "==1.10.11"
+ }
+ }
+}
diff --git a/scripts/jira/jira-bug-closer/README.md b/scripts/jira/jira-bug-closer/README.md
new file mode 100644
index 00000000..0b261a3c
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/README.md
@@ -0,0 +1,50 @@
+# JIRA bot to close issues
+
+Listen to gerrit events and close corresponding JIRA tasks when there is a fixes tag in the commit message.
+
+## Prerequisites and building
+
+You need to have [pipenv](https://pipenv.readthedocs.io/en/latest/) installed.
+Run `make` to install dependencies.
+
+## Connecting to a JIRA account using OAuth
+
+* Generate a private/public rsa certificate pair (`jiracloser.pem`, `jiracloser.pub`)
+ * See for example https://www.madboa.com/geek/openssl/#key-rsa
+
+* Log in to JIRA as admin.
+* Find "Integrations" -> "Application Links"
+ * Enter a random URL (e.g. https://www.qt.io) and click "Create new link"
+ * Fill out the fields, it does not really matter:
+ * Name: Gerrit Issue Bot
+ * Type: Generic
+ * Service Provider Name: Qt JIRA bot (anything goes)
+ * Consumer key: jira-gerrit-bot-oauth-consumer
+ * Shared Secret: 8aG2#dwV24$e9J43@s8b
+ * Request Token URL: https://www.qt.io
+ * Access token URL: https://www.qt.io
+ * Authorize URL: https://www.qt.io
+ * Create incoming link: yes
+ * Next page (this is important, can be edited later under incoming authentication)
+ * Consumer key: jira-gerrit-oauth
+ * Consumer Name: Gerrit Issue Closer
+ * Public Key: content of `jiracloser.pub`
+ * You can delete the outgoing auth after this excercise
+
+* Log in to JIRA with the bot user.
+* In a terminal run: `make oauth`
+* The script puts out a URL, which must be *opened as the bot user*
+* Click Allow
+* Press enter in the terminal
+* Copy the `oauth_token` and `oauth_token_secret` into config.ini.
+
+## Running the bot
+
+`make run`
+
+## Running tests
+
+Run `make test` which runs a style check, type checking and the automated tests.
+Please make sure that all of them pass before contributing.
+
+It's also possible to generate coverage information (`make coverage` will open a a browser).
diff --git a/scripts/jira/jira-bug-closer/_ssh_config b/scripts/jira/jira-bug-closer/_ssh_config
new file mode 100644
index 00000000..df1ae8c5
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/_ssh_config
@@ -0,0 +1,6 @@
+Host codereview.qt-project.org
+ Port 29418
+ ServerAliveInterval 240
+ Ciphers +aes256-cbc
+ User qt_ci_bot
+ IdentityFile ~/jira-bug-closer/jira_gerrit_bot_id_rsa
diff --git a/scripts/jira/jira-bug-closer/bot/__init__.py b/scripts/jira/jira-bug-closer/bot/__init__.py
new file mode 100644
index 00000000..1a8251b7
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/bot/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ["Bot"]
+
+from .bot import Bot
diff --git a/scripts/jira/jira-bug-closer/bot/args.py b/scripts/jira/jira-bug-closer/bot/args.py
new file mode 100644
index 00000000..cdfd40a0
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/bot/args.py
@@ -0,0 +1,49 @@
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import argparse
+import sys
+from typing import Optional
+
+
+class Args():
+ def __init__(self) -> None:
+ parser = argparse.ArgumentParser(description='JIRA bot to close issues according to Gerrit events')
+ parser.add_argument('--production', action='store_true', help='Run in production mode (real jira instance)')
+ parser.add_argument('--since', help='Scan all commits since the given date (passed to git as since)')
+ self.parsed_args = parser.parse_args(args=sys.argv[1:])
+
+ @property
+ def config_section(self) -> str:
+ if self.parsed_args.production:
+ return 'production'
+ return 'test'
+
+ @property
+ def since(self) -> Optional[str]:
+ return self.parsed_args.since or None
diff --git a/scripts/jira/jira-bug-closer/bot/bot.py b/scripts/jira/jira-bug-closer/bot/bot.py
new file mode 100644
index 00000000..ac755dda
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/bot/bot.py
@@ -0,0 +1,90 @@
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import asyncio
+from typing import Optional
+from gerrit import GerritStreamEvents, GerritStreamParser
+from logger import logger
+from git import Repository
+from jiracloser import JiraCloser
+from config import Config
+from .args import Args
+
+
+log = logger("bot")
+
+
+class Bot:
+ def __init__(self) -> None:
+ self.loop = asyncio.get_event_loop()
+ self.g = GerritStreamEvents()
+ self.parser = GerritStreamParser()
+ self.args = Args()
+ log.info("Using '%s' configuration", self.args.config_section)
+ config = Config(self.args.config_section)
+ self.jira_closer = JiraCloser(config)
+
+ async def update_project(self, name: str, since: Optional[str] = None) -> None:
+ async with Repository(name) as repo:
+ changes = await repo.new_changes(since=since)
+ for change in changes:
+ log.info("Checking changes for relevant tags: '%s'", change)
+ # check commit message
+ fixes = await repo.parse_commit_messages(change)
+ for fix in fixes:
+ # do jira magic
+ self.jira_closer.run(fix)
+
+ async def event_handler(self, data: str) -> None:
+ event = self.parser.parse(data)
+ log.debug(event)
+ log.debug("Raw data: >>>%s<<<", data)
+ if event and event.type == 'ref-updated' and 'staging' not in event.branch:
+ log.info(event)
+ await self.update_project(event.project)
+
+ async def check_gerrit_projects(self) -> None:
+ projects = await(self.g.list_all_projects())
+ # we could parallelize, but we cannot have too many git connections
+ # at the same time, and this is not the common case, so do it sequential for now.
+ # updates = []
+ # for project in projects:
+ # updates.append(self.update_project(project))
+ # await asyncio.gather(*updates)
+ for project in projects:
+ await self.update_project(project, since=self.args.since)
+
+ def run(self) -> None:
+ while True:
+ try:
+ check_projects_on_startup = asyncio.ensure_future(self.check_gerrit_projects())
+ self.g.setDataCallback(self.event_handler)
+ run_monitor = asyncio.ensure_future(self.g.run())
+ self.loop.run_until_complete(asyncio.gather(check_projects_on_startup, run_monitor))
+ except Exception as exc:
+ log.exception('Caught exception: ' + str(exc))
diff --git a/scripts/jira/jira-bug-closer/config.ini b/scripts/jira/jira-bug-closer/config.ini
new file mode 100644
index 00000000..91cff21a
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/config.ini
@@ -0,0 +1,18 @@
+# test jira instance, used for auto tests and general testing
+[test]
+add_comment_to_issues = no
+jira_url = https://bugreports-test.qt.io
+
+oauth_token=get_this_by_running_oauth_dance.py
+oauth_token_secret=get_this_by_running_oauth_dance.py
+consumer_key = jira-gerrit-oauth
+key_cert_file = jiracloser.pem
+
+[production]
+add_comment_to_issues = no
+jira_url = https://bugreports.qt.io
+
+oauth_token=get_this_by_running_oauth_dance.py
+oauth_token_secret=get_this_by_running_oauth_dance.py
+consumer_key = jira-gerrit-oauth
+key_cert_file = jiracloser.pem
diff --git a/scripts/jira/jira-bug-closer/config/__init__.py b/scripts/jira/jira-bug-closer/config/__init__.py
new file mode 100644
index 00000000..a286bbb1
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/config/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ["Config"]
+
+from .config import Config
diff --git a/scripts/jira/jira-bug-closer/config/config.py b/scripts/jira/jira-bug-closer/config/config.py
new file mode 100644
index 00000000..3446c9fc
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/config/config.py
@@ -0,0 +1,70 @@
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import os
+from typing import Dict
+from configparser import ConfigParser
+
+
+from logger import logger
+log = logger('config')
+
+
+class Config:
+ def __init__(self, section: str) -> None:
+ self.section = section
+ self.file_path = os.path.dirname(os.path.abspath(__file__))
+ config_path = os.path.abspath(os.path.join(self.file_path, "../config.ini"))
+ self.config = ConfigParser()
+ self.config.read(config_path)
+
+ @property
+ def jira_url(self) -> str:
+ return self.config[self.section]['jira_url']
+
+ def get_oauth_data(self) -> Dict[str, str]:
+ section = self.config[self.section]
+ cert_file = section['key_cert_file']
+ cert_path = os.path.abspath(os.path.join(self.file_path, "..", cert_file))
+ with open(cert_path, 'r') as key_cert_file:
+ key_cert_data = key_cert_file.read()
+
+ oauth_data = {
+ 'access_token': section['oauth_token'],
+ 'access_token_secret': section['oauth_token_secret'],
+ 'consumer_key': section['consumer_key'],
+ 'key_cert': key_cert_data
+ }
+ return oauth_data
+
+ @property
+ def add_comment_to_issues(self) -> bool:
+ try:
+ return self.config[self.section].getboolean('add_comment_to_issues')
+ except KeyError:
+ return False
diff --git a/scripts/jira/jira-bug-closer/dump_jira_versions.py b/scripts/jira/jira-bug-closer/dump_jira_versions.py
new file mode 100755
index 00000000..b93e9cc3
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/dump_jira_versions.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+from distutils.version import LooseVersion
+from typing import List, Tuple
+from config import Config
+from jiracloser import JiraCloser
+
+
+def print_versions(version_list: List[Tuple[LooseVersion, str, bool]]) -> None:
+ print(" {name:35}{description:35}{released}".format(name="Version", description="Stuff", released="Released", width=30))
+ for version in version_list:
+ print(" {name:35}{description:35}{released}".format(name=version[0].vstring, description=version[1], released=str(version[2]), width=30))
+
+
+config = Config('test')
+j = JiraCloser(config)
+print("Fix versions for QTBUG:")
+issue = j.jira_client.issue('QTBUG-1')
+print_versions(j._jira_version_list(issue))
+
+print("Fix versions for QTCREATORBUG:")
+issue = j.jira_client.issue('QTCREATORBUG-1')
+print_versions(j._jira_version_list(issue))
diff --git a/scripts/jira/jira-bug-closer/gerrit/__init__.py b/scripts/jira/jira-bug-closer/gerrit/__init__.py
new file mode 100644
index 00000000..8c6b22f3
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/gerrit/__init__.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+
+__all__ = [
+ "GerritStreamEvents",
+ "GerritStreamParser",
+ "GerritEvent",
+]
+
+from .streamevents import GerritStreamEvents
+from .streamparser import GerritStreamParser, GerritEvent
diff --git a/scripts/jira/jira-bug-closer/gerrit/streamevents.py b/scripts/jira/jira-bug-closer/gerrit/streamevents.py
new file mode 100644
index 00000000..174cac80
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/gerrit/streamevents.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import asyncio
+import asyncssh
+import os
+from typing import Any, Callable, Coroutine, List
+from logger import logger
+log = logger("gerrit_stream")
+
+codereview = "codereview.qt-project.org"
+port = 29418
+user = 'qt_ci_bot'
+file_path = os.path.dirname(os.path.abspath(__file__))
+cibot_key_file = os.path.abspath(os.path.join(file_path, '../jira_gerrit_bot_id_rsa'))
+loop = asyncio.get_event_loop()
+
+
+class GerritSshClientSession(asyncssh.SSHClientSession): # type: ignore
+ def __init__(self) -> None:
+ self._buffer: str = ""
+ self._callback: Callable[[str], Coroutine[Any, Any, None]]
+
+ def setDataCallback(self, callback: Callable[[str], Coroutine[Any, Any, None]]) -> None:
+ self._callback = callback
+
+ def data_received(self, data: str, datatype: Any) -> None:
+ # Very long messages can be split in two, so we read into a buffer until we get
+ # a newline '\n' (which is the common case).
+ log.debug("Data received: '%s'", data)
+ self._buffer += data
+ if self._buffer.endswith('\n'):
+ if self._callback:
+ loop.create_task(self._callback(self._buffer))
+ self._buffer = ""
+
+ def eof_received(self) -> None:
+ log.warning('EOF received.')
+
+ def connection_lost(self, exc: Exception) -> None:
+ if exc:
+ log.error('SSH session error: "%s"', str(exc))
+ else:
+ log.warning('Connection lost (no exception).')
+
+
+class GerritSshClient(asyncssh.SSHClient): # type: ignore
+ def connection_made(self, conn: asyncssh.SSHClientConnection) -> None:
+ log.info('Connection made to %s.' % conn.get_extra_info('peername')[0])
+
+ def auth_completed(self) -> None:
+ log.info('Authentication successful.')
+
+ def connection_lost(self, exc: Exception) -> None:
+ log.warning('connection_lost %s', str(exc))
+
+
+class GerritStreamEvents():
+ def __init__(self) -> None:
+ self._session = None
+ self._connection = None
+ self._client = None
+ self._connection_lock = asyncio.Lock()
+
+ async def _create_connection(self) -> None:
+ async with self._connection_lock:
+ if self._connection:
+ return
+ conn, client = await asyncssh.create_connection(
+ GerritSshClient, host=codereview, port=port, username=user,
+ client_keys=[cibot_key_file])
+ log.info("Gerrit SSH client connected.")
+ self._connection = conn
+ self._client = client
+
+ def setDataCallback(self, callback: Callable[[str], Coroutine[Any, Any, None]]) -> None:
+ self._callback = callback
+ if self._session:
+ self._session.setDataCallback(callback)
+
+ async def list_all_projects(self) -> List[str]:
+ while True:
+ connection_attempts = 0
+ try:
+ await self._create_connection()
+ connection_attempts = 0
+ gerrit_process = await self._connection.run('gerrit ls-projects', check=True) # type: ignore
+ if gerrit_process.exit_status == 0:
+ projects = [project for project in gerrit_process.stdout.splitlines() if not project.startswith('{graveyard}')]
+ return projects
+ else:
+ raise Exception("failed to list gerrit projects")
+ except Exception:
+ self._connection = None
+ log.exception("Error listing gerrit projects:", exc_info=True)
+ connection_attempts += 1
+ await asyncio.sleep(connection_attempts * 2)
+
+ async def _run_client(self) -> None:
+ async with self._connection: # type: ignore
+ chan, session = await self._connection.create_session(GerritSshClientSession, 'gerrit stream-events') # type: ignore
+ self._session = session
+ if self._callback:
+ session.setDataCallback(self._callback)
+
+ # at the moment there seems to be no way
+ # for asyncssh to keep the connection alive
+ # so just manually do what openssh does.
+ while True:
+ channel_closed = chan.wait_closed()
+ done, _ = await asyncio.wait((asyncio.sleep(100), channel_closed), return_when=asyncio.FIRST_COMPLETED)
+ if channel_closed in done:
+ break
+ chan.write('keepalive@openssh.com')
+ log.debug("Sent SSH keep alive.")
+
+ log.warning("Session done.")
+
+ async def run(self) -> None:
+ connection_attempts = 0
+ while True:
+ try:
+ await self._create_connection()
+ connection_attempts = 0
+ await self._run_client()
+ except Exception:
+ log.exception('SSH connection failed.')
+ self._connection = None
+ connection_attempts += 1
+ await asyncio.sleep(connection_attempts * 2)
diff --git a/scripts/jira/jira-bug-closer/gerrit/streamparser.py b/scripts/jira/jira-bug-closer/gerrit/streamparser.py
new file mode 100644
index 00000000..e1900169
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/gerrit/streamparser.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import json
+from typing import Optional
+from logger import logger
+
+log = logger(name="gerrit_stream_parser")
+
+
+class GerritEvent:
+ def __init__(self, type: str, project: str, branch: str) -> None:
+ self.type = type
+ self.project = project
+ self.branch = branch
+
+ def __eq__(self, other: object) -> bool:
+ return self.__dict__ == other.__dict__
+
+ def __repr__(self) -> str:
+ return "<GerritEvent '%s': '%s' '%s'>" % (self.type, self.project, self.branch)
+
+
+class GerritStreamParser:
+ def parse(self, data: str) -> Optional[GerritEvent]:
+ try:
+ event = json.loads(data)
+ except json.decoder.JSONDecodeError:
+ log.exception('Invalid JSON: "%s"', data)
+ return None
+ eventType = event.get('type')
+ if eventType in ('comment-added', 'change-abandoned', 'change-deferred', 'change-merged', 'change-restored',
+ 'draft-published', 'merge-failed', 'patchset-created', 'reviewer-added'):
+ return GerritEvent(type=eventType, project=event['change']['project'], branch=event['change']['branch'])
+ if eventType in ('ref-updated',):
+ return GerritEvent(type=eventType, project=event['refUpdate']['project'], branch=event['refUpdate']['refName'])
+ log.warning('unhandled event type in gerrit ssh stream: "%s" data: "%s"', eventType, data)
+ return None
diff --git a/scripts/jira/jira-bug-closer/git/__init__.py b/scripts/jira/jira-bug-closer/git/__init__.py
new file mode 100644
index 00000000..99db5af4
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/git/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+__all__ = ["Repository", "Change", "FixedByTag"]
+
+from .repository import Repository, Change, FixedByTag
diff --git a/scripts/jira/jira-bug-closer/git/repository.py b/scripts/jira/jira-bug-closer/git/repository.py
new file mode 100644
index 00000000..a4cc5536
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/git/repository.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import asyncio
+from distutils.version import StrictVersion
+import fcntl
+import os
+from pathlib import Path
+import re
+from typing import Any, Dict, List, Optional, Tuple
+
+from logger import logger
+log = logger('repository')
+
+
+repo_base = 'ssh://codereview.qt-project.org:29418/'
+file_path = os.path.dirname(os.path.abspath(__file__))
+working_dir = os.path.abspath(os.path.join(file_path, '../git_repos'))
+Path(working_dir).mkdir(parents=True, exist_ok=True)
+
+
+class Version(StrictVersion):
+ def __init__(self, version_string: str) -> None:
+ super().__init__(version_string)
+ self.original_version_string = version_string
+
+ def __lt__(self, other: Any) -> Any:
+ """ Compare versions taking the original_version_string into account.
+
+ There are some cases where the default comparison is not good enough: we want 5.12.0 > 5.12,
+ otherwise changes going into 5.12 while 5.12.0 exists will end up in 5.12.0 instead of 5.12.1. """
+ if super().__eq__(other):
+ return self.original_version_string < other.original_version_string
+ return super().__lt__(other)
+
+ def __eq__(self, other: Any) -> Any:
+ return self.original_version_string == other.original_version_string
+
+ def __gt__(self, other: Any) -> Any:
+ if super().__eq__(other):
+ return self.original_version_string > other.original_version_string
+ return super().__gt__(other)
+
+ def __repr__(self) -> str:
+ return self.original_version_string + " - " + super().__repr__()
+
+
+class Change:
+ def __init__(self, repository: str, branch: str, before: Optional[str], after: str, since: Optional[str] = None) -> None:
+ self.repository = repository
+ self.branch = branch
+ self.before = before
+ self.after = after
+ self.since = since
+
+ def __repr__(self) -> str:
+ return "<Change(repository='%s', branch='%s', before='%s', after='%s', since='%s')>" % (self.repository, self.branch, self.before, self.after, self.since)
+
+
+class FixedByTag:
+ def __init__(self, repository: str, branch: str, sha1: str, author: str, subject: str, version: Optional[str], task_numbers: List[str], fixes: List[str]) -> None:
+ self.repository = repository
+ self.branch = branch
+ self.sha1 = sha1
+ self.author = author
+ self.subject = subject
+ self.version = version # Can be None in case we failed to guess it. E.g. wip/foobar does not result in anything.
+ self.task_numbers = task_numbers
+ self.fixes = fixes
+
+ def __eq__(self, other: object) -> bool:
+ return self.__dict__ == other.__dict__
+
+ def __repr__(self) -> str:
+ return "<FixedByTag(repository='%s', branch='%s', version='%s', sha1='%s', author='%s', fixes=%s, task_numbers=%s, subject='%s')>" % (self.repository, self.branch, self.version, self.sha1, self.author, self.fixes, self.task_numbers, self.subject)
+
+ def __hash__(self) -> int:
+ return hash(self.__dict__.values())
+
+
+class Repository:
+ def __init__(self, name: str) -> None:
+ self.name = name
+ self._issue_key_regexp = re.compile(r'^[A-Z]+-\d+$')
+ # self._fd: int = -1 # lock file descriptor
+ Path(os.path.dirname(self.repo_path)).mkdir(parents=True, exist_ok=True)
+
+ async def __aenter__(self) -> "Repository":
+ lock_path = self.repo_path + '_lock'
+ self._fd = os.open(lock_path, os.O_CREAT | os.O_RDWR)
+ try:
+ fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except BlockingIOError:
+ loop = asyncio.get_event_loop()
+ await loop.run_in_executor(None, lambda: fcntl.flock(self._fd, fcntl.LOCK_EX)) # type: ignore
+ return self
+
+ async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
+ os.close(self._fd)
+
+ @property
+ def repo_path(self) -> str:
+ return os.path.join(working_dir, self.name)
+
+ def git_command(self, command: str) -> str:
+ return "git --git-dir=%s %s" % (self.repo_path, command)
+
+ async def _check_repo(self) -> None:
+ if os.path.exists(self.repo_path):
+ return
+ Path(os.path.dirname(self.repo_path)).mkdir(parents=True, exist_ok=True)
+ log.info("Cloning '%s", self.name)
+ command = "git clone --bare %s %s" % (repo_base + self.name, self.repo_path)
+ process = await asyncio.create_subprocess_exec(*command.split())
+ # wait for the process to finish
+ await asyncio.wait_for(process.communicate(), 360)
+
+ async def _git_fetch_heads(self) -> None:
+ log.info("git fetch '%s'", self.name)
+ command = self.git_command("fetch origin +refs/heads/*:refs/heads/* --prune")
+ process = await asyncio.create_subprocess_exec(*command.split(), stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
+ # wait for the process to finish
+ await asyncio.wait_for(process.communicate(), 180)
+
+ async def _git_show_ref(self, tags: bool = False) -> str:
+ refType = '--tags' if tags else '--heads'
+ command = self.git_command("show-ref %s" % refType)
+ process = await asyncio.create_subprocess_exec(*command.split(), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
+ # wait for the process to finish
+ stdout, stderr = await asyncio.wait_for(process.communicate(), 60)
+ if stderr:
+ log.warning("Error when running git show-ref: '%s'", stderr.decode('utf-8'))
+ return stdout.decode('utf-8').strip()
+
+ def _show_ref_output_to_dict(self, output: str) -> Dict[str, str]:
+ d = {}
+ for line in output.splitlines():
+ sha1_ref = line.split()
+ d[sha1_ref[1]] = sha1_ref[0]
+ return d
+
+ async def new_changes(self, since: Optional[str] = None) -> List[Change]:
+ # git show-ref --heads
+ # git fetch origin +refs/heads/*:refs/heads/* --prune
+ # git show-ref --heads
+
+ await self._check_repo()
+ before = self._show_ref_output_to_dict(await self._git_show_ref())
+ log.debug('before %s', before)
+ await self._git_fetch_heads()
+ after = self._show_ref_output_to_dict(await self._git_show_ref())
+ log.debug('after %s', after)
+
+ changes: List[Change] = []
+ for branch, sha1 in after.items():
+ if since:
+ # We ignore recent changes and only take since into account
+ changes.append(Change(repository=self.name, branch=branch, before=None, after=sha1, since=since))
+ elif before.get(branch) != sha1:
+ changes.append(Change(repository=self.name, branch=branch, before=before.get(branch), after=sha1, since=None))
+ return changes
+
+ def get_task_number_and_fixes(self, body: str) -> Tuple[List[str], List[str]]:
+ task_numbers = []
+ fixes = []
+ for line in body.splitlines():
+ if line.startswith('Task-number:'):
+ issue_key = line[12:].strip()
+ if self._issue_key_regexp.fullmatch(issue_key):
+ task_numbers.append(issue_key)
+ if line.startswith('Fixes:'):
+ issue_key = line[6:].strip()
+ if self._issue_key_regexp.fullmatch(issue_key):
+ fixes.append(issue_key)
+ return task_numbers, fixes
+
+ @staticmethod
+ def _clean_branch_name(ref: str) -> str:
+ refs_heads = 'refs/heads/'
+ if ref.startswith(refs_heads):
+ return ref[len(refs_heads):]
+ return ref
+
+ @staticmethod
+ def _clean_tag_name(ref: str) -> str:
+ refs_tags = 'refs/tags/'
+ if ref.startswith(refs_tags):
+ ref = ref[len(refs_tags):]
+ if ref.startswith('v'):
+ return ref[1:]
+ return ref
+
+ @staticmethod
+ def _find_first_comparable_minor_version(ref: Version, sorted_versions: List[Version]) -> Optional[Version]:
+ for v in sorted_versions:
+ if v.version[0] == ref.version[0] and v.version[1] == ref.version[1]:
+ return v
+ return None
+
+ @staticmethod
+ async def _guess_version(ref: str, branches: List[str], tags: List[str]) -> Optional[str]:
+ ref = Repository._clean_branch_name(ref)
+ if ref.count('.') == 2:
+ return ref
+
+ branch_list: List[Version] = []
+ for b in branches:
+ try:
+ branch_list.append(Version(Repository._clean_branch_name(b)))
+ except ValueError:
+ # skip versions that are not x.y.z
+ pass
+ branch_list = sorted(branch_list, reverse=True)
+
+ tag_list: List[Version] = []
+ for t in tags:
+ try:
+ tag_list.append(Version(Repository._clean_tag_name(t)))
+ except ValueError:
+ # skip versions that are not x.y.z
+ pass
+ tag_list = sorted(tag_list, reverse=True)
+
+ if ref in ['dev', 'master'] and len(branch_list) > 0:
+ # take the last version found and increase minor by one
+ previous = branch_list[0].version
+ return '%s.%s.0' % (previous[0], str(int(previous[1] + 1)))
+
+ # x.y - find the hightest tag or branch of the same version
+ if ref.count('.') == 1:
+ try:
+ ref_version = Version(ref)
+ log.warning("found highest version: %s", sorted(branch_list + tag_list, reverse=True))
+ highest = Repository._find_first_comparable_minor_version(ref_version, sorted(branch_list + tag_list, reverse=True))
+ log.warning("found highest version: %s", highest)
+ if highest and highest.original_version_string.count('.') > 1:
+ # assume that 5.12 will be 5.12.7 if we find 5.12.6 in tags or branches
+ # the only exception is that if we got '5.12' as original version, we must assume 5.12.0, so end up in else
+ return '%s.%s.%s' % (highest.version[0], highest.version[1], str(int(highest.version[2]) + 1))
+ else:
+ return '%s.%s.0' % (ref_version.version[0], ref_version.version[1])
+ except ValueError:
+ log.debug("Invalid version number: '%s'", ref)
+ return None
+ log.error("Could not determine version for ref: '%s' (branches: %s, tags: %s)", ref, branches, tags)
+ return None
+
+ async def parse_commit_messages(self, change: Change) -> List[FixedByTag]:
+ format_options = {
+ "id": "%H",
+ "author_name": "%an",
+ "author_email": "%ae",
+ "date": "%ad",
+ "subject": "%s",
+ "body": "%b"}
+
+ git_log_fields = "%x1f".join((format_options['id'], format_options['author_name'], format_options['subject'], format_options['body']))
+ # use '1e' as start and '1f' as field separator
+ format_string = "%x1e" + git_log_fields + "%x1f"
+
+ # if a new branch is created, before will be None
+ commit_range = "%s..%s" % (change.before, change.after) if change.before else change.after
+ since = ''
+ if change.since:
+ since = '--since %s' % change.since
+ command = self.git_command("log %s --format=%s %s" % (commit_range, format_string, since))
+ process = await asyncio.create_subprocess_exec(*command.split(), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
+ stdout, stderr = await asyncio.wait_for(process.communicate(), 60)
+
+ if stderr:
+ log.warning("Error when running git command '%s': '%s'", command, stderr.decode('utf-8'))
+ commits = stdout.decode('utf-8', errors='replace').strip('\x1e').split('\x1e')
+
+ result: List[FixedByTag] = []
+ if commits == ['']: # ### FIXME: see test_gitlog, qt/qtlocation-mapboxgl comes up empty here
+ return result
+
+ for commit in commits:
+ # -2 to remove \x1f\n
+ sha1, author, subject, body = commit[:-2].split('\x1f')
+ task_numbers, fixes = self.get_task_number_and_fixes(body)
+ if task_numbers or fixes:
+ version = await self._guess_version(
+ change.branch,
+ branches=list(self._show_ref_output_to_dict(await self._git_show_ref(tags=False)).keys()),
+ tags=list(self._show_ref_output_to_dict(await self._git_show_ref(tags=True)).keys()))
+ result.append(FixedByTag(repository=self.name, branch=self._clean_branch_name(change.branch),
+ version=version,
+ sha1=sha1, author=author, subject=subject,
+ task_numbers=task_numbers, fixes=fixes))
+ return result
diff --git a/scripts/jira/jira-bug-closer/jiracloser/__init__.py b/scripts/jira/jira-bug-closer/jiracloser/__init__.py
new file mode 100644
index 00000000..8cb943a2
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/jiracloser/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+__all__ = ['JiraCloser']
+
+from .closer import JiraCloser
diff --git a/scripts/jira/jira-bug-closer/jiracloser/closer.py b/scripts/jira/jira-bug-closer/jiracloser/closer.py
new file mode 100644
index 00000000..0b564918
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/jiracloser/closer.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+from string import Template
+from time import sleep
+from distutils.version import LooseVersion
+from typing import Any, Dict, List, Optional, Tuple
+import jira
+from config import Config
+from git import FixedByTag
+from logger import logger
+
+log = logger('jira')
+
+comment_template = Template(
+ """A change related to this issue (sha1 '$sha1') was integrated in '$repository' in the '$branch' branch.
+This change will be in version: $fix_version - (JIRA: $version_id).
+Subject: {{$subject}}"
+""")
+
+
+class JiraCloser:
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.jira_url = self.config.jira_url
+ self.jira_client = jira.JIRA(self.jira_url, oauth=self.config.get_oauth_data())
+
+ @staticmethod
+ def _clean_jira_versions(jira_version_list: List[Dict[str, str]]) -> List[Tuple[LooseVersion, str, bool]]:
+ versions: List[Tuple[LooseVersion, str, bool]] = []
+ for version_data in jira_version_list:
+ # Skip empty descriptions, they are only missing for old versions and irrelevant
+ version_description = version_data.get('description')
+ if not version_description:
+ continue
+ looseVersion = LooseVersion(version_description)
+ # Skip versions that are for example only two digits, e.g. "6.0"
+ if len(looseVersion.version) < 3:
+ continue
+ id = version_data['id']
+ released = bool(version_data['released'])
+ versions.append((looseVersion, id, released))
+ return versions
+
+ def _jira_version_list(self, issue: jira.Issue) -> List[Tuple[LooseVersion, str, bool]]:
+ try:
+ project_key = issue.key.split('-')[0]
+ meta = self.jira_client.createmeta(
+ projectKeys=project_key,
+ issuetypeIds=[issue.fields.issuetype.id], expand='projects.issuetypes.fields')
+ projects = meta['projects'][0]
+ fields = projects['issuetypes'][0]['fields']
+ versions = fields.get('versions')
+ # tasks have fixVersions instead
+ if not versions:
+ versions = fields.get('fixVersions')
+ if not versions:
+ log.error("Could not get versions or fixVersions for issue '%s'", issue.key)
+ return []
+
+ allowed_versions: List[Dict[str, str]] = versions['allowedValues']
+ return JiraCloser._clean_jira_versions(allowed_versions)
+ except Exception as e:
+ log.error("Could not determine allowed versions.")
+ log.warning(str(e))
+ return []
+
+ def _guess_fix_version(self, version: str, known_versions: List[Tuple[LooseVersion, str, bool]]) -> Optional[str]:
+ if not version.count('.') == 2:
+ log.error("Invalid version: '%s' (must be 'x.y.z')", version)
+ return None
+
+ def is_same_version(left: LooseVersion, right: LooseVersion) -> bool:
+ assert len(left.version) > 2
+ assert len(right.version) > 2
+ return left.version[0] == right.version[0] and left.version[1] == right.version[1] and left.version[2] == right.version[2]
+
+ needle = LooseVersion(version)
+ candidates = [v for v in known_versions if is_same_version(needle, v[0])]
+ if not candidates:
+ return None
+
+ # take everything where major, minor, patch are right
+ if len(candidates) == 1:
+ return candidates[0][1]
+
+ # Sort, except for one silly thing in LooseVersion: no alphanumeric component is smallest, so 5.13.0 < 5.13.0 Alpha 1
+ candidates.sort()
+ if len(candidates[0][0].version) == 3:
+ candidates.append(candidates.pop(0))
+
+ # if there are released and unreleased versions, narrow it down to unreleased
+ unreleased = [v for v in candidates if not v[2]]
+ if unreleased:
+ candidates = unreleased
+ return candidates[0][1]
+
+ # we seem to have no unreleased versions, that means the last one is correct
+ return candidates[-1][1]
+
+ def _get_fix_version_field(self, issue: jira.Issue, fix_version: Optional[str]) -> Tuple[str, Dict[str, Any]]:
+ """ Returns the version_id and the needed fields to update the fix version in JIRA. """
+ if not fix_version:
+ return 'unknown version', {}
+
+ jira_versions = self._jira_version_list(issue)
+ version_id = self._guess_fix_version(fix_version, jira_versions)
+ if not version_id:
+ log.warning("Could not guess fix version for issue '%s': %s - versions: %s", issue.key, fix_version, jira_versions)
+ return 'unknown version', {}
+
+ # check if this version should be added at all
+ # only operate on major/minor/patch and ignore alpha/beta/...
+ new_version_major, new_version_minor, new_version_patch = LooseVersion(fix_version).version[0:3]
+ assert isinstance(new_version_major, int)
+ assert isinstance(new_version_minor, int)
+ assert isinstance(new_version_patch, int)
+
+ set_versions = [LooseVersion(version.description) for version in issue.fields.fixVersions]
+ for old_version in set_versions:
+ if len(old_version.version) < 3:
+ continue
+
+ old_version_major, old_version_minor, old_version_patch = old_version.version[0:3]
+ # skip existing random fix versions that do not have major/minor/patch set
+ if not (isinstance(old_version_major, int) and isinstance(old_version_minor, int) and isinstance(old_version_patch, int)):
+ continue
+
+ # if 5.12.2 is there, don't add 5.12.3
+ if (new_version_major, new_version_minor) == (old_version_major, old_version_minor) and int(new_version_patch) >= int(old_version_patch):
+ log.info("Skipping adding version '%s' because of '%s' for '%s'", fix_version, old_version, issue.key)
+ return '', {} # ### FIXME is there any point in returning the version id here?
+
+ # if 5.12.0 is there, don't add 5.13.x
+ # if 5.x.0 is there, don't add 6.y.z
+ if (new_version_major, new_version_minor) > (old_version_major, old_version_minor) and old_version_patch == 0:
+ log.info("Skipping adding version '%s' because of '%s' for '%s'", fix_version, old_version, issue.key)
+ return '', {} # ### FIXME is there any point in returning the version id here?
+
+ version_ids = [version.id for version in issue.fields.fixVersions]
+ version_ids.append(version_id)
+ versions: List[Dict[str, str]] = []
+ for v in version_ids:
+ versions.append({'id': v})
+ log.info("Added version '%s' (%s) to '%s'", fix_version, version_id, issue.key)
+ return version_id, {'fixVersions': versions}
+
+ def _get_change_sha1_field(self, issue: jira.Issue, fix: FixedByTag) -> Dict[str, Any]:
+ change_field = issue.fields.customfield_10142 or ''
+ changes = change_field.split()
+ if fix.sha1 in changes:
+ return {}
+ changes.append('%s (%s/%s)' % (fix.sha1, fix.repository, fix.branch))
+ return {'customfield_10142': ' '.join(changes)}
+
+ def _close_issue(self, issue: jira.Issue, fields: Dict[str, Any]) -> None:
+ if issue.fields.status.name == 'In Progress':
+ fields.update({'resolution': {'name': 'Done'}})
+ self.jira_client.transition_issue(issue.key, transition='Fixed', fields=fields)
+ elif issue.fields.status.name == 'Closed':
+ issue.update(fields=fields)
+ else:
+ fields.update({'resolution': {'name': 'Done'}})
+ self.jira_client.transition_issue(issue.key, transition='Close', fields=fields)
+
+ def _update_issue(self, fix: FixedByTag, issue_key: str, fixes: bool) -> None:
+ try:
+ issue = self.jira_client.issue(issue_key)
+ version_id = None
+ if fixes:
+ # get the fix version to update
+ version_id, extra_fields = self._get_fix_version_field(issue, fix.version)
+ # get changes in the form of sha1 and repo + branch
+ extra_fields.update(self._get_change_sha1_field(issue, fix))
+ # close the issue
+ self._close_issue(issue, extra_fields)
+
+ if self.config.add_comment_to_issues:
+ comment = comment_template.substitute(sha1=fix.sha1, repository=fix.repository, branch=fix.branch, fix_version=fix.version or 'unknown version', version_id=version_id or 'unknown version', subject=fix.subject)
+ self.jira_client.add_comment(issue, comment)
+ log.info('Added comment to %s', self.config.jira_url + '/browse/' + issue_key)
+
+ log.info("Finished updating %s successfully", fix)
+
+ except jira.exceptions.JIRAError as e:
+ if e.status_code == 404:
+ log.warning("Issue could not be found: %s", issue_key)
+ else:
+ raise e
+
+ def _update_issue_with_retry(self, fix: FixedByTag, issue_key: str, fixes: bool) -> None:
+ for attempt in range(5):
+ sleep(attempt) # wait for up to 4 seconds, try 5 times
+ try:
+ self._update_issue(fix, issue_key, fixes)
+ break
+ except jira.exceptions.JIRAError as e:
+ if e.status_code == 500:
+ log.warning("Got internal server error from jira for: %s", e.url)
+ log.info(str(e))
+ else:
+ raise e
+
+ def run(self, fix: FixedByTag) -> None:
+ log.info("Processing %s", fix)
+ for issue_key in fix.task_numbers:
+ self._update_issue_with_retry(fix, issue_key, fixes=False)
+ for issue_key in fix.fixes:
+ self._update_issue_with_retry(fix, issue_key, fixes=True)
diff --git a/scripts/jira/jira-bug-closer/logger/__init__.py b/scripts/jira/jira-bug-closer/logger/__init__.py
new file mode 100644
index 00000000..7dbd9c46
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/logger/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+__all__ = ["logger"]
+
+from .logger import logger
diff --git a/scripts/jira/jira-bug-closer/logger/logger.py b/scripts/jira/jira-bug-closer/logger/logger.py
new file mode 100644
index 00000000..7fbf4c78
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/logger/logger.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import coloredlogs
+import logging
+
+level = logging.INFO
+
+
+def logger(name: str) -> logging.Logger:
+ log = logging.getLogger(name)
+ log.setLevel(level)
+ log_format = "%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s"
+ coloredlogs.install(level=level, logger=log, fmt=log_format)
+ return log
diff --git a/scripts/jira/jira-bug-closer/main.py b/scripts/jira/jira-bug-closer/main.py
new file mode 100755
index 00000000..3be1712d
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/main.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+from bot import Bot
+from logger import logger
+
+
+log = logger("main")
+
+
+if __name__ == "__main__":
+ try:
+ bot = Bot()
+ bot.run()
+ except KeyboardInterrupt:
+ log.info("Stopped by keyboard interrupt.")
diff --git a/scripts/jira/jira-bug-closer/oauth_dance.py b/scripts/jira/jira-bug-closer/oauth_dance.py
new file mode 100755
index 00000000..2dc08281
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/oauth_dance.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+
+# Convenience tool to get the JIRA tokens in place.
+# This helps with the initial setup when connecting the bot for the first time.
+
+# This is example code from Atlassian - https://bitbucket.org/atlassianlabs/atlassian-oauth-examples/
+# The modified version for Python requests was copied from this fork:
+# https://bitbucket.org/MicahCarrick/atlassian-oauth-examples/src/68d005311b9b74d6a85787bb87ccc948766486d3/python-requests/example.py?at=default&fileviewer=file-view-default
+
+from oauthlib.oauth1 import SIGNATURE_RSA
+from requests_oauthlib import OAuth1Session
+from jira.client import JIRA
+
+
+def read(file_path: str) -> str:
+ """ Read a file and return it's contents. """
+ with open(file_path) as f:
+ return f.read()
+
+
+# The Consumer Key created while setting up the "Incoming Authentication" in
+# JIRA for the Application Link.
+CONSUMER_KEY = 'jira-gerrit-oauth'
+
+# The contents of the rsa.pem file generated (the private RSA key)
+RSA_KEY = read('jiracloser.pem')
+
+# The URLs for the JIRA instance
+JIRA_SERVER = 'https://bugreports-test.qt.io'
+REQUEST_TOKEN_URL = JIRA_SERVER + '/plugins/servlet/oauth/request-token'
+AUTHORIZE_URL = JIRA_SERVER + '/plugins/servlet/oauth/authorize'
+ACCESS_TOKEN_URL = JIRA_SERVER + '/plugins/servlet/oauth/access-token'
+
+
+# Step 1: Get a request token
+
+oauth = OAuth1Session(CONSUMER_KEY, signature_type='auth_header',
+ signature_method=SIGNATURE_RSA, rsa_key=RSA_KEY)
+request_token = oauth.fetch_request_token(REQUEST_TOKEN_URL)
+
+print("STEP 1: GET REQUEST TOKEN")
+print(" oauth_token={}".format(request_token['oauth_token']))
+print(" oauth_token_secret={}".format(request_token['oauth_token_secret']))
+print("\n")
+
+
+# Step 2: Get the end-user's authorization
+
+print("STEP2: AUTHORIZATION")
+print(" Visit to the following URL to provide authorization:")
+print(" {}?oauth_token={}".format(AUTHORIZE_URL, request_token['oauth_token']))
+print("\n")
+
+while input("Press any key to continue..."):
+ pass
+
+
+# Step 3: Get the access token
+
+access_token = oauth.fetch_access_token(ACCESS_TOKEN_URL, verifier="some_verifier")
+
+print("STEP2: GET ACCESS TOKEN")
+print(" oauth_token={}".format(access_token['oauth_token']))
+print(" oauth_token_secret={}".format(access_token['oauth_token_secret']))
+print("\n")
+
+
+# Now you can use the access tokens with the JIRA client. Hooray!
+
+jira = JIRA(options={'server': JIRA_SERVER}, oauth={
+ 'access_token': access_token['oauth_token'],
+ 'access_token_secret': access_token['oauth_token_secret'],
+ 'consumer_key': CONSUMER_KEY,
+ 'key_cert': RSA_KEY
+})
+
+# print all of the project keys just as an example
+print("Verifying that the access works, listing JIRA projects:")
+for project in jira.projects():
+ print(project.key)
diff --git a/scripts/jira/jira-bug-closer/systemd/jira_gerrit_bot.service b/scripts/jira/jira-bug-closer/systemd/jira_gerrit_bot.service
new file mode 100644
index 00000000..4ea22ad9
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/systemd/jira_gerrit_bot.service
@@ -0,0 +1,40 @@
+# systemd unit file, copy to /etc/systemd/system and enable the service
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+[Unit]
+Description=Jira Issue Closer - Monitor Gerrit and close JIRA issues
+
+[Service]
+ExecStart=/usr/bin/make -C /home/jirabot/jira-bug-closer production
+Restart=always
+User=jirabot
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/scripts/jira/jira-bug-closer/tests/__init__.py b/scripts/jira/jira-bug-closer/tests/__init__.py
new file mode 100644
index 00000000..e5a0d9b4
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/__init__.py
@@ -0,0 +1 @@
+#!/usr/bin/env python3
diff --git a/scripts/jira/jira-bug-closer/tests/test_gitlog.py b/scripts/jira/jira-bug-closer/tests/test_gitlog.py
new file mode 100644
index 00000000..637b6f21
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/test_gitlog.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import asyncio
+import pytest
+from typing import List
+from git import Repository, Change, FixedByTag
+from logger import logger
+from git.repository import Version
+
+log = logger('test')
+
+# make sure we have a checkout, otherwise this fails
+loop = asyncio.get_event_loop()
+repo = Repository('qt/qtbase')
+loop.run_until_complete(repo._check_repo())
+repo = Repository('qt/qtdeclarative')
+loop.run_until_complete(repo._check_repo())
+repo = Repository('qt/qtdatavis3d')
+loop.run_until_complete(repo._check_repo())
+repo = Repository('yocto/meta-qt5')
+loop.run_until_complete(repo._check_repo())
+
+
+@pytest.mark.parametrize("branch,expected,branches,tags", [
+ ('dev', '5.12.0',
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('wip/myfeature', None,
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.12.0', '5.12.0',
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.9.4', '5.9.4',
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.11', '5.11.2',
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.11', '5.11.2',
+ ['5.10', '5.11', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.12', '5.12.0',
+ ['5.10', '5.11', '5.11.0', '5.11.1', 'dev'],
+ ['v5.11.0', 'v5.11.0-alpha1', 'v5.11.0-beta1', 'v5.11.0-beta2', 'v5.11.0-beta3', 'v5.11.0-beta4', 'v5.11.0-rc1', 'v5.11.0-rc2', 'v5.11.1']),
+ ('5.12', '5.12.1',
+ ['5.12.0', '5.12'],
+ []),
+ ('5.12', '5.12.1',
+ ['5.12', '5.12.0'],
+ []),
+])
+@pytest.mark.asyncio
+async def test_versions(branch: str, expected: str, branches: List[str], tags: List[str]):
+ version = await Repository._guess_version(branch, branches, tags)
+ assert version == expected
+
+
+@pytest.mark.parametrize("change,expected", [
+ (
+ Change(repository='qt/qtbase', branch='dev', before='128a6eec065dfe683e6d776183d63908ca02e8f', after='b0085dbeeac47d0ce566750d93f1b1f865d07cd'),
+ [FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0',
+ sha1='bd0279c4173eb627d432d9a05411bbc725240d4e', task_numbers=["QTBUG-69548"], fixes=[],
+ author='Kai Koehne', subject='Logging: Accept .ini files written by QSettings')],
+ ),
+ (
+ Change(repository='qt/qtbase', branch='dev', before='0bb760260eb055f813247bf9ef06e372cac219d3', after='b0085dbeeac47d0ce566750d93f1b1f865d07cd'),
+ [FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0',
+ sha1='bd0279c4173eb627d432d9a05411bbc725240d4e', task_numbers=["QTBUG-69548"], fixes=[],
+ author='Kai Koehne', subject='Logging: Accept .ini files written by QSettings'),
+
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='8a450f570b8dc40f61a68db0ca5eb69a7a97272c', author='Robbert Proost',
+ subject='QUrl: Support IPv6 addresses with zone id', fixes=[], task_numbers=["QTBUG-25550"]),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='3f80783b1188afdf032571b48bc47a160d6dccf6', author='Ryan Chu',
+ subject='Rework QNetworkReply tests to use docker-based test servers', fixes=[], task_numbers=["QTQAINFRA-1686"])]
+ ),
+ (
+ Change(repository='qt/qtbase', branch='refs/heads/dev', before='ed7f86cb077d33d0dd9e646af28e3f57c160b570', after='458b0ba8e04349a0a7ca82598a5bf7472991ebc8'),
+ [
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='823acb069d92b68b36f1b2bb59575bb0595275b4', author='Tor Arne Vestbø', fixes=[], task_numbers=["QTBUG-63572"], subject='macOS: Don\'t call [NSOpenGLContext update] for every frame'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='491e427bb2d3cafccbb26d2ca3b7e128d786a564', author='Thiago Macieira', fixes=[], task_numbers=["QTBUG-69800"], subject='QTimer: Add const to some singleShot methods'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='ca14151a0cdd3bc5fa364b2816bcd3b51af4bf3d', author='Mitch Curtis', fixes=[], task_numbers=["QTBUG-69492"], subject='tst_qspinbox: include actual emission count in failure message'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='58e3e32adf227e91771fa421f2657f758ef1411b', author='Mitch Curtis', fixes=[], task_numbers=["QTBUG-69492"], subject='tst_qdatetimeedit: hide testWidget when creating widgets on the stack'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='64a560d977a0a511ef541d6116d82e7b5c911a92', author='Thiago Macieira', fixes=[], task_numbers=["QTBUG-69744"], subject='QObject: do allow setProperty() to change the type of the property'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='c6cca0f492717582cb113f3d62e97f554798cf14', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-58420"], subject='Doc: Update out-of-date image in QColorDialog documentation'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='6953e513f9034b98a48d83b67afd671f1ee33aeb', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-56077"], subject='Doc: Clean up Qt::ApplicationAttribute docs'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='87704611151af78cfef17ae518c40bfb49c7b934', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-63248"], subject='Doc: Update really old screenshot in Sliders Example'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='ae289884db05cbaac71156983974eebfb9b59730', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-62072"], subject='Doc: Fix wrong link in QFont documentation'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='cdf154e65a3137597f62880361c407e368aae0d6', author='Allan Sandfeld Jensen', fixes=[], task_numbers=["QTBUG-69724"], subject='Optimize blits of any compatible formats'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='d2d59e77d5e16bc79ddfed37f4f29d1dcd9b92a7', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-53856"], subject='Doc: Increase precision in description of convenience typedefs'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='1c8f9eb79da837db8e37cf6348de459088c3a20e', author='Allan Sandfeld Jensen', fixes=[], task_numbers=["QTBUG-69724"], subject='Add missing optimization for loading RGB32 to RGBA64 using NEON'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='66be5445e64b54bf60069dfee5dd918459e3deed', author='Friedemann Kleint', fixes=[], task_numbers=["QTBUG-53717"], subject='Windows: Implement Qt::WindowStaysOnBottomHint'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='f0ff73f631093b11c77d8d6fb548acfe8eb62583', author='Joerg Bornemann', fixes=[], task_numbers=["QTBUG-67905"], subject='QProcess::startDetached: Fix behavior change on Windows'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='8c4207dddf9b2af0767de2ef0a10652612d462a5', author='Eirik Aavitsland', fixes=[], task_numbers=["QTBUG-69449"], subject='Fix crash in qppmhandler for certain malformed image files'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='81910b5f3cfb8c8b0c009913d62dacff4e73bc3b', author='Timur Pocheptsov', fixes=[], task_numbers=["QTBUG-69677"], subject='SecureTransport - disable lock on sleep for the custom keychain'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='db738cbaf1ba7a4886f7869db16dbb9107a8e65e', author='Ales Erjavec', fixes=[], task_numbers=["QTBUG-69404", "QTBUG-30116"], subject='QCommonStylePrivate::viewItemSize: Fix text width bounds calculation'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='780dc2291bc0e114bab8b9ccd8706708f6b47270', author='Kai Koehne', fixes=[], task_numbers=["QTBUG-67443"], subject='Fix builds with some MinGW distributions'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='c5af04cf8aa7bf2fbeaaf2a40f169fe8c17239f1', author='Błażej Szczygieł', fixes=[], task_numbers=["QTBUG-61948"], subject='HiDPI: Fix calculating window mask from pixmap on drag and drop'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='4126de887799c61793bf1f9efc8b7ac7b66c8b32', author='Gabriel de Dietrich', fixes=[], task_numbers=["QTBUG-69496"], subject='QCocoaMenuLoader - ensure that ensureAppMenuInMenu indeed, ensures'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='6f87926df55edb119e5eeb53c3beac135fdf72e2', author='Gatis Paeglis', fixes=[], task_numbers=["QTBUG-68501", "QTBUG-69628"], subject='xcb: partly revert 3bc0f1724ae49c2fd7e6d7bcb650350d20d12246'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='0dfdf23d05d09cbffcec4021c9cbebfb6eeddfa7', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-59487"], subject='Doc: Synchronize documentation with code snippet'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='46fc3d3729df9e81e42f87c46907d6eb81a0c669', author='Friedemann Kleint', fixes=[], task_numbers=["QTBUG-69637"], subject='Windows QPA: Fix override cursor being cleared when crossing window borders'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='e386cd03d12e401b9e3945602e9621a86009fa11', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-68109"], subject='Doc: Remove reference to QTestEvent'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='341d967068516ff850227f718eaff46530cd97c2', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-69678"], subject='Doc: Fix broken links after page rename'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='6a1c26b08a56cd71315fcbbf2743c32072d806d2', author='Paul Wicking', fixes=[], task_numbers=["QTBUG-69483"], subject='Doc: Update signals and slots introduction page'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='9a30a8f4fc19a90835e4d1032f9ab753ff3b2ae6', author='Edward Welbourne', fixes=[], task_numbers=["QTBUG-23307"], subject='Link from QLocale to where date-time formats are explained'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='2dfa41e0eac65f5772ec61364f9afd0ce49fecc7', author='Mårten Nordheim', fixes=[], task_numbers=["QTBUG-65960"], subject='Return to eventloop after emitting encrypted'),
+ FixedByTag(repository='qt/qtbase', branch='dev', version='5.13.0', sha1='f43e947dc405b6a2324656f631c804db8e8dec3d', author='Jüri Valdmann', fixes=[], task_numbers=["QTBUG-69626"], subject='QJsonDocument: Make emptyObject an object')
+ ],
+ ),
+ (
+ # There is a commit with a line "Fixes:" which doesn't have any bug number since it's part of the normal commit message.
+ # Should not trigger anything here.
+ Change(repository='yocto/meta-qt5', branch='refs/heads/upstream/jansa/master', before='4587cc3b2b8707ed71eb15b9a0a460d76099606e', after='a563a6f0e7f4bbbadf8b0d85b06f63878e6142c2'),
+ []
+ ),
+ (
+ # test that a newly created branch works (before will be None), using the very first commits of the dev branch
+ Change(repository='qt/qtbase', branch='dev', before=None, after='07bed9a211115c56bfa63983b0502f691f19f789'),
+ []
+ ),
+ (
+ # The first commit has broken encoding in the author name, check that we don't crash on that
+ Change(repository='qt/qtdatavis3d', branch='refs/heads/5.11.2', before=None, after='7997c3aca1d6e03dd31e145d70a7a40df17e5330'),
+ []
+ ),
+ (
+ # This has a long Fixes: random comment line, skip it
+ Change(repository='qt/qtlocation-mapboxgl', branch='upstream/12268-android-collator-wrapper', before='d9e4c61923813b61ffccb6439d0fd3e9993a1a05', after='7e51e52f0cabd909557b763f10e90ac0444e90a1'),
+ []
+ ),
+ (
+ # Invalid version number: tqtc/5.12
+ Change(repository='qt/tqtc-qt5', branch='refs/heads/tqtc/5.12', before='33276c1719d2623dff6aec11e1f3dc1cb0e45847', after='bc644fd6c9b4ef409efc5a4378420c3aca2d07b8'),
+ [
+ FixedByTag(repository='qt/tqtc-qt5', branch='tqtc/5.12', version=None, sha1='bb6a91d5d4c684e8a97feca61449b41628afaefa', author='Joni Jantti', fixes=[], task_numbers=['QTQAINFRA-2103'], subject='Provisioning: PyPFD2')
+ ]
+ ),
+ (
+ Change(repository='qt/qtdeclarative', branch='refs/heads/5.12.0', before='920f50731a8fe7507aece1318c9e91f3f12b525e', after='9e9acff340032bd4ec5ee6fbd1b13cd51e14ca3d'),
+ [
+ FixedByTag(repository='qt/qtdeclarative', branch='5.12.0', version='5.12.0', sha1='9e9acff340032bd4ec5ee6fbd1b13cd51e14ca3d', author='Shawn Rutledge', fixes=['QTBUG-70258'], task_numbers=[], subject='MultiPointTouchArea: capture the mouse position on press')
+ ]
+ ),
+ (
+ Change(repository='qt/qtlocation-mapboxgl', branch='refs/heads/upstream/user-location-delegate-method', before='246be964f2e222118643bacac1a70c2692f2bdec', after='04add9801e557b06c08189659c4fbb8bdc7d235b'),
+ []
+ ),
+ (
+ Change(repository='qt/qtdeclarative', branch='refs/heads/5.13.0', before='722fd8b86e7c3b5d6e4c3382f2710e4d3bfed3ec~', after='722fd8b86e7c3b5d6e4c3382f2710e4d3bfed3ec'),
+ [
+ FixedByTag(repository='qt/qtdeclarative', branch='5.13.0', version='5.13.0', sha1='722fd8b86e7c3b5d6e4c3382f2710e4d3bfed3ec', author='Allan Sandfeld Jensen', fixes=['QTBUG-32525', 'QTBUG-70748'], task_numbers=[], subject='Render inline custom text objects'),
+ ]
+ ),
+])
+@pytest.mark.asyncio
+async def test_parsing(event_loop, change: Change, expected: List[FixedByTag]):
+ async with Repository(change.repository) as repo:
+ fixes = await repo.parse_commit_messages(change)
+ change.__repr__()
+ for fix in fixes:
+ fix.__repr__()
+ assert fixes == expected
+
+
+@pytest.mark.parametrize("versions,sorted_versions", [
+ (
+ [Version("5.12"), Version("5.12.0"), Version("5.12.1")],
+ [Version("5.12"), Version("5.12.0"), Version("5.12.1")],
+ ),
+ (
+ [Version("5.12.0"), Version("5.12"), Version("5.12.1")],
+ [Version("5.12"), Version("5.12.0"), Version("5.12.1")],
+ ),
+ (
+ [Version("5.12.1"), Version("5.12.0"), Version("5.12")],
+ [Version("5.12"), Version("5.12.0"), Version("5.12.1")],
+ ),
+])
+def test_version_class(versions: List[Version], sorted_versions: List[Version]):
+ assert sorted(versions) == sorted_versions
+ assert Version("5.12") < Version("5.12.0")
+ assert Version("5.12.0") > Version("5.12")
+ assert Version("5.12") <= Version("5.12.0")
+ assert Version("5.12.0") >= Version("5.12")
+ assert Version("5.12.0") != Version("5.12")
diff --git a/scripts/jira/jira-bug-closer/tests/test_jira_close_issue.py b/scripts/jira/jira-bug-closer/tests/test_jira_close_issue.py
new file mode 100644
index 00000000..e5c79eb8
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/test_jira_close_issue.py
@@ -0,0 +1,102 @@
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+from config import Config
+from jiracloser import JiraCloser
+from logger import logger
+from git import FixedByTag
+
+log = logger('test')
+
+
+def test_close_issue():
+ config = Config('test')
+ j = JiraCloser(config)
+ issue_key = 'QTBUG-4795'
+
+ issue = j.jira_client.issue(issue_key)
+
+ if issue.fields.status.name != 'Open':
+ log.info('Re-opening issue from "%s"', issue.fields.status)
+ if issue.fields.status.name == 'In Progress':
+ j.jira_client.transition_issue(issue.key, transition='Stop Work')
+ else:
+ j.jira_client.transition_issue(issue_key, transition='Re-open')
+ # clear fix versions and commits
+ issue.update(fields={'fixVersions': [], 'customfield_10142': ''})
+
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Open'
+ fix = FixedByTag(repository='foo/bar', branch='dev', version='5.13.0',
+ sha1='bd0279c4173eb627d432d9a05411bbc725240d4e', task_numbers=[], fixes=['CON-5'],
+ author='Some One', subject='Close a test issue')
+ j._update_issue_with_retry(fix, issue_key, True)
+
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Closed'
+ assert issue.fields.resolution.name == 'Done'
+ assert len(issue.fields.fixVersions) == 1
+ assert issue.fields.fixVersions[0].name.startswith('5.13.0')
+ assert issue.fields.customfield_10142 == 'bd0279c4173eb627d432d9a05411bbc725240d4e (foo/bar/dev)'
+
+ j.jira_client.transition_issue(issue_key, transition='Re-open')
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Open'
+ assert not issue.fields.resolution
+
+ # Assign to bot and start work
+ assert j.jira_client.assign_issue(issue.key, assignee='qtgerritbot')
+ j.jira_client.transition_issue(issue_key, transition='Start Work')
+ j._update_issue_with_retry(fix, issue_key, True)
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Closed'
+ assert issue.fields.resolution.name == 'Done'
+
+ # Close it a second time (and that should just do nothing, not raise an exception)
+ j._update_issue_with_retry(fix, issue_key, True)
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Closed'
+ assert issue.fields.resolution.name == 'Done'
+
+ # Sometimes we have less sensical fix versions set already, such as "Some future release".
+ # Make sure that doesn't bother the bot.
+ j.jira_client.transition_issue(issue_key, transition='Re-open')
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Open'
+ assert not issue.fields.resolution
+ version_some_future_release = '11533'
+ issue.update(fields={'fixVersions': [{'id': version_some_future_release}]})
+ issue = j.jira_client.issue(issue_key)
+ assert len(issue.fields.fixVersions) == 1
+
+ j._update_issue_with_retry(fix, issue_key, True)
+ issue = j.jira_client.issue(issue_key)
+ assert issue.fields.status.name == 'Closed'
+ assert issue.fields.resolution.name == 'Done'
+ # Verify that the new fix version was added and the "some future release" is still there.
+ assert len(issue.fields.fixVersions) == 2
diff --git a/scripts/jira/jira-bug-closer/tests/test_jira_versions.py b/scripts/jira/jira-bug-closer/tests/test_jira_versions.py
new file mode 100644
index 00000000..05c2033b
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/test_jira_versions.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+from distutils.version import LooseVersion
+from typing import Dict, List, Tuple
+import pytest
+from config import Config
+from jiracloser import JiraCloser
+
+config = Config('test')
+closer = JiraCloser(config)
+
+
+def test_set_jira_versions():
+ issue_key = 'QTBUG-4795'
+ issue = closer.jira_client.issue(issue_key)
+
+ # clear any version that was set before
+ issue.update(fields={'fixVersions': []})
+ issue = closer.jira_client.issue(issue_key)
+ assert not issue.fields.fixVersions
+
+ version_id, version_field = closer._get_fix_version_field(issue, fix_version='5.11.2')
+ assert version_field == {'fixVersions': [{'id': '16916'}]}
+ issue.update(fields=version_field)
+ issue = closer.jira_client.issue(issue_key)
+ assert len(issue.fields.fixVersions) == 1
+ assert issue.fields.fixVersions[0].id == '16916'
+ assert issue.fields.fixVersions[0].id == version_id
+ assert issue.fields.fixVersions[0].name == '5.11.2'
+
+ # now merge to 5.12.0, we want that version added
+ version_id, version_field = closer._get_fix_version_field(issue, fix_version='5.12.0')
+ assert version_field == {'fixVersions': [{'id': '16916'}, {'id': '16832'}]}
+ issue.update(fields=version_field)
+ issue = closer.jira_client.issue(issue_key)
+ assert len(issue.fields.fixVersions) == 2
+
+ # now merge to 5.12.1, nothing should happen since we have 5.12.0 in the list
+ version_id, version_field = closer._get_fix_version_field(issue, fix_version='5.12.1')
+ assert version_field == {}
+ issue.update(fields=version_field)
+ issue = closer.jira_client.issue(issue_key)
+ assert len(issue.fields.fixVersions) == 2
+
+ # also add the change in 5.13.0, again nothing to be done, 5.12.0 is in the list
+ version_id, version_field = closer._get_fix_version_field(issue, fix_version='5.13.0')
+ assert version_field == {}
+ issue.update(fields=version_field)
+ issue = closer.jira_client.issue(issue_key)
+ assert len(issue.fields.fixVersions) == 2
+
+
+# in order to keep the test stable, use a version data dump:
+jira_qt_versions = [
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11486', 'id': '11486', 'description': '3.x', 'name': '3.x', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11487', 'id': '11487', 'description': '4.0.0', 'name': '4.0.0', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11489', 'id': '11489', 'description': '4.0.1', 'name': '4.0.1', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11490', 'id': '11490', 'description': '4.1.0', 'name': '4.1.0', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11492', 'id': '11492', 'description': '4.1.1', 'name': '4.1.1', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11493', 'id': '11493', 'description': '4.1.2', 'name': '4.1.2', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11494', 'id': '11494', 'description': '4.1.3', 'name': '4.1.3', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11495', 'id': '11495', 'description': '4.1.4', 'name': '4.1.4', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11496', 'id': '11496', 'description': '4.1.5', 'name': '4.1.5', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11497', 'id': '11497', 'description': '4.2.0', 'name': '4.2.0', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11500', 'id': '11500', 'description': '4.2.1', 'name': '4.2.1', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11501', 'id': '11501', 'description': '4.2.2', 'name': '4.2.2', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11502', 'id': '11502', 'description': '4.2.3', 'name': '4.2.3', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11503', 'id': '11503', 'description': '4.3.0', 'name': '4.3.0', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11506', 'id': '11506', 'description': '4.3.1', 'name': '4.3.1', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11507', 'id': '11507', 'description': '4.3.2', 'name': '4.3.2', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11508', 'id': '11508', 'description': '4.3.3', 'name': '4.3.3', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11509', 'id': '11509', 'description': '4.3.4', 'name': '4.3.4', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11510', 'id': '11510', 'description': '4.3.5', 'name': '4.3.5', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11511', 'id': '11511', 'description': '4.4.0', 'name': '4.4.0', 'archived': False, 'released': True, 'releaseDate': '2008-05-06', 'userReleaseDate': "06 May '08", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11515', 'id': '11515', 'description': '4.4.1', 'name': '4.4.1', 'archived': False, 'released': True, 'releaseDate': '2008-06-24', 'userReleaseDate': "24 Jun '08", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11516', 'id': '11516', 'description': '4.4.2', 'name': '4.4.2', 'archived': False, 'released': True, 'releaseDate': '2008-09-18', 'userReleaseDate': "18 Sep '08", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11517', 'id': '11517', 'description': '4.4.3', 'name': '4.4.3', 'archived': False, 'released': True, 'releaseDate': '2008-09-29', 'userReleaseDate': "29 Sep '08", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11518', 'id': '11518', 'description': '4.5.0', 'name': '4.5.0', 'archived': False, 'released': True, 'releaseDate': '2009-03-03', 'userReleaseDate': "03 Mar '09", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11522', 'id': '11522', 'description': '4.5.1', 'name': '4.5.1', 'archived': False, 'released': True, 'releaseDate': '2009-04-23', 'userReleaseDate': "23 Apr '09", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11523', 'id': '11523', 'description': '4.5.2', 'name': '4.5.2', 'archived': False, 'released': True, 'releaseDate': '2009-06-23', 'userReleaseDate': "23 Jun '09", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11525', 'id': '11525', 'description': '4.5.3', 'name': '4.5.3', 'archived': False, 'released': True, 'releaseDate': '2009-10-01', 'userReleaseDate': "01 Oct '09", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11527', 'id': '11527', 'description': '4.6.0', 'name': '4.6.0', 'archived': False, 'released': True, 'releaseDate': '2009-12-01', 'userReleaseDate': "01 Dec '09", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11561', 'id': '11561', 'description': '4.6.1', 'name': '4.6.1', 'archived': False, 'released': True, 'releaseDate': '2010-01-19', 'userReleaseDate': "19 Jan '10", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11569', 'id': '11569', 'description': '4.6.2', 'name': '4.6.2', 'archived': False, 'released': True, 'releaseDate': '2010-02-15', 'userReleaseDate': "15 Feb '10", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11572', 'id': '11572', 'description': '4.6.3', 'name': '4.6.3', 'archived': False, 'released': True, 'releaseDate': '2010-06-08', 'userReleaseDate': "08 Jun '10", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11563', 'id': '11563', 'description': '4.7.0', 'name': '4.7.0', 'archived': False, 'released': True, 'releaseDate': '2010-09-21', 'userReleaseDate': "21 Sep '10", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11664', 'id': '11664', 'description': '4.7.1', 'name': '4.7.1', 'archived': False, 'released': True, 'releaseDate': '2010-11-09', 'userReleaseDate': "09 Nov '10", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11701', 'id': '11701', 'description': '4.7.2', 'name': '4.7.2', 'archived': False, 'released': True, 'releaseDate': '2011-03-01', 'userReleaseDate': "01 Mar '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11761', 'id': '11761', 'description': '4.7.3', 'name': '4.7.3', 'archived': False, 'released': True, 'releaseDate': '2011-05-04', 'userReleaseDate': "04 May '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11804', 'id': '11804', 'description': '4.7.4', 'name': '4.7.4', 'archived': False, 'released': True, 'releaseDate': '2011-09-01', 'userReleaseDate': "01 Sep '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11830', 'id': '11830', 'description': 'Qt3D Tech preview 1', 'name': 'Qt3D TP1', 'archived': False, 'released': True, 'releaseDate': '2011-05-20', 'userReleaseDate': "20 May '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11831', 'id': '11831', 'description': 'Qt3D Tech preview 2', 'name': 'Qt3D TP2', 'archived': False, 'released': True, 'releaseDate': '2011-09-23', 'userReleaseDate': "23 Sep '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11788', 'id': '11788', 'description': 'Qt3D Team backlog', 'name': 'Qt3D 1.0', 'archived': False, 'released': True, 'releaseDate': '2012-03-12', 'userReleaseDate': "12 Mar '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12122', 'id': '12122', 'description': 'Qt3D 2.0', 'name': 'Qt3D 2.0', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12123', 'id': '12123', 'description': 'Qt3D 2.0.1 - patch', 'name': 'Qt3D 2.0.1', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12125', 'id': '12125', 'description': 'Qt3D 2.1', 'name': 'Qt3D 2.1', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11620', 'id': '11620', 'description': '4.8.0', 'name': '4.8.0', 'archived': False, 'released': True, 'releaseDate': '2011-12-15', 'userReleaseDate': "15 Dec '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11905', 'id': '11905', 'description': '4.8.1', 'name': '4.8.1', 'archived': False, 'released': True, 'releaseDate': '2012-03-28', 'userReleaseDate': "28 Mar '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12120', 'id': '12120', 'description': '4.8.2', 'name': '4.8.2', 'archived': False, 'released': True, 'releaseDate': '2012-05-22', 'userReleaseDate': "22 May '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12200', 'id': '12200', 'description': '4.8.3', 'name': '4.8.3', 'archived': False, 'released': True, 'releaseDate': '2012-09-13', 'userReleaseDate': "13 Sep '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12501', 'id': '12501', 'description': '4.8.4', 'name': '4.8.4', 'archived': False, 'released': True, 'releaseDate': '2012-11-29', 'userReleaseDate': "29 Nov '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12505', 'id': '12505', 'description': '4.8.5', 'name': '4.8.5', 'archived': False, 'released': True, 'releaseDate': '2013-07-02', 'userReleaseDate': "02 Jul '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13008', 'id': '13008', 'description': '4.8.6', 'name': '4.8.6', 'archived': False, 'released': True, 'releaseDate': '2014-04-24', 'userReleaseDate': "24 Apr '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14000', 'id': '14000', 'description': '4.8.7', 'name': '4.8.7', 'archived': False, 'released': True, 'releaseDate': '2015-05-26', 'userReleaseDate': "26 May '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11621', 'id': '11621', 'description': '4.8.x', 'name': '4.8.x', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13403', 'id': '13403', 'description': '5.0.0 Alpha 1', 'name': '5.0.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2011-04-27', 'userReleaseDate': "27 Apr '11", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12126', 'id': '12126', 'description': '5.0.0 Beta 1', 'name': '5.0.0 Beta 1', 'archived': False, 'released': True, 'releaseDate': '2012-08-31', 'userReleaseDate': "31 Aug '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12500', 'id': '12500', 'description': '5.0.0 Beta 2', 'name': '5.0.0 Beta 2', 'archived': False, 'released': True, 'releaseDate': '2012-11-13', 'userReleaseDate': "13 Nov '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12300', 'id': '12300', 'description': '5.0.0 RC 1', 'name': '5.0.0 RC 1', 'archived': False, 'released': True, 'releaseDate': '2012-12-06', 'userReleaseDate': "06 Dec '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12600', 'id': '12600', 'description': '5.0.0 RC 2', 'name': '5.0.0 RC 2', 'archived': False, 'released': True, 'releaseDate': '2012-12-13', 'userReleaseDate': "13 Dec '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11529', 'id': '11529', 'description': '5.0.0', 'name': '5.0.0', 'archived': False, 'released': True, 'releaseDate': '2012-12-19', 'userReleaseDate': "19 Dec '12", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12602', 'id': '12602', 'description': '5.0.1', 'name': '5.0.1', 'archived': False, 'released': True, 'releaseDate': '2013-01-31', 'userReleaseDate': "31 Jan '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12607', 'id': '12607', 'description': '5.0.2', 'name': '5.0.2', 'archived': False, 'released': True, 'releaseDate': '2013-04-10', 'userReleaseDate': "10 Apr '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12618', 'id': '12618', 'description': '5.1.0 Beta 1', 'name': '5.1.0 Beta 1', 'archived': False, 'released': True, 'releaseDate': '2013-05-14', 'userReleaseDate': "14 May '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12700', 'id': '12700', 'description': '5.1.0 RC 1', 'name': '5.1.0 RC1', 'archived': False, 'released': True, 'releaseDate': '2013-06-18', 'userReleaseDate': "18 Jun '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13007', 'id': '13007', 'description': '5.1.0 RC 2', 'name': '5.1.0 RC2', 'archived': False, 'released': True, 'releaseDate': '2013-06-29', 'userReleaseDate': "29 Jun '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12121', 'id': '12121', 'description': '5.1.0', 'name': '5.1.0 ', 'archived': False, 'released': True, 'releaseDate': '2013-07-03', 'userReleaseDate': "03 Jul '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12900', 'id': '12900', 'description': '5.1.1', 'name': '5.1.1', 'archived': False, 'released': True, 'releaseDate': '2013-08-28', 'userReleaseDate': "28 Aug '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13202', 'id': '13202', 'description': '5.2.0 Alpha 1', 'name': '5.2.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2013-09-30', 'userReleaseDate': "30 Sep '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13203', 'id': '13203', 'description': '5.2.0 Beta 1', 'name': '5.2.0 Beta1 ', 'archived': False, 'released': True, 'releaseDate': '2013-10-23', 'userReleaseDate': "23 Oct '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13205', 'id': '13205', 'description': '5.2.0 RC 1', 'name': '5.2.0 RC1', 'archived': False, 'released': True, 'releaseDate': '2013-11-29', 'userReleaseDate': "29 Nov '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12617', 'id': '12617', 'description': '5.2.0', 'name': '5.2.0', 'archived': False, 'released': True, 'releaseDate': '2013-12-12', 'userReleaseDate': "12 Dec '13", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13400', 'id': '13400', 'description': '5.2.1', 'name': '5.2.1', 'archived': False, 'released': True, 'releaseDate': '2014-02-05', 'userReleaseDate': "05 Feb '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13602', 'id': '13602', 'description': '5.3.0 Alpha 1', 'name': '5.3.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2014-02-27', 'userReleaseDate': "27 Feb '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13603', 'id': '13603', 'description': '5.3.0 Beta 1', 'name': '5.3.0 Beta1', 'archived': False, 'released': True, 'releaseDate': '2014-03-25', 'userReleaseDate': "25 Mar '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13604', 'id': '13604', 'description': '5.3.0 RC 1', 'name': '5.3.0 RC1', 'archived': False, 'released': True, 'releaseDate': '2014-05-08', 'userReleaseDate': "08 May '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13201', 'id': '13201', 'description': '5.3.0', 'name': '5.3.0', 'archived': False, 'released': True, 'releaseDate': '2014-05-20', 'userReleaseDate': "20 May '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13900', 'id': '13900', 'description': '5.3.1', 'name': '5.3.1', 'archived': False, 'released': True, 'releaseDate': '2014-06-25', 'userReleaseDate': "25 Jun '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14005', 'id': '14005', 'description': '5.3.2', 'name': '5.3.2', 'archived': False, 'released': True, 'releaseDate': '2014-09-16', 'userReleaseDate': "16 Sep '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14300', 'id': '14300', 'description': '5.4.0 Alpha 1', 'name': '5.4.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2014-09-08', 'userReleaseDate': "08 Sep '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14301', 'id': '14301', 'description': '5.4.0 Beta 1', 'name': '5.4.0 Beta', 'archived': False, 'released': True, 'releaseDate': '2014-10-17', 'userReleaseDate': "17 Oct '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14302', 'id': '14302', 'description': '5.4.0 RC 1', 'name': '5.4.0 RC', 'archived': False, 'released': True, 'releaseDate': '2014-11-27', 'userReleaseDate': "27 Nov '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13601', 'id': '13601', 'description': '5.4.0', 'name': '5.4.0', 'archived': False, 'released': True, 'releaseDate': '2014-12-10', 'userReleaseDate': "10 Dec '14", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14400', 'id': '14400', 'description': '5.4.1', 'name': '5.4.1', 'archived': False, 'released': True, 'releaseDate': '2015-02-24', 'userReleaseDate': "24 Feb '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14600', 'id': '14600', 'description': '5.4.2', 'name': '5.4.2', 'archived': False, 'released': True, 'releaseDate': '2015-06-02', 'userReleaseDate': "02 Jun '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15102', 'id': '15102', 'description': '5.4.3', 'name': '5.4.3', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14702', 'id': '14702', 'description': '5.4.0 Alpha 1', 'name': '5.5.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2015-03-17', 'userReleaseDate': "17 Mar '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14703', 'id': '14703', 'description': '5.5.0 Beta 1', 'name': '5.5.0 Beta', 'archived': False, 'released': True, 'releaseDate': '2015-05-15', 'userReleaseDate': "15 May '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14704', 'id': '14704', 'description': '5.5.0 RC 1', 'name': '5.5.0 RC', 'archived': False, 'released': True, 'releaseDate': '2015-06-22', 'userReleaseDate': "22 Jun '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14200', 'id': '14200', 'description': '5.5.0', 'name': '5.5.0', 'archived': False, 'released': True, 'releaseDate': '2015-07-01', 'userReleaseDate': "01 Jul '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15105', 'id': '15105', 'description': '5.5.1', 'name': '5.5.1', 'archived': False, 'released': True, 'releaseDate': '2015-10-15', 'userReleaseDate': "15 Oct '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15301', 'id': '15301', 'description': '5.6.0 Alpha 1', 'name': '5.6.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2015-09-08', 'userReleaseDate': "08 Sep '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15302', 'id': '15302', 'description': '5.6.0 Beta 1', 'name': '5.6.0 Beta', 'archived': False, 'released': True, 'releaseDate': '2015-12-18', 'userReleaseDate': "18 Dec '15", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15303', 'id': '15303', 'description': '5.6.0 RC 1', 'name': '5.6.0 RC', 'archived': False, 'released': True, 'releaseDate': '2016-02-23', 'userReleaseDate': "23 Feb '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15304', 'id': '15304', 'description': '5.6.0', 'name': '5.6.0', 'archived': False, 'released': True, 'releaseDate': '2016-03-16', 'userReleaseDate': "16 Mar '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15305', 'id': '15305', 'description': '5.6.1', 'name': '5.6.1', 'archived': False, 'released': True, 'releaseDate': '2016-06-08', 'userReleaseDate': "08 Jun '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15792', 'id': '15792', 'description': '5.6.2', 'name': '5.6.2', 'archived': False, 'released': True, 'releaseDate': '2016-10-12', 'userReleaseDate': "12 Oct '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15907', 'id': '15907', 'description': '5.6.3', 'name': '5.6.3', 'archived': False, 'released': True, 'releaseDate': '2017-09-21', 'userReleaseDate': "21 Sep '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16617', 'id': '16617', 'description': '5.6.4', 'name': '5.6.4', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14901', 'id': '14901', 'description': '5.6', 'name': '5.6', 'archived': False, 'released': True, 'releaseDate': '2016-03-16', 'userReleaseDate': "16 Mar '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15602', 'id': '15602', 'description': '5.7.0 Alpha 1', 'name': '5.7.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2016-03-11', 'userReleaseDate': "11 Mar '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15603', 'id': '15603', 'description': '5.7.0 Beta 1', 'name': '5.7.0 Beta', 'archived': False, 'released': True, 'releaseDate': '2016-04-21', 'userReleaseDate': "21 Apr '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15604', 'id': '15604', 'description': '5.7.0 RC 1', 'name': '5.7.0 RC', 'archived': False, 'released': True, 'releaseDate': '2016-06-03', 'userReleaseDate': "03 Jun '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15605', 'id': '15605', 'description': '5.7.0', 'name': '5.7.0', 'archived': False, 'released': True, 'releaseDate': '2016-06-16', 'userReleaseDate': "16 Jun '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15794', 'id': '15794', 'description': '5.7.1', 'name': '5.7.1', 'archived': False, 'released': True, 'releaseDate': '2016-12-14', 'userReleaseDate': "14 Dec '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16004', 'id': '16004', 'description': '5.7.2', 'name': '5.7.2', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15205', 'id': '15205', 'description': '5.7', 'name': '5.7', 'archived': False, 'released': True, 'releaseDate': '2016-06-16', 'userReleaseDate': "16 Jun '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15701', 'id': '15701', 'description': '5.8.0 Alpha 1', 'name': '5.8.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2016-09-05', 'userReleaseDate': "05 Sep '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15702', 'id': '15702', 'description': '5.8.0 Beta 1', 'name': '5.8.0 Beta', 'archived': False, 'released': True, 'releaseDate': '2016-11-04', 'userReleaseDate': "04 Nov '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15703', 'id': '15703', 'description': '5.8.0 RC 1', 'name': '5.8.0 RC', 'archived': False, 'released': True, 'releaseDate': '2016-12-22', 'userReleaseDate': "22 Dec '16", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15704', 'id': '15704', 'description': '5.8.0', 'name': '5.8.0', 'archived': False, 'released': True, 'releaseDate': '2017-01-23', 'userReleaseDate': "23 Jan '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15700', 'id': '15700', 'description': '5.8', 'name': '5.8', 'archived': False, 'released': True, 'releaseDate': '2017-01-23', 'userReleaseDate': "23 Jan '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15915', 'id': '15915', 'description': '5.9.0 Alpha 1', 'name': '5.9.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2017-02-23', 'userReleaseDate': "23 Feb '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16003', 'id': '16003', 'description': '5.9.0 Beta 1', 'name': '5.9.0 Beta 1', 'archived': False, 'released': True, 'releaseDate': '2017-04-07', 'userReleaseDate': "07 Apr '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16328', 'id': '16328', 'description': '5.9.0 Beta 2', 'name': '5.9.0 Beta 2', 'archived': False, 'released': True, 'releaseDate': '2017-04-21', 'userReleaseDate': "21 Apr '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16329', 'id': '16329', 'description': '5.9.0 Beta 3', 'name': '5.9.0 Beta 3', 'archived': False, 'released': True, 'releaseDate': '2017-05-02', 'userReleaseDate': "02 May '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16332', 'id': '16332', 'description': '5.9.0 Beta 4', 'name': '5.9.0 Beta 4', 'archived': False, 'released': True, 'releaseDate': '2017-05-16', 'userReleaseDate': "16 May '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16311', 'id': '16311', 'description': '5.9.0 RC 1', 'name': '5.9.0 RC', 'archived': False, 'released': True, 'releaseDate': '2017-05-24', 'userReleaseDate': "24 May '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16607', 'id': '16607', 'description': '5.9.0 RC 2', 'name': '5.9.0 RC 2', 'archived': False, 'released': True, 'releaseDate': '2017-05-29', 'userReleaseDate': "29 May '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16312', 'id': '16312', 'description': '5.9.0', 'name': '5.9.0', 'archived': False, 'released': True, 'releaseDate': '2017-05-31', 'userReleaseDate': "31 May '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16334', 'id': '16334', 'description': '5.9.1', 'name': '5.9.1', 'archived': False, 'released': True, 'releaseDate': '2017-06-30', 'userReleaseDate': "30 Jun '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16611', 'id': '16611', 'description': '5.9.2', 'name': '5.9.2', 'archived': False, 'released': True, 'releaseDate': '2017-10-06', 'userReleaseDate': "06 Oct '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16612', 'id': '16612', 'description': '5.9.3', 'name': '5.9.3', 'archived': False, 'released': True, 'releaseDate': '2017-11-22', 'userReleaseDate': "22 Nov '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16781', 'id': '16781', 'description': '5.10.0 RC2', 'name': '5.10.0 RC2', 'archived': False, 'released': True, 'releaseDate': '2017-12-01', 'userReleaseDate': "01 Dec '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16784', 'id': '16784', 'description': '5.10.0 RC3', 'name': '5.10.0 RC3', 'archived': False, 'released': True, 'releaseDate': '2017-12-04', 'userReleaseDate': "04 Dec '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16613', 'id': '16613', 'description': '5.10 Alpha 1', 'name': '5.10.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2017-09-13', 'userReleaseDate': "13 Sep '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16614', 'id': '16614', 'description': '5.10.0 Beta 1', 'name': '5.10.0 Beta 1', 'archived': False, 'released': True, 'releaseDate': '2017-10-09', 'userReleaseDate': "09 Oct '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16726', 'id': '16726', 'description': '5.10.0 Beta 2', 'name': '5.10.0 Beta 2', 'archived': False, 'released': True, 'releaseDate': '2017-10-25', 'userReleaseDate': "25 Oct '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16727', 'id': '16727', 'description': '5.10.0 Beta 3', 'name': '5.10.0 Beta 3', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16752', 'id': '16752', 'description': '5.10.0 Beta 4', 'name': '5.10.0 Beta 4', 'archived': False, 'released': True, 'releaseDate': '2017-11-10', 'userReleaseDate': "10 Nov '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16615', 'id': '16615', 'description': '5.10.0 RC 1', 'name': '5.10.0 RC', 'archived': False, 'released': True, 'releaseDate': '2017-11-27', 'userReleaseDate': "27 Nov '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16616', 'id': '16616', 'description': '5.10.0', 'name': '5.10.0', 'archived': False, 'released': True, 'releaseDate': '2017-12-07', 'userReleaseDate': "07 Dec '17", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16757', 'id': '16757', 'description': '5.9.4', 'name': '5.9.4', 'archived': False, 'released': True, 'releaseDate': '2018-01-23', 'userReleaseDate': "23 Jan '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16802', 'id': '16802', 'description': '5.9.5', 'name': '5.9.5', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16836', 'id': '16836', 'description': '5.9.6', 'name': '5.9.6', 'archived': False, 'released': True, 'releaseDate': '2018-06-11', 'userReleaseDate': "11 Jun '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16917', 'id': '16917', 'description': '5.9.7', 'name': '5.9.7', 'archived': False, 'released': False, 'startDate': '2018-05-22', 'releaseDate': '2018-09-26', 'overdue': False, 'userStartDate': "22 May '18", 'userReleaseDate': "26 Sep '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16002', 'id': '16002', 'description': '5.9', 'name': '5.9', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16776', 'id': '16776', 'description': '5.10.1', 'name': '5.10.1', 'archived': False, 'released': True, 'releaseDate': '2018-02-13', 'userReleaseDate': "13 Feb '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16309', 'id': '16309', 'description': '5.10', 'name': '5.10', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16909', 'id': '16909', 'description': '5.11.0 RC2', 'name': '5.11.0 RC2', 'archived': False, 'released': True, 'startDate': '2018-05-08', 'releaseDate': '2018-05-16', 'userStartDate': "08 May '18", 'userReleaseDate': "16 May '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16791', 'id': '16791', 'description': '5.11.0 Alpha', 'name': '5.11.0 Alpha', 'archived': False, 'released': True, 'releaseDate': '2018-02-20', 'userReleaseDate': "20 Feb '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16792', 'id': '16792', 'description': '5.11.0 Beta 1', 'name': '5.11.0 Beta 1', 'archived': False, 'released': True, 'releaseDate': '2018-03-21', 'userReleaseDate': "21 Mar '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16793', 'id': '16793', 'description': '5.11.0 Beta 2', 'name': '5.11.0 Beta 2', 'archived': False, 'released': True, 'releaseDate': '2018-03-28', 'userReleaseDate': "28 Mar '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16794', 'id': '16794', 'description': '5.11.0 Beta 3', 'name': '5.11.0 Beta 3', 'archived': False, 'released': True, 'releaseDate': '2018-04-11', 'userReleaseDate': "11 Apr '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16795', 'id': '16795', 'description': '5.11.0 Beta 4', 'name': '5.11.0 Beta 4', 'archived': False, 'released': True, 'releaseDate': '2018-04-20', 'userReleaseDate': "20 Apr '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16796', 'id': '16796', 'description': '5.11.0 RC 1', 'name': '5.11.0 RC 1', 'archived': False, 'released': True, 'releaseDate': '2018-05-08', 'userReleaseDate': "08 May '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16790', 'id': '16790', 'description': '5.11.0', 'name': '5.11.0', 'archived': False, 'released': True, 'releaseDate': '2018-05-22', 'userReleaseDate': "22 May '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16852', 'id': '16852', 'description': '5.11.1', 'name': '5.11.1', 'archived': False, 'released': True, 'releaseDate': '2018-06-19', 'userReleaseDate': "19 Jun '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16916', 'id': '16916', 'description': '5.11.2', 'name': '5.11.2', 'archived': False, 'released': False, 'startDate': '2018-06-07', 'releaseDate': '2018-08-31', 'overdue': False, 'userStartDate': "07 Jun '18", 'userReleaseDate': "31 Aug '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16310', 'id': '16310', 'description': '5.11', 'name': '5.11', 'archived': False, 'released': False, 'releaseDate': '2020-05-22', 'overdue': False, 'userReleaseDate': "22 May '20", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16832', 'id': '16832', 'description': '5.12.0 Alpha 1', 'name': '5.12.0 Alpha', 'archived': False, 'released': False, 'releaseDate': '2018-08-20', 'overdue': True, 'userReleaseDate': "20 Aug '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16837', 'id': '16837', 'description': '5.12.0 Beta 1', 'name': '5.12.0 Beta 1', 'archived': False, 'released': False, 'releaseDate': '2018-09-18', 'overdue': False, 'userReleaseDate': "18 Sep '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16853', 'id': '16853', 'description': '5.12.0 RC 1', 'name': '5.12.0 RC', 'archived': False, 'released': False, 'releaseDate': '2018-11-15', 'overdue': False, 'userReleaseDate': "15 Nov '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16854', 'id': '16854', 'description': '5.12.0', 'name': '5.12.0', 'archived': False, 'released': False, 'releaseDate': '2018-11-29', 'overdue': False, 'userReleaseDate': "29 Nov '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16855', 'id': '16855', 'description': '5.12.1', 'name': '5.12.1', 'archived': False, 'released': False, 'releaseDate': '2019-01-17', 'overdue': False, 'userReleaseDate': "17 Jan '19", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16710', 'id': '16710', 'description': '5.12', 'name': '5.12', 'archived': False, 'released': False, 'releaseDate': '2018-11-30', 'overdue': False, 'userReleaseDate': "30 Nov '18", 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17007', 'id': '17007', 'description': '5.13.0 Alpha 1', 'name': '5.13.0 Alpha 1', 'archived': False, 'released': True, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17008', 'id': '17008', 'description': '5.13.0 Beta 1', 'name': '5.13.0 Beta 1', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17009', 'id': '17009', 'description': '5.13.0 RC 1', 'name': '5.13.0 RC 1', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17006', 'id': '17006', 'description': '5.13.0', 'name': '5.13.0', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17000', 'id': '17000', 'description': '5.13', 'name': '5.13', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/17010', 'id': '17010', 'description': '6.0.0', 'name': '6.0.0', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12127', 'id': '12127', 'description': '6.0', 'name': '6.0 (Next Major Release)', 'archived': False, 'released': False, 'projectId': 10510},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11533', 'id': '11533', 'description': 'Some future release', 'name': 'Some future release', 'archived': False, 'released': False, 'projectId': 10510},
+]
+
+
+@pytest.mark.parametrize("branch,expected", [
+ ('', None),
+ ('something', None),
+ ('5.9', None),
+ ('5.9.5', '16802'),
+ ('5.9.6', '16836'),
+ ('5.9.7', '16917'),
+ ('5.12', None),
+ ('5.12.0', '16832'),
+ ('5.12.1', '16855'),
+ ('5.12.2', None),
+ ('5.13.0', '17008'), # should give Beta 1, the data above is manipulated to test this case
+ ('dev', None),
+ ('master', None),
+ ('6.0.0', '17010'),
+])
+def test_jira_versions(branch: str, expected: str):
+ version_id = closer._guess_fix_version(branch, JiraCloser._clean_jira_versions(jira_qt_versions))
+ assert version_id == expected
+
+
+# Qt Creator
+creator_versions = [
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11470', 'id': '11470', 'name': 'Qt Creator 1.0', 'archived': False, 'released': True, 'releaseDate': '2009-03-12', 'userReleaseDate': "12 Mar '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11471', 'id': '11471', 'name': 'Qt Creator 1.1', 'archived': False, 'released': True, 'releaseDate': '2009-04-23', 'userReleaseDate': "23 Apr '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11550', 'id': '11550', 'name': 'Qt Creator 1.1.1', 'archived': False, 'released': True, 'releaseDate': '2009-05-27', 'userReleaseDate': "27 May '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11472', 'id': '11472', 'name': 'Qt Creator 1.2', 'archived': False, 'released': True, 'releaseDate': '2009-06-25', 'userReleaseDate': "25 Jun '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11473', 'id': '11473', 'name': 'Qt Creator 1.2.1', 'archived': False, 'released': True, 'releaseDate': '2009-07-14', 'userReleaseDate': "14 Jul '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11551', 'id': '11551', 'name': 'Qt Creator 1.2.90', 'archived': False, 'released': True, 'releaseDate': '2009-09-10', 'userReleaseDate': "10 Sep '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11475', 'id': '11475', 'name': 'Qt Creator 1.3.0 rc1', 'archived': False, 'released': True, 'releaseDate': '2009-11-17', 'userReleaseDate': "17 Nov '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11568', 'id': '11568', 'name': 'Qt Creator 1.3.0', 'archived': False, 'released': True, 'releaseDate': '2009-12-01', 'userReleaseDate': "01 Dec '09", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11474', 'id': '11474', 'name': 'Qt Creator 1.3.1', 'archived': False, 'released': True, 'releaseDate': '2010-01-19', 'userReleaseDate': "19 Jan '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11590', 'id': '11590', 'name': 'Qt Creator 1.3.81 (2.0.0-alpha)', 'archived': False, 'released': True, 'releaseDate': '2010-03-11', 'userReleaseDate': "11 Mar '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11565', 'id': '11565', 'name': 'Qt Creator 1.3.83 (2.0.0-beta)', 'archived': False, 'released': True, 'releaseDate': '2010-05-06', 'userReleaseDate': "06 May '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11610', 'id': '11610', 'name': 'Qt Creator 1.3.85 (2.0.0-rc1)', 'archived': False, 'released': True, 'releaseDate': '2010-06-09', 'userReleaseDate': "09 Jun '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11652', 'id': '11652', 'name': 'Qt Creator 2.0.0', 'archived': False, 'released': True, 'releaseDate': '2010-06-23', 'userReleaseDate': "23 Jun '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11675', 'id': '11675', 'name': 'Qt Creator 2.0.1', 'archived': False, 'released': True, 'releaseDate': '2010-08-25', 'userReleaseDate': "25 Aug '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11719', 'id': '11719', 'name': 'Qt Creator 2.1.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2010-10-07', 'userReleaseDate': "07 Oct '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11732', 'id': '11732', 'name': 'Qt Creator 2.1.0-beta2', 'archived': False, 'released': True, 'releaseDate': '2010-11-09', 'userReleaseDate': "09 Nov '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11720', 'id': '11720', 'name': 'Qt Creator 2.1.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2010-11-25', 'userReleaseDate': "25 Nov '10", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11704', 'id': '11704', 'name': 'Qt Creator 2.1.0', 'archived': False, 'released': True, 'releaseDate': '2011-03-01', 'userReleaseDate': "01 Mar '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11832', 'id': '11832', 'name': 'Qt Creator 2.2.0-beta', 'archived': False, 'released': True, 'releaseDate': '2011-03-24', 'userReleaseDate': "24 Mar '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11841', 'id': '11841', 'name': 'Qt Creator 2.2.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2011-04-19', 'userReleaseDate': "19 Apr '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11705', 'id': '11705', 'name': 'Qt Creator 2.2.0', 'archived': False, 'released': True, 'releaseDate': '2011-05-06', 'userReleaseDate': "06 May '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11860', 'id': '11860', 'name': 'Qt Creator 2.2.1', 'archived': False, 'released': True, 'releaseDate': '2011-06-21', 'userReleaseDate': "21 Jun '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11877', 'id': '11877', 'name': 'Qt Creator 2.3.0-beta', 'archived': False, 'released': True, 'releaseDate': '2011-07-13', 'userReleaseDate': "13 Jul '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11880', 'id': '11880', 'name': 'Qt Creator 2.3.0-rc', 'archived': False, 'released': True, 'releaseDate': '2011-08-11', 'userReleaseDate': "11 Aug '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11802', 'id': '11802', 'name': 'Qt Creator 2.3.0', 'archived': False, 'released': True, 'releaseDate': '2011-09-01', 'userReleaseDate': "01 Sep '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11885', 'id': '11885', 'name': 'Qt Creator 2.3.1', 'archived': False, 'released': True, 'releaseDate': '2011-09-29', 'userReleaseDate': "29 Sep '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11897', 'id': '11897', 'name': 'Qt Creator 2.4.0-beta', 'archived': False, 'released': True, 'releaseDate': '2011-10-20', 'userReleaseDate': "20 Oct '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11904', 'id': '11904', 'name': 'Qt Creator 2.4.0-rc', 'archived': False, 'released': True, 'releaseDate': '2011-11-16', 'userReleaseDate': "16 Nov '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11835', 'id': '11835', 'name': 'Qt Creator 2.4.0', 'archived': False, 'released': True, 'releaseDate': '2011-12-13', 'userReleaseDate': "13 Dec '11", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11903', 'id': '11903', 'name': 'Qt Creator 2.4.1', 'archived': False, 'released': True, 'releaseDate': '2012-02-01', 'userReleaseDate': "01 Feb '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11876', 'id': '11876', 'name': 'Qt Creator 2.5.0-beta', 'archived': False, 'released': True, 'releaseDate': '2012-03-15', 'userReleaseDate': "15 Mar '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12119', 'id': '12119', 'name': 'Qt Creator 2.5.0-rc', 'archived': False, 'released': True, 'releaseDate': '2012-04-23', 'userReleaseDate': "23 Apr '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12131', 'id': '12131', 'name': 'Qt Creator 2.5.0', 'archived': False, 'released': True, 'releaseDate': '2012-05-09', 'userReleaseDate': "09 May '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12133', 'id': '12133', 'name': 'Qt Creator 2.5.1', 'archived': False, 'released': True, 'releaseDate': '2012-07-25', 'userReleaseDate': "25 Jul '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12401', 'id': '12401', 'name': 'Qt Creator 2.5.2', 'archived': False, 'released': True, 'releaseDate': '2012-08-09', 'userReleaseDate': "09 Aug '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12405', 'id': '12405', 'name': 'Qt Creator 2.6.0-beta', 'archived': False, 'released': True, 'releaseDate': '2012-09-11', 'userReleaseDate': "11 Sep '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/11894', 'id': '11894', 'name': 'Qt Creator 2.6.0-rc', 'archived': False, 'released': True, 'releaseDate': '2012-10-17', 'userReleaseDate': "17 Oct '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12503', 'id': '12503', 'name': 'Qt Creator 2.6.0', 'archived': False, 'released': True, 'releaseDate': '2012-11-08', 'userReleaseDate': "08 Nov '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12504', 'id': '12504', 'name': 'Qt Creator 2.6.1', 'archived': False, 'released': True, 'releaseDate': '2012-12-19', 'userReleaseDate': "19 Dec '12", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12603', 'id': '12603', 'name': 'Qt Creator 2.6.2', 'archived': False, 'released': True, 'releaseDate': '2013-01-31', 'userReleaseDate': "31 Jan '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12611', 'id': '12611', 'name': 'Qt Creator 2.7.0-beta', 'archived': False, 'released': True, 'releaseDate': '2013-02-07', 'userReleaseDate': "07 Feb '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12615', 'id': '12615', 'name': 'Qt Creator 2.7.0-rc', 'archived': False, 'released': True, 'releaseDate': '2013-03-07', 'userReleaseDate': "07 Mar '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12117', 'id': '12117', 'name': 'Qt Creator 2.7.0', 'archived': False, 'released': True, 'releaseDate': '2013-03-21', 'userReleaseDate': "21 Mar '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12614', 'id': '12614', 'name': 'Qt Creator 2.7.1', 'archived': False, 'released': True, 'releaseDate': '2013-05-14', 'userReleaseDate': "14 May '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12701', 'id': '12701', 'name': 'Qt Creator 2.7.2', 'archived': False, 'released': True, 'releaseDate': '2013-07-03', 'userReleaseDate': "03 Jul '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12608', 'id': '12608', 'name': 'Qt Creator 2.8.0-beta', 'archived': False, 'released': True, 'releaseDate': '2013-05-30', 'userReleaseDate': "30 May '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12800', 'id': '12800', 'name': 'Qt Creator 2.8.0-rc', 'archived': False, 'released': True, 'releaseDate': '2013-06-28', 'userReleaseDate': "28 Jun '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13005', 'id': '13005', 'name': 'Qt Creator 2.8.0', 'archived': False, 'released': True, 'releaseDate': '2013-07-11', 'userReleaseDate': "11 Jul '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13000', 'id': '13000', 'name': 'Qt Creator 2.8.1', 'archived': False, 'released': True, 'releaseDate': '2013-08-28', 'userReleaseDate': "28 Aug '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13206', 'id': '13206', 'name': 'Qt Creator 3.0.0-beta', 'archived': False, 'released': True, 'releaseDate': '2013-10-23', 'userReleaseDate': "23 Oct '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/12702', 'id': '12702', 'name': 'Qt Creator 3.0.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2013-11-29', 'userReleaseDate': "29 Nov '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13401', 'id': '13401', 'name': 'Qt Creator 3.0.0', 'archived': False, 'released': True, 'releaseDate': '2013-12-12', 'userReleaseDate': "12 Dec '13", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13402', 'id': '13402', 'name': 'Qt Creator 3.0.1', 'archived': False, 'released': True, 'releaseDate': '2014-02-05', 'userReleaseDate': "05 Feb '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13204', 'id': '13204', 'name': 'Qt Creator 3.1.0-beta', 'archived': False, 'released': True, 'releaseDate': '2014-03-04', 'userReleaseDate': "04 Mar '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13700', 'id': '13700', 'name': 'Qt Creator 3.1.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2014-04-03', 'userReleaseDate': "03 Apr '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13800', 'id': '13800', 'name': 'Qt Creator 3.1.0', 'archived': False, 'released': True, 'releaseDate': '2014-04-15', 'userReleaseDate': "15 Apr '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13902', 'id': '13902', 'name': 'Qt Creator 3.1.1', 'archived': False, 'released': True, 'releaseDate': '2014-05-20', 'userReleaseDate': "20 May '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14002', 'id': '14002', 'name': 'Qt Creator 3.1.2', 'archived': False, 'released': True, 'releaseDate': '2014-06-25', 'userReleaseDate': "25 Jun '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/13600', 'id': '13600', 'name': 'Qt Creator 3.2.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2014-07-08', 'userReleaseDate': "08 Jul '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14011', 'id': '14011', 'name': 'Qt Creator 3.2.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2014-08-05', 'userReleaseDate': "05 Aug '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14101', 'id': '14101', 'name': 'Qt Creator 3.2.0', 'archived': False, 'released': True, 'releaseDate': '2014-08-19', 'userReleaseDate': "19 Aug '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14102', 'id': '14102', 'name': 'Qt Creator 3.2.1', 'archived': False, 'released': True, 'releaseDate': '2014-09-16', 'userReleaseDate': "16 Sep '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14304', 'id': '14304', 'name': 'Qt Creator 3.2.2', 'archived': False, 'released': True, 'releaseDate': '2014-10-14', 'userReleaseDate': "14 Oct '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14006', 'id': '14006', 'name': 'Qt Creator 3.3.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2014-10-30', 'userReleaseDate': "30 Oct '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14309', 'id': '14309', 'name': 'Qt Creator 3.3.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2014-11-27', 'userReleaseDate': "27 Nov '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14401', 'id': '14401', 'name': 'Qt Creator 3.3.0', 'archived': False, 'released': True, 'releaseDate': '2014-12-10', 'userReleaseDate': "10 Dec '14", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14402', 'id': '14402', 'name': 'Qt Creator 3.3.1', 'archived': False, 'released': True, 'releaseDate': '2015-02-24', 'userReleaseDate': "24 Feb '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14800', 'id': '14800', 'name': 'Qt Creator 3.3.2', 'archived': False, 'released': True, 'releaseDate': '2015-03-05', 'userReleaseDate': "05 Mar '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14308', 'id': '14308', 'name': 'Qt Creator 3.4.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2015-03-05', 'userReleaseDate': "05 Mar '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14902', 'id': '14902', 'name': 'Qt Creator 3.4.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2015-04-01', 'userReleaseDate': "01 Apr '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14904', 'id': '14904', 'name': 'Qt Creator 3.4.0', 'archived': False, 'released': True, 'releaseDate': '2015-04-23', 'userReleaseDate': "23 Apr '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15000', 'id': '15000', 'name': 'Qt Creator 3.4.1', 'archived': False, 'released': True, 'releaseDate': '2015-06-02', 'userReleaseDate': "02 Jun '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15104', 'id': '15104', 'name': 'Qt Creator 3.4.2', 'archived': False, 'released': True, 'releaseDate': '2015-07-01', 'userReleaseDate': "01 Jul '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/14701', 'id': '14701', 'name': 'Qt Creator 3.5.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2015-07-08', 'userReleaseDate': "08 Jul '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15202', 'id': '15202', 'name': 'Qt Creator 3.5.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2015-08-06', 'userReleaseDate': "06 Aug '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15204', 'id': '15204', 'name': 'Qt Creator 3.5.0', 'archived': False, 'released': True, 'releaseDate': '2015-08-20', 'userReleaseDate': "20 Aug '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15300', 'id': '15300', 'name': 'Qt Creator 3.5.1', 'archived': False, 'released': True, 'releaseDate': '2015-10-15', 'userReleaseDate': "15 Oct '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15200', 'id': '15200', 'name': 'Qt Creator 3.6.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2015-10-27', 'userReleaseDate': "27 Oct '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15405', 'id': '15405', 'name': 'Qt Creator 3.6.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2015-11-26', 'userReleaseDate': "26 Nov '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15408', 'id': '15408', 'name': 'Qt Creator 3.6.0', 'archived': False, 'released': True, 'releaseDate': '2015-12-15', 'userReleaseDate': "15 Dec '15", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15413', 'id': '15413', 'name': 'Qt Creator 3.6.1', 'archived': False, 'released': True, 'releaseDate': '2016-03-16', 'userReleaseDate': "16 Mar '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15404', 'id': '15404', 'name': 'Qt Creator 4.0.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2016-03-23', 'userReleaseDate': "23 Mar '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15766', 'id': '15766', 'name': 'Qt Creator 4.0.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2016-04-20', 'userReleaseDate': "20 Apr '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15787', 'id': '15787', 'name': 'Qt Creator 4.0.0', 'archived': False, 'released': True, 'releaseDate': '2016-05-11', 'userReleaseDate': "11 May '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15789', 'id': '15789', 'name': 'Qt Creator 4.0.1', 'archived': False, 'released': True, 'releaseDate': '2016-06-08', 'userReleaseDate': "08 Jun '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15800', 'id': '15800', 'name': 'Qt Creator 4.0.2', 'archived': False, 'released': True, 'releaseDate': '2016-06-16', 'userReleaseDate': "16 Jun '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15804', 'id': '15804', 'name': 'Qt Creator 4.0.3', 'archived': False, 'released': True, 'releaseDate': '2016-07-07', 'userReleaseDate': "07 Jul '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15706', 'id': '15706', 'name': 'Qt Creator 4.1.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2016-07-06', 'userReleaseDate': "06 Jul '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15903', 'id': '15903', 'name': 'Qt Creator 4.1.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2016-08-08', 'userReleaseDate': "08 Aug '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15910', 'id': '15910', 'name': 'Qt Creator 4.1.0', 'archived': False, 'released': True, 'releaseDate': '2016-08-25', 'userReleaseDate': "25 Aug '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/15807', 'id': '15807', 'name': 'Qt Creator 4.2.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2016-10-27', 'userReleaseDate': "27 Oct '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16101', 'id': '16101', 'name': 'Qt Creator 4.2.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2016-11-30', 'userReleaseDate': "30 Nov '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16207', 'id': '16207', 'name': 'Qt Creator 4.2.0', 'archived': False, 'released': True, 'releaseDate': '2016-12-14', 'userReleaseDate': "14 Dec '16", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16210', 'id': '16210', 'name': 'Qt Creator 4.2.1', 'archived': False, 'released': True, 'releaseDate': '2017-01-23', 'userReleaseDate': "23 Jan '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16301', 'id': '16301', 'name': 'Qt Creator 4.2.2', 'archived': False, 'released': True, 'releaseDate': '2017-04-21', 'userReleaseDate': "21 Apr '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16001', 'id': '16001', 'name': 'Qt Creator 4.3.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2017-03-30', 'userReleaseDate': "30 Mar '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16323', 'id': '16323', 'name': 'Qt Creator 4.3.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2017-05-09', 'userReleaseDate': "09 May '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16335', 'id': '16335', 'name': 'Qt Creator 4.3.0', 'archived': False, 'released': True, 'releaseDate': '2017-05-24', 'userReleaseDate': "24 May '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16600', 'id': '16600', 'name': 'Qt Creator 4.3.1', 'archived': False, 'released': True, 'releaseDate': '2017-06-30', 'userReleaseDate': "30 Jun '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16306', 'id': '16306', 'name': 'Qt Creator 4.4.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2017-07-20', 'userReleaseDate': "20 Jul '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16701', 'id': '16701', 'name': 'Qt Creator 4.4.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2017-08-17', 'userReleaseDate': "17 Aug '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16716', 'id': '16716', 'name': 'Qt Creator 4.4.0', 'archived': False, 'released': True, 'releaseDate': '2017-09-05', 'userReleaseDate': "05 Sep '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16721', 'id': '16721', 'name': 'Qt Creator 4.4.1', 'archived': False, 'released': True, 'releaseDate': '2017-10-06', 'userReleaseDate': "06 Oct '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16308', 'id': '16308', 'name': 'Qt Creator 4.5.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2017-10-12', 'userReleaseDate': "12 Oct '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16744', 'id': '16744', 'name': 'Qt Creator 4.5.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2017-11-22', 'userReleaseDate': "22 Nov '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16779', 'id': '16779', 'description': '4.5.0', 'name': 'Qt Creator 4.5.0', 'archived': False, 'released': True, 'releaseDate': '2017-12-07', 'userReleaseDate': "07 Dec '17", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16787', 'id': '16787', 'description': '4.5.1', 'name': 'Qt Creator 4.5.1', 'archived': False, 'released': True, 'releaseDate': '2018-02-13', 'userReleaseDate': "13 Feb '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16830', 'id': '16830', 'description': '4.5.2', 'name': 'Qt Creator 4.5.2', 'archived': False, 'released': True, 'releaseDate': '2018-03-13', 'userReleaseDate': "13 Mar '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16737', 'id': '16737', 'description': '4.6.0 Beta 1', 'name': 'Qt Creator 4.6.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2018-02-07', 'userReleaseDate': "07 Feb '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16812', 'id': '16812', 'description': '4.6.0 RC 1', 'name': 'Qt Creator 4.6.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2018-03-15', 'userReleaseDate': "15 Mar '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16838', 'id': '16838', 'description': '4.6.0', 'name': 'Qt Creator 4.6.0 ', 'archived': False, 'released': True, 'releaseDate': '2018-03-28', 'userReleaseDate': "28 Mar '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16840', 'id': '16840', 'description': '4.6.1', 'name': 'Qt Creator 4.6.1', 'archived': False, 'released': True, 'releaseDate': '2018-05-03', 'userReleaseDate': "03 May '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16900', 'id': '16900', 'description': '4.6.2', 'name': 'Qt Creator 4.6.2', 'archived': False, 'released': True, 'releaseDate': '2018-06-11', 'userReleaseDate': "11 Jun '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16758', 'id': '16758', 'description': '4.7.0 Beta 1', 'name': 'Qt Creator 4.7.0-beta1', 'archived': False, 'released': True, 'releaseDate': '2018-06-05', 'userReleaseDate': "05 Jun '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16921', 'id': '16921', 'description': '4.7.0 Beta 2', 'name': 'Qt Creator 4.7.0-beta2', 'archived': False, 'released': True, 'releaseDate': '2018-06-21', 'userReleaseDate': "21 Jun '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16932', 'id': '16932', 'description': '4.7.0 RC 1', 'name': 'Qt Creator 4.7.0-rc1', 'archived': False, 'released': True, 'releaseDate': '2018-07-05', 'userReleaseDate': "05 Jul '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16935', 'id': '16935', 'description': '4.7.0', 'name': 'Qt Creator 4.7.0', 'archived': False, 'released': True, 'releaseDate': '2018-07-18', 'userReleaseDate': "18 Jul '18", 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16937', 'id': '16937', 'description': '4.7.1', 'name': 'Qt Creator 4.7.1 (4.7 branch)', 'archived': False, 'released': False, 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16759', 'id': '16759', 'description': '4.8.0 Beta 1', 'name': 'Qt Creator 4.8.0 (master branch)', 'archived': False, 'released': False, 'projectId': 10512},
+ {'self': 'https://bugreports-test.qt.io/rest/api/2/version/16819', 'id': '16819', 'description': '4.9.0 Beta 1', 'name': 'Qt Creator 4.9.0', 'archived': False, 'released': False, 'projectId': 10512},
+]
+
+
+@pytest.mark.parametrize("branch,expected", [
+ ('', None),
+ ('something', None),
+ ('4.5.0', '16779'),
+ ('4.6.0', '16838'),
+ ('4.6.1', '16840'),
+ ('4.6.2', '16900'),
+ ('4.6.3', None),
+ ('4.7.0', '16935'),
+ ('4.7.1', '16937'),
+ # resolved before, should never get there, so rather make sure we return just None
+ ('master', None),
+ ('dev', None),
+])
+def test_jira_versions_creator(branch: str, expected: str):
+ version_id = closer._guess_fix_version(branch, JiraCloser._clean_jira_versions(creator_versions))
+ assert version_id == expected
+
+
+@pytest.mark.parametrize("jira_version_list,expected", [
+ (creator_versions, [(LooseVersion('4.5.0'), '16779', True), (LooseVersion('4.5.1'), '16787', True), (LooseVersion('4.5.2'), '16830', True), (LooseVersion('4.6.0 Beta 1'), '16737', True), (LooseVersion('4.6.0 RC 1'), '16812', True), (LooseVersion('4.6.0'), '16838', True), (LooseVersion('4.6.1'), '16840', True), (LooseVersion('4.6.2'), '16900', True), (LooseVersion('4.7.0 Beta 1'), '16758', True), (LooseVersion('4.7.0 Beta 2'), '16921', True), (LooseVersion('4.7.0 RC 1'), '16932', True), (LooseVersion('4.7.0'), '16935', True), (LooseVersion('4.7.1'), '16937', False), (LooseVersion('4.8.0 Beta 1'), '16759', False), (LooseVersion('4.9.0 Beta 1'), '16819', False)]),
+])
+def test_jira_versions_to_dict(jira_version_list: List[Dict[str, str]], expected: List[Tuple[LooseVersion, str, bool]]):
+ versions = JiraCloser._clean_jira_versions(jira_version_list)
+ assert versions == expected
diff --git a/scripts/jira/jira-bug-closer/tests/test_listing_projects.py b/scripts/jira/jira-bug-closer/tests/test_listing_projects.py
new file mode 100644
index 00000000..f354d9fa
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/test_listing_projects.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import pytest
+from gerrit import GerritStreamEvents
+
+
+@pytest.mark.asyncio
+async def test_list_projects(event_loop):
+ g = GerritStreamEvents()
+ list = await g.list_all_projects()
+ assert 'qt/qtbase' in list
+ assert 'qt/qt5' in list
diff --git a/scripts/jira/jira-bug-closer/tests/test_streamparser.py b/scripts/jira/jira-bug-closer/tests/test_streamparser.py
new file mode 100644
index 00000000..8437b354
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tests/test_streamparser.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+#############################################################################
+##
+## Copyright (C) 2019 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Quality Assurance module of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## Commercial License Usage
+## Licensees holding valid commercial Qt licenses may use this file in
+## accordance with the commercial license agreement provided with the
+## Software or, alternatively, in accordance with the terms contained in
+## a written agreement between you and The Qt Company. For licensing terms
+## and conditions see https://www.qt.io/terms-conditions. For further
+## information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import pytest
+from gerrit import GerritStreamParser, GerritEvent
+
+
+@pytest.mark.parametrize("message,expected", [
+ ("", None),
+ ("invalid", None),
+ ("{}", None),
+ ("""{"random_json": "yeah"}""", None),
+ ("""{"type":"comment-added","change":{"project":"qt/qtdeclarative","branch":"dev","id":"I2ebe8fdd5ca121bf884a0f1aaac2272e9ff564d9",
+ "number":"236085","subject":"WIP: Implement support for uninitialized variables","owner":{"name":"Lars Knoll","email":"lars.knoll@qt.io","username":"laknoll"},
+ "url":"https://codereview.qt-project.org/236085"},"patchSet":{"number":"1","revision":"e0f9e7d6792348ab307e67ac0f775bc5abfc6e07",
+ "parents":["3d0e18a0e24d3a475301bc4e9f9ccb7f0074e307"],"ref":"refs/changes/85/236085/1",
+ "uploader":{"name":"Lars Knoll","email":"lars.knoll@qt.io","username":"laknoll"},"createdOn":1533480530,
+ "author":{"name":"Lars Knoll","email":"lars.knoll@qt.io","username":"laknoll"},"sizeInsertions":37,"sizeDeletions":-1},
+ "author":{"name":"Qt Sanity Bot","email":"qt_sanitybot@qt-project.org","username":"qt_sanity_bot"},
+ "approvals":[{"type":"Sanity-Review","description":"Sanity-Review","value":"1"},{"type":"Code-Review","description":"Code-Review","value":"-2"}],
+ "comment":"Patch Set 1: Code-Review-2 Sanity-Review+1\\n\\nApparently pushing a Work In Progress"}""",
+ GerritEvent(type="comment-added", project="qt/qtdeclarative", branch="dev")),
+ ("""{"type":"change-merged","change":{"project":"qt/qtbase","branch":"dev","id":"I4857e9b43918243af66cc09ff352619595c081c9",
+ "number":"235677","subject":"QTextureFileData: Fix build with -no-opengl","owner":{"name":"Jüri Valdmann","email":"juri.valdmann@qt.io","username":"juri.valdmann"},
+ "url":"https://codereview.qt-project.org/235677"},"patchSet":{"number":"1","revision":"41d29efb4196d5fd447190d3b8ec26d70b9f8eec",
+ "parents":["b0085dbeeac47d0ce566750d93f1b1f865d07cdb"],"ref":"refs/changes/77/235677/1",
+ "uploader":{"name":"Jüri Valdmann","email":"juri.valdmann@qt.io","username":"juri.valdmann"},"createdOn":1533042510,
+ "author":{"name":"Jüri Valdmann","email":"juri.valdmann@qt.io","username":"juri.valdmann"},"sizeInsertions":1,"sizeDeletions":0},
+ "submitter":{"name":"Jüri Valdmann","email":"juri.valdmann@qt.io","username":"juri.valdmann"}}""",
+ GerritEvent(type="change-merged", project="qt/qtbase", branch="dev")),
+ ("""{"type":"ref-updated","submitter":{"name":"Qt CI Bot","email":"qt_ci_bot@qt-project.org","username":"qt_ci_bot"},
+ "refUpdate":{"oldRev":"8cde4a825638a414ef55a57662b38e2746b83668","newRev":"a80ed61a98fd0a1d13eab95252db189cdeb0fe96",
+ "refName":"master","project":"qtqa/tqtc-coin-ci"}}""",
+ GerritEvent(type='ref-updated', project='qtqa/tqtc-coin-ci', branch='master')),
+ ("""{"type":"ref-updated","submitter":{"name":"Qt CI Bot","email":"qt_ci_bot@qt-project.org","username":"qt_ci_bot"},
+ "refUpdate":{"oldRev":"8cde4a825638a414ef55a57662b38e2746b83668","newRev":"a80ed61a98fd0a1d13eab95252db189cdeb0fe96",
+ "refName":"refs/staging/master","project":"qtqa/tqtc-coin-ci"}}""",
+ GerritEvent(type='ref-updated', project='qtqa/tqtc-coin-ci', branch='refs/staging/master')),
+ ("""{"type":"patchset-created","change":{"project":"qt/qtopcua","branch":"dev","id":"I0c6ba3451a29e20508b2d59671e9b8d50d47158f","number":"235852",
+ "subject":"Split qopcuabrowsing.h/.cpp","owner":{"name":"Jannis Völker","email":"jannis.voelker@basyskom.com","username":"basyskom.jannis.voelker"},
+ "url":"https://codereview.qt-project.org/235852"},"patchSet":{"number":"2","revision":"0316904a4a5274166e8b785b09c1727aa6485ede","parents":["8a6ef588fc0de9876a5d64964fb958f7818e24a4"],
+ "ref":"refs/changes/52/235852/2","uploader":{"name":"Jannis Völker","email":"jannis.voelker@basyskom.com","username":"basyskom.jannis.voelker"},
+ "createdOn":1533542575,"author":{"name":"Jannis Völker","email":"jannis.voelker@basyskom.com","username":"basyskom.jannis.voelker"},
+ "sizeInsertions":182,"sizeDeletions":-206},"uploader":{"name":"Jannis Völker","email":"jannis.voelker@basyskom.com","username":"basyskom.jannis.voelker"}}""",
+ GerritEvent(type='patchset-created', project='qt/qtopcua', branch='dev')),
+ ("""{"type":"reviewer-added","change":{"project":"qtqa/tqtc-coin-ci","branch":"master","id":"I40486a0eabeac788ac1c857f47b1dbf9cf538a61",
+ "number":"236086","subject":"Webui: Add task failure summary in task search targets",
+ "owner":{"name":"Aapo Keskimolo","email":"aapo.keskimolo@qt.io","username":"aakeskimo"},
+ "url":"https://codereview.qt-project.org/236086"},"patchSet":{"number":"1","revision":"27c7d3c2e5fa3362cac14f54779c8ba273f782c3",
+ "parents":["335d191e4611b6a3af2d0c89b37661d82a217cdc"],"ref":"refs/changes/86/236086/1",
+ "uploader":{"name":"Aapo Keskimolo","email":"aapo.keskimolo@qt.io","username":"aakeskimo"},"createdOn":1533498876,
+ "author":{"name":"Aapo Keskimolo","email":"aapo.keskimolo@qt.io","username":"aakeskimo"},
+ "sizeInsertions":1,"sizeDeletions":-1},"reviewer":{"name":"Joni Jäntti","email":"joni.jantti@qt.io","username":"jojantti"}}""",
+ GerritEvent(type='reviewer-added', project='qtqa/tqtc-coin-ci', branch='master')),
+ ("""{"type":"change-restored","change":{"project":"qt/qtbase","branch":"5.9","id":"I587534fc5723b3d198fe2065fbcf1bee4871a768",
+ "number":"236064","subject":"Doc: Fix wrong link in QFont documentation","owner":{"name":"Paul Wicking","email":"paul.wicking@qt.io",
+ "username":"paulwicking"},"url":"https://codereview.qt-project.org/236064"},
+ "restorer":{"name":"Paul Wicking","email":"paul.wicking@qt.io","username":"paulwicking"}}""",
+ GerritEvent(type='change-restored', project='qt/qtbase', branch='5.9')),
+ ("""{"type":"draft-published","change":{"project":"qt/qtbase","branch":"dev","id":"I91f4e8d43d95c5f30c5bc2571393804209b7a843","number":"236135",
+ "subject":"NeworkAccessBackend: Remove duplicated/shadowed member","owner":{"name":"Mårten Nordheim","email":"marten.nordheim@qt.io","username":"manordheim"},
+ "url":"https://codereview.qt-project.org/236135"},"patchSet":{"number":"1","revision":"34d84d08e0f4171f4a28729ed8f62762af8d4d2e","parents":["9f2a6715600bf872e41dcd8c4492480b93b4f599"],
+ "ref":"refs/changes/35/236135/1","uploader":{"name":"Mårten Nordheim","email":"marten.nordheim@qt.io","username":"manordheim"},"createdOn":1533556341,
+ "author":{"name":"Mårten Nordheim","email":"marten.nordheim@qt.io","username":"manordheim"},"sizeInsertions":5,"sizeDeletions":-8},
+ "uploader":{"name":"Mårten Nordheim","email":"marten.nordheim@qt.io","username":"manordheim"}}""",
+ GerritEvent(type='draft-published', project='qt/qtbase', branch='dev')),
+ ("""{"type":"change-abandoned","change":{"project":"qt/qtbase","branch":"dev","id":"I5f5d8da9e7af10a26e8271a6488850f120f3a23e","number":"235959","subject":"Blacklist tst_QSharedPointer::invalidConstructs","owner":{"name":"Joni Jäntti","email":"joni.jantti@qt.io","username":"jojantti"},"url":"https://codereview.qt-project.org/235959"},"abandoner":{"name":"Joni Jäntti","email":"joni.jantti@qt.io","username":"jojantti"},"reason":"Problem fixed by: https://codereview.qt-project.org/#/c/236054/"}""",
+ GerritEvent(type='change-abandoned', project='qt/qtbase', branch='dev')),
+ ("""{"type":"merge-failed","change":{"project":"qt/qtvirtualkeyboard","branch":"dev","id":"I7c1f41dfd7ddd25faf2d197652ba04d3d7e12941","number":"215270",
+ "subject":"myscript: initial integration","owner":{"name":"Yuntaek Rim","email":"yuntaek.rim@myscript.com","username":"yuntaek.rim"},
+ "url":"https://codereview.qt-project.org/215270"},"patchSet":{"number":"18","revision":"1589057210234577d24fdff8ae286a04eb44469d",
+ "parents":["fbbd9d5db5fd2547c54d19e7441e761dcfcc213b"],"ref":"refs/changes/70/215270/18",
+ "uploader":{"name":"Mitch Curtis","email":"mitch.curtis@qt.io","username":"mitch_curtis"},"createdOn":1531913238,
+ "author":{"name":"Yuntaek Rim","email":"yuntaek.rim@myscript.com","username":"yuntaek.rim"},"sizeInsertions":2889,"sizeDeletions":-737},
+ "submitter":{"name":"Mitch Curtis","email":"mitch.curtis@qt.io","username":"mitch_curtis"},
+ "reason":"Your change could not be merged due to a path conflict.\\n\\nMake sure you staged all dependencies of this change. If the change has dependencies which are currently INTEGRATING, try again when the integration finishes.\\n\\nOtherwise please rebase the change locally and upload the rebased commit for review."}""",
+ GerritEvent(type='merge-failed', project='qt/qtvirtualkeyboard', branch='dev')),
+ ("""{"type":"change-deferred","change":{"project":"pyside/pyside-setup","branch":"5.11","id":"I56796bcf51cae31d885e7cefed8de1f94794ee04","number":"236319","subject":"Qt3DAnimation: add missing classes","owner":{"name":"Cristian Maureira-Fredes","email":"cristian.maureira-fredes@qt.io","username":"crmaurei"},"url":"https://codereview.qt-project.org/236319"},"deferrer":{"name":"Cristian Maureira-Fredes","email":"cristian.maureira-fredes@qt.io","username":"crmaurei"},"reason":"ups, duplicated -\u003e https://codereview.qt-project.org/#/c/236315/"}""",
+ GerritEvent(type='change-deferred', project='pyside/pyside-setup', branch='5.11')),
+])
+def test_parser(message: str, expected: GerritEvent):
+ parser = GerritStreamParser()
+ result = parser.parse(message)
+ result.__repr__()
+ assert result == expected
diff --git a/scripts/jira/jira-bug-closer/tox.ini b/scripts/jira/jira-bug-closer/tox.ini
new file mode 100644
index 00000000..97765754
--- /dev/null
+++ b/scripts/jira/jira-bug-closer/tox.ini
@@ -0,0 +1,3 @@
+[flake8]
+ignore = E501
+