diff --git a/0001-Display-real-line-numbers-on-pull-request-s-diff-vie.patch b/0001-Display-real-line-numbers-on-pull-request-s-diff-vie.patch new file mode 100644 index 0000000..6123262 --- /dev/null +++ b/0001-Display-real-line-numbers-on-pull-request-s-diff-vie.patch @@ -0,0 +1,194 @@ +From f48278682d9552670b7d5cb9e99a7e0a74bdd689 Mon Sep 17 00:00:00 2001 +From: Julen Landa Alustiza +Date: Fri, 7 Aug 2020 12:35:32 +0200 +Subject: [PATCH 1/9] Display real line numbers on pull request's diff view + Fixes #724 + +--- + pagure/templates/_repo_renderdiff.html | 1 - + pagure/ui/filters.py | 101 ++++++++++--------------- + tests/test_pagure_flask_ui_fork.py | 24 +++++- + 3 files changed, 64 insertions(+), 62 deletions(-) + +diff --git a/pagure/templates/_repo_renderdiff.html b/pagure/templates/_repo_renderdiff.html +index a0922169..aa9a062e 100644 +--- a/pagure/templates/_repo_renderdiff.html ++++ b/pagure/templates/_repo_renderdiff.html +@@ -164,7 +164,6 @@ + commit=patchstats["new_id"], + prequest=pull_request, + index=loop.index, +- isprdiff=True, + tree_id=diff_commits[0].tree.id)}} + + {% endautoescape %} +diff --git a/pagure/ui/filters.py b/pagure/ui/filters.py +index 73799ed7..98b2741f 100644 +--- a/pagure/ui/filters.py ++++ b/pagure/ui/filters.py +@@ -149,38 +149,33 @@ def format_loc( + for key in comments: + comments[key] = sorted(comments[key], key=lambda obj: obj.date_created) + +- if not index: +- index = "" ++ if isinstance(filename, str) and six.PY2: ++ filename = filename.decode("UTF-8") + + cnt = 1 + for line in loc.split("\n"): +- if filename and commit: +- if isinstance(filename, str) and six.PY2: +- filename = filename.decode("UTF-8") +- +- if isprdiff and ( +- line.startswith("@@") +- or line.startswith("+") +- or line.startswith("-") +- ): +- if line.startswith("@@"): +- output.append( +- '' +- % ({"cnt_lbl": cnt, "commit": commit}) +- ) +- elif line.startswith("+"): +- output.append( +- '' +- % ({"cnt_lbl": cnt, "commit": commit}) +- ) +- elif line.startswith("-"): +- output.append( +- '' +- % ({"cnt_lbl": cnt, "commit": commit}) +- ) ++ if line.startswith("@@"): ++ output.append( ++ '' ++ % ({"cnt_lbl": cnt, "commit": commit}) ++ ) ++ output.append( ++ '' ++ ) ++ else: ++ if line.startswith("+"): ++ output.append( ++ '' ++ % ({"cnt_lbl": cnt, "commit": commit}) ++ ) ++ elif line.startswith("-"): ++ output.append( ++ '' ++ % ({"cnt_lbl": cnt, "commit": commit}) ++ ) + else: + output.append( + '' +@@ -211,13 +206,6 @@ def format_loc( + } + ) + ) +- else: +- output.append( +- '' +- '' +- % ({"cnt": "%s_%s" % (index, cnt), "cnt_lbl": cnt}) +- ) + + cnt += 1 + if not line: +@@ -254,29 +242,24 @@ def format_loc( + + 'title="Open changed file">' + ) + +- if isprdiff and ( +- line.startswith("@@") +- or line.startswith("+") +- or line.startswith("-") +- ): +- if line.startswith("@@"): +- output.append( +- '\ +-
%s
' +- % line +- ) +- elif line.startswith("+"): +- output.append( +- '\ +-
%s
' +- % escape(line) +- ) +- elif line.startswith("-"): +- output.append( +- '\ +-
%s
' +- % escape(line) +- ) ++ if line.startswith("@@"): ++ output.append( ++ '\ ++
%s
' ++ % line ++ ) ++ elif line.startswith("+"): ++ output.append( ++ '\ ++
%s
' ++ % escape(line) ++ ) ++ elif line.startswith("-"): ++ output.append( ++ '\ ++
%s
' ++ % escape(line) ++ ) + else: + output.append( + '
%s
' +diff --git a/tests/test_pagure_flask_ui_fork.py b/tests/test_pagure_flask_ui_fork.py +index 21ecd7df..8e8dce1c 100644 +--- a/tests/test_pagure_flask_ui_fork.py ++++ b/tests/test_pagure_flask_ui_fork.py +@@ -302,11 +302,31 @@ class PagureFlaskForktests(tests.Modeltests): + ) + + self.assertIn( +- '+3', ++ '+3', + output_text, + ) + self.assertIn( +- '-1', ++ '-1', ++ output_text, ++ ) ++ ++ # Test if hunk headline is rendered without line numbers ++ self.assertIn( ++ '\n
@@ -1,2 +1,4 @@',
++            output_text,
++        )
++        # Tests if line number 1 is displayed
++        self.assertNotIn(
++            '',
++            output_text,
++        )
++        # Test if line number 2 is displayed
++        self.assertIn(
++            '',
+             output_text,
+         )
+ 
+-- 
+2.26.2
+
diff --git a/0002-Show-the-assignee-s-avatar-on-the-board.patch b/0002-Show-the-assignee-s-avatar-on-the-board.patch
new file mode 100644
index 0000000..b870b13
--- /dev/null
+++ b/0002-Show-the-assignee-s-avatar-on-the-board.patch
@@ -0,0 +1,33 @@
+From d3fb1c32746580bb109ac21ff349a655a01c46f0 Mon Sep 17 00:00:00 2001
+From: Pierre-Yves Chibon 
+Date: Wed, 12 Aug 2020 20:47:22 +0200
+Subject: [PATCH 2/9] Show the assignee's avatar on the board
+
+If the ticket shown in the board is assigned to someone, show this
+person's avatar. This helps not only seeing what is in progress or
+blocked but also who is working on what or being blocked.
+
+Fixes https://pagure.io/pagure/issue/4959
+
+Signed-off-by: Pierre-Yves Chibon 
+---
+ pagure/templates/board.html | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/pagure/templates/board.html b/pagure/templates/board.html
+index 26509ae0..46a7eb65 100644
+--- a/pagure/templates/board.html
++++ b/pagure/templates/board.html
+@@ -65,6 +65,9 @@
+             {% elif bissue.issue.status == 'Closed' %}
+               
+               #{{ bissue.issue.id }}
++            {% endif %}
++            {% if bissue.issue.assignee %}
++            - {{ bissue.issue.assignee.username | avatar(size=20) | safe}}
+             {% endif %}
+               - {{ bissue.issue.title | truncate(80, False, '...') }}
+               
+-- 
+2.26.2
+
diff --git a/0003-Allow-setting-a-status-as-closing-even-if-the-projec.patch b/0003-Allow-setting-a-status-as-closing-even-if-the-projec.patch
new file mode 100644
index 0000000..6d2ab2e
--- /dev/null
+++ b/0003-Allow-setting-a-status-as-closing-even-if-the-projec.patch
@@ -0,0 +1,128 @@
+From 9fd32d4d948fc4231eee68e8044b844060db140c Mon Sep 17 00:00:00 2001
+From: Pierre-Yves Chibon 
+Date: Wed, 12 Aug 2020 20:56:18 +0200
+Subject: [PATCH 3/9] Allow setting a status as closing even if the project has
+ no close_status
+
+When determining if a status "closes" tickets or not, we should first
+check if it was set to and only if it wasn't then check if there was a
+close_status set for it. Otherwise the logic wasn't correctly interpreted.
+
+Fixes https://pagure.io/pagure/issue/4958
+
+Signed-off-by: Pierre-Yves Chibon 
+---
+ pagure/api/boards.py                  |  4 +-
+ tests/test_pagure_flask_api_boards.py | 81 +++++++++++++++++++++++++++
+ 2 files changed, 84 insertions(+), 1 deletion(-)
+
+diff --git a/pagure/api/boards.py b/pagure/api/boards.py
+index e42f7cb6..669fd457 100644
+--- a/pagure/api/boards.py
++++ b/pagure/api/boards.py
+@@ -510,7 +510,9 @@ def api_board_status(repo, board_name, username=None, namespace=None):
+ 
+         try:
+             close_status = data[name].get("close_status") or None
+-            close = data[name].get("close") or True if close_status else False
++            close = data[name].get("close") or (
++                True if close_status else False
++            )
+             if close_status not in repo.close_status:
+                 close_status = None
+ 
+diff --git a/tests/test_pagure_flask_api_boards.py b/tests/test_pagure_flask_api_boards.py
+index ef125878..67199703 100644
+--- a/tests/test_pagure_flask_api_boards.py
++++ b/tests/test_pagure_flask_api_boards.py
+@@ -801,6 +801,87 @@ class PagureFlaskApiBoardsWithBoardtests(tests.SimplePagureTest):
+             },
+         )
+ 
++    def test_api_board_api_board_status_no_close_status(self):
++        headers = {
++            "Authorization": "token aaabbbcccddd",
++            "Content-Type": "application/json",
++        }
++
++        data = json.dumps(
++            {
++                "Backlog": {
++                    "close": False,
++                    "close_status": None,
++                    "bg_color": "#FFB300",
++                    "default": True,
++                    "rank": 1,
++                },
++                "Triaged": {
++                    "close": False,
++                    "close_status": None,
++                    "bg_color": "#ca0dcd",
++                    "default": False,
++                    "rank": 2,
++                },
++                "Done": {
++                    "close": True,
++                    "close_status": None,
++                    "bg_color": "#34d240",
++                    "default": False,
++                    "rank": 4,
++                },
++                "  ": {
++                    "close": True,
++                    "close_status": None,
++                    "bg_color": "#34d240",
++                    "default": False,
++                    "rank": 5,
++                },
++            }
++        )
++        output = self.app.post(
++            "/api/0/test/boards/dev/status", headers=headers, data=data
++        )
++        self.assertEqual(output.status_code, 200)
++        data = json.loads(output.get_data(as_text=True))
++        self.assertDictEqual(
++            data,
++            {
++                "board": {
++                    "active": True,
++                    "name": "dev",
++                    "status": [
++                        {
++                            "bg_color": "#FFB300",
++                            "close": False,
++                            "close_status": None,
++                            "default": True,
++                            "name": "Backlog",
++                        },
++                        {
++                            "bg_color": "#ca0dcd",
++                            "close": False,
++                            "close_status": None,
++                            "default": False,
++                            "name": "Triaged",
++                        },
++                        {
++                            "name": "Done",
++                            "close": True,
++                            "close_status": None,
++                            "default": False,
++                            "bg_color": "#34d240",
++                        },
++                    ],
++                    "tag": {
++                        "tag": "dev",
++                        "tag_color": "DeepBlueSky",
++                        "tag_description": "",
++                    },
++                }
++            },
++        )
++
+     def test_api_board_api_board_status_adding_removing(self):
+         headers = {
+             "Authorization": "token aaabbbcccddd",
+-- 
+2.26.2
+
diff --git a/0004-Include-the-assignee-in-the-list-of-people-notified-.patch b/0004-Include-the-assignee-in-the-list-of-people-notified-.patch
new file mode 100644
index 0000000..e3803c8
--- /dev/null
+++ b/0004-Include-the-assignee-in-the-list-of-people-notified-.patch
@@ -0,0 +1,36 @@
+From 1e129038cf63f619fd5b69f92057cb29c53a83d3 Mon Sep 17 00:00:00 2001
+From: Pierre-Yves Chibon 
+Date: Wed, 12 Aug 2020 21:19:22 +0200
+Subject: [PATCH 4/9] Include the assignee in the list of people notified on a
+ ticket/PR
+
+We always include the assignee in the notifications (even on private
+tickets), but so far we did not show them in the list of subscriber.
+
+With this commit, this oversight is now fixed.
+
+Fixes https://pagure.io/pagure/issue/4957
+
+Signed-off-by: Pierre-Yves Chibon 
+---
+ pagure/lib/query.py | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/pagure/lib/query.py b/pagure/lib/query.py
+index 8ee4df6c..498148df 100644
+--- a/pagure/lib/query.py
++++ b/pagure/lib/query.py
+@@ -4957,6 +4957,10 @@ def get_watch_list(session, obj):
+     # Add the user of the project
+     users.add(obj.project.user.username)
+ 
++    # Add the assignee if there is one
++    if obj.assignee:
++        users.add(obj.assignee.username)
++
+     # Add the regular contributors
+     for contributor in obj.project.users:
+         users.add(contributor.username)
+-- 
+2.26.2
+
diff --git a/0005-Introduce-the-collaborator_project_groups-mapping.patch b/0005-Introduce-the-collaborator_project_groups-mapping.patch
new file mode 100644
index 0000000..67da894
--- /dev/null
+++ b/0005-Introduce-the-collaborator_project_groups-mapping.patch
@@ -0,0 +1,104 @@
+From d91182e9bd01582c3ba1671a758038819943fb9b Mon Sep 17 00:00:00 2001
+From: Pierre-Yves Chibon 
+Date: Tue, 18 Aug 2020 13:33:21 +0200
+Subject: [PATCH 5/9] Introduce the collaborator_project_groups mapping
+
+We need two mappings on a regular basis. One linking a project to its
+group of collaborator and pointing to the PagureGroup objects directly.
+This is used for example to retrieve the list of collaborator groups.
+The one mapping is between a project and the corresponding ProjectGroup
+for collaborators which is where the "branches" that the group is
+restricted to is stored.
+
+So we now have both mappings available and we need to be careful to use
+the proper one, but pagure will quickly indicate (ie: crash) if one
+uses the wrong one and tries to access either the group name or the
+branches attribute.
+
+Fixes https://pagure.io/fedora-infrastructure/issue/9247
+
+Signed-off-by: Pierre-Yves Chibon 
+---
+ pagure/lib/model.py                    | 10 ++++++++++
+ pagure/utils.py                        |  2 +-
+ tests/test_pagure_flask_api_project.py | 23 ++++++++++++++++++++++-
+ 3 files changed, 33 insertions(+), 2 deletions(-)
+
+diff --git a/pagure/lib/model.py b/pagure/lib/model.py
+index 325a5452..4eea9f58 100644
+--- a/pagure/lib/model.py
++++ b/pagure/lib/model.py
+@@ -487,6 +487,16 @@ class Project(BASE):
+     )
+ 
+     collaborator_groups = relation(
++        "PagureGroup",
++        secondary="projects_groups",
++        primaryjoin="projects.c.id==projects_groups.c.project_id",
++        secondaryjoin="and_(pagure_group.c.id==projects_groups.c.group_id,\
++                projects_groups.c.access=='collaborator')",
++        order_by="PagureGroup.group_name.asc()",
++        viewonly=True,
++    )
++
++    collaborator_project_groups = relation(
+         "ProjectGroup",
+         primaryjoin="and_(projects.c.id==projects_groups.c.project_id,\
+                     projects_groups.c.access=='collaborator')",
+diff --git a/pagure/utils.py b/pagure/utils.py
+index 24a0ce4d..915ba499 100644
+--- a/pagure/utils.py
++++ b/pagure/utils.py
+@@ -311,7 +311,7 @@ def is_repo_collaborator(repo_obj, refname, username=None, session=None):
+                     return True
+ 
+     # If they are in a group that has commit access -> maybe
+-    for project_group in repo_obj.collaborator_groups:
++    for project_group in repo_obj.collaborator_project_groups:
+         if project_group.group.group_name in usergroups:
+             # if branch is None when the user tries to read,
+             # so we'll allow that
+diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py
+index 3c364be2..51dd8e9f 100644
+--- a/tests/test_pagure_flask_api_project.py
++++ b/tests/test_pagure_flask_api_project.py
+@@ -1010,6 +1010,27 @@ class PagureFlaskApiProjecttests(tests.Modeltests):
+         )
+         self.session.commit()
+ 
++        # Add a collaborator group
++        msg = pagure.lib.query.add_group(
++            self.session,
++            group_name="some_group",
++            display_name="Some Group",
++            description=None,
++            group_type="bar",
++            user="pingou",
++            is_admin=False,
++            blacklist=[],
++        )
++        pagure.lib.query.add_group_to_project(
++            session=self.session,
++            project=project,
++            new_group="some_group",
++            user="pingou",
++            access="collaborator",
++            branches="features/*",
++        )
++        self.session.commit()
++
+         # Existing project
+         output = self.app.get("/api/0/test")
+         self.assertEqual(output.status_code, 200)
+@@ -1019,7 +1040,7 @@ class PagureFlaskApiProjecttests(tests.Modeltests):
+         expected_data = {
+             "access_groups": {
+                 "admin": [],
+-                "collaborator": [],
++                "collaborator": ["some_group"],
+                 "commit": [],
+                 "ticket": [],
+             },
+-- 
+2.26.2
+
diff --git a/0006-When-a-file-a-detected-as-a-binary-file-return-the-r.patch b/0006-When-a-file-a-detected-as-a-binary-file-return-the-r.patch
new file mode 100644
index 0000000..fc7290c
--- /dev/null
+++ b/0006-When-a-file-a-detected-as-a-binary-file-return-the-r.patch
@@ -0,0 +1,219 @@
+From 3f65d123faa3a1fcd0b93921e5d42fe659b79fa7 Mon Sep 17 00:00:00 2001
+From: Pierre-Yves Chibon 
+Date: Wed, 12 Aug 2020 21:33:07 +0200
+Subject: [PATCH 6/9] When a file a detected as a binary file, return the raw
+ file
+
+Basically, with the code until now, if an user was trying to view
+a binary file in the UI, it was then prompted to download the file
+(as pagure doesn't know how to display binary file), but the file
+the users were downloading wasn't the raw file but rather the html
+page that would contain the file.
+Not ideal.
+
+So with this commit we're now just saying: if the file is binary and
+we can't render it, just return the raw file to the user and let
+them deal with it.
+
+Fixes https://pagure.io/pagure/issue/4907
+
+Signed-off-by: Pierre-Yves Chibon 
+---
+ pagure/ui/repo.py                            |  8 +++-
+ tests/test_pagure_flask_ui_fork.py           | 18 ++-------
+ tests/test_pagure_flask_ui_repo.py           | 25 ++++---------
+ tests/test_pagure_flask_ui_repo_view_file.py | 39 +++++++++-----------
+ 4 files changed, 35 insertions(+), 55 deletions(-)
+
+diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py
+index b6aacc8e..06afb71d 100644
+--- a/pagure/ui/repo.py
++++ b/pagure/ui/repo.py
+@@ -639,7 +639,13 @@ def view_file(repo, identifier, filename, username=None, namespace=None):
+         output_type = "tree"
+ 
+     if output_type == "binary":
+-        headers[str("Content-Disposition")] = "attachment"
++        return view_raw_file(
++            repo,
++            identifier,
++            filename=filename,
++            username=username,
++            namespace=namespace,
++        )
+ 
+     return flask.Response(
+         flask.stream_with_context(
+diff --git a/tests/test_pagure_flask_ui_fork.py b/tests/test_pagure_flask_ui_fork.py
+index 8e8dce1c..1bff134f 100644
+--- a/tests/test_pagure_flask_ui_fork.py
++++ b/tests/test_pagure_flask_ui_fork.py
+@@ -4676,10 +4676,7 @@ More information
+             # Check fork-edit doesn't show for binary files
+             output = self.app.get("/test/blob/master/f/test.jpg")
+             self.assertEqual(output.status_code, 200)
+-            self.assertNotIn(
+-                "Fork and Edit\n                    \n",
+-                output.get_data(as_text=True),
+-            )
++            self.assertNotIn(b"
+             )
+             self.assertEqual(output.status_code, 400)
+             self.assertIn(
+-                "

Cannot edit binary files

", +- output.get_data(as_text=True), ++ b"

Cannot edit binary files

", output.data, + ) + + # Check fork-edit shows when user is not logged in +@@ -4764,10 +4760,7 @@ More information + # Check fork-edit doesn't show for binary + output = self.app.get("/test/blob/master/f/test.jpg") + self.assertEqual(output.status_code, 200) +- self.assertNotIn( +- "Edit in your fork\n \n", +- output.get_data(as_text=True), +- ) ++ self.assertNotIn(b" + "/somenamespace/test3/blob/master/f/test.jpg" + ) + self.assertEqual(output.status_code, 200) +- self.assertNotIn( +- "Fork and Edit\n \n", +- output.get_data(as_text=True), +- ) ++ self.assertNotIn(b"", output_text) +- self.assertIn( +- 'view the raw version', +- output_text, +- ) ++ self.assertNotIn(b"", output_text) +- self.assertIn('/f/test.jpg">view the raw version', output_text) ++ self.assertNotIn(b"", output_text) +- self.assertIn('/f/test.jpg">view the raw version', output_text) ++ self.assertNotIn(b"view the raw version', output_text) +- self.assertIn("Binary files cannot be rendered.
", output_text) ++ self.assertNotIn(b"", output_text) ++ self.assertEqual("foo\n bar", output_text) + + def test_view_raw_file(self): + """ Test the view_raw_file endpoint. """ +diff --git a/tests/test_pagure_flask_ui_repo_view_file.py b/tests/test_pagure_flask_ui_repo_view_file.py +index 16a1ce18..effa06b6 100644 +--- a/tests/test_pagure_flask_ui_repo_view_file.py ++++ b/tests/test_pagure_flask_ui_repo_view_file.py +@@ -135,12 +135,7 @@ class PagureFlaskRepoViewFiletests(LocalBasetests): + # View what's supposed to be an image + output = self.app.get("/test/blob/master/f/test.jpg") + self.assertEqual(output.status_code, 200) +- output_text = output.get_data(as_text=True) +- self.assertIn("Binary files cannot be rendered.
", output_text) +- self.assertIn( +- '
view the raw version', +- output_text, +- ) ++ self.assertNotIn(b"", output_text) +- self.assertIn('/f/test.jpg">view the raw version', output_text) ++ self.assertNotIn(b"", output_text) +- self.assertIn('/f/test.jpg">view the raw version', output_text) ++ self.assertEqual(output.status_code, 404) + +- def test_view_file_binary_file2(self): ++ def test_view_file_invalid_branch2(self): + """ Test the view_file with a binary file (2). """ +- +- # View binary file + output = self.app.get("/test/blob/sources/f/test_binary") ++ self.assertEqual(output.status_code, 404) ++ ++ def test_view_file_invalid_branch(self): ++ """ Test the view_file via a image name. """ ++ output = self.app.get("/test/blob/master/f/test.jpg") + self.assertEqual(output.status_code, 200) +- output_text = output.get_data(as_text=True) +- self.assertIn('/f/test_binary">view the raw version', output_text) +- self.assertTrue("Binary files cannot be rendered.
" in output_text) ++ self.assertNotIn(b" +Date: Thu, 3 Sep 2020 16:44:20 +0200 +Subject: [PATCH 7/9] Remove fenced code block when checking mention + +Fix bug #4978 + +We have to remove code using the '~~~~' markup since +this result in incorrect trigger, especially on private +ticket. +--- + pagure/lib/notify.py | 6 +++++- + tests/test_pagure_lib_notify.py | 26 ++++++++++++++++++++++++++ + 2 files changed, 31 insertions(+), 1 deletion(-) + +diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py +index 0c124db9..02c277c0 100644 +--- a/pagure/lib/notify.py ++++ b/pagure/lib/notify.py +@@ -35,6 +35,7 @@ import pagure.lib.query + import pagure.lib.tasks_services + from pagure.config import config as pagure_config + from pagure.pfmarkdown import MENTION_RE ++from markdown.extensions.fenced_code import FencedBlockPreprocessor + + + _log = logging.getLogger(__name__) +@@ -234,7 +235,10 @@ def _add_mentioned_users(emails, comment): + """ Check the comment to see if an user is mentioned in it and if + so add this user to the list of people to notify. + """ +- for username in re.findall(MENTION_RE, comment): ++ filtered_comment = re.sub( ++ FencedBlockPreprocessor.FENCED_BLOCK_RE, "", comment ++ ) ++ for username in re.findall(MENTION_RE, filtered_comment): + user = pagure.lib.query.search_user(flask.g.session, username=username) + if user: + emails.add(user.default_email) +diff --git a/tests/test_pagure_lib_notify.py b/tests/test_pagure_lib_notify.py +index 8d31cb70..78af57af 100644 +--- a/tests/test_pagure_lib_notify.py ++++ b/tests/test_pagure_lib_notify.py +@@ -25,6 +25,7 @@ import pagure.lib.model + import pagure.lib.notify + import pagure.lib.query + import tests ++import munch + + + class PagureLibNotifytests(tests.Modeltests): +@@ -543,6 +544,31 @@ RW1haWwgY29udGVudA== + """ + self.assertEqual(email.as_string(), exp) + ++ def test_notification_mention(self): ++ g = munch.Munch() ++ g.session = self.session ++ with patch("flask.g", g): ++ ++ def _check_mention(comment, exp): ++ emails = set([]) ++ emails = pagure.lib.notify._add_mentioned_users( ++ emails, comment ++ ) ++ ++ self.assertEqual(emails, exp) ++ ++ exp = set(["bar@pingou.com"]) ++ comment = "I think we should ask @pingou how to pronounce pagure" ++ _check_mention(comment, exp) ++ ++ exp = set([]) ++ comment = """Let me quote him: ++~~~~ ++ @pingou> Pagure is pronounced 'pa-gure', not 'pagu-re' ++~~~~ ++""" ++ _check_mention(comment, exp) ++ + + if __name__ == "__main__": + unittest.main(verbosity=2) +-- +2.26.2 + diff --git a/0008-Add-support-for-using-cchardet-to-detect-files-encod.patch b/0008-Add-support-for-using-cchardet-to-detect-files-encod.patch new file mode 100644 index 0000000..0bdd52f --- /dev/null +++ b/0008-Add-support-for-using-cchardet-to-detect-files-encod.patch @@ -0,0 +1,284 @@ +From a8b2e001d6f4d2e4683aa65d6331b971f8e2ce33 Mon Sep 17 00:00:00 2001 +From: Pierre-Yves Chibon +Date: Mon, 14 Sep 2020 16:03:31 +0200 +Subject: [PATCH 8/9] Add support for using cchardet to detect files' encoding + +cchardet is a much faster version of the chardet library that can +be used to automatically detect the encoding of a file. + +Since this library is only available on python3, we're making it +an optional dependency for now. + +Fixes https://pagure.io/pagure/issue/4977 + +Signed-off-by: Pierre-Yves Chibon +--- + pagure/lib/encoding_utils.py | 12 ++++- + tests/test_pagure_flask_ui_repo.py | 58 ++++++++++++++++++++----- + tests/test_pagure_lib_encoding_utils.py | 40 ++++++++++++----- + tests/test_pagure_lib_mimetype.py | 28 ++++++++++-- + 4 files changed, 111 insertions(+), 27 deletions(-) + +diff --git a/pagure/lib/encoding_utils.py b/pagure/lib/encoding_utils.py +index 66f7dced..304e7d84 100644 +--- a/pagure/lib/encoding_utils.py ++++ b/pagure/lib/encoding_utils.py +@@ -15,7 +15,12 @@ from __future__ import unicode_literals, division, absolute_import + from collections import namedtuple + import logging + +-from chardet import universaldetector, __version__ as ch_version ++try: ++ import cchardet ++ from cchardet import __version__ as ch_version ++except ImportError: ++ cchardet = None ++ from chardet import universaldetector, __version__ as ch_version + + from pagure.exceptions import PagureEncodingException + +@@ -44,7 +49,10 @@ def detect_encodings(data): + + # We can't use ``chardet.detect`` because we want to dig in the internals + # of the detector to bias the utf-8 result. +- detector = universaldetector.UniversalDetector() ++ if cchardet is not None: ++ detector = cchardet.UniversalDetector() ++ else: ++ detector = universaldetector.UniversalDetector() + detector.reset() + detector.feed(data) + result = detector.close() +diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py +index b4322e7d..e816e7f7 100644 +--- a/tests/test_pagure_flask_ui_repo.py ++++ b/tests/test_pagure_flask_ui_repo.py +@@ -20,6 +20,12 @@ import tempfile + import time + import os + ++cchardet = None ++try: ++ import cchardet ++except ImportError: ++ pass ++ + import pygit2 + import six + from mock import ANY, patch, MagicMock +@@ -2763,9 +2769,16 @@ class PagureFlaskRepotests(tests.Modeltests): + output = self.app.get("/test/raw/master") + self.assertEqual(output.status_code, 200) + output_text = output.get_data(as_text=True) +- self.assertEqual( +- output.headers["Content-Type"].lower(), "text/plain; charset=ascii" +- ) ++ if cchardet is not None: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=utf-8", ++ ) ++ else: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=ascii", ++ ) + self.assertIn(":Author: Pierre-Yves Chibon", output_text) + + # Add some more content to the repo +@@ -2784,9 +2797,16 @@ class PagureFlaskRepotests(tests.Modeltests): + + # View in a branch + output = self.app.get("/test/raw/master/f/sources") +- self.assertEqual( +- output.headers["Content-Type"].lower(), "text/plain; charset=ascii" +- ) ++ if cchardet is not None: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=utf-8", ++ ) ++ else: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=ascii", ++ ) + self.assertEqual(output.status_code, 200) + output_text = output.get_data(as_text=True) + self.assertIn("foo\n bar", output_text) +@@ -2837,9 +2857,16 @@ class PagureFlaskRepotests(tests.Modeltests): + output = self.app.get("/test/raw/master") + self.assertEqual(output.status_code, 200) + output_text = output.get_data(as_text=True) +- self.assertEqual( +- output.headers["Content-Type"].lower(), "text/plain; charset=ascii" +- ) ++ if cchardet is not None: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=utf-8", ++ ) ++ else: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=ascii", ++ ) + self.assertTrue( + output_text.startswith("diff --git a/test_binary b/test_binary\n") + ) +@@ -2877,9 +2904,16 @@ class PagureFlaskRepotests(tests.Modeltests): + output = self.app.get("/fork/pingou/test3/raw/master/f/sources") + self.assertEqual(output.status_code, 200) + output_text = output.get_data(as_text=True) +- self.assertEqual( +- output.headers["Content-Type"].lower(), "text/plain; charset=ascii" +- ) ++ if cchardet is not None: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=utf-8", ++ ) ++ else: ++ self.assertEqual( ++ output.headers["Content-Type"].lower(), ++ "text/plain; charset=ascii", ++ ) + self.assertIn("foo\n bar", output_text) + + def test_view_commit(self): +diff --git a/tests/test_pagure_lib_encoding_utils.py b/tests/test_pagure_lib_encoding_utils.py +index ccc8825f..aff7d8ba 100644 +--- a/tests/test_pagure_lib_encoding_utils.py ++++ b/tests/test_pagure_lib_encoding_utils.py +@@ -5,11 +5,18 @@ Tests for :module:`pagure.lib.encoding_utils`. + + from __future__ import unicode_literals, absolute_import + +-import chardet + import os + import unittest + import sys + ++cchardet = None ++try: ++ import cchardet ++except ImportError: ++ pass ++ ++import chardet ++ + sys.path.insert( + 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") + ) +@@ -24,7 +31,10 @@ class TestGuessEncoding(unittest.TestCase): + """ + data = "Twas bryllyg, and the slythy toves did gyre and gymble" + result = encoding_utils.guess_encoding(data.encode("ascii")) +- self.assertEqual(result, "ascii") ++ if cchardet is not None: ++ self.assertEqual(result, "utf-8") ++ else: ++ self.assertEqual(result, "ascii") + + def test_guess_encoding_favor_utf_8(self): + """ +@@ -56,17 +66,24 @@ class TestGuessEncodings(unittest.TestCase): + chardet_result = chardet.detect(data) + if chardet.__version__[0] == "3": + # The first three have different confidence values ++ if cchardet is not None: ++ expexted_list = ["utf-8"] ++ # The last one in the list (which apparently has only one) ++ self.assertEqual(result[-1].encoding, "utf-8") ++ else: ++ expexted_list = ["utf-8", "ISO-8859-9", "ISO-8859-1"] ++ # This is the one with the least confidence ++ self.assertEqual(result[-1].encoding, "windows-1255") + self.assertListEqual( +- [encoding.encoding for encoding in result][:3], +- ["utf-8", "ISO-8859-9", "ISO-8859-1"], ++ [encoding.encoding for encoding in result][:3], expexted_list + ) +- # This is the one with the least confidence +- self.assertEqual(result[-1].encoding, "windows-1255") ++ + # The values in the middle of the list all have the same confidence + # value and can't be sorted reliably: use sets. +- self.assertEqual( +- set([encoding.encoding for encoding in result]), +- set( ++ if cchardet is not None: ++ expected_list = sorted(["utf-8"]) ++ else: ++ expected_list = sorted( + [ + "utf-8", + "ISO-8859-9", +@@ -89,7 +106,10 @@ class TestGuessEncodings(unittest.TestCase): + "windows-1251", + "windows-1255", + ] +- ), ++ ) ++ self.assertListEqual( ++ sorted(set([encoding.encoding for encoding in result])), ++ expected_list, + ) + self.assertEqual(chardet_result["encoding"], "ISO-8859-9") + else: +diff --git a/tests/test_pagure_lib_mimetype.py b/tests/test_pagure_lib_mimetype.py +index d5947bee..8c2f4a31 100644 +--- a/tests/test_pagure_lib_mimetype.py ++++ b/tests/test_pagure_lib_mimetype.py +@@ -9,6 +9,12 @@ import os + import unittest + import sys + ++cchardet = None ++try: ++ import cchardet ++except ImportError: ++ pass ++ + from pagure.lib import mimetype + + sys.path.insert( +@@ -20,8 +26,18 @@ class TestMIMEType(unittest.TestCase): + def test_guess_type(self): + dataset = [ + ("hello.html", None, "text/html", None), +- ("hello.html", b"#!", "text/html", "ascii"), +- ("hello", b"#!", "text/plain", "ascii"), ++ ( ++ "hello.html", ++ b"#!", ++ "text/html", ++ "ascii" if cchardet is None else "utf-8", ++ ), ++ ( ++ "hello", ++ b"#!", ++ "text/plain", ++ "ascii" if cchardet is None else "utf-8", ++ ), + ("hello.jpg", None, "image/jpeg", None), + ("hello.jpg", b"#!", "image/jpeg", None), + ("hello.jpg", b"\0", "image/jpeg", None), +@@ -49,7 +65,13 @@ class TestMIMEType(unittest.TestCase): + + def test_get_normal_headers(self): + dataset = [ +- ("hello", b"#!", "text/plain; charset=ascii"), ++ ( ++ "hello", ++ b"#!", ++ "text/plain; charset=ascii" ++ if cchardet is None ++ else "text/plain; charset=utf-8", ++ ), + ("hello.jpg", None, "image/jpeg"), + ("hello.jpg", b"#!", "image/jpeg"), + ("hello.jpg", b"\0", "image/jpeg"), +-- +2.26.2 + diff --git a/0009-Add-support-for-disabling-user-registration.patch b/0009-Add-support-for-disabling-user-registration.patch new file mode 100644 index 0000000..fb67cf7 --- /dev/null +++ b/0009-Add-support-for-disabling-user-registration.patch @@ -0,0 +1,134 @@ +From 8e23c79fb64d4dd4e6f17f809d7e629840f7e91c Mon Sep 17 00:00:00 2001 +From: Neal Gompa +Date: Thu, 24 Sep 2020 06:40:06 -0400 +Subject: [PATCH 9/9] Add support for disabling user registration + +For public/private Pagure instances where it is intended to be used +by a single user, having the ability to turn off user registration +prevents confusion and closes an avenue of potential denial of service +attacks. + +Signed-off-by: Neal Gompa +--- + doc/configuration.rst | 13 +++++++++++++ + pagure/default_config.py | 3 +++ + pagure/templates/login/login.html | 2 ++ + pagure/ui/login.py | 3 +++ + tests/test_pagure_flask_ui_login.py | 24 ++++++++++++++++++++++++ + 5 files changed, 45 insertions(+) + +diff --git a/doc/configuration.rst b/doc/configuration.rst +index 735e378c..2ea7a66d 100644 +--- a/doc/configuration.rst ++++ b/doc/configuration.rst +@@ -1117,6 +1117,7 @@ Valid options are ``fas``, ``openid``, ``oidc``, or ``local``. + the configuration options starting with ``OIDC_`` (see below) to be provided. + + * ``local`` causes pagure to use the local pagure database for user management. ++ User registration can be disabled with the ALLOW_USER_REGISTRATION configuration key. + + Defaults to: ``local``. + +@@ -1784,6 +1785,18 @@ If turned off, users are managed outside of pagure. + Defaults to: ``True`` + + ++ALLOW_USER_REGISTRATION ++~~~~~~~~~~~~~~~~~~~~~~~ ++ ++This configuration key can be used to turn on or off user registration ++(that is, the ability for users to create an account) in this pagure instance. ++If turned off, user accounts cannot be created through the UI or API. ++Currently, this key only applies to pagure instances configured with the ``local`` ++authentication backend and has no effect with the other authentication backends. ++ ++Defaults to: ``True`` ++ ++ + SESSION_COOKIE_NAME + ~~~~~~~~~~~~~~~~~~~ + +diff --git a/pagure/default_config.py b/pagure/default_config.py +index 045f2704..df0cd6b0 100644 +--- a/pagure/default_config.py ++++ b/pagure/default_config.py +@@ -78,6 +78,9 @@ ENABLE_GROUP_MNGT = True + # Enables / Disables private projects + PRIVATE_PROJECTS = True + ++# Enable / Disable user registration (local auth only) ++ALLOW_USER_REGISTRATION = True ++ + # Enable / Disable deleting branches in the UI + ALLOW_DELETE_BRANCH = True + +diff --git a/pagure/templates/login/login.html b/pagure/templates/login/login.html +index a65b10ae..e209c400 100644 +--- a/pagure/templates/login/login.html ++++ b/pagure/templates/login/login.html +@@ -18,11 +18,13 @@ + + {{ form.csrf_token }} + ++ {% if config.get('ALLOW_USER_REGISTRATION', True) %} +
++ {% endif %} + + + +diff --git a/pagure/ui/login.py b/pagure/ui/login.py +index 1a0dbd24..7da94a37 100644 +--- a/pagure/ui/login.py ++++ b/pagure/ui/login.py +@@ -38,6 +38,9 @@ _log = logging.getLogger(__name__) + def new_user(): + """ Create a new user. + """ ++ if not pagure.config.config.get("ALLOW_USER_REGISTRATION", True): ++ flask.flash("User registration is disabled.", "error") ++ return flask.redirect(flask.url_for("auth_login")) + form = forms.NewUserForm() + if form.validate_on_submit(): + +diff --git a/tests/test_pagure_flask_ui_login.py b/tests/test_pagure_flask_ui_login.py +index f11a2b22..8a1d16c7 100644 +--- a/tests/test_pagure_flask_ui_login.py ++++ b/tests/test_pagure_flask_ui_login.py +@@ -149,6 +149,30 @@ class PagureFlaskLogintests(tests.SimplePagureTest): + items = pagure.lib.query.search_user(self.session) + self.assertEqual(3, len(items)) + ++ @patch.dict("pagure.config.config", {"PAGURE_AUTH": "local"}) ++ @patch.dict("pagure.config.config", {"ALLOW_USER_REGISTRATION": False}) ++ @patch("pagure.lib.notify.send_email", MagicMock(return_value=True)) ++ def test_new_user_disabled(self): ++ """ Test the disabling of the new_user endpoint. """ ++ ++ # Check before: ++ items = pagure.lib.query.search_user(self.session) ++ self.assertEqual(2, len(items)) ++ ++ # Attempt to access the new user page ++ output = self.app.get("/user/new", follow_redirects=True) ++ self.assertEqual(output.status_code, 200) ++ self.assertIn( ++ "Login - Pagure", output.get_data(as_text=True) ++ ) ++ self.assertIn( ++ "User registration is disabled.", output.get_data(as_text=True) ++ ) ++ ++ # Check after: ++ items = pagure.lib.query.search_user(self.session) ++ self.assertEqual(2, len(items)) ++ + @patch.dict("pagure.config.config", {"PAGURE_AUTH": "local"}) + @patch.dict("pagure.config.config", {"CHECK_SESSION_IP": False}) + def test_do_login(self): +-- +2.26.2 + diff --git a/pagure.changes b/pagure.changes index 8e5c4d2..6d1d372 100644 --- a/pagure.changes +++ b/pagure.changes @@ -1,3 +1,18 @@ +------------------------------------------------------------------- +Thu Sep 24 22:57:42 UTC 2020 - Neal Gompa + +- Backport various fixes from upstream + + Patch: 0001-Display-real-line-numbers-on-pull-request-s-diff-vie.patch + + Patch: 0002-Show-the-assignee-s-avatar-on-the-board.patch + + Patch: 0003-Allow-setting-a-status-as-closing-even-if-the-projec.patch + + Patch: 0004-Include-the-assignee-in-the-list-of-people-notified-.patch + + Patch: 0005-Introduce-the-collaborator_project_groups-mapping.patch + + Patch: 0006-When-a-file-a-detected-as-a-binary-file-return-the-r.patch + + Patch: 0007-Remove-fenced-code-block-when-checking-mention.patch + + Patch: 0008-Add-support-for-using-cchardet-to-detect-files-encod.patch + + Patch: 0009-Add-support-for-disabling-user-registration.patch +- Remove mandatory dependency on systemd to ease containerization + ------------------------------------------------------------------- Sun Aug 30 14:25:21 UTC 2020 - Neal Gompa diff --git a/pagure.spec b/pagure.spec index ae32afd..0ca373b 100644 --- a/pagure.spec +++ b/pagure.spec @@ -41,6 +41,18 @@ Source1: https://raw.githubusercontent.com/fedora-infra/python-fedora Source10: pagure-README.SUSE +# Backports from upstream +Patch0001: 0001-Display-real-line-numbers-on-pull-request-s-diff-vie.patch +Patch0002: 0002-Show-the-assignee-s-avatar-on-the-board.patch +Patch0003: 0003-Allow-setting-a-status-as-closing-even-if-the-projec.patch +Patch0004: 0004-Include-the-assignee-in-the-list-of-people-notified-.patch +Patch0005: 0005-Introduce-the-collaborator_project_groups-mapping.patch +Patch0006: 0006-When-a-file-a-detected-as-a-binary-file-return-the-r.patch +Patch0007: 0007-Remove-fenced-code-block-when-checking-mention.patch +Patch0008: 0008-Add-support-for-using-cchardet-to-detect-files-encod.patch +Patch0009: 0009-Add-support-for-disabling-user-registration.patch + + # SUSE-specific fixes ## Change the defaults in the example config to match packaging Patch1000: pagure-5.0-default-example-cfg.patch @@ -122,6 +134,9 @@ Requires: python3-dbm Requires: python3-kitchen Requires: python3-requests +# We want to use cchardet whenever it's available +Recommends: python3-cchardet + # If using PostgreSQL, the correct driver should be installed Recommends: (python3-psycopg2 if postgresql-server) @@ -137,7 +152,7 @@ Recommends: (%{name}-web-nginx if nginx) # The default theme is required Requires: %{name}-theme-default -%{?systemd_requires} +%{?systemd_ordering} # We use the git tools for some actions due to deficiencies in libgit2 and pygit2 Requires: git-core